c7e197d107
35 CNC-spezifische Hazard Patterns (HP1400-HP1434): - Werkzeugbruch, Schleifscheibenbruch, Spaeneflug, Kollision - KSS-Exposition (Aerosol, Hautkontakt, Keimbelastung, Brand) - Schweissrauch, UV-Strahlung, Spritzer, Stromschlag, Ex-Hohlkoerper - Maschinenspezifisch: Quetschung Tuer, Spindelerfassung, Walzeneinzug - Alle mit MachineTypes, OperationalStates, HumanRoles annotiert 18 Metalworking-Massnahmen (M404-M421), RAG-validiert gegen TRGS 551/528: - KSS: Substitution, Aerosolabsaugung, Konzentrationskontrolle, Wechselintervalle, Hautschutzplan - Schleifen: Schleifscheiben-Pruefung, Drehzahlbegrenzung - Schweissen: Fortluft-Absaugung, brennerintegrierte Absaugung, raeumliche Trennung, Schweisserschutzschild - Allgemein: AGW-Ueberwachung, Arbeitsmedizin, Reinigung, Unterweisung 5 Evidenztypen (E51-E55): KSS-Analyse, Schleifscheiben-/Spannmittel-Pruefung, Schweissnaht-Qualifikation Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
453 lines
17 KiB
Go
453 lines
17 KiB
Go
package iace
|
|
|
|
import "sort"
|
|
|
|
// MatchInput defines the inputs for the pattern-matching engine.
|
|
type MatchInput struct {
|
|
ComponentLibraryIDs []string `json:"component_library_ids"`
|
|
EnergySourceIDs []string `json:"energy_source_ids"`
|
|
LifecyclePhases []string `json:"lifecycle_phases"`
|
|
CustomTags []string `json:"custom_tags"`
|
|
// OperationalStates are the active operational states of the machine.
|
|
// Used to filter patterns that only apply in specific states (e.g. teach_mode, maintenance).
|
|
OperationalStates []string `json:"operational_states,omitempty"`
|
|
// StateTransitions are active state transitions (format: "from→to").
|
|
// Used to detect transition-specific hazards like unexpected restart.
|
|
StateTransitions []string `json:"state_transitions,omitempty"`
|
|
// HumanRoles are the human roles interacting with the machine in this project.
|
|
// Used to filter patterns that only apply to specific roles (e.g. programmer, maintenance_tech).
|
|
HumanRoles []string `json:"human_roles,omitempty"`
|
|
}
|
|
|
|
// MatchOutput contains the results of pattern matching.
|
|
type MatchOutput struct {
|
|
MatchedPatterns []PatternMatch `json:"matched_patterns"`
|
|
SuggestedHazards []HazardSuggestion `json:"suggested_hazards"`
|
|
SuggestedMeasures []MeasureSuggestion `json:"suggested_measures"`
|
|
SuggestedEvidence []EvidenceSuggestion `json:"suggested_evidence"`
|
|
ResolvedTags []string `json:"resolved_tags"`
|
|
}
|
|
|
|
// MatchReason explains why a specific check passed or was relevant for a pattern match.
|
|
type MatchReason struct {
|
|
Type string `json:"type"` // "required_component_tag", "required_energy_tag", "lifecycle_match", "no_exclusion"
|
|
Tag string `json:"tag"`
|
|
Met bool `json:"met"`
|
|
}
|
|
|
|
// PatternMatch records which pattern fired and why.
|
|
type PatternMatch struct {
|
|
PatternID string `json:"pattern_id"`
|
|
PatternName string `json:"pattern_name"`
|
|
Priority int `json:"priority"`
|
|
MatchedTags []string `json:"matched_tags"`
|
|
// Explainability: structured reasons why this pattern fired
|
|
MatchReasons []MatchReason `json:"match_reasons,omitempty"`
|
|
// Detail fields from the pattern definition
|
|
ScenarioDE string `json:"scenario_de,omitempty"`
|
|
TriggerDE string `json:"trigger_de,omitempty"`
|
|
HarmDE string `json:"harm_de,omitempty"`
|
|
AffectedDE string `json:"affected_de,omitempty"`
|
|
ZoneDE string `json:"zone_de,omitempty"`
|
|
DefaultSeverity int `json:"default_severity,omitempty"`
|
|
DefaultExposure int `json:"default_exposure,omitempty"`
|
|
HazardCats []string `json:"hazard_categories,omitempty"`
|
|
ExpertHintDE string `json:"expert_hint_de,omitempty"`
|
|
RequiresExpert bool `json:"requires_expert,omitempty"`
|
|
OperationalStates []string `json:"operational_states,omitempty"`
|
|
StateTransitions []string `json:"state_transitions,omitempty"`
|
|
HumanRoles []string `json:"human_roles,omitempty"`
|
|
}
|
|
|
|
// HazardSuggestion is a suggested hazard from pattern matching.
|
|
type HazardSuggestion struct {
|
|
Category string `json:"category"`
|
|
SourcePatterns []string `json:"source_patterns"`
|
|
Confidence float64 `json:"confidence"`
|
|
}
|
|
|
|
// MeasureSuggestion is a suggested protective measure from pattern matching.
|
|
type MeasureSuggestion struct {
|
|
MeasureID string `json:"measure_id"`
|
|
SourcePatterns []string `json:"source_patterns"`
|
|
}
|
|
|
|
// EvidenceSuggestion is a suggested evidence type from pattern matching.
|
|
type EvidenceSuggestion struct {
|
|
EvidenceID string `json:"evidence_id"`
|
|
SourcePatterns []string `json:"source_patterns"`
|
|
}
|
|
|
|
// PatternEngine evaluates hazard patterns against resolved tags.
|
|
type PatternEngine struct {
|
|
resolver *TagResolver
|
|
patterns []HazardPattern
|
|
}
|
|
|
|
// NewPatternEngine creates a PatternEngine with all pattern sources and resolver.
|
|
func NewPatternEngine() *PatternEngine {
|
|
// Combine all pattern sources
|
|
patterns := GetBuiltinHazardPatterns() // HP001-HP044
|
|
patterns = append(patterns, GetExtendedHazardPatterns()...) // HP045+ from rule library
|
|
patterns = append(patterns, GetPressHazardPatterns()...) // HP045-HP058 press-specific
|
|
patterns = append(patterns, GetCobotHazardPatterns()...) // HP059-HP065 cobot-specific
|
|
patterns = append(patterns, GetOperationalHazardPatterns()...) // HP066-HP093 operational states
|
|
patterns = append(patterns, GetDGUVExtendedPatterns()...) // HP094-HP133 DGUV themes
|
|
patterns = append(patterns, GetExtendedHazardPatterns2()...) // HP134-HP173 additional hazards
|
|
patterns = append(patterns, GetElevatorPatterns()...) // HP174-HP198 elevator/lift
|
|
patterns = append(patterns, GetAGVAgriPatterns()...) // HP199-HP228 AGV + agricultural
|
|
patterns = append(patterns, GetFoodProcessingPatterns()...) // HP300-HP319 food processing
|
|
patterns = append(patterns, GetPackagingPatterns()...) // HP320-HP334 packaging machines
|
|
patterns = append(patterns, GetLaserPatterns()...) // HP335-HP349 laser machines
|
|
patterns = append(patterns, GetMedicalDevicePatterns()...) // HP350-HP364 medical devices (IEC 60601)
|
|
patterns = append(patterns, GetPressureEquipmentPatterns()...) // HP365-HP374 pressure equipment
|
|
patterns = append(patterns, GetConstructionPatterns()...) // HP400-HP419 construction/crane
|
|
patterns = append(patterns, GetForestryConveyorPatterns()...) // HP420-HP450 forestry/conveyor
|
|
patterns = append(patterns, GetPlasticsMetalPatterns()...) // HP500-HP529 plastics + metalworking
|
|
patterns = append(patterns, GetWeldingGlassTextilePatterns()...) // HP530-HP559 welding + glass + textile
|
|
patterns = append(patterns, GetSpecificMachinePatterns()...) // HP730-HP755 pressure/wind/solar/battery
|
|
patterns = append(patterns, GetSpecificMachinePatterns2()...) // HP756-HP784 escalator/pool/playground/fitness/laundry/glass
|
|
patterns = append(patterns, GetCyberExtendedPatterns()...) // HP800-HP829 software faults/cyber-security
|
|
patterns = append(patterns, GetCyberExtendedPatterns2()...) // HP830-HP844 AI-ML specific
|
|
patterns = append(patterns, GetCyberExtendedPatterns3()...) // HP845-HP864 network/communication + HMI
|
|
patterns = append(patterns, GetWorkshopPatterns()...) // HP600-HP664 cross-machine workshop
|
|
patterns = append(patterns, GetMaintenanceExtPatterns()...) // HP700-HP729,HP900-HP934 maintenance lifecycle
|
|
patterns = append(patterns, GetFinalPatternsA()...) // HP1000-HP1084 mechanical body-part variants
|
|
patterns = append(patterns, GetFinalPatternsB()...) // HP1085-HP1169 electrical/thermal/chemical/bio/radiation
|
|
patterns = append(patterns, GetFinalPatternsC()...) // HP1170-HP1254 software/control/org/ergonomic/fire
|
|
patterns = append(patterns, GetFinalPatternsD()...) // HP1255-HP1335 lifecycle/special situations
|
|
patterns = append(patterns, GetCNCHazardPatterns()...) // HP1400-HP1419 CNC/metalworking part 1 (Phase 3)
|
|
patterns = append(patterns, GetCNCHazardPatternsExt()...) // HP1420-HP1434 CNC/metalworking part 2 (Phase 3)
|
|
return &PatternEngine{
|
|
resolver: NewTagResolver(),
|
|
patterns: patterns,
|
|
}
|
|
}
|
|
|
|
// Match executes the pattern-matching algorithm:
|
|
// 1. Resolve component + energy IDs → tags
|
|
// 2. Merge with custom tags
|
|
// 3. Sort patterns by priority (descending)
|
|
// 4. For each pattern: check required_tags (AND) and excluded_tags (NOT)
|
|
// 5. Collect hazards, measures, evidence from fired patterns (deduplicated)
|
|
// 6. Calculate confidence scores
|
|
func (e *PatternEngine) Match(input MatchInput) *MatchOutput {
|
|
// Step 1+2: resolve all tags
|
|
allTags := e.resolver.ResolveTags(input.ComponentLibraryIDs, input.EnergySourceIDs, input.CustomTags)
|
|
|
|
if len(allTags) == 0 {
|
|
return &MatchOutput{ResolvedTags: []string{}}
|
|
}
|
|
|
|
tagSet := toSet(allTags)
|
|
|
|
// Step 3: sort patterns by priority descending
|
|
patterns := make([]HazardPattern, len(e.patterns))
|
|
copy(patterns, e.patterns)
|
|
sort.Slice(patterns, func(i, j int) bool {
|
|
return patterns[i].Priority > patterns[j].Priority
|
|
})
|
|
|
|
// Step 4+5: evaluate each pattern
|
|
var matchedPatterns []PatternMatch
|
|
hazardCatSources := make(map[string][]string) // category → pattern IDs
|
|
measureSources := make(map[string][]string) // measure ID → pattern IDs
|
|
evidenceSources := make(map[string][]string) // evidence ID → pattern IDs
|
|
|
|
for _, p := range patterns {
|
|
if !patternMatches(p, tagSet, input) {
|
|
continue
|
|
}
|
|
|
|
// Collect the tags that contributed + build explainability reasons
|
|
var matchedTags []string
|
|
var reasons []MatchReason
|
|
for _, t := range p.RequiredComponentTags {
|
|
if tagSet[t] {
|
|
matchedTags = append(matchedTags, t)
|
|
reasons = append(reasons, MatchReason{Type: "required_component_tag", Tag: t, Met: true})
|
|
}
|
|
}
|
|
for _, t := range p.RequiredEnergyTags {
|
|
if tagSet[t] {
|
|
matchedTags = append(matchedTags, t)
|
|
reasons = append(reasons, MatchReason{Type: "required_energy_tag", Tag: t, Met: true})
|
|
}
|
|
}
|
|
for _, t := range p.ExcludedComponentTags {
|
|
reasons = append(reasons, MatchReason{Type: "no_exclusion", Tag: t, Met: !tagSet[t]})
|
|
}
|
|
if len(p.RequiredLifecycles) > 0 {
|
|
for _, lc := range p.RequiredLifecycles {
|
|
found := false
|
|
for _, ilc := range input.LifecyclePhases {
|
|
if ilc == lc {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
reasons = append(reasons, MatchReason{Type: "lifecycle_match", Tag: lc, Met: found})
|
|
}
|
|
}
|
|
if len(p.OperationalStates) > 0 {
|
|
stateSet := toSet(input.OperationalStates)
|
|
for _, s := range p.OperationalStates {
|
|
reasons = append(reasons, MatchReason{Type: "operational_state", Tag: s, Met: stateSet[s]})
|
|
}
|
|
}
|
|
if len(p.StateTransitions) > 0 {
|
|
transSet := toSet(input.StateTransitions)
|
|
for _, t := range p.StateTransitions {
|
|
reasons = append(reasons, MatchReason{Type: "state_transition", Tag: t, Met: transSet[t]})
|
|
}
|
|
}
|
|
if len(p.HumanRoles) > 0 {
|
|
roleSet := toSet(input.HumanRoles)
|
|
for _, r := range p.HumanRoles {
|
|
reasons = append(reasons, MatchReason{Type: "human_role", Tag: r, Met: roleSet[r]})
|
|
}
|
|
}
|
|
|
|
matchedPatterns = append(matchedPatterns, PatternMatch{
|
|
PatternID: p.ID,
|
|
PatternName: p.NameDE,
|
|
Priority: p.Priority,
|
|
MatchedTags: matchedTags,
|
|
MatchReasons: reasons,
|
|
ScenarioDE: p.ScenarioDE,
|
|
TriggerDE: p.TriggerDE,
|
|
HarmDE: p.HarmDE,
|
|
AffectedDE: p.AffectedDE,
|
|
ZoneDE: p.ZoneDE,
|
|
DefaultSeverity: p.DefaultSeverity,
|
|
DefaultExposure: p.DefaultExposure,
|
|
HazardCats: p.GeneratedHazardCats,
|
|
ExpertHintDE: p.ExpertHintDE,
|
|
RequiresExpert: p.RequiresExpertCalculation,
|
|
OperationalStates: p.OperationalStates,
|
|
StateTransitions: p.StateTransitions,
|
|
HumanRoles: p.HumanRoles,
|
|
})
|
|
|
|
for _, cat := range p.GeneratedHazardCats {
|
|
hazardCatSources[cat] = appendUnique(hazardCatSources[cat], p.ID)
|
|
}
|
|
for _, mid := range p.SuggestedMeasureIDs {
|
|
measureSources[mid] = appendUnique(measureSources[mid], p.ID)
|
|
}
|
|
for _, eid := range p.SuggestedEvidenceIDs {
|
|
evidenceSources[eid] = appendUnique(evidenceSources[eid], p.ID)
|
|
}
|
|
}
|
|
|
|
// Step 6: build output with confidence scores
|
|
totalPatterns := len(patterns)
|
|
firedCount := len(matchedPatterns)
|
|
|
|
var suggestedHazards []HazardSuggestion
|
|
for cat, sources := range hazardCatSources {
|
|
confidence := float64(len(sources)) / float64(maxInt(firedCount, 1))
|
|
if confidence > 1.0 {
|
|
confidence = 1.0
|
|
}
|
|
suggestedHazards = append(suggestedHazards, HazardSuggestion{
|
|
Category: cat,
|
|
SourcePatterns: sources,
|
|
Confidence: confidence,
|
|
})
|
|
}
|
|
// Sort by confidence descending
|
|
sort.Slice(suggestedHazards, func(i, j int) bool {
|
|
return suggestedHazards[i].Confidence > suggestedHazards[j].Confidence
|
|
})
|
|
|
|
var suggestedMeasures []MeasureSuggestion
|
|
for mid, sources := range measureSources {
|
|
suggestedMeasures = append(suggestedMeasures, MeasureSuggestion{
|
|
MeasureID: mid,
|
|
SourcePatterns: sources,
|
|
})
|
|
}
|
|
sort.Slice(suggestedMeasures, func(i, j int) bool {
|
|
return suggestedMeasures[i].MeasureID < suggestedMeasures[j].MeasureID
|
|
})
|
|
|
|
var suggestedEvidence []EvidenceSuggestion
|
|
for eid, sources := range evidenceSources {
|
|
suggestedEvidence = append(suggestedEvidence, EvidenceSuggestion{
|
|
EvidenceID: eid,
|
|
SourcePatterns: sources,
|
|
})
|
|
}
|
|
sort.Slice(suggestedEvidence, func(i, j int) bool {
|
|
return suggestedEvidence[i].EvidenceID < suggestedEvidence[j].EvidenceID
|
|
})
|
|
|
|
_ = totalPatterns // used conceptually for confidence normalization
|
|
|
|
return &MatchOutput{
|
|
MatchedPatterns: matchedPatterns,
|
|
SuggestedHazards: suggestedHazards,
|
|
SuggestedMeasures: suggestedMeasures,
|
|
SuggestedEvidence: suggestedEvidence,
|
|
ResolvedTags: allTags,
|
|
}
|
|
}
|
|
|
|
// patternMatches checks if a pattern fires given the resolved tag set, lifecycle phases,
|
|
// operational states, and state transitions.
|
|
func patternMatches(p HazardPattern, tagSet map[string]bool, input MatchInput) bool {
|
|
// All required component tags must be present (AND)
|
|
for _, t := range p.RequiredComponentTags {
|
|
if !tagSet[t] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// All required energy tags must be present (AND)
|
|
for _, t := range p.RequiredEnergyTags {
|
|
if !tagSet[t] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// None of the excluded tags may be present (NOT)
|
|
for _, t := range p.ExcludedComponentTags {
|
|
if tagSet[t] {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If pattern requires specific lifecycle phases, at least one must match
|
|
if len(p.RequiredLifecycles) > 0 && len(input.LifecyclePhases) > 0 {
|
|
found := false
|
|
phaseSet := toSet(input.LifecyclePhases)
|
|
for _, lp := range p.RequiredLifecycles {
|
|
if phaseSet[lp] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If pattern requires specific operational states, at least one must match.
|
|
// nil/empty OperationalStates on pattern = fires in ALL states (backwards compatible).
|
|
if len(p.OperationalStates) > 0 && len(input.OperationalStates) > 0 {
|
|
found := false
|
|
stateSet := toSet(input.OperationalStates)
|
|
for _, s := range p.OperationalStates {
|
|
if stateSet[s] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If pattern requires specific state transitions, at least one must match.
|
|
if len(p.StateTransitions) > 0 && len(input.StateTransitions) > 0 {
|
|
found := false
|
|
transSet := toSet(input.StateTransitions)
|
|
for _, t := range p.StateTransitions {
|
|
if transSet[t] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
// If pattern requires specific human roles, at least one must match.
|
|
if len(p.HumanRoles) > 0 && len(input.HumanRoles) > 0 {
|
|
found := false
|
|
roleSet := toSet(input.HumanRoles)
|
|
for _, r := range p.HumanRoles {
|
|
if roleSet[r] {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
if !found {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
}
|
|
|
|
// appendUnique appends val to slice if not already present.
|
|
func appendUnique(slice []string, val string) []string {
|
|
for _, s := range slice {
|
|
if s == val {
|
|
return slice
|
|
}
|
|
}
|
|
return append(slice, val)
|
|
}
|
|
|
|
// maxInt returns the larger of a and b.
|
|
func maxInt(a, b int) int {
|
|
if a > b {
|
|
return a
|
|
}
|
|
return b
|
|
}
|
|
|
|
// ── Operational State Graph constants ──────────────────────────────
|
|
|
|
// Standard operational states for machinery (ISO 12100 + BetrSichV).
|
|
const (
|
|
StateStartup = "startup"
|
|
StateHoming = "homing"
|
|
StateAutomaticOperation = "automatic_operation"
|
|
StateManualOperation = "manual_operation"
|
|
StateTeachMode = "teach_mode"
|
|
StateMaintenance = "maintenance"
|
|
StateCleaning = "cleaning"
|
|
StateEmergencyStop = "emergency_stop"
|
|
StateRecoveryMode = "recovery_mode"
|
|
)
|
|
|
|
// AllOperationalStates returns the 9 standard operational states.
|
|
func AllOperationalStates() []string {
|
|
return []string{
|
|
StateStartup, StateHoming, StateAutomaticOperation,
|
|
StateManualOperation, StateTeachMode, StateMaintenance,
|
|
StateCleaning, StateEmergencyStop, StateRecoveryMode,
|
|
}
|
|
}
|
|
|
|
// StandardStateTransitions returns the valid transitions between states.
|
|
// Format: "from→to". These represent the directed edges of the state graph.
|
|
func StandardStateTransitions() []string {
|
|
return []string{
|
|
"startup→homing",
|
|
"homing→automatic_operation",
|
|
"automatic_operation→manual_operation",
|
|
"manual_operation→automatic_operation",
|
|
"automatic_operation→teach_mode",
|
|
"teach_mode→automatic_operation",
|
|
"automatic_operation→maintenance",
|
|
"manual_operation→maintenance",
|
|
"maintenance→automatic_operation",
|
|
"maintenance→manual_operation",
|
|
"automatic_operation→cleaning",
|
|
"cleaning→automatic_operation",
|
|
"automatic_operation→emergency_stop",
|
|
"manual_operation→emergency_stop",
|
|
"teach_mode→emergency_stop",
|
|
"maintenance→emergency_stop",
|
|
"cleaning→emergency_stop",
|
|
"emergency_stop→recovery_mode",
|
|
"recovery_mode→homing",
|
|
"recovery_mode→manual_operation",
|
|
}
|
|
}
|