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
100 changes: 100 additions & 0 deletions src/AR.Iec61850/Discovery/CdcInferenceEngine.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,43 @@ public static CdcInferenceResult Infer(
string dataObjectName,
IReadOnlyCollection<string> attributePaths,
IReadOnlyCollection<string> functionalConstraints)
=> InferCore(
logicalNodeClass,
dataObjectName,
attributePaths,
functionalConstraints,
Array.Empty<LiveIedDataAttributeModel>());

/// <summary>
/// Type-authoritative overload used by live discovery when exact MMS
/// TypeSpecification evidence is available for DataAttributes.
/// </summary>
public static CdcInferenceResult Infer(
string logicalNodeClass,
string dataObjectName,
IReadOnlyCollection<LiveIedDataAttributeModel> attributes)
{
ArgumentNullException.ThrowIfNull(attributes);
return InferCore(
logicalNodeClass,
dataObjectName,
attributes
.Select(attribute => attribute.AttributePath)
.Where(path => !string.IsNullOrWhiteSpace(path))
.ToArray(),
attributes
.Select(attribute => attribute.FunctionalConstraint)
.Where(fc => !string.IsNullOrWhiteSpace(fc))
.ToArray(),
attributes);
}

private static CdcInferenceResult InferCore(
string logicalNodeClass,
string dataObjectName,
IReadOnlyCollection<string> attributePaths,
IReadOnlyCollection<string> functionalConstraints,
IReadOnlyCollection<LiveIedDataAttributeModel> typedAttributes)
{
var evidence = new List<string>();
var doName = dataObjectName.Trim();
Expand Down Expand Up @@ -93,6 +130,9 @@ public static CdcInferenceResult Infer(

if (ContainsAny(attrs, "stVal") && ContainsAny(attrs, "q") && ContainsAny(attrs, "t"))
{
if (TryInferTypedStatusCdc(typedAttributes, out var typedStatus))
return typedStatus;

evidence.Add("contains stVal/q/t status triplet");
if (IsProtectionOperation(lnClass, doName))
return Result("ACT", 0.86, evidence.Append("protection LN operation/status DO pattern"));
Expand Down Expand Up @@ -133,6 +173,66 @@ public static CdcInferenceResult Infer(
return Result(string.Empty, 0.0, evidence.Append("CDC cannot be inferred from current online discovery only"));
}

private static bool TryInferTypedStatusCdc(
IReadOnlyCollection<LiveIedDataAttributeModel> attributes,
out CdcInferenceResult result)
{
result = new CdcInferenceResult();
if (attributes.Count == 0)
return false;

var stVal = attributes.FirstOrDefault(attribute =>
string.Equals(
(attribute.AttributePath ?? string.Empty).Trim(),
"stVal",
StringComparison.OrdinalIgnoreCase));
if (stVal is null ||
stVal.TypeConfidence != LiveIedDiscoveryConfidenceLevel.Exact)
{
return false;
}

var declaredType = FirstNonEmpty(stVal.SclBType, stVal.MmsType)
.Replace("_", string.Empty, StringComparison.Ordinal)
.Replace("-", string.Empty, StringComparison.Ordinal)
.Replace(" ", string.Empty, StringComparison.Ordinal)
.ToUpperInvariant();

if (declaredType is "BOOLEAN" or "BOOL")
{
result = Result(
"SPS",
0.98,
new[] { "exact stVal type is BOOLEAN" });
return true;
}

if (declaredType.StartsWith("INT", StringComparison.Ordinal) ||
declaredType.StartsWith("UINT", StringComparison.Ordinal) ||
declaredType is "INTEGER" or "SIGNED" or "UNSIGNED")
{
result = Result(
"INS",
0.98,
new[] { $"exact stVal type is {declaredType}" });
return true;
}

if (declaredType.StartsWith("ENUM", StringComparison.Ordinal))
{
result = Result(
"ENS",
0.98,
new[] { $"exact stVal type is {declaredType}" });
return true;
}

return false;
}

private static string FirstNonEmpty(params string?[] values)
=> values.FirstOrDefault(value => !string.IsNullOrWhiteSpace(value))?.Trim() ?? string.Empty;

private static bool IsProtectionOperation(string lnClass, string dataObjectName)
=> string.Equals(dataObjectName, "Op", StringComparison.OrdinalIgnoreCase) &&
((lnClass.Length > 0 && lnClass[0] == 'P') || string.Equals(lnClass, "RREC", StringComparison.OrdinalIgnoreCase));
Expand Down
4 changes: 2 additions & 2 deletions src/AR.Iec61850/Discovery/Iec61850StandardModelRegistry.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,13 +51,13 @@ public static class Iec61850StandardModelRegistry
[Key("MMXU", "PF")] = Def("MMXU", "PF", "WYE", 0.84, "power factor"),

// Harmonic measurements use phase-group CDCs. Physical AA1E1F06R4
// plus IEDScout SCL prove that MHAI ThdA / ThdPhV are WYE objects whose
// plus independent interoperability SCL prove that MHAI ThdA / ThdPhV are WYE objects whose
// phase children are CMV SDOs, not ACD indication structures.
[Key("MHAI", "ThdA")] = Def("MHAI", "ThdA", "WYE", 0.98, "three-phase current THD"),
[Key("MHAI", "ThdPhV")] = Def("MHAI", "ThdPhV", "WYE", 0.98, "three-phase voltage THD"),

// Instrument-transformer semantics are standardized and were also observed in
// the AA1E1F06R4 IEDScout engineering model. Exact registry entries prevent
// the AA1E1F06R4 independent interoperability engineering model. Exact registry entries prevent
// live discovery from dropping these configuration DOs when attribute-name
// heuristics alone cannot infer their CDC safely.
[Key("TCTR", "Mod")] = Def("TCTR", "Mod", "ENC", 0.98, "mode control"),
Expand Down
4 changes: 1 addition & 3 deletions src/AR.Iec61850/Discovery/LiveIedModelDiscoveryBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -213,9 +213,7 @@ private static IEnumerable<LiveIedDataObjectModel> BuildDataObjects(
.ThenBy(x => x.FunctionalConstraint, StringComparer.OrdinalIgnoreCase)
.ThenBy(x => x.AttributePath, StringComparer.OrdinalIgnoreCase)
.ToArray();
var attrPaths = attributes.Select(x => x.AttributePath).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
var fcs = group.Select(x => x.FunctionalConstraint).Where(x => !string.IsNullOrWhiteSpace(x)).ToArray();
var cdc = CdcInferenceEngine.Infer(parsedLn.SclLnClass, group.Key, attrPaths, fcs);
var cdc = CdcInferenceEngine.Infer(parsedLn.SclLnClass, group.Key, attributes);
var reference = $"{ln.Domain}/{ln.Name}.{group.Key}";
yield return new LiveIedDataObjectModel
{
Expand Down
52 changes: 51 additions & 1 deletion src/AR.Iec61850/Scl/Export/LiveIedSclExporter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -742,7 +742,7 @@ private static bool IsEdition2ServiceTrackingCdc(string cdc)

private static XElement BuildDoType(LiveIedDataObjectModel dataObject, string doTypeId, LiveIedSclBuildContext context, IReadOnlyCollection<LiveIedDataAttributeModel>? exportAttributes = null, string logicalNodeClass = "")
{
var cdc = dataObject.InferredCdc.Trim();
var cdc = ResolveExportCdc(dataObject, logicalNodeClass, context);
if (!CdcInferenceEngine.IsKnownCdc(cdc))
{
cdc = "SPS";
Expand Down Expand Up @@ -803,6 +803,56 @@ private static XElement BuildDoType(LiveIedDataObjectModel dataObject, string do
return doType;
}

private static string ResolveExportCdc(
LiveIedDataObjectModel dataObject,
string logicalNodeClass,
LiveIedSclBuildContext context)
{
var declaredCdc = (dataObject.InferredCdc ?? string.Empty).Trim();

// Export is the last semantic boundary before the model becomes portable SCL.
// Exact live TypeSpecification evidence for stVal must not be downgraded by a
// stale conservative CDC that may have been inferred before typed evidence was
// available. Re-run the typed CDC inference only for exact LIVE stVal evidence;
// an opened SCL's explicit DOType@cdc remains its own engineering authority.
var exactLiveStVal = dataObject.Attributes.FirstOrDefault(attribute =>
string.Equals(
(attribute.AttributePath ?? string.Empty).Trim(),
"stVal",
StringComparison.OrdinalIgnoreCase) &&
attribute.TypeConfidence == LiveIedDiscoveryConfidenceLevel.Exact &&
!string.Equals(
(attribute.TypeSource ?? string.Empty).Trim(),
"SCL",
StringComparison.OrdinalIgnoreCase));

if (exactLiveStVal is null)
return declaredCdc;

var typed = CdcInferenceEngine.Infer(
logicalNodeClass,
dataObject.Name,
dataObject.Attributes);

if (!CdcInferenceEngine.IsKnownCdc(typed.Cdc) ||
typed.Confidence < 0.95 ||
string.Equals(typed.Cdc, declaredCdc, StringComparison.OrdinalIgnoreCase))
{
return declaredCdc;
}

context.Warnings.Add(new LiveIedSclExportWarning
{
Code = "ExactLiveTypeCdcAuthority",
Reference = dataObject.Reference,
Message =
$"Exact live stVal TypeSpecification corrected exported CDC from " +
$"'{declaredCdc}' to '{typed.Cdc}'."
});

return typed.Cdc;
}

private static XElement BuildSubDataObjectType(
TypeTreeNode node,
string doTypeId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
using AR.Iec61850.Discovery;

namespace AR.Iec61850.Tests.Discovery;

public sealed class CdcInferenceTypeAuthorityTests
{
[Fact]
public void ExactIntegerStatusType_UsesInsInsteadOfBooleanShapeGuess()
{
var attributes = StatusTriplet("INT32", "integer");

var result = CdcInferenceEngine.Infer("GGIO", "CBClsCounter", attributes);

Assert.Equal("INS", result.Cdc);
Assert.Equal(LiveIedDiscoveryConfidenceLevel.High, result.Level);
Assert.Contains(result.Evidence, item =>
item.Contains("exact stVal type", StringComparison.OrdinalIgnoreCase));
}

[Fact]
public void ExactBooleanStatusType_UsesSps()
{
var attributes = StatusTriplet("BOOLEAN", "boolean");

var result = CdcInferenceEngine.Infer("GGIO", "BinaryStatus", attributes);

Assert.Equal("SPS", result.Cdc);
Assert.Equal(LiveIedDiscoveryConfidenceLevel.High, result.Level);
}

[Fact]
public void HeuristicIntegerLabel_DoesNotOverrideExistingConservativeStatusInference()
{
var attributes = StatusTriplet(
"INT32",
"integer",
LiveIedDiscoveryConfidenceLevel.Low);

var result = CdcInferenceEngine.Infer("GGIO", "UnprovenStatus", attributes);

Assert.Equal("SPS", result.Cdc);
}

private static LiveIedDataAttributeModel[] StatusTriplet(
string sclBType,
string mmsType,
LiveIedDiscoveryConfidenceLevel confidence = LiveIedDiscoveryConfidenceLevel.Exact)
=>
[
new LiveIedDataAttributeModel
{
AttributePath = "stVal",
FunctionalConstraint = "ST",
SclBType = sclBType,
MmsType = mmsType,
TypeConfidence = confidence,
TypeDiscoveryStatus = confidence == LiveIedDiscoveryConfidenceLevel.Exact ? "Exact" : "NotRead"
},
new LiveIedDataAttributeModel
{
AttributePath = "q",
FunctionalConstraint = "ST",
SclBType = "Quality",
MmsType = "bit-string",
TypeConfidence = confidence
},
new LiveIedDataAttributeModel
{
AttributePath = "t",
FunctionalConstraint = "ST",
SclBType = "Timestamp",
MmsType = "utc-time",
TypeConfidence = confidence
}
];
}
Loading