...
Anomaly Detection outputs
| Code Block |
|---|
|
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/AnomalyScore", "DataType": "Double", "Description": "Anomaly score (0=normal, higher=anomalous)" },
{ "Name": "<AssetPath>/ML/IsAnomaly", "DataType": "Boolean", "Description": "True when anomaly detected" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
Forecasting outputs
| Code Block |
language |
|---|
| json | {
"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" }
]
}
|
Regression outputs
| Code Block |
|---|
|
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/PredictedValue", "DataType": "Double", "Description": "Model predicted value" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
Binary Classification outputs
| Code Block |
|---|
|
{
"table_type": "UnsTags",
"data": [
{ "Name": "<AssetPath>/ML/PredictedLabel", "DataType": "Boolean", "Description": "Predicted outcome (true/false)" },
{ "Name": "<AssetPath>/ML/Probability", "DataType": "Double", "Description": "Prediction probability (0-1)" },
{ "Name": "<AssetPath>/ML/LastPrediction", "DataType": "DateTime", "Description": "Timestamp of last prediction" }
]
}
|
...
Every ML Script Class follows this structure — always include persistence and LoadModel. No stripped-down versions.
| Code Block |
|---|
|
// 1. Data classes — define input/output schemas for ML.NET
public class SensorData
{
public float Value { get; set; }
}
public class PredictionResult
{
// Fields vary by ML task (see task-specific examples below)
}
// 2. Static fields — MLContext and model persist across calls
private static MLContext mlContext = new MLContext(seed: 0);
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static bool modelTrained = false;
// 3. Training buffer — collects data until enough for training
private static List<SensorData> trainingBuffer = new List<SensorData>();
private const int MinTrainingSize = 100; // adjust per task
// 4. Model path — persisted to solution execution folder
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
// 5. Public entry method — called from Expression or Task
public void Predict(double inputValue)
{
trainingBuffer.Add(new SensorData { Value = (float)inputValue });
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction(inputValue);
}
// 6. LoadModel — called from ServerStartup to reload persisted model
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
modelTrained = true;
}
}
// 7. TrainModel — build, fit, and persist the ML pipeline
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
// pipeline.Fit() call here — see task-specific examples below
model = pipeline.Fit(lastTrainingDataView);
modelTrained = true;
SaveModel();
}
// 8. SaveModel — persist to disk after training
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
// 9. RunPrediction — transform input and write to output tags
private void RunPrediction(double inputValue) { /* ... */ }
|
...
Always use the @Tag. prefix to read or write tag values:
| Code Block |
|---|
language | csharp |
|---|
// Read from a tag
double temp = @Tag.Plant/Reactor1/Temperature.Value;
// Write to a tag
@Tag.Plant/Reactor1/ML/AnomalyScore.Value = score;
@Tag.Plant/Reactor1/ML/IsAnomaly.Value = true;
@Tag.Plant/Reactor1/ML/LastPrediction.Value = DateTime.Now;
|
...
Write the complete class with write_objects. The AI generates the full C# code based on the ML task chosen in Step 0.
| Code Block |
|---|
language | json |
|---|
{
"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 following the class structure pattern>"
}
]
}
|
...
SSA Spike Detection pipeline
| Code Block |
|---|
|
var pipeline = mlContext.Transforms.DetectSpikeBySsa(
outputColumnName: "Prediction",
inputColumnName: nameof(SensorData.Value),
confidence: 95.0,
pvalueHistoryLength: 10,
trainingWindowSize: 100,
seasonalityWindowSize: 10);
|
...
SSA Change Point Detection pipeline
| Code Block |
|---|
|
var pipeline = mlContext.Transforms.DetectChangePointBySsa(
outputColumnName: "Prediction",
inputColumnName: nameof(SensorData.Value),
confidence: 95.0,
changeHistoryLength: 10,
trainingWindowSize: 100,
seasonalityWindowSize: 10);
|
...
Full class example — Anomaly Detection (Spike)
| Code Block |
|---|
|
public class SensorData
{
public float Value { get; set; }
}
public class SpikePrediction
{
[VectorType(3)]
public double[] Prediction { get; set; }
}
private static MLContext mlContext = new MLContext(seed: 0);
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 _);
modelTrained = true;
}
}
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var pipeline = mlContext.Transforms.DetectSpikeBySsa(
outputColumnName: "Prediction",
inputColumnName: nameof(SensorData.Value),
confidence: 95.0,
pvalueHistoryLength: 10,
trainingWindowSize: 100,
seasonalityWindowSize: 10);
model = pipeline.Fit(lastTrainingDataView);
modelTrained = true;
SaveModel();
}
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
private void RunPrediction()
{
var dataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var transformed = model.Transform(dataView);
var predictions = mlContext.Data.CreateEnumerable<SpikePrediction>(transformed, reuseRowObject: false).ToList();
var latest = predictions.Last();
@Tag.<AssetPath>/ML/IsAnomaly.Value = latest.Prediction[0] == 1;
@Tag.<AssetPath>/ML/AnomalyScore.Value = latest.Prediction[1];
@Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}
|
...
Full class example — Binary Classification
| Code Block |
|---|
|
public class ProcessData
{
public float Feature1 { get; set; } // e.g., Vibration
public float Feature2 { get; set; } // e.g., Temperature
public float Feature3 { get; set; } // e.g., Current
public bool Label { get; set; } // e.g., DidFault (true/false)
}
public class ClassificationPrediction
{
public bool PredictedLabel { get; set; }
public float Score { get; set; }
public float Probability { get; set; }
}
private static MLContext mlContext = new MLContext(seed: 0);
private static ITransformer model;
private static IDataView lastTrainingDataView;
private static PredictionEngine<ProcessData, ClassificationPrediction> predictionEngine;
private static bool modelTrained = false;
private static List<ProcessData> trainingBuffer = new List<ProcessData>();
private const int MinTrainingSize = 200;
private static readonly string ModelPath = Path.Combine(@Info.GetExecutionPath(), "<ClassName>.mlnet");
public void Predict(double input1, double input2, double input3, bool label)
{
trainingBuffer.Add(new ProcessData
{
Feature1 = (float)input1,
Feature2 = (float)input2,
Feature3 = (float)input3,
Label = label
});
if (!modelTrained && trainingBuffer.Count >= MinTrainingSize)
TrainModel();
if (modelTrained)
RunPrediction(input1, input2, input3);
}
public void LoadModel()
{
if (File.Exists(ModelPath))
{
model = mlContext.Model.Load(ModelPath, out _);
predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, ClassificationPrediction>(model);
modelTrained = true;
}
}
private void TrainModel()
{
lastTrainingDataView = mlContext.Data.LoadFromEnumerable(trainingBuffer);
var pipeline = mlContext.Transforms.Concatenate("Features",
nameof(ProcessData.Feature1),
nameof(ProcessData.Feature2),
nameof(ProcessData.Feature3))
.Append(mlContext.BinaryClassification.Trainers.FastTree(
labelColumnName: "Label",
featureColumnName: "Features"));
model = pipeline.Fit(lastTrainingDataView);
predictionEngine = mlContext.Model.CreatePredictionEngine<ProcessData, ClassificationPrediction>(model);
modelTrained = true;
SaveModel();
}
private void SaveModel()
{
mlContext.Model.Save(model, lastTrainingDataView.Schema, ModelPath);
}
private void RunPrediction(double input1, double input2, double input3)
{
var input = new ProcessData
{
Feature1 = (float)input1,
Feature2 = (float)input2,
Feature3 = (float)input3
};
var result = predictionEngine.Predict(input);
@Tag.<AssetPath>/ML/PredictedLabel.Value = result.PredictedLabel;
@Tag.<AssetPath>/ML/Probability.Value = (double)result.Probability;
@Tag.<AssetPath>/ML/LastPrediction.Value = DateTime.Now;
}
|
...
| Code Block |
|---|
get_table_schema('ScriptsExpressions')
|
| Code Block |
|---|
language | json |
|---|
{
"table_type": "ScriptsExpressions",
"data": [
{
"Name": "ML_Predict_<SensorName>",
"ObjectName": "",
"Expression": "@Script.Class.<ClassName>.Predict(@Tag.<AssetPath>.<Member>)",
"Execution": "OnChange",
"Trigger": "<AssetPath>"
}
]
}
|
...