Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions changes/unreleased/qualitative-failure-analysis.added.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Add a reusable RAAML-aligned qualitative failure-analysis library, typed DFMEA query and native fault-tree rendering with AND/OR symbols, shared causes and explicit partial-view notices.
55 changes: 55 additions & 0 deletions docs/guide/failure-analysis.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Qualitative failure analysis

`FailureAnalysis` is a non-normative, RAAML-aligned SysML v2 library. It reuses
RAAML's basic-event, house-event, undeveloped-event and AND/OR distinctions;
it is not an import of RAAML's UML profile or a claim of RAAML conformance.
See [OMG RAAML 1.1](https://www.omg.org/spec/RAAML/1.1).

`Event.present` is unbound until supplied. `OrGate` and `AndGate` compute it
from their `inputs` references. These are propositions about one declared
exposure interval: conjunction implies neither temporal ordering nor statistical
independence. The library assigns no probabilities or failure rates.

Declare causal links with standard `CauseAndEffect::Causation` connections.
Specialize `FailureAnalysisViews::FaultTreeView` and expose the event usages and
connections to draw. Gate types, including specializations, determine the symbols;
model names do not. A short name supplies the display label. Shared causes remain
one node with multiple outgoing edges.

The native `fault-tree` rendering provides text/IR and Mermaid output with embedded
SVG AND/OR symbols. It requires a Mermaid release supporting image nodes (11.3+).
Other event kinds currently use labelled rectangles, not a complete fault-tree
symbol alphabet. DOT, PlantUML and D2 return a typed unsupported-form error.
Layout, palette and style decorations are not applied to this rendering.

This is a declaration view. Owned nested events are flattened, while `inputs` references are not extra event nodes. Single statically bound
event references share their target node; unresolved or computed references fail. Instantiated inherited
failure members are rejected: expose their declarations explicitly or redeclare
them in the usage. Gate inputs must be statically bound event references (one
reference or a sequence), not computed expressions. Binary causation is supported;
multicausation, ambiguous AND/OR specializations, cycles, duplicate inputs and
links contradicting executable inputs are rejected. Inputs or links omitted by
the view produce notices and a visible **partial view** label on the gate.
A partial view is not evidence that the complete model has no other causes.

`ReviewRow` records the function, affected part, causal events, detection proposal,
action, owner and evidence disposition. `Review` groups rows for any system.
`DesignFailureReview` reports counts; its readiness result requires nonempty,
reviewed scope and accepted disposition/evidence for every safety-critical row.
It does not certify physical safety or close every noncritical action.

Specialize `FailureAnalysisViews::ReviewRows` and bind `reviewName` to a review's
qualified name. The query selects descendant row declarations by semantic type,
including specializations, and projects referenced descriptions and actions.
New owned rows appear without editing a list of row names. As a declaration
query it does not expand inherited runtime instances. A native `DocumentQueries`
Table can consume this query with no application-specific renderer.

[The redundant-power example](../../examples/fault-tree.sysml) demonstrates the
shared cause, both gates, a local view specialization and a reusable DFMEA query:

```sh
sysml examples/fault-tree.sysml -render PowerSupply::tree
sysml examples/fault-tree.sysml -run-query PowerReview::Rows
sysml examples/fault-tree.sysml -render-document PowerReport::Report
```
12 changes: 12 additions & 0 deletions docs/project/spec-compliance.md
Original file line number Diff line number Diff line change
Expand Up @@ -3562,3 +3562,15 @@ SysML v2 defines *evaluation* of a constraint, requirement or `assert satisfy` a
**Decided fragment and undecided reasons.** The questions decide what the translatable subset decides: linear and nonlinear real/integer arithmetic within the solver's decision power (a nonlinear case the backend cannot close comes back `unknown` → undecided), quantities normalized to base units, booleans and enumerations. Outside it — exponentiation and computed divisors in a violation query, collections, quantifiers, invocations and the rest the subset refuses — the verdict is `undecided` with the refusal named, as is an absent solver or a rounded unsat without a proof. One divergence is recorded rather than fixed: an `evaluate` verdict reads `assume` conditions as trusted facts about the point evaluated, so a point violating an assumption may evaluate `violated` while `holds` — the library's assumption-means-entailment semantics — is proved. The two answer different questions and the verdict's `question` says which one it answered.

**Clients and conformance:** the question is `question` on the three verify requests; `Verdict` carries `question`, `status` and `witness` (`WitnessAssignment` of feature, replayed value, unit, exact); the clients take it as their engines option's sibling — Python `question=`, Go `opensysml.Asking(...)`, Java `VerifyOptions.asking(...)`, Node's options object — each refusing a symbolic question against a service predating `verification_questions` before anything is sent.

## Qualitative failure-analysis extension

This non-normative extension does not assert RAAML or normative SysML fault-tree
notation conformance. See [supported scope](../guide/failure-analysis.md).

| Contract | Implementation | Test | Status |
|---|---|---|---|
| Semantic gate types and shared causal identity | `internal/ir/view/fault_tree.go:renderFaultTree` | `TestFaultTreeTypesSharedCausesAndRenderings` | ✅ faithful to declared subset |
| Partial views and causal consistency | `renderFaultTree`, `faultGateInputs` | `TestFaultTreeIntegrity`, `TestFaultTreePartialAndCycle` | ✅ faithful to declared subset |
| Instantiated inherited event members, computed input lists, nonbinary gates | `renderFaultTree`, `faultGateInputs` | `TestFaultTreeIntegrity` | ❌ unsupported; typed error |
| RAAML complete profile and quantitative analysis | — | — | ❌ not implemented |
52 changes: 52 additions & 0 deletions examples/fault-tree.sysml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package PowerSupply {
private import FailureAnalysis::*;
private import CauseAndEffect::*;
occurrence def SupplyFault :> BasicEvent;
occurrence def EitherFailure :> OrGate;
occurrence a : SupplyFault;
occurrence b : SupplyFault;
occurrence common : BasicEvent { attribute :>> description = "Common supply fault"; }
occurrence left : EitherFailure { ref occurrence :>> inputs = (a,common); }
occurrence right : OrGate { ref occurrence :>> inputs = (b,common); }
occurrence loss : AndGate { ref occurrence :>> inputs = (left,right); attribute :>> description = "Both supplies unavailable"; }
connection al : Causation connect a to left;
connection cl : Causation connect common to left;
connection br : Causation connect b to right;
connection cr : Causation connect common to right;
connection lt : Causation connect left to loss;
connection rt : Causation connect right to loss;
view def LocalView :> OpenSysMLRenderings::FaultTreeView;
view tree : LocalView { expose PowerSupply::*; }
view partial : LocalView { expose a; expose al; }
}
package PowerReport {
private import DocumentQueries::*;
part def Report :> Document {
attribute :>> title = "Redundant power supply: qualitative fault tree";
part tree : Diagram { ref :>> source = PowerSupply::tree; }
}
}

package PowerReview {
part supply : Parts::Part;
part review : FailureAnalysis::Review {
ref part :>> system = supply;
attribute :>> scopeReviewed = false;
part outage : FailureAnalysis::ReviewRow subsets modes {
attribute :>> identifier = "P-01";
attribute :>> intendedFunction = "Maintain redundant power";
ref part :>> affected = supply;
ref occurrence :>> cause = PowerSupply::common;
ref occurrence :>> failureMode = PowerSupply::loss;
ref occurrence :>> localEffect = PowerSupply::loss;
ref occurrence :>> missionEffect = PowerSupply::loss;
attribute :>> proposedDetection = "Bus monitoring";
attribute :>> recommendedAction = "Test isolation against a common supply fault";
attribute :>> ownerRole = "Power";
attribute :>> safetyCritical = true;
}
}
calc def Rows :> FailureAnalysisViews::ReviewRows {
in :>> reviewName = "PowerReview::review";
}
}
2 changes: 1 addition & 1 deletion examples/self-model/pipeline.sysml
Original file line number Diff line number Diff line change
Expand Up @@ -348,7 +348,7 @@ package OpenSysMLPipeline {
// snapshot when that matches the files, and parsed from the files otherwise.
#SemanticEngine part def StandardLibrary :> Stage {
attribute :>> goPackage = "internal/workspace/libs";
attribute bundledFileCount : Integer = 110;
attribute bundledFileCount : Integer = 112;
attribute cached : Boolean = true;
attribute snapshotFile : String = "internal/workspace/libs/stdlib.snapshot";
attribute snapshotEmbedded : Boolean = true;
Expand Down
2 changes: 1 addition & 1 deletion examples/self-model/quality.sysml
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ package OpenSysMLInvariants {
doc /* Every bundled standard library file parses clean. */
subject stdlib : StandardLibrary;
require constraint {
stdlib.bundledFileCount == 110
stdlib.bundledFileCount == 112
}
}

Expand Down
7 changes: 4 additions & 3 deletions examples/self-model/surfaces.sysml
Original file line number Diff line number Diff line change
Expand Up @@ -284,8 +284,8 @@ package OpenSysMLSurfaces {
// Every rendering kind the implementation recognizes is modelled here.
part def ViewEngine :> CodeUnit {
attribute :>> goPackage = "internal/ir/view";
attribute recognizedKinds : Integer = 14;
attribute supportedKinds : Integer = 12;
attribute recognizedKinds : Integer = 15;
attribute supportedKinds : Integer = 13;
part tree : RenderingKind { attribute :>> kind = "tree"; }
part interconnection : RenderingKind { attribute :>> kind = "interconnection"; }
part stateDiagram : RenderingKind { attribute :>> kind = "state"; }
Expand All @@ -299,6 +299,7 @@ package OpenSysMLSurfaces {
part sequence : RenderingKind { attribute :>> kind = "sequence"; }
part caseDiagram : RenderingKind { attribute :>> kind = "case"; }
part mixedDiagram : RenderingKind { attribute :>> kind = "mixed"; }
part faultTree : RenderingKind { attribute :>> kind = "fault-tree"; }
part requirementGraph : RenderingKind { attribute :>> kind = "requirement"; }
part definitionGraph : RenderingKind { attribute :>> kind = "definition"; }
part packageGraph : RenderingKind { attribute :>> kind = "package"; }
Expand Down Expand Up @@ -699,7 +700,7 @@ package OpenSysMLSurfaces {
* say so instead of being drawn as something else. */
subject views : ViewEngine;
require constraint {
views.recognizedKinds == 14 and views.supportedKinds == 12
views.recognizedKinds == 15 and views.supportedKinds == 13
and not views.textual.supported and not views.geometry.supported
and views.sequence.supported and views.matrix.supported
}
Expand Down
38 changes: 38 additions & 0 deletions internal/doc/queryexec/failure_analysis_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package queryexec

import (
"slices"
"testing"
)

func TestFailureAnalysisReusableRows(t *testing.T) {
fixture := loadExecutionFixture(t, `
occurrence cause : FailureAnalysis::BasicEvent { attribute :>> description = "Common power loss"; }
occurrence effect : FailureAnalysis::Event { attribute :>> description = "Both buses unavailable"; }
part def LocalRow :> FailureAnalysis::ReviewRow {
attribute :>> intendedFunction = "Supply power";
ref occurrence :>> cause = Observatory::cause;
ref occurrence :>> failureMode = effect;
attribute :>> recommendedAction = "Inject a supply fault";
}
part review : FailureAnalysis::Review {
part rowOne : LocalRow subsets modes { attribute :>> identifier = "P-01"; }
part rowTwo : LocalRow subsets modes { attribute :>> identifier = "P-02"; }
part unrelated;
}
calc def Rows :> FailureAnalysisViews::ReviewRows { in :>> reviewName = "Observatory::review"; }
`)
rows, err := fixture.execute(t, "Rows", nil, Options{})
if err != nil {
t.Fatal(err)
}
if got := cellTexts(t, rows, 0); !slices.Equal(got, []string{"P-01", "P-02"}) {
t.Fatalf("rows=%v", got)
}
if got := cellTexts(t, rows, 2); !slices.Equal(got, []string{"Common power loss", "Common power loss"}) {
t.Fatalf("causes=%v", got)
}
if got := cellTexts(t, rows, 3); !slices.Equal(got, []string{"Both buses unavailable", "Both buses unavailable"}) {
t.Fatalf("effects=%v", got)
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package test {
part review : FailureAnalysis::Review {
attribute :>> scopeReviewed = true;
part row : FailureAnalysis::ReviewRow subsets modes {
attribute :>> safetyCritical = true;
attribute :>> dispositionAccepted = true;
attribute :>> actionEvidenceAccepted = true;
attribute :>> evidenceReference = "";
}
}
analysis check : FailureAnalysis::DesignFailureReview { subject :>> review = test::review; }
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package test { private import FailureAnalysis::*;
occurrence a0 : BasicEvent { attribute :>> present = false; }
occurrence b0 : BasicEvent { attribute :>> present = false; }
occurrence or0 : OrGate { ref occurrence :>> inputs = (a0,b0); }
occurrence and0 : AndGate { ref occurrence :>> inputs = (a0,b0); }
occurrence a1 : BasicEvent { attribute :>> present = false; }
occurrence b1 : BasicEvent { attribute :>> present = true; }
occurrence or1 : OrGate { ref occurrence :>> inputs = (a1,b1); }
occurrence and1 : AndGate { ref occurrence :>> inputs = (a1,b1); }
occurrence a2 : BasicEvent { attribute :>> present = true; }
occurrence b2 : BasicEvent { attribute :>> present = false; }
occurrence or2 : OrGate { ref occurrence :>> inputs = (a2,b2); }
occurrence and2 : AndGate { ref occurrence :>> inputs = (a2,b2); }
occurrence a3 : BasicEvent { attribute :>> present = true; }
occurrence b3 : BasicEvent { attribute :>> present = true; }
occurrence or3 : OrGate { ref occurrence :>> inputs = (a3,b3); }
occurrence and3 : AndGate { ref occurrence :>> inputs = (a3,b3); }
analysis check {
out attribute or0 : ScalarValues::Boolean = test::or0.present;
out attribute and0 : ScalarValues::Boolean = test::and0.present;
out attribute or1 : ScalarValues::Boolean = test::or1.present;
out attribute and1 : ScalarValues::Boolean = test::and1.present;
out attribute or2 : ScalarValues::Boolean = test::or2.present;
out attribute and2 : ScalarValues::Boolean = test::and2.present;
out attribute or3 : ScalarValues::Boolean = test::or3.present;
out attribute and3 : ScalarValues::Boolean = test::and3.present;
}
}
2 changes: 1 addition & 1 deletion internal/ir/view/direction.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func ParseDirection(name string) (Direction, bool) {
// table has no direction, and a sequence diagram always reads top-down.
func (k Kind) SupportsDirection() bool {
switch k {
case KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindRequirement, KindDefinition, KindPackage:
case KindFaultTree, KindTree, KindInterconnection, KindState, KindAction, KindCase, KindMixed, KindRequirement, KindDefinition, KindPackage:
return true
}
return false
Expand Down
Loading