7.1 The .fun file: structure and intent
In this book, a function is a named behavioral capability of the system. It is not an implementation; that lives in the architecture (Chapter 8). It is not a requirement; that lives in .req (Chapter 9). It is a behavior the system must be able to perform, named at a level you can break into finer behaviors and allocate to architectural blocks.
Function versus architecture is the distinction that matters most in MBSE practice, and the one people mix up most often. Take AlphaProtectionLaw from the autopilot anchor. It names what the system does: it limits pitch command authority as alpha approaches alpha_max.
The implementing block could take several shapes. It might be a software module in the flight-control computer, or a hardware-software partition split between the FCC and a dedicated stall-warning computer. It might also be a triplex-redundant arrangement across three FCC channels. The function is the same in all three cases; only the architecture changes.
File structure
A .fun file usually starts with use featureset <Name> (typically one), so the file's enables relationships resolve. You can add use interfaceset <Name> so that needs and provides resolve too. The header hdef functionset <Name> names the file and carries owner, safety level, and status. Function definitions follow as def function clauses at column 2.
use featureset AutopilotFeatures
hdef functionset AutopilotFunctions
name "Autopilot Control Functions"
owner "Flight Controls Software"
safetylevel ASIL-D
status approved
def function ModeController
name "Autopilot Mode Controller"
description "Manages active mode (HDG, ALT, VS, LNAV, VNAV, APP, etc.)."
level system
safetylevel ASIL-D
functiontype function
enables ref feature CoreAutopilot
decomposesto ref function ModeTransitionGuard, ModeAnnunciatorEach def function carries identity (name, description), classification, status (status), and relationship properties. Classification covers three properties: level, safetylevel, and functiontype.
For level, use system for top-level functions, subsystem for the middle of the decomposition, and component or module at the bottom. functiontype is almost always function. You'll see solution only at the very top levels and solutionelement only at the very bottom.
The main relationships
Four relationships do most of the work in a .fun file.
- enables ref feature: the function realizes the named feature. It is the inverse of the
.fmlfile's performs, and together they form the bilateral feature-function trace that coverage analysis checks. Every non-trivial function should enable at least one feature. A function that enables nothing is usually an orphan, left behind by a decomposition with no clear customer-facing purpose. - decomposesto / decomposedfrom ref function: the function hierarchy. decomposesto points down (parent to children); decomposedfrom points up (child to parent). They are bilateral inverses. The validator strictly needs only one, but the convention is to write both for clarity. From the autopilot anchor: ModeController decomposesto ModeTransitionGuard, ModeAnnunciator; ModeTransitionGuard decomposedfrom ModeController.
- needs / provides ref operation|signal: the function's interfaces. needs names the operations and signals the function consumes; provides names the ones it produces. These references resolve into the
.ifcfile (Chapter 8) and give you the function-to-interface trace. - allocatedto ref block: the function's allocation to an architectural block. This is the main forward trace from function to architecture. Its inverse, implements ref function, lives on the
.blkside (Chapter 8). The pair is bilateral, and the convention is to write both.
A few less common relationships complete the vocabulary. Reach for each one only when your project's domain calls for it.
- derivedfrom ref requirement and implementedby ref requirement: ASPICE bilateral traceability, for when functions are derived from requirements rather than the other way round.
- detects ref failuremode: for safety-diagnostic functions.
- meets ref characteristic|safetygoal: for functions that satisfy a quantitative characteristic or a safety goal.
What's ahead in Chapter 7
Subchapter 7.2 is about decomposition strategy: picking the right granularity at each layer, how deep to nest, and when to split a function or leave it whole. Subchapter 7.3 covers bilateral traceability (enables ↔ performs, decomposesto ↔ decomposedfrom, allocatedto ↔ implements) and the coverage analysis that depends on it.
Subchapter 7.4 picks up the variant-aware patterns introduced briefly in Chapter 6: functions gated on when ref config. Subchapter 7.5 walks the autopilot anchor's complete function model, with mirror examples from the cobot, EPB, and infusion pump.
Exercises
7.2 Function decomposition strategy
How useful your function model is depends heavily on its granularity. Make it too coarse, say three or four top-level functions with no decomposition, and it misses the structure the architecture and requirements need to point at. Make it too fine, with every conditional branch and helper routine as its own function, and you lose the abstraction that separates function modeling from code structure.
If you've ever opened a function model that read like a call graph, you've met the second failure. Below are practical rules for choosing granularity, and the anti-patterns to avoid.
The granularity principle
Name a function at the level where a stakeholder would talk about the system's behavior. AlphaProtectionLaw is a function: an aerospace systems engineer or a pilot would discuss alpha protection as one named capability of the autopilot. ClampLowPassFilter is not a function in the MBSE sense, though it may be one in the C-language sense. No stakeholder talks about it as a capability in its own right, so it is an implementation detail for the architecture or the source code.
Three operational tests help you tell functions from implementation details. A candidate that fails any of them is too fine.
- Allocation. Can you allocate it to one architectural block? If it could plausibly sit in any of several blocks, it is usually implementation.
- Safety requirement. Can it be the subject of a safety requirement? If that requirement reads awkwardly (REQ-001: The system shall execute ClampLowPassFilter correctly), it is too fine.
- Conversation. Would it come up in a stakeholder discussion? If pilots, mechanics, regulators, or product managers would never use its name, it is too fine.
The tests work in the other direction too. A function is too coarse if it is allocated to several blocks at the same level of architectural decomposition. It is also too coarse if you can't test it with a finite, well-bounded test suite, or can't describe it without breaking it into sub-functions in the same sentence.
Decomposition depth
The production function models in the four anchor projects have two to four levels of decomposition; three is typical. The autopilot anchor has three levels. At the top sit ModeController and AutopilotEngageManager. In the middle, ModeTransitionGuard and ModeAnnunciator sit under ModeController.
The bottom level appears only where it is needed. The protection laws have no sub-decomposition, because each is conceptually atomic at the function-modeling level. The cobot anchor is similar, with two to three levels: MotionController at the top, then TrajectoryPlanner and JointController. JointController could split further into per-axis controllers at the bottom if the engineering precision warrants it.
Deeper decompositions are a warning sign. Five or more levels usually point to one of three problems.
- You are using the function tree as a general-purpose hierarchy. It isn't one; it is specifically the behavioral hierarchy.
- You are decomposing too eagerly. Some leaf functions may still be too coarse and need splitting, but that applies to a few leaves, not the whole tree.
- The system really is complex enough to justify deep decomposition. My working rule is that this is rare: most safety-critical systems decompose well at three or four levels.
When to split a function
Split a function into two or more sub-functions as soon as any of these becomes true:
- Distinct safety levels. Sub-behaviors of one function carry materially different safety levels. Take the aerospace example. AutopilotEngageManager is DAL-A; fold the engage indicator (annunciator) into it and you misrepresent the annunciator's DAL-B classification. Splitting them into AutopilotEngageManager (DAL-A) and ModeAnnunciator (DAL-B) makes the safety analysis clearer. In the actual anchor, we kept ModeAnnunciator at DAL-A for safety margin.
- Distinct allocation. Sub-behaviors headed for different architectural blocks should be split. You may allocate one function to two blocks (allocatedto is multi-valued), but it is uncommon. It usually means a decomposition is missing, or an architectural choice needs another look.
- Distinct variant gating. A behavior that is active in some variants and not others deserves its own function, gated on the relevant when ref config clause. Mix variant-active and variant-inactive logic in one function, and the variant analysis becomes intractable.
- Distinct testing strategy. Sub-behaviors verified in materially different ways (HIL for one, SIL for another, formal verification for a third) are worth making separately addressable functions, even when they share a lot of implementation.
Don't split a function just because it is long, complex, or spread across several source files. Size is a metric for code structure, not for function decomposition. Those are separate concerns.
decomposesto / decomposedfrom in practice
The decomposition pair is the most-used relationship in any .fun file. How you write it matters for two reasons. The conventions help the agent reason about the function tree, and the coverage analysis relies on them to check completeness.
Convention 1. Every non-leaf function should list its children in an explicit decomposesto. That lets the agent (and you) walk down from a function without scanning the whole .fun file. From the autopilot anchor: ModeController decomposesto ModeTransitionGuard, ModeAnnunciator.
Convention 2. Every non-root function should name its parent with an explicit decomposedfrom, as in ModeTransitionGuard decomposedfrom ModeController. That enables the upward walk. It also gives the validator the bilateral edges it uses to flag one-sided authoring.
Convention 3. If a function has exactly one child, ask whether the decomposition means anything. A single-child decomposition usually means parent and child are the same function under two names. That is a refactoring opportunity, not a real decomposition.
What's next
With these rules, you can write function models that are neither too coarse nor too fine, neither too flat nor too deep. Subchapter 7.3 turns to bilateral traceability: the authoring conventions that tie the function model into the wider engineering spine. Subchapter 7.4 covers the variant-aware patterns, and Subchapter 7.5 walks the autopilot anchor's complete function model with cross-anchor mirrors.
Exercises
7.3 Bilateral traceability and the function-model coverage
The function model sits at a crossroads in the engineering graph. Upstream, every function should enable one or more features (the bilateral inverse of the .fml file's performs). Downstream, every function should be allocatedto an architectural block (the bilateral inverse of the .blk file's implements). Inside the function model, the decomposesto / decomposedfrom edges form the function tree.
Write all four edge types bilaterally, on both ends of every relationship. That discipline is what makes the function model fully navigable, and it is what powers the coverage analyses in Chapter 15. This subchapter shows you how.
The three inverse pairs
Three bilateral inverse pairs live around the function model.
- performs (feature → function) ↔ enables (function → feature). The feature-function pair. From the autopilot anchor: AlphaProtection performs ref function AlphaProtectionLaw in the
.fml, and AlphaProtectionLaw enables ref feature AlphaProtection in the.fun. Both sides should be there. The coverage analysis flags a missing side as a single-sided trace warning. - decomposesto ↔ decomposedfrom (both targeting function within the same .fun). The function tree's pair. With edges both ways, the agent and the coverage analysis can walk the tree in either direction without scanning.
- allocatedto (function → block) ↔ implements (block → function). The function-architecture pair. The function side lives in the
.fun; the block side lives in the.blk. Writing both is the strongest discipline. Many projects write only one, usually the architecture side (block implements ref function), because architects often own the architecture files while function modelers own the function files.
A fourth pair belongs to the ASPICE bilateral discipline: derivedfrom (function → requirement) ↔ implementedby (requirement → function). You use it when the function model is derived from the requirement set, rather than the more common function-then-requirement direction. It is less widely used than the other three, and shows up where the project's ASPICE assessment specifically calls for it.
Authoring discipline: both sides every time
The principle is simple: write every relationship on both sides. It costs a few extra lines per function. In return, the model is navigable in both directions and the coverage analysis gives you useful output. With 50 functions you could check every pair by hand; with 500 you couldn't, but the validator does it for you.
In practice, three approaches keep the discipline manageable.
- Agent-authored bilateral edges. AI agents working under the RequirementsEngineer or ArchitectureLead roles write both sides of each relationship by default, and the skill files reinforce the convention. In a project written mostly by agents, you should find most bilateral pairs already complete.
- CI-time validator runs. The validator's SYLANG_BILATERAL_TRACE_INCOMPLETE warning surfaces incomplete pairs at build time. Treat that warning as a build-failing error, and the discipline stays tight.
- Coverage-report-driven cleanup. Hold periodic cleanup sessions that work through the coverage report's single-sided trace category and add the missing edges. It is less rigorous than the CI approach, but more practical in projects with a lot of hand-written legacy.
Function orphans: what they mean
A function that enables no feature is a function orphan upstream: it exists in the model but serves no customer-facing purpose. There are three common causes.
- Legitimate infrastructure functions. Some functions (initialization, diagnostics, calibration) really have no customer-facing feature; they are infrastructure, not customer-visible behavior. Tag them (tags "infrastructure") and exclude them from the orphan analysis with a coverage-report filter.
- Missing feature. The function realizes a behavior the customer cares about, but nobody has added the matching feature to the feature model. The fix: add the feature.
- Stale function. The function existed for a feature that has since been removed from the model. The fix: remove the function.
Likewise, a function with no allocatedto block is an orphan downstream: nothing in the architecture realizes it. The same three causes apply, with small adjustments. The third is more common here. Functions written before their architecture was decided sometimes never get their allocation in later revisions.
What's next
Bilateral traceability is what the function model contributes to the integrity of the engineering spine. You'll know it's working when the coverage report's single-sided trace list stays empty from one build to the next. Subchapter 7.4 covers the variant-aware patterns (functions that exist only in some variants), and Subchapter 7.5 walks the autopilot anchor's complete function model at production scale.
Exercises
7.4 Variant-aware function modeling
Real product lines include features that exist only in some variants. The autopilot's AutolandCapability exists only in advanced-trim aircraft. The cobot's ForceFeedbackController exists only in variants with the force-torque sensor. Your function model has to represent these variant-conditional functions in a form the rest of the project's tooling can reason about.
The mechanism is the when ref config clause from Chapter 6 Subchapter 5. Here we apply it to function modeling, along with the propagation idioms that keep variant-aware function models tractable.
The simple pattern: gate the function on its enabling feature
The simplest variant-aware pattern is the one you already saw in Subchapter 6.5. A function that enables an optional feature carries a matching when ref config c_<Feature> clause. From the autopilot anchor:
def function AutolandSequencer
name "Autoland Sequencer"
description "Manages glide-slope capture, flare, derotation, rollout."
when ref config c_AutolandCapability
safetylevel ASIL-D
enables ref feature AutolandCapabilityThis pattern is clean, and I recommend it whenever a function's existence depends on a single feature. Run coverage analysis for a specific variant, such as a Standard_Trim variant whose .vcf sets c_AutolandCapability = 0. The function drops out cleanly, along with its downstream requirements and the test cases that verify it.
The multi-gate pattern: AND semantics
A function that should be active only when several features are selected together carries several when ref config clauses. They combine with AND semantics (Chapter 6 Subchapter 5). The pattern is rare in production projects, since most variant-aware logic depends on a single feature gate, but the language supports it cleanly. Here is a hypothetical example from the autopilot domain:
// Hypothetical: an autoland mode that requires both autoland and a
// specific high-precision GPS receiver feature
def function PrecisionAutolandSequencer
name "Precision (Cat IIIb) Autoland Sequencer"
when ref config c_AutolandCapability
when ref config c_PrecisionGps
safetylevel ASIL-DIf you find yourself using this pattern a lot, consider lifting the gating logic into the feature model. Introduce a parent feature PrecisionAutoland that requires both AutolandCapability and PrecisionGps, then gate the function on that single parent. Lifting moves the variability into the feature model, where the variant-matrix tool can reason about it. Your function model stays simpler.
Propagation through the function tree
When a parent function carries when ref config, the gating flows down to its decomposed children unless overridden. The rule: a child of an inactive parent is itself inactive, whatever its own when clauses say. A child's own when clauses can narrow the conditions under which it is active. They can never widen them beyond the parent's.
So you don't need to repeat the parent's when clauses on every child. In a hypothetical cobot example, if VisionPipeline (the top-level vision function) gates on c_IntegratedVision, its children (ImageAcquisition, ObjectLocalization, PoseEstimation) inherit that gating automatically. Explicit when clauses on them would be redundant. The cobot's actual .fun follows this convention: VisionPipeline carries the gating clause, and the decomposition is implicit.
The exception is a child that needs extra gating beyond its parent's. That child carries its own when clauses, which AND with the inherited gating. Staying with the hypothetical cobot, say one localization algorithm is used only in cleanroom-certified variants, because its lighting assumption requires it. The CleanroomLocalization child would then carry the inherited c_IntegratedVision gating (implicit) plus its own when ref config c_CleanroomCert clause (explicit).
When to split into variant-specific files
If a large share of a function set is variant-specific (more than roughly one-third), the model gets harder to read, for you and for agents. In that case, split the function set into several .fun files: one for the variant-agnostic core, and one or more for the variant-specific extensions. Each file's hdef carries the right when clause, and the functions inside inherit the gating.
The autopilot anchor's function set is small enough not to need splitting. A production-scale autopilot might split like this:
- AutopilotCoreFunctions.fun: no gating; the protection laws and basic modes.
- AutopilotAutolandFunctions.fun: gated on c_AutolandCapability; the autoland sequencer and supporting modes.
- AutopilotAdvancedFunctions.fun: gated on c_AdvancedTrim; flight-management integration not present in standard-trim aircraft.
Each file is internally coherent and variant-uniform, and the cross-file structure stays clean.
What's next
These patterns complete the function model's variability vocabulary. Subchapter 7.5 walks the autopilot anchor's complete function model in light of everything in this chapter. Its mirror examples show the same patterns at work in automotive, medical, and industrial domains.
Exercises
7.5 The autopilot function model: a complete walkthrough
The previous four subchapters built up the principles, conventions, and patterns of function modeling. This one walks the autopilot anchor's complete .fun file and points out which principle is at work in each construct. Then come one-page mirror examples from the cobot, EPB, and infusion pump anchors. Read it with AutopilotFunctions.fun open from the companion repository, and you'll see the chapter's guidance end to end.
Top-level functions and their role
The autopilot function set has nine functions. Two sit at the top level (ModeController, AutopilotEngageManager). Two are children of ModeController (ModeTransitionGuard, ModeAnnunciator). Four are the protection laws (AlphaProtectionLaw, BankAngleLaw, OverspeedLaw, PitchAttitudeLaw), and one is variant-gated (AutolandSequencer).
That structure is the decomposition-depth principle from Subchapter 7.2 at work: two to three levels, no deeper.
The two top-level functions share their enables target. ModeController enables ref feature CoreAutopilot, and so does AutopilotEngageManager. Both contribute to the core feature, but they decompose differently: the controller has children, the engage manager does not. The split reflects an engineering decision: engage/disengage logic is operationally separable from active-mode logic, and the safety analysis benefits from treating them apart.
The four protection laws
The four protection laws are siblings at the same level of decomposition. Each is a leaf function with no children, each enables a different feature, and each carries safetylevel ASIL-D. Together they show the splitting on distinct allocation principle from Subchapter 7.2.
The four laws share a lot of implementation infrastructure. They all read aircraft state, all output control surface commands, and all take part in the priority resolution between competing protection demands. They are still modeled as separate functions, because each is allocated to a different sub-block of the flight-control computer's protection module. Each is also the subject of distinct safety requirements.
The four laws also show the bilateral discipline from Subchapter 7.3. Each enables a feature, whose .fml carries the inverse performs. In the architecture chapter that follows, each will be allocated to a block, whose .blk carries the inverse implements. Traceability across all four directions (feature, function, requirement, block) is complete by construction.
The variant-gated function
AutolandSequencer is the only variant-gated function in the autopilot's current scope. It carries when ref config c_AutolandCapability and enables ref feature AutolandCapability. In the StandardTrim variant, whose .vcf sets c_AutolandCapability = 0, the function is inactive. Three things follow from that.
- Coverage analysis for that variant excludes the function.
- The architecture's implements ref function AutolandSequencer is treated as an inactive reference. This is consistency-rule case 2 from Chapter 6 Subchapter 5: both ends are inactive, so it is silently skipped.
- The test cases satisfying its derived requirements are left out as well.
The function doesn't need its own file (Subchapter 7.4's when to split discussion). The variant-specific content is one function out of nine, well below the one-third threshold. A real autopilot might have a dozen or more autoland-related functions: glide-slope tracking, flare law, derotation, rollout, throttle retard, decision-altitude monitoring. At that point, splitting out AutopilotAutolandFunctions.fun would be well justified.
Mirror: the cobot's function model
The cobot anchor's CobotFunctions.fun shows similar patterns at comparable scale: nine functions across three levels. MotionController sits at the top, with TrajectoryPlanner and JointController in the middle. Then come the four mandatory safety functions (StopController, SpeedMonitor, DirectionMonitor, CollisionMonitor) and two variant-gated optional functions (ForceFeedbackController on c_ForceTorqueSensor, VisionPipeline on c_IntegratedVision). The safety-function siblings mirror the autopilot's protection laws, and the variant-gated optionals mirror its autoland sequencer.
Mirror: the EPB's function model
The EPB anchor's EpbFunctions.fun has 15 functions, already written and reviewed in earlier chapters. Its variant gating is richer. SingleMotorDriver and DualMotorDriver are alternatives, gated on c_SingleMotorCaliper and c_DualMotorCaliper respectively. MotorCrosscheck is gated on c_DualMotorCaliper, and AutoEngageLogic, HillHoldController, AvhController, and AutoReleaseLogic are each gated on their own optional feature.
It is the most variant-rich function set of the four anchors. That makes it a good place to see how the function model behaves at the high end of variant complexity.
Mirror: the insulin pump's function model
The insulin pump anchor's function model (started in Chapter 4 as part of the IDE tutorial, and completed in Chapter 9 for requirements integration) stresses a different pattern. Safety-critical functions (BolusController, DoseLimiter, OcclusionDetector) dominate the set, and non-safety functions are a much smaller share. That reflects the medical-device safety profile: nearly every function the device performs has a safety implication. In the automotive and aerospace cases, safety functions are a substantial subset but not the dominant one.
Closing Chapter 7
That completes the function model, the .fun file type, at the depth you need for production authoring. You've covered the file's structure (7.1), decomposition strategy (7.2), bilateral traceability (7.3), variant-aware patterns (7.4), and this walkthrough of the autopilot anchor with its cross-anchor mirrors. Chapter 8 turns to the architecture: the .blk, .ifc, .smd, .ucd, and .seq file types that give the functions a physical realization. It is anchored on the EPB project, whose architecture is the most fully developed of the four anchors.
Exercises
Key takeaways
You’ve got this chapter if you remember:
- A function names what the system does, not how it is built, so the same function can survive very different architectures.
- Choose granularity with three tests (allocation, safety requirement, stakeholder conversation), and expect two to four levels of decomposition, with three typical.
- Write every relationship on both sides (enables ↔ performs, decomposesto ↔ decomposedfrom, allocatedto ↔ implements), because the validator and the coverage report depend on it.
- Gate variant-specific functions with when ref config on the parent, and split into variant-specific files once more than about one-third of the set is variant-specific.
- An orphan function is a signal: it is legitimate infrastructure, a sign of a missing feature, or stale.
Check yourself
Answer in your head first, then reveal.
1Is ClampLowPassFilter a function in the MBSE sense?
No. No stakeholder would discuss it as a capability in its own right, so it is an implementation detail for the architecture or the source code.
2A parent function gated on c_IntegratedVision is inactive in some variant. Can a child's own when clause make that child active there?
No. A child's when clauses can only narrow the parent's active range, never widen it.
3What happens when a parent lists a child in decomposesto but the child has no decomposedfrom?
The validator emits SYLANG_BILATERAL_TRACE_INCOMPLETE at warning severity.
4A function enables no feature. What are the three likely causes?
It is a legitimate infrastructure function (tag it), the matching feature is missing (add it), or the function is stale (remove it).