Versions Compared

Key

  • This line was added.
  • This line was removed.
  • Formatting was changed.

How to build an ML Model.

Tutorials →

Edge AI with ML.NET Anomaly Detection Tutorial

Scripts → Tutorial | Concept | How-to Guide | Reference


Overview

This Tutorial Teaches you to:

This tutorial demonstrates

using ML.NET 4.0

how to use machine learning for real-time anomaly detection on sensor data using FrameworX Script Classes

and displays

.

Prerequisites

  • FrameworX with Scripts module
  • ML.NET 4.0 references (included in FrameworX)
  • Basic understanding of tags and displays

:

Table of Contents
maxLevel2
minLevel2
indent10px
excludeSteps
stylenone


Step 1: Create

Monitoring Tags
  • Navigate to Unified Namespace → Tags
  • Create the following tags:
  • Tag.MixerVibration (Double) - Simulated sensor value
  • Tag.Anomalies (String) - Will hold the anomaly list
  • Tag.SimulatorSeed (Integer) - For simulation consistency

    Value Simulator

    1. With these tags created: Pressure (Integer) and AnomalyBuffer (Text Array 9 position)

    2. In Devices → Protocols, select the Value Simulator and click the "New Channel..." button.

    3. In Devices → Points, create points to generate simulated data.

    TagName

    Node

    Address

    DataType

    AccessType

    Tag.Pressure

    Node.ValueSimulator1Node1

    INTEGER:0,100,1

    Native

    AccessType.Read

    For more information about the Value Simulator, see: Value Simulator Connector

    Step 2: Create ML Anomaly Detection Script Class

    1. Navigate to Scripts → Classes

    2. Click

    "Create new script"Configure
    1. the

    new script:
    • Name: ML_Anomaly
    • Language: CSharp
    • Domain: Server
    • Select "Create new Code"Methods (Default)
    • ? Check "Add ML.NET namespaces" (Required for ML functionality)
  • Click OK
  • Replace the default code with:
    1. "Create a New Class" button

    2. In "Import code from Library:", select AnomalyML

    3. Open the script and uncomment the line that returns the detection to the AnomalyBuffer tag in Check() method.

    Step 3: Create an Expression

    This expression will check for anomalies each time the tag value changes.

    1. Go to Scripts → Expressions

    2. Create the following expression:

    ObjectName

    Expression

    Execution


    Script.Class.AnomalyML.Check(<DesiredTag>)

    OnChange

    Where:

    • <DesiredTag> is the tag you want to monitor for anomalies

    Example:

    ObjectName

    Expression

    Execution


    Script.Class.AnomalyML.Check(Tag.Pressure)

    OnChange

    Step 4: Test the System

    1. Go in Runtime → “Run Startup”

    2. Wait a couple minutes to have some data in the model.

    3. Open the PropertyWatch

    4. See the values in the AnomalyBuffer, to see the predictions.

    csharp

    // HOW TO USE
    // Create an expression or Task to run periodically with the code: 
    // @Script.Class.ML_Anomaly.Check(@Tag.YourSensorValue);
    // To retrieve anomalies: var list = @Script.Class.ML_Anomaly.GetAnomalies();
    
    // Check if value is anomalous
    public void Check(double currentValue) {
        // Initialize ML engine if needed
        if (_engine == null)
            Initialize();
        
        // Create data point
        var data = new SensorData {
            Timestamp = DateTime.Now,
            SensorValue = currentValue
        };
        
        // Get prediction from ML model
        AnomalyPrediction prediction = _engine.Predict(data);
        
        // Store anomaly if detected
        if (prediction.Prediction[0] >= 1) {  // >= 1 means anomaly detected
            if (_anomalyBuffer.Count >= 10)
                _anomalyBuffer.Dequeue();
            _anomalyBuffer.Enqueue($"{DateTime.Now:yyyy-MM-dd HH:mm:ss}|{currentValue:F3}|{prediction.Prediction[1]:F2}");
        }
    }
    
    // Internal buffer to store last 10 anomalies
    private static Queue<string> _anomalyBuffer = new Queue<string>(10);
    
    // Method to retrieve anomalies and clear buffer
    public string GetAnomalies() {
        if (_anomalyBuffer.Count == 0)
            return "No new anomalies detected";
        
        var anomalies = string.Join(Environment.NewLine, _anomalyBuffer);
        _anomalyBuffer.Clear();
        return anomalies;
    }
    
    //The following code is standard - no changes needed
    public class SensorData {
        public DateTime Timestamp { get; set; }
        public double SensorValue { get; set; }
    }
    
    public class AnomalyPrediction {
        // SR-CNN returns an array with 3 doubles:
        // [0] = flag (0 or 1) - Binary indicator if anomaly detected  
        // [1] = score (0.0 to 1.0) - Confidence/severity of the anomaly
        // [2] = baseline - Expected normal value at this point
        // Note: This tutorial uses [0] for simplicity. Advanced users can leverage [1] and [2] for custom thresholds.
        [VectorType(3)]
        public double[] Prediction { get; set; }
    }
    
    private ITransformer _model;
    private TimeSeriesPredictionEngine<SensorData, AnomalyPrediction> _engine;
    private MLContext _mlContext;
    List<SensorData> dataTrain;
    
    private void Initialize() {
        _mlContext = new MLContext();
        
        if (dataTrain == null) {
            DateTime now = DateTime.Now;
            
            // Training data with clear normal pattern (wave-like)
            var trainingData = new List<(int minutesAgo, double value)> {
                (15, 0.20), (14, 0.25), (13, 0.30), (12, 0.25),  // Rising
                (11, 0.20), (10, 0.15), (9, 0.20), (8, 0.25),   // Valley to peak
                (7, 0.30), (6, 0.25), (5, 0.20), (4, 0.15),    // Falling
                (3, 0.20), (2, 0.25), (1, 0.30), (0, 0.25)     // Rising
            };
            
            dataTrain = new List<SensorData>();
            foreach (var item in trainingData) {
                dataTrain.Add(new SensorData {
                    Timestamp = now.AddMinutes(-item.minutesAgo),
                    SensorValue = item.value
                });
            }
        }
        
        // SR-CNN in ML.NET - use these default values for most applications
        SrCnnAnomalyEstimator pipeline = _mlContext.Transforms
            .DetectAnomalyBySrCnn(
                outputColumnName: nameof(AnomalyPrediction.Prediction),
                inputColumnName: nameof(SensorData.SensorValue),
                windowSize: 32,
                backAddWindowSize: 32,
                lookaheadWindowSize: 16,
                averagingWindowSize: 16,
                judgementWindowSize: 16,
                threshold: 0.15);
        
        IDataView dataView = _mlContext.Data.LoadFromEnumerable(dataTrain);
        _model = pipeline.Fit(dataView);
        _engine = _model.CreateTimeSeriesEngine<SensorData, AnomalyPrediction>(_mlContext);
        
        // Train the model with initial data
        foreach (var row in dataTrain) {
            _engine.Predict(row);
        }
    }

    Step 3: Create Value Simulator

    1. Go to Scripts → Tasks
    2. Create task: SimulateVibration
    3. Set trigger: Period = 500ms
    4. Add code:

    csharp

    // Simulate normal sensor data with occasional anomalies
    if (@Tag.SimulatorSeed == 0)
        @Tag.SimulatorSeed = Environment.TickCount;
    
    Random rand = new Random(@Tag.SimulatorSeed + DateTime.Now.Millisecond);
    
    // Base vibration level with sine wave pattern
    double time = DateTime.Now.Ticks / 10000000.0;
    double baseValue = 0.25 + 0.05 * Math.Sin(time);
    double noise = (rand.NextDouble() - 0.5) * 0.02;
    
    // 5% chance of anomaly
    if (rand.NextDouble() < 0.05) {
        @Tag.MixerVibration = baseValue + (rand.NextDouble() * 0.5 + 0.3); // Spike
    } else {
        @Tag.MixerVibration = baseValue + noise; // Normal
    }

    Step 4: Create Monitoring Task

    1. Go to Scripts → Tasks
    2. Create task: MonitorVibration
    3. Set trigger: Period = 1000ms (1 second)
    4. Add code:

    csharp

    // Call the ML anomaly detection
    @Script.Class.ML_Anomaly.Check(@Tag.MixerVibration);

    Step 5: Create Display Components

    1. Open Displays → Create New Display
    2. Add a TextBox for Results:
      • Add a TextBox control
      • Configure in Settings tab:
        • Label Text: Last 10 anomalies
        • Linked Value: Tag.Anomalies
        • Binding: OutputOnly
        • ? Enable Multiline
    3. Add a Refresh Button:
      • Add a Button control
      • Label: Refresh
      • In the Dynamics tab, add an Action:
        • Event: MouseLeftButtonDown
        • Action: RunExpressions
        • Result (optional): Tag.Anomalies
        • Expression: Script.Class.ML_Anomaly.GetAnomalies()

    Step 6: Test the System

    1. Start Runtime (F5)
    2. Open your display
    3. Observe the simulated vibration values
    4. Click "Refresh" button periodically
    5. The TextBox will display:
      • "No new anomalies detected" when buffer is empty
      • List of anomalies with format: timestamp|value|severity

    Example output:

    2025-01-15 10:23:45|0.895|0.82
    2025-01-15 10:24:12|0.756|0.91
    2025-01-15 10:25:33|0.923|0.75

    How It Works

    1. SimulateVibration generates realistic sensor data with 5% anomalies
    2. MonitorVibration checks each value using ML model
    3. ML_Anomaly class uses SR-CNN algorithm to detect spikes
    4. Anomalies are buffered internally (max 10)
    5. Refresh button retrieves and clears the buffer

    Key Learning Points

    ? ML.NET Integration: Must enable ML.NET namespaces when creating script class
    ? Simple API: Single method call Check() for detection
    ? Self-contained: No external tags needed for ML state
    ? Production Ready: Can replace simulator with real sensor tags

    Next Steps

    • Connect real sensor data instead of simulator
    • Adjust SR-CNN threshold (0.15) for sensitivity
    • Crate alarms for critical anomalies
    • Store anomaly history in Historian
    • Create trending displays for pattern analysis
    This completes the ML.NET anomaly detection implementation in FrameworX!

    In this section...

    Page Tree
    root@parent