Forecast future values of a single sensor tag using ML.NET SSA (Singular Spectrum Analysis). The AI generates the full C# Script Class with time-series prediction engine, connects it to a live tag, creates output tags for forecast values with confidence bounds, and configures model persistence.
| Table of Contents |
|---|
Prerequisites
Category | Items |
|---|---|
Tools |
|
Tables |
|
| Code Block |
|---|
get_table_schema('UnsTags')
|
| Code Block |
|---|
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/Forecast", "DataType": "Double", "Description": "Forecasted value" },
{ "Name": "<AssetPath>/ML/ForecastLower", "DataType": "Double", "Description": "Lower confidence bound" },
{ "Name": "<AssetPath>/ML/ForecastUpper", "DataType": "Double", "Description": "Upper confidence bound" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
Replace <AssetPath> with the actual asset folder path (e.g., Plant/Tank01).
| Warning |
|---|
Critical: The field |
| Code Block |
|---|
"NamespaceDeclarations": "Microsoft.ML;Microsoft.ML.Data;Microsoft.ML.Transforms;Microsoft.ML.Transforms.TimeSeries;Microsoft.ML.Transforms.Text;Microsoft.ML.Trainers;Microsoft.ML.TimeSeries"
|
Always use @Tag. prefix. ML.NET expects float but FrameworX tags use double — always cast with (float) when feeding ML.NET and (double) when writing to tags.
| Code Block | ||
|---|---|---|
| ||
var pipeline = mlContext.Forecasting.ForecastBySsa(
outputColumnName: "ForecastedValues",
inputColumnName: nameof(SensorData.Value),
windowSize: 10,
seriesLength: 100,
trainSize: trainingBuffer.Count,
horizon: 5,
confidenceLevel: 0.95f,
confidenceLowerBoundColumn: "LowerBound",
confidenceUpperBoundColumn: "UpperBound");
|
Output: float[] ForecastedValues, float[] LowerBound, float[] UpperBound
| Code Block | ||
|---|---|---|
| ||
public class SensorData
{
public float Value { get; set; }
}
public class ForecastOutput
{
public float[] ForecastedValues { get; set; }
public float[] LowerBound { get; set; }
public float[] UpperBound { get; set; }
}
private static MLContext mlContext = new MLContext(seed: 0);
private static TimeSeriesPredictionEngine<SensorData, ForecastOutput> forecastEngine;
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static bool modelTrained = false;
private static List<SensorData> trainingBuffer = new List<SensorData>();
private const int MinTrainingSize = 100;
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
public void Predict(double inputValue)
{
trainingBuffer.Add(new SensorData { Value = (float)inputValue });
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction();
}
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
forecastEngine = model.CreateTimeSeriesEngine<SensorData, ForecastOutput>(mlContext);
modelTrained = true;
}
}
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var pipeline = mlContext.Forecasting.ForecastBySsa(
outputColumnName: "ForecastedValues",
inputColumnName: nameof(SensorData.Value),
windowSize: 10,
seriesLength: 100,
trainSize: trainingBuffer.Count,
horizon: 5,
confidenceLevel: 0.95f,
confidenceLowerBoundColumn: "LowerBound",
confidenceUpperBoundColumn: "UpperBound");
model = pipeline.Fit(lastTrainingDataView);
forecastEngine = model.CreateTimeSeriesEngine<SensorData, ForecastOutput>(mlContext);
modelTrained = true;
SaveModel();
}
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
private void RunPrediction()
{
var forecast = forecastEngine.Predict();
@Tag.<AssetPath>/ML/Forecast.Value = (double)forecast.ForecastedValues[0];
@Tag.<AssetPath>/ML/ForecastLower.Value = (double)forecast.LowerBound[0];
@Tag.<AssetPath>/ML/ForecastUpper.Value = (double)forecast.UpperBound[0];
@Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}
|
| Code Block |
|---|
get_table_schema('ScriptsClasses')
|
| Code Block |
|---|
{
"table_type": "ScriptsClasses",
"data": [
{
"Name": "<ClassName>",
"Code": "CSharp",
"Domain": "Server",
"ClassContent": "Methods",
"NamespaceDeclarations": "Microsoft.ML;Microsoft.ML.Data;Microsoft.ML.Transforms;Microsoft.ML.Transforms.TimeSeries;Microsoft.ML.Transforms.Text;Microsoft.ML.Trainers;Microsoft.ML.TimeSeries",
"Contents": "<AI-generated C# code from full class example above>"
}
]
}
|
| Warning |
|---|
Field names matter: the language field is |
For single-input forecasting, an Expression OnChange trigger works well. For periodic forecasting (e.g., every 5 seconds), use a Task.
| Code Block |
|---|
get_table_schema('ScriptsExpressions')
|
| Code Block |
|---|
{
"table_type": "ScriptsExpressions",
"data": [
{
"Name": "ML_Forecast_<SensorName>",
"ObjectName": "",
"Expression": "@Script.Class.<ClassName>.Predict(@Tag.<AssetPath>.<Member>)",
"Execution": "OnChange",
"Trigger": "<AssetPath>"
}
]
}
|
| Warning |
|---|
|
Read the existing ServerStartup task first (document object — read-modify-write), then append:
| Code Block | ||
|---|---|---|
| ||
Script.Class.<ClassName>.LoadModel();
|
MinTrainingSize data points (default 100)LastPrediction timestamp updates and Forecast has reasonable valuesMistake | Why It Happens | How to Avoid |
|---|---|---|
Missing ML.NET namespaces | Used | Always set |
| Missing | Always use |
Non-empty | Void method + tag assignment | Leave |
Used | Field does not exist | Use |
| Build order issue | Set |
| Solution targets .NET 4.8 | Switch to Multiplatform (.NET 8+) |
Wrong data types | ML.NET=float, tags=double | Cast with |
Model lost on restart | SaveModel/LoadModel missing | Always include both + wire LoadModel in ServerStartup |
ForecastEngine null after LoadModel | Forgot to create engine after loading | Call |
| Children Display |
|---|