From baca029e23fa27abbc62976ff7b32321f5a38686 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Steinar=20Elgs=C3=A6ter?= Date: Wed, 4 Dec 2024 13:13:40 +0100 Subject: [PATCH] Refactoring ClosedLoopIdentifier ahead of rewrite. --- .../ClosedLoopUnitIdentifier.cs | 72 +++-- .../Identification/DisturbanceIdentifier.cs | 289 ++++++++---------- Dynamic/Identification/UnitIdentifier.cs | 7 +- Dynamic/PlantSimulator/PlantSimulator.cs | 30 +- TimeSeriesAnalysis/TimeSeriesDataSet.cs | 113 ++++--- 5 files changed, 273 insertions(+), 238 deletions(-) diff --git a/Dynamic/Identification/ClosedLoopUnitIdentifier.cs b/Dynamic/Identification/ClosedLoopUnitIdentifier.cs index 5425f53..a4416c1 100644 --- a/Dynamic/Identification/ClosedLoopUnitIdentifier.cs +++ b/Dynamic/Identification/ClosedLoopUnitIdentifier.cs @@ -108,8 +108,8 @@ public static (UnitModel, double[]) Identify(UnitDataSet dataSet, PidParameters double[] u0 = SignificantDigits.Format(dataSet.U.GetRow(0), nDigits); double y0 = dataSet.Y_meas[0]; bool isOK; - var dataSet1 = new UnitDataSet(dataSet); - var dataSet2 = new UnitDataSet(dataSet); + var dataSetRun1 = new UnitDataSet(dataSet); + var dataSetRun2 = new UnitDataSet(dataSet); FittingSpecs fittingSpecs = new FittingSpecs(); fittingSpecs.u0 = u0; @@ -123,18 +123,17 @@ public static (UnitModel, double[]) Identify(UnitDataSet dataSet, PidParameters { ///////////////// - // step1: + // run1, step1: DisturbanceIdResult distIdResult1 = DisturbanceIdentifier.EstimateDisturbance - (dataSet1, null, pidInputIdx, pidParams); + (dataSetRun1, null, pidInputIdx, pidParams); - dataSet1.D = distIdResult1.d_est; - var unitModel_run1 = UnitIdentifier.IdentifyLinearAndStatic(ref dataSet1, fittingSpecs, doTimeDelayEstOnRun1); - // nGains = unitModel_run1.modelParameters.GetProcessGains().Length; + dataSetRun1.D = distIdResult1.d_est; + var unitModel_run1 = UnitIdentifier.IdentifyLinearAndStatic(ref dataSetRun1, fittingSpecs, doTimeDelayEstOnRun1); idDisturbancesList.Add(distIdResult1); idUnitModelsList.Add(unitModel_run1); - isOK = ClosedLoopSim(dataSet1, unitModel_run1.GetModelParameters(), pidParams, distIdResult1.d_est, "run1"); + isOK = ClosedLoopSim(dataSetRun1, unitModel_run1.GetModelParameters(), pidParams, distIdResult1.d_est, "run1"); - // "step2" global search" for linear pid-gainsgains + // run1, "step2" : "global search" for linear pid-gainsgains if(unitModel_run1.modelParameters.GetProcessGains()!= null) { wasGainGlobalSearchDone = true; @@ -152,9 +151,7 @@ public static (UnitModel, double[]) Identify(UnitDataSet dataSet, PidParameters var max_gain = Math.Abs(pidProcessInputInitalGainEstimate * initalGuessFactor_higherbound); var min_gain = - max_gain; - // when debugging, it might be advantageous to set min_gain equal to the known true value - //TODO:Remove!!! - // min_gain = 0; + // min_gain = 0; // when debugging, it might be advantageous to set min_gain equal to the known true value // first pass(wider grid with larger grid size) var retPass1 = GlobalSearchLinearPidGain(dataSet, pidParams, pidInputIdx, @@ -181,43 +178,48 @@ public static (UnitModel, double[]) Identify(UnitDataSet dataSet, PidParameters } } } - bool doStep3 = true; - // ---------------- - // - issue is that after run 4 modelled output does not match measurement. - // - the reason that we want this run is that after run3 the time constant and + // ---------------- + // - issue is that after run 1 modelled output does not match measurement. + // - the reason that we want this run is that after run1 the time constant and // time delays are way off if the process is excited mainly by disturbances. - // - the reason that we cannot do run4 immediately, is that that formulation + // - the reason that we cannot do run2 immediately, is that that formulation // does not appear to give a solution if the guess disturbance vector is bad. + const bool doRun2 = true; + const double LARGEST_TIME_CONSTANT_TO_CONSIDER_TIMEBASE_MULTIPLE = 30;//too low + // run2: do a run where it is no longer assumed that x[k-1] = y[k], // this run has the best chance of estimating correct time constants, but it requires a good inital guess of d - if (doStep3 && idUnitModelsList.Last() != null) + + // run 2 version 1: try looking for the time constant that gives the smallest average disturbance amplitude + + if (doRun2 && idUnitModelsList.Last() != null) { var model = idUnitModelsList.Last(); - DisturbanceIdResult distIdResult2 = DisturbanceIdentifier.EstimateDisturbance - (dataSet2, model, pidInputIdx, pidParams); - List estDisturbances = new List(); - List distDevs = new List(); + var distIdResult2 = DisturbanceIdentifier.EstimateDisturbance + (dataSetRun2, model, pidInputIdx, pidParams); + var estDisturbances = new List(); + var distDevs = new List(); estDisturbances.Add(distIdResult2.d_est); - var timeBase = dataSet2.GetTimeBase(); + var timeBase = dataSetRun2.GetTimeBase(); double candiateTc_s = 0; bool curDevIsDecreasing = true; double firstDev = vec.Sum(vec.Abs(vec.Diff(distIdResult2.d_est))).Value; distDevs.Add(firstDev); - while (candiateTc_s < 30 * timeBase && curDevIsDecreasing) + while (candiateTc_s < LARGEST_TIME_CONSTANT_TO_CONSIDER_TIMEBASE_MULTIPLE * timeBase && curDevIsDecreasing) { candiateTc_s += timeBase; var newParams = model.GetModelParameters().CreateCopy(); newParams.TimeConstant_s = candiateTc_s; var newModel = new UnitModel(newParams); - DisturbanceIdResult distIdResult_Test = DisturbanceIdentifier.EstimateDisturbance - (dataSet2, newModel,pidInputIdx,pidParams); + var distIdResult_Test = DisturbanceIdentifier.EstimateDisturbance + (dataSetRun2, newModel,pidInputIdx,pidParams); estDisturbances.Add(distIdResult_Test.d_est); - double curDev = vec.Sum(vec.Abs(vec.Diff(distIdResult_Test.d_est))).Value; + var curDev = vec.Sum(vec.Abs(vec.Diff(distIdResult_Test.d_est))).Value; if (curDev < distDevs.Last()) curDevIsDecreasing = true; else @@ -235,11 +237,23 @@ public static (UnitModel, double[]) Identify(UnitDataSet dataSet, PidParameters step2params.TimeConstant_s = candiateTc_s; var step2Model = new UnitModel(step2params); idUnitModelsList.Add(step2Model); - DisturbanceIdResult distIdResult_step4 = DisturbanceIdentifier.EstimateDisturbance - (dataSet2, step2Model, pidInputIdx, pidParams); + var distIdResult_step4 = DisturbanceIdentifier.EstimateDisturbance + (dataSetRun2, step2Model, pidInputIdx, pidParams); idDisturbancesList.Add(distIdResult_step4); } + { + // version 2: choose the time-constant that gives the best closed-loop plant fit + /* var pidModel = new PidModel(pidParams, "PID"); + (var plantSim, var inputData) = PlantSimulator.CreateFeedbackLoop(unitDataSet_raw, pidModel, unitModel, pidInputIdx); + + plantSim.Simulate(inputData, out var simData); + var test = plantSim.PlantFitScore; + + */ + } + + // debugging plots, should normally be off if (false) { diff --git a/Dynamic/Identification/DisturbanceIdentifier.cs b/Dynamic/Identification/DisturbanceIdentifier.cs index e196465..94636af 100644 --- a/Dynamic/Identification/DisturbanceIdentifier.cs +++ b/Dynamic/Identification/DisturbanceIdentifier.cs @@ -125,7 +125,7 @@ private static UnitDataSet RemoveSetpointAndOtherInputChangeEffectsFromDataSet(U { // BEGIN check if both y_setpoint and all U externals are constant, if so just return the original dataset bool isYsetConstant = false; - if (Vec.IsConstant(unitDataSet.Y_setpoint) ) // and: if all + if (Vec.IsConstant(unitDataSet.Y_setpoint)) // and: if all { isYsetConstant = true; } @@ -154,7 +154,7 @@ private static UnitDataSet RemoveSetpointAndOtherInputChangeEffectsFromDataSet(U // disturbances are visible in this simulation var pidModel1 = new PidModel(pidParams, "PID"); - var processSim_noDist = new PlantSimulator( + /*var processSim_noDist = new PlantSimulator( new List { pidModel1, unitModel }); processSim_noDist.ConnectModels(unitModel, pidModel1); processSim_noDist.ConnectModels(pidModel1, unitModel,pidInputIdx); @@ -174,6 +174,8 @@ private static UnitDataSet RemoveSetpointAndOtherInputChangeEffectsFromDataSet(U inputData_noDist.Add(processSim_noDist.AddExternalSignal(pidModel1, SignalType.Setpoint_Yset), unitDataSet.Y_setpoint); inputData_noDist.CreateTimestamps(unitDataSet.GetTimeBase()); inputData_noDist.SetIndicesToIgnore(unitDataSet.IndicesToIgnore); + */ + (var processSim_noDist, var inputData_noDist) = PlantSimulator.CreateFeedbackLoop(unitDataSet, pidModel1, unitModel, pidInputIdx); var noDist_isOk = processSim_noDist.Simulate(inputData_noDist, out TimeSeriesDataSet simData_noDist); if (noDist_isOk) @@ -183,15 +185,15 @@ private static UnitDataSet RemoveSetpointAndOtherInputChangeEffectsFromDataSet(U { if (unitDataSet.GetNumDataPoints() > 0) { - while (unitDataSet.IndicesToIgnore.Contains(idxFirstGoodValue) && - idxFirstGoodValue < unitDataSet.GetNumDataPoints()-1) + while (unitDataSet.IndicesToIgnore.Contains(idxFirstGoodValue) && + idxFirstGoodValue < unitDataSet.GetNumDataPoints() - 1) { idxFirstGoodValue++; } } } var vec = new Vec(); - + // create a new Y_meas that excludes the influence of any disturabnce using "no_Dist" simulation // this is used to find d_HF var procOutputY = simData_noDist.GetValues(unitModel.GetID(), SignalType.Output_Y); @@ -221,24 +223,11 @@ private static UnitDataSet RemoveSetpointAndOtherInputChangeEffectsFromDataSet(U var newU = Vec.Fill(unitDataSet.U.GetColumn(inputIdx)[idxFirstGoodValue], N); unitDataSet_adjusted.U = Matrix.ReplaceColumn(unitDataSet_adjusted.U, inputIdx, newU); } - } - - - // original code, works only for SISO systems - /* { - var pidOutputU = simData_noDist.GetValues(pidModel1.GetID(), SignalType.PID_U); - var pidDeltaU = vec.Subtract(pidOutputU, pidOutputU[idxFirstGoodValue]); - var newU = vec.Subtract(unitDataSet.U.GetColumn(pidInputIdx), pidDeltaU); - unitDataSet_setpointEffectsAndExternalEffectsRemoved.U = Matrix.ReplaceColumn(unitDataSet_setpointEffectsAndExternalEffectsRemoved.U, pidInputIdx, newU); - } - */ unitDataSet_adjusted.IndicesToIgnore = unitDataSet.IndicesToIgnore; - bool doDebugPlot = false; - if (doDebugPlot) + if (false) // debugging plots, normally set to false { - Shared.EnablePlots(); Plot.FromList( new List { @@ -252,105 +241,43 @@ private static UnitDataSet RemoveSetpointAndOtherInputChangeEffectsFromDataSet(U new List { "y1=y_meas(new)", "y1=y_meas(old)", "y1=y_set(new)", "y1=y_set(old)", "y3=u_pid(new)", "y3=u_pid(old)" }, inputData_noDist.GetTimeBase(), "distIdent_setpointTest"); Shared.DisablePlots(); - } + } } } return unitDataSet_adjusted; } - - - /// - /// Estimates the disturbance time-series over a given unit data set - /// given an estimate of the unit model (reference unit model) for a closed loop system. - /// - /// the dataset descrbing the unit, over which the disturbance is to be found, datset must specify Y_setpoint,Y_meas and U - /// the estimate of the unit - /// the index of the pid-input in the unitModel - /// the parameters if known of the pid-controller in the closed loop - /// - public static DisturbanceIdResult EstimateDisturbance(UnitDataSet unitDataSet_raw, - UnitModel unitModel, int pidInputIdx =0, PidParameters pidParams = null) + public static UnitModel EstimateClosedLoopProcessGain(UnitDataSet unitDataSet, int pidInputIdx) { - const bool tryToModelDisturbanceIfSetpointChangesInDataset = true; + var unitModel = new UnitModel(); var vec = new Vec(); - DisturbanceIdResult result = new DisturbanceIdResult(unitDataSet_raw); - if (unitDataSet_raw.Y_setpoint == null || unitDataSet_raw.Y_meas == null || unitDataSet_raw.U == null) - { - return result; - } - - bool doesSetpointChange = !(vec.Max(unitDataSet_raw.Y_setpoint, unitDataSet_raw.IndicesToIgnore) - == vec.Min(unitDataSet_raw.Y_setpoint, unitDataSet_raw.IndicesToIgnore)); - if (!tryToModelDisturbanceIfSetpointChangesInDataset && doesSetpointChange) + //var result = new DisturbanceIdResult(unitDataSet); + if (unitDataSet.Y_setpoint == null || unitDataSet.Y_meas == null || unitDataSet.U == null) { - result.SetToZero();//the default anyway,added for clarity. - return result; + return null; } - // determine if process gains are given, otherwise the algorithm will need to make a rough estimate - bool isProcessGainSet = false; + bool doesSetpointChange = !(vec.Max(unitDataSet.Y_setpoint, unitDataSet.IndicesToIgnore) + == vec.Min(unitDataSet.Y_setpoint, unitDataSet.IndicesToIgnore)); double estPidInputProcessGain = 0; - if (unitModel != null) - { - bool updateEstGain = false; - if (unitModel.modelParameters.Fitting == null)// a priori model - { - updateEstGain = true; - } - else if (unitModel.modelParameters.Fitting.WasAbleToIdentify == true) - { - updateEstGain = true; - } - if (updateEstGain == true) - { - var processGains = unitModel.modelParameters.GetProcessGains(); - if (processGains == null) - { - return result; - } - if (!Double.IsNaN(processGains[pidInputIdx])) - { - estPidInputProcessGain = processGains[pidInputIdx]; - isProcessGainSet = true; - } - } - } - // Instead of using index 0 use the first index that is not "bad". - int indexOfFirstGoodValue = 0; - if (unitDataSet_raw.IndicesToIgnore != null) + // try to find a rough first estimate by heuristics { - if (unitDataSet_raw.GetNumDataPoints() > 0) - { - while (unitDataSet_raw.IndicesToIgnore.Contains(indexOfFirstGoodValue) && indexOfFirstGoodValue < unitDataSet_raw.GetNumDataPoints() - 1) - { - indexOfFirstGoodValue++; - } - } - } - - // if process gains are not given, then try to find a rough first estimate by heuristics - // TODO: consider if this should code should be moved out of this method? - if (!isProcessGainSet) - { - double[] pidInput_u0 = Vec.Fill(unitDataSet_raw.U[pidInputIdx, 0], - unitDataSet_raw.GetNumDataPoints()); - double yset0 = unitDataSet_raw.Y_setpoint[0]; + double[] pidInput_u0 = Vec.Fill(unitDataSet.U[pidInputIdx, 0], + unitDataSet.GetNumDataPoints()); + double yset0 = unitDataSet.Y_setpoint[0]; // y0,u0 is at the first data point // disadvantage, is that you are not sure that the time series starts at steady state // but works better than candiate 2 when disturbance is a step double FilterTc_s = 0; - // initalizaing(rough estimate): this should only be used as an inital guess on the first - // run when no process model exists! - if (!isProcessGainSet) + // initalizaing(rough estimate): { - LowPass lowPass = new LowPass(unitDataSet_raw.GetTimeBase()); - double[] e = vec.Subtract(unitDataSet_raw.Y_meas, unitDataSet_raw.Y_setpoint); + LowPass lowPass = new LowPass(unitDataSet.GetTimeBase()); + double[] e = vec.Subtract(unitDataSet.Y_meas, unitDataSet.Y_setpoint); // knowing the sign of the process gain is quite important! // if a system has negative gain and is given a positive process disturbance, then y and u will both increase in a way that is // correlated @@ -360,10 +287,10 @@ public static DisturbanceIdResult EstimateDisturbance(UnitDataSet unitDataSet_ra // If an increase in _y(by means of a disturbance)_ causes PID-controller to _increase_ u then the processGainSign is negative // If an increase in y causes PID to _decrease_ u, then processGainSign is positive! { - var indGreaterThanZeroE = vec.FindValues(e, 0, VectorFindValueType.BiggerOrEqual, unitDataSet_raw.IndicesToIgnore); - var indLessThanZeroE = vec.FindValues(e, 0, VectorFindValueType.SmallerOrEqual, unitDataSet_raw.IndicesToIgnore); + var indGreaterThanZeroE = vec.FindValues(e, 0, VectorFindValueType.BiggerOrEqual, unitDataSet.IndicesToIgnore); + var indLessThanZeroE = vec.FindValues(e, 0, VectorFindValueType.SmallerOrEqual, unitDataSet.IndicesToIgnore); - var u_pid = unitDataSet_raw.U.GetColumn(pidInputIdx); + var u_pid = unitDataSet.U.GetColumn(pidInputIdx); var uAvgWhenEgreatherThanZero = vec.Mean(Vec.GetValuesAtIndices(u_pid, indGreaterThanZeroE)); var uAvgWhenElessThanZero = vec.Mean(Vec.GetValuesAtIndices(u_pid, indLessThanZeroE)); @@ -379,51 +306,93 @@ public static DisturbanceIdResult EstimateDisturbance(UnitDataSet unitDataSet_ra } } } - double[] pidInput_deltaU = vec.Subtract(unitDataSet_raw.U.GetColumn(pidInputIdx), pidInput_u0);//TODO : U including feed-forward? - double[] eFiltered = lowPass.Filter(e, FilterTc_s, 2, unitDataSet_raw.IndicesToIgnore); - double maxDE = vec.Max(vec.Abs(eFiltered), unitDataSet_raw.IndicesToIgnore); // this has to be sensitive to noise? - double[] uFiltered = lowPass.Filter(pidInput_deltaU, FilterTc_s, 2, unitDataSet_raw.IndicesToIgnore); - double maxU = vec.Max(vec.Abs(uFiltered), unitDataSet_raw.IndicesToIgnore); // sensitive to output noise/controller overshoot - double minU = vec.Min(vec.Abs(uFiltered), unitDataSet_raw.IndicesToIgnore); // sensitive to output noise/controller overshoot + double[] pidInput_deltaU = vec.Subtract(unitDataSet.U.GetColumn(pidInputIdx), pidInput_u0);//TODO : U including feed-forward? + double[] eFiltered = lowPass.Filter(e, FilterTc_s, 2, unitDataSet.IndicesToIgnore); + double maxDE = vec.Max(vec.Abs(eFiltered), unitDataSet.IndicesToIgnore); // this has to be sensitive to noise? + double[] uFiltered = lowPass.Filter(pidInput_deltaU, FilterTc_s, 2, unitDataSet.IndicesToIgnore); + double maxU = vec.Max(vec.Abs(uFiltered), unitDataSet.IndicesToIgnore); // sensitive to output noise/controller overshoot + double minU = vec.Min(vec.Abs(uFiltered), unitDataSet.IndicesToIgnore); // sensitive to output noise/controller overshoot estPidInputProcessGain = pidInput_processGainSign * maxDE / (maxU - minU); } - bool isFittedButFittingFailed = false; - if (unitModel != null) - if (unitModel.GetModelParameters().Fitting != null) - if (unitModel.GetModelParameters().Fitting.WasAbleToIdentify == false) - isFittedButFittingFailed = true; - - // if no unit model from regression, create on useing a "guesstimated" process gain - if (unitModel == null || isFittedButFittingFailed) + + int indexOfFirstGoodValue = 0; + if (unitDataSet.IndicesToIgnore != null) { - int nGains = unitDataSet_raw.U.GetNColumns(); - if (nGains == 1) + if (unitDataSet.GetNumDataPoints() > 0) { - var unitParamters = new UnitParameters(); - unitParamters.LinearGains = new double[nGains]; - unitParamters.LinearGains[pidInputIdx] = estPidInputProcessGain; - // TODO: first guess of linear gains and u0 for non-pid inputs if more than one input ?? - unitParamters.U0 = new double[nGains]; - unitParamters.U0[pidInputIdx] = pidInput_u0[indexOfFirstGoodValue]; - unitParamters.UNorm = Vec.Fill(1, nGains); - unitParamters.Bias = unitDataSet_raw.Y_meas[indexOfFirstGoodValue]; - unitModel = new UnitModel(unitParamters); - } - else - { - unitModel = UnitIdentifier.IdentifyLinearAndStaticWhileKeepingLinearGainFixed(unitDataSet_raw, pidInputIdx, estPidInputProcessGain, - pidInput_u0[indexOfFirstGoodValue], 1); + while (unitDataSet.IndicesToIgnore.Contains(indexOfFirstGoodValue) && indexOfFirstGoodValue < + unitDataSet.GetNumDataPoints() - 1) + { + indexOfFirstGoodValue++; + } } } + + int nGains = unitDataSet.U.GetNColumns(); + if (nGains == 1) + { + var unitParamters = new UnitParameters(); + unitParamters.LinearGains = new double[nGains]; + unitParamters.LinearGains[pidInputIdx] = estPidInputProcessGain; + unitParamters.U0 = new double[nGains]; + unitParamters.U0[pidInputIdx] = pidInput_u0[pidInputIdx]; + unitParamters.UNorm = Vec.Fill(1, nGains); + unitParamters.Bias = unitDataSet.Y_meas[indexOfFirstGoodValue]; + unitModel = new UnitModel(unitParamters); + } + else + { + unitModel = UnitIdentifier.IdentifyLinearAndStaticWhileKeepingLinearGainFixed(unitDataSet, pidInputIdx, estPidInputProcessGain, + pidInput_u0[indexOfFirstGoodValue], 1); + } } + // END STEP 1 + //////////////////////////// + + return unitModel; + } + + + + + /// + /// Estimates the disturbance time-series over a given unit data set + /// given an estimate of the unit model (reference unit model) for a closed loop system. + /// + /// the dataset describing the unit, over which the disturbance is to be found, datset must specify Y_setpoint,Y_meas and U + /// the estimate of the unit + /// the index of the pid-input in the unitModel + /// the parameters if known of the pid-controller in the closed loop + /// + public static DisturbanceIdResult EstimateDisturbance(UnitDataSet unitDataSet, + UnitModel unitModel, int pidInputIdx = 0, PidParameters pidParams = null) + { + if (unitModel == null) + { + unitModel = EstimateClosedLoopProcessGain(unitDataSet, pidInputIdx); + } + else if (unitModel.GetModelParameters()==null) + { + unitModel = EstimateClosedLoopProcessGain(unitDataSet, pidInputIdx); + } + else if (unitModel.GetModelParameters().LinearGains == null) + { + unitModel = EstimateClosedLoopProcessGain(unitDataSet, pidInputIdx); + } + else if (unitModel.GetModelParameters().LinearGains.Count() == 0) + { + unitModel = EstimateClosedLoopProcessGain(unitDataSet, pidInputIdx); + } + + var result = new DisturbanceIdResult(unitDataSet); + ///////////////////////////// + // STEP 2: + var vec = new Vec(unitDataSet.BadDataID); // using the pidParams and unitModel, and if relevant any given y_set and external U, try to subtract the effects of // non-disturbance related changes in the dataset producing "unitDataSet_adjusted" - var unitDataSet_adjusted = RemoveSetpointAndOtherInputChangeEffectsFromDataSet(unitDataSet_raw, unitModel, pidInputIdx, pidParams); - unitModel.WarmStart(); - // var sim = new UnitSimulator(unitModel); + var unitDataSet_adjusted = RemoveSetpointAndOtherInputChangeEffectsFromDataSet(unitDataSet, unitModel, pidInputIdx, pidParams); unitDataSet_adjusted.D = null; - // double[] y_sim = sim.Simulate(ref unitDataSet_adjusted); (bool isOk, double[] y_sim) = PlantSimulator.SimulateSingle(unitDataSet_adjusted, unitModel, false); if (y_sim == null) @@ -432,54 +401,60 @@ public static DisturbanceIdResult EstimateDisturbance(UnitDataSet unitDataSet_ra return result; } + int indexOfFirstGoodValue = 0; + if (unitDataSet.IndicesToIgnore != null) + { + if (unitDataSet.GetNumDataPoints() > 0) + { + while (unitDataSet.IndicesToIgnore.Contains(indexOfFirstGoodValue) && indexOfFirstGoodValue < + unitDataSet.GetNumDataPoints() - 1) + { + indexOfFirstGoodValue++; + } + } + } + + + double[] d_LF = vec.Multiply(vec.Subtract(y_sim, y_sim[indexOfFirstGoodValue]), -1); + double[] d_HF = vec.Subtract(unitDataSet_adjusted.Y_meas, unitDataSet_adjusted.Y_setpoint); + // d_u : (low-pass) back-estimation of disturbances by the effect that they have on u as the pid-controller integrates to // counteract them // d_y : (high-pass) disturbances appear for a short while on the output y before they can be counter-acted by the pid-controller // nb!candiateGainD is an estimate for the process gain, and the value chosen in this class // will influence the process model identification afterwards. - double[] d_LF = vec.Multiply(vec.Subtract(y_sim, y_sim[indexOfFirstGoodValue]), -1); - double[] d_HF = vec.Subtract(unitDataSet_adjusted.Y_meas, unitDataSet_adjusted.Y_setpoint); // d = d_HF+d_LF double[] d_est = vec.Add(d_HF, d_LF); + result.d_est = d_est; + result.d_LF = d_LF; + result.d_HF = d_HF; + result.adjustedUnitDataSet = unitDataSet_adjusted; - bool doDebugPlot = false; + // END STEP 2 + ///////////////////////////// - if (doDebugPlot) + if (false)// debugging plots, should normally be set to "false" { var variableList = new List { - unitDataSet_raw.Y_meas, - unitDataSet_adjusted.Y_meas/*, - unitDataSet_setpointAndExternalUChangeEffectsRemoved.Y_setpoint, - y_sim, - d_LF, - d_HF, - d_est, - unitDataSet_setpointAndExternalUChangeEffectsRemoved.U.GetColumn(pidInputIdx),*/ + unitDataSet.Y_meas, + result.adjustedUnitDataSet.Y_meas }; - var variableNameList = new List { "y1=y_meas", "y1=y_meas(extUrem)"/*, "y1=y_set", "y1=y_sim", "y3=d_LF", "y3=d_HF", "y3=d_est", "y2=u_pid"*/ }; + var variableNameList = new List { "y1=y_meas", "y1=y_meas(extUrem)" }; - if (unitDataSet_adjusted.U.GetNColumns() == 2) + if (result.adjustedUnitDataSet.U.GetNColumns() == 2) { var nonPidIdx = 0; if (pidInputIdx == 0) nonPidIdx = 1; - variableList.Add(unitDataSet_adjusted.U.GetColumn(nonPidIdx)); + variableList.Add(result.adjustedUnitDataSet.U.GetColumn(nonPidIdx)); variableNameList.Add("y2=u_nonpid"); } Shared.EnablePlots(); Plot.FromList( - variableList, variableNameList, unitDataSet_adjusted.GetTimeBase(), "distIdent_dLF_est"); + variableList, variableNameList, result.adjustedUnitDataSet.GetTimeBase(), "distIdent_dLF_est"); Shared.DisablePlots(); } - // - - // copy result to result class - result.estPidProcessGain = estPidInputProcessGain; - result.d_est = d_est; - result.d_LF = d_LF; - result.d_HF = d_HF; - result.adjustedUnitDataSet = unitDataSet_adjusted; return result; } diff --git a/Dynamic/Identification/UnitIdentifier.cs b/Dynamic/Identification/UnitIdentifier.cs index 54e02cb..480e2c0 100644 --- a/Dynamic/Identification/UnitIdentifier.cs +++ b/Dynamic/Identification/UnitIdentifier.cs @@ -1192,8 +1192,10 @@ static public (double?, double[]) SimulateAndReEstimateBias(UnitDataSet dataSet, /// identified model, to check if identification suceeded, check /// .modelParameters.Fitting.WasAbleToIdentify public static UnitModel IdentifyLinearAndStaticWhileKeepingLinearGainFixed(UnitDataSet dataSet, int inputIdxToFix, - double inputProcessGainValueToFix, double u0_fixedInput, double uNorm_fixedInput) + double inputProcessGainValueToFix, double u0_fixedInput, double uNorm_fixedInput ) { + + var fittingSpecs = new FittingSpecs(); var internalDataset = new UnitDataSet(dataSet); var vec = new Vec(dataSet.BadDataID); var D_fixedInput = vec.Multiply(vec.Multiply(vec.Subtract(dataSet.U.GetColumn(inputIdxToFix), u0_fixedInput),uNorm_fixedInput), @@ -1221,7 +1223,8 @@ public static UnitModel IdentifyLinearAndStaticWhileKeepingLinearGainFixed(UnitD } internalDataset.U = newU; - var idUnitModel = IdentifyLinearAndStatic(ref internalDataset,new FittingSpecs()); + var idUnitModel = IdentifyLinearAndStatic(ref internalDataset, fittingSpecs, false); + if (!idUnitModel.modelParameters.Fitting.WasAbleToIdentify) return idUnitModel; //trick now is to add back the paramters that are fixed to the returned model: diff --git a/Dynamic/PlantSimulator/PlantSimulator.cs b/Dynamic/PlantSimulator/PlantSimulator.cs index 77c6cbe..d3f6007 100644 --- a/Dynamic/PlantSimulator/PlantSimulator.cs +++ b/Dynamic/PlantSimulator/PlantSimulator.cs @@ -396,6 +396,33 @@ public string ConnectModels(ISimulatableModel upstreamModel, ISimulatableModel d return outputId; } + public static (PlantSimulator, TimeSeriesDataSet) CreateFeedbackLoop(UnitDataSet unitDataSet, PidModel pidModel, UnitModel unitModel, int pidInputIdx=0) + { + var processSim = new PlantSimulator( + new List { pidModel, unitModel }); + processSim.ConnectModels(unitModel, pidModel); + processSim.ConnectModels(pidModel, unitModel, pidInputIdx); + + var inputData = new TimeSeriesDataSet(); + if (unitDataSet.U.GetNColumns() > 1) + { + for (int curColIdx = 0; curColIdx < unitDataSet.U.GetNColumns(); curColIdx++) + { + if (curColIdx == pidInputIdx) + continue; + inputData.Add(processSim.AddExternalSignal(unitModel, SignalType.External_U, curColIdx), + unitDataSet.U.GetColumn(curColIdx)); + } + } + + inputData.Add(processSim.AddExternalSignal(pidModel, SignalType.Setpoint_Yset), unitDataSet.Y_setpoint); + inputData.CreateTimestamps(unitDataSet.GetTimeBase()); + inputData.SetIndicesToIgnore(unitDataSet.IndicesToIgnore); + + return (processSim, inputData); + } + + /// /// Get a TimeSeriesDataSet of all external signals of model. /// @@ -884,9 +911,6 @@ public bool Serialize(string newPlantName = null, string path= null) if (!fileName.EndsWith(".json")) fileName += ".json"; - // var options = new JsonSerializerOptions { WriteIndented = true }; - // var serializedTxt = JsonSerializer.Serialize(this,options); - var serializedTxt = SerializeTxt(); var fileWriter = new StringToFileWriter(fileName); diff --git a/TimeSeriesAnalysis/TimeSeriesDataSet.cs b/TimeSeriesAnalysis/TimeSeriesDataSet.cs index 4062602..5281dc7 100644 --- a/TimeSeriesAnalysis/TimeSeriesDataSet.cs +++ b/TimeSeriesAnalysis/TimeSeriesDataSet.cs @@ -43,7 +43,7 @@ public TimeSeriesDataSet() //didSimulationReturnOk = false; } /// - /// Constructor + /// Constructor that copies another dataset into the returned object /// /// @@ -59,9 +59,28 @@ public TimeSeriesDataSet(TimeSeriesDataSet inputDataSet) timeStamps = inputDataSet.timeStamps; } + /// + /// Constructor that builds a dataset object from a csv-file, by loading LoadFromCsv() + /// This version of the constructor is useful when receiving the csv-data across an API. + /// + /// the csv data loaded into a CsvContent object + /// the separator in the csv-file + /// the format of date-time strings in the csv-file + public TimeSeriesDataSet(CsvContent csvContent, char separator = ';', string dateTimeFormat = "yyyy-MM-dd HH:mm:ss") + { + LoadFromCsv(csvContent, separator, dateTimeFormat); + } - - + /// + /// Constructor that builds a dataset object from a csv-file, by loading LoadFromCsv() + /// + /// name of csv-file + /// the separator in the csv-file + /// the format of date-time strings in the csv-file + public TimeSeriesDataSet(string csvFileName, char separator = ';', string dateTimeFormat = "yyyy-MM-dd HH:mm:ss") + { + LoadFromCsv(csvFileName, separator, dateTimeFormat); + } /// /// Add an entire time-series to the dataset @@ -224,6 +243,50 @@ public TimeSeriesDataSet Combine(TimeSeriesDataSet inputDataSet) } + + + /// + /// Returns a copy of the dataset that is downsampled by the given factor + /// + /// value greater than 1 indicating that every nth value of the orignal data will be transferred + /// + public TimeSeriesDataSet CreateDownsampledCopy(int downsampleFactor) + { + TimeSeriesDataSet ret = new TimeSeriesDataSet(); + + ret.timeStamps = Vec.Downsample(timeStamps.ToArray(), downsampleFactor).ToList(); + ret.N = ret.timeStamps.Count(); + ret.dataset_constants = dataset_constants; + foreach (var item in dataset) + { + ret.dataset[item.Key] = Vec.Downsample(item.Value, downsampleFactor); + } + return ret; + } + + /// + /// Creates internal timestamps from a given start time and timebase, must be called after filling the values + /// + /// the time between samples in the dataset, in total seconds + /// start time, can be null, which can be usedful for testing + public void CreateTimestamps(double timeBase_s, DateTime? t0 = null) + { + if (t0 == null) + { + t0 = new DateTime(2010, 1, 1);//intended for testing + } + + var times = new List(); + DateTime time = t0.Value; + for (int i = 0; i < N; i++) + { + times.Add(time); + time = time.AddSeconds(timeBase_s); + } + timeStamps = times; + } + + /// /// Fills a dataset with variables, values and dates, removes "time" or "Time" from variableDict if present, and stores timestamps in internal dateTimes /// @@ -512,50 +575,6 @@ public void InitNewSignal(string signalName, double initalValue, int N, double n - - /// - /// Returns a copy of the dataset that is downsampled by the given factor - /// - /// value greater than 1 indicating that every nth value of the orignal data will be transferred - /// - public TimeSeriesDataSet CreateDownsampledCopy(int downsampleFactor) - { - TimeSeriesDataSet ret = new TimeSeriesDataSet(); - - ret.timeStamps = Vec.Downsample(timeStamps.ToArray(), downsampleFactor).ToList(); - ret.N = ret.timeStamps.Count(); - ret.dataset_constants = dataset_constants; - foreach (var item in dataset) - { - ret.dataset[item.Key] = Vec.Downsample(item.Value, downsampleFactor); - } - return ret; - } - - /// - /// Creates internal timestamps from a given start time and timebase, must be called after filling the values - /// - /// the time between samples in the dataset, in total seconds - /// start time, can be null, which can be usedful for testing - public void CreateTimestamps(double timeBase_s, DateTime? t0 = null) - { - if (t0 == null) - { - t0 = new DateTime(2010, 1, 1);//intended for testing - } - - var times = new List(); - DateTime time = t0.Value; - for (int i = 0; i < N; i++) - { - times.Add(time); - time = time.AddSeconds(timeBase_s); - } - timeStamps = times; - } - - - /// /// Reads data form a csv-file (such as that created by ToCSV()) ///