652 lines
27 KiB
Matlab
652 lines
27 KiB
Matlab
function result = prepare_steering_iddata(experimentRoot, outputDirectory)
|
||||
|
|
%PREPARE_STEERING_IDDATA Convert steering snapshot CSV files to MATLAB iddata.
|
|||
|
|
% RESULT = PREPARE_STEERING_IDDATA() reads the two experiment folders next
|
|||
|
|
% to this file, detects every steering target step, aligns command and
|
|||
|
|
% feedback timestamps, resamples each response to 0.04 s, and saves:
|
|||
|
|
%
|
|||
|
|
% dataEstLF/LR/RF/RR uDiff -> delta actual steering angle
|
|||
|
|
% dataValLF/LR/RF/RR independent validation data
|
|||
|
|
% closedLoopEst* delta target angle -> delta actual angle
|
|||
|
|
% closedLoopVal* independent closed-loop validation data
|
|||
|
|
%
|
|||
|
|
% Each variable is a multi-experiment iddata object. Import one wheel at a
|
|||
|
|
% time into the System Identification app. The first experiment folder
|
|||
|
|
% (Kp=0.0035, dead zone=0.1 deg) is used for estimation and the third
|
|||
|
|
% folder (Kp=0.003, dead zone=0.1 deg) for validation. The second folder
|
|||
|
|
% with a 0.5 deg dead zone is retained as supplementary data and is not
|
|||
|
|
% used by this primary identification workflow. Raw CSV files are never
|
|||
|
|
% modified.
|
|||
|
|
%
|
|||
|
|
% Example:
|
|||
|
|
% result = prepare_steering_iddata;
|
|||
|
|
% load(result.MatFile);
|
|||
|
|
% systemIdentification;
|
|||
|
|
%
|
|||
|
|
% Units: uDiff in m/s, steering angle in degrees, time in seconds.
|
|||
|
|
|
|||
|
|
if nargin < 1 || isempty(experimentRoot)
|
|||
|
|
experimentRoot = fileparts(mfilename('fullpath'));
|
|||
|
|
end
|
|||
|
|
if nargin < 2 || isempty(outputDirectory)
|
|||
|
|
outputDirectory = fullfile(experimentRoot, 'matlab_ready');
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
if exist('iddata', 'file') ~= 2
|
|||
|
|
error('prepare_steering_iddata:MissingToolbox', ...
|
|||
|
|
['找不到 iddata。请安装或启用 MATLAB System Identification ' ...
|
|||
|
|
'Toolbox 后重新运行。']);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
settings = struct();
|
|||
|
|
settings.SampleTimeSeconds = 0.04;
|
|||
|
|
settings.PreStepSeconds = 0.8;
|
|||
|
|
settings.PostStepSeconds = 4.0;
|
|||
|
|
settings.BaselineSeconds = 0.5;
|
|||
|
|
settings.NextStepGuardSeconds = 0.25;
|
|||
|
|
settings.MinimumTargetStepDegrees = 5.0;
|
|||
|
|
settings.MinimumSegmentSamples = 30;
|
|||
|
|
settings.EstimationFolder = '第一次0.0035--死区0.1度';
|
|||
|
|
settings.ValidationFolder = '第三次0.0030--死区0.1度';
|
|||
|
|
|
|||
|
|
wheels = steeringWheelDefinitions();
|
|||
|
|
estimationPath = fullfile(experimentRoot, settings.EstimationFolder);
|
|||
|
|
validationPath = fullfile(experimentRoot, settings.ValidationFolder);
|
|||
|
|
requireFolder(estimationPath);
|
|||
|
|
requireFolder(validationPath);
|
|||
|
|
|
|||
|
|
fprintf('读取估计数据:%s\n', estimationPath);
|
|||
|
|
estimationSegments = processExperimentFolder( ...
|
|||
|
|
estimationPath, 'Est', wheels, settings);
|
|||
|
|
fprintf('读取验证数据:%s\n', validationPath);
|
|||
|
|
validationSegments = processExperimentFolder( ...
|
|||
|
|
validationPath, 'Val', wheels, settings);
|
|||
|
|
|
|||
|
|
allSegments = [estimationSegments, validationSegments];
|
|||
|
|
if isempty(allSegments)
|
|||
|
|
error('prepare_steering_iddata:NoSegments', ...
|
|||
|
|
'没有找到满足条件的舵角切换片段。');
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
dataEstLF = buildIdData(estimationSegments, 'LF', 'plant', settings);
|
|||
|
|
dataEstLR = buildIdData(estimationSegments, 'LR', 'plant', settings);
|
|||
|
|
dataEstRF = buildIdData(estimationSegments, 'RF', 'plant', settings);
|
|||
|
|
dataEstRR = buildIdData(estimationSegments, 'RR', 'plant', settings);
|
|||
|
|
|
|||
|
|
dataValLF = buildIdData(validationSegments, 'LF', 'plant', settings);
|
|||
|
|
dataValLR = buildIdData(validationSegments, 'LR', 'plant', settings);
|
|||
|
|
dataValRF = buildIdData(validationSegments, 'RF', 'plant', settings);
|
|||
|
|
dataValRR = buildIdData(validationSegments, 'RR', 'plant', settings);
|
|||
|
|
|
|||
|
|
closedLoopEstLF = buildIdData( ...
|
|||
|
|
estimationSegments, 'LF', 'closedLoop', settings);
|
|||
|
|
closedLoopEstLR = buildIdData( ...
|
|||
|
|
estimationSegments, 'LR', 'closedLoop', settings);
|
|||
|
|
closedLoopEstRF = buildIdData( ...
|
|||
|
|
estimationSegments, 'RF', 'closedLoop', settings);
|
|||
|
|
closedLoopEstRR = buildIdData( ...
|
|||
|
|
estimationSegments, 'RR', 'closedLoop', settings);
|
|||
|
|
|
|||
|
|
closedLoopValLF = buildIdData( ...
|
|||
|
|
validationSegments, 'LF', 'closedLoop', settings);
|
|||
|
|
closedLoopValLR = buildIdData( ...
|
|||
|
|
validationSegments, 'LR', 'closedLoop', settings);
|
|||
|
|
closedLoopValRF = buildIdData( ...
|
|||
|
|
validationSegments, 'RF', 'closedLoop', settings);
|
|||
|
|
closedLoopValRR = buildIdData( ...
|
|||
|
|
validationSegments, 'RR', 'closedLoop', settings);
|
|||
|
|
|
|||
|
|
metadata = buildMetadataTable(allSegments);
|
|||
|
|
samples = buildSampleTable(allSegments);
|
|||
|
|
|
|||
|
|
if ~exist(outputDirectory, 'dir')
|
|||
|
|
mkdir(outputDirectory);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
matFile = fullfile(outputDirectory, 'steering_identification_data.mat');
|
|||
|
|
metadataFile = fullfile(outputDirectory, 'steering_segment_metadata.csv');
|
|||
|
|
samplesFile = fullfile(outputDirectory, 'steering_identification_samples.csv');
|
|||
|
|
|
|||
|
|
save(matFile, ...
|
|||
|
|
'dataEstLF', 'dataEstLR', 'dataEstRF', 'dataEstRR', ...
|
|||
|
|
'dataValLF', 'dataValLR', 'dataValRF', 'dataValRR', ...
|
|||
|
|
'closedLoopEstLF', 'closedLoopEstLR', ...
|
|||
|
|
'closedLoopEstRF', 'closedLoopEstRR', ...
|
|||
|
|
'closedLoopValLF', 'closedLoopValLR', ...
|
|||
|
|
'closedLoopValRF', 'closedLoopValRR', ...
|
|||
|
|
'metadata', 'settings', '-v7.3');
|
|||
|
|
writetable(metadata, metadataFile);
|
|||
|
|
writetable(samples, samplesFile);
|
|||
|
|
|
|||
|
|
result = struct();
|
|||
|
|
result.MatFile = matFile;
|
|||
|
|
result.MetadataFile = metadataFile;
|
|||
|
|
result.SamplesFile = samplesFile;
|
|||
|
|
result.EstimationSegmentCount = numel(estimationSegments);
|
|||
|
|
result.ValidationSegmentCount = numel(validationSegments);
|
|||
|
|
result.SampleTimeSeconds = settings.SampleTimeSeconds;
|
|||
|
|
|
|||
|
|
fprintf('\n处理完成。每个舵轮单独建立多实验数据:\n');
|
|||
|
|
printSegmentCounts(estimationSegments, validationSegments, wheels);
|
|||
|
|
fprintf('MATLAB辨识数据:%s\n', matFile);
|
|||
|
|
fprintf('片段元数据:%s\n', metadataFile);
|
|||
|
|
fprintf('长表CSV:%s\n', samplesFile);
|
|||
|
|
fprintf(['\n在工作区加载 MAT 文件后,将 dataEstLF 等变量作为估计数据,' ...
|
|||
|
|
'将对应的 dataValLF 等变量作为验证数据。\n']);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function wheels = steeringWheelDefinitions()
|
|||
|
|
%STEERINGWHEELDEFINITIONS Describe the four differential steering modules.
|
|||
|
|
wheels = struct( ...
|
|||
|
|
'Code', {'LF', 'LR', 'RF', 'RR'}, ...
|
|||
|
|
'Name', {'LeftFront', 'LeftRear', 'RightFront', 'RightRear'}, ...
|
|||
|
|
'TargetColumn', { ...
|
|||
|
|
'TargetThLeftFront', 'TargetThLeftRear', ...
|
|||
|
|
'TargetThRightFront', 'TargetThRightRear'}, ...
|
|||
|
|
'ActualColumn', { ...
|
|||
|
|
'ActualThLeftFront', 'ActualThLeftRear', ...
|
|||
|
|
'ActualThRightFront', 'ActualThRightRear'}, ...
|
|||
|
|
'ActualTimeColumn', { ...
|
|||
|
|
'ActualThLeftFrontReceiveElapsedMs', ...
|
|||
|
|
'ActualThLeftRearReceiveElapsedMs', ...
|
|||
|
|
'ActualThRightFrontReceiveElapsedMs', ...
|
|||
|
|
'ActualThRightRearReceiveElapsedMs'}, ...
|
|||
|
|
'SentLeftColumn', { ...
|
|||
|
|
'SentLFLMps', 'SentLRLMps', 'SentRFLMps', 'SentRRLMps'}, ...
|
|||
|
|
'SentRightColumn', { ...
|
|||
|
|
'SentLFRMps', 'SentLRRMps', 'SentRFRMps', 'SentRRRMps'}, ...
|
|||
|
|
'LeftLimitedColumn', { ...
|
|||
|
|
'CommandLimitedLFL', 'CommandLimitedLRL', ...
|
|||
|
|
'CommandLimitedRFL', 'CommandLimitedRRL'}, ...
|
|||
|
|
'RightLimitedColumn', { ...
|
|||
|
|
'CommandLimitedLFR', 'CommandLimitedLRR', ...
|
|||
|
|
'CommandLimitedRFR', 'CommandLimitedRRR'}, ...
|
|||
|
|
'PairLimitedColumn', { ...
|
|||
|
|
'PairCommandLimitedLeftFront', ...
|
|||
|
|
'PairCommandLimitedLeftRear', ...
|
|||
|
|
'PairCommandLimitedRightFront', ...
|
|||
|
|
'PairCommandLimitedRightRear'});
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function segments = processExperimentFolder(folder, datasetCode, wheels, settings)
|
|||
|
|
%PROCESSEXPERIMENTFOLDER Convert all snapshot files in one experiment set.
|
|||
|
|
files = dir(fullfile(folder, '*_snapshot.csv'));
|
|||
|
|
if isempty(files)
|
|||
|
|
error('prepare_steering_iddata:NoSnapshotFiles', ...
|
|||
|
|
'目录中没有 *_snapshot.csv:%s', folder);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
[~, order] = sort({files.name});
|
|||
|
|
files = files(order);
|
|||
|
|
segments = emptySegmentArray();
|
|||
|
|
|
|||
|
|
for fileIndex = 1:numel(files)
|
|||
|
|
sourcePath = fullfile(files(fileIndex).folder, files(fileIndex).name);
|
|||
|
|
fprintf(' %s\n', files(fileIndex).name);
|
|||
|
|
snapshot = readNumericSnapshot(sourcePath);
|
|||
|
|
validateSnapshotColumns(snapshot, wheels);
|
|||
|
|
|
|||
|
|
transitionType = regexprep( ...
|
|||
|
|
files(fileIndex).name, '_Car\d+_snapshot\.csv$', '');
|
|||
|
|
fileSegments = processSnapshot( ...
|
|||
|
|
snapshot, files(fileIndex).name, transitionType, ...
|
|||
|
|
datasetCode, wheels, settings);
|
|||
|
|
segments = [segments, fileSegments]; %#ok<AGROW>
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function snapshot = readNumericSnapshot(sourcePath)
|
|||
|
|
%READNUMERICSNAPSHOT Read the numeric diagnostic snapshot without mutation.
|
|||
|
|
importOptions = detectImportOptions(sourcePath, 'Delimiter', ',');
|
|||
|
|
importOptions = setvartype( ...
|
|||
|
|
importOptions, importOptions.VariableNames, 'double');
|
|||
|
|
snapshot = readtable(sourcePath, importOptions);
|
|||
|
|
if isempty(snapshot)
|
|||
|
|
error('prepare_steering_iddata:EmptySnapshot', ...
|
|||
|
|
'CSV中没有数据:%s', sourcePath);
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function validateSnapshotColumns(snapshot, wheels)
|
|||
|
|
%VALIDATESNAPSHOTCOLUMNS Ensure every signal needed for identification exists.
|
|||
|
|
required = { ...
|
|||
|
|
'ElapsedMs', 'ControlElapsedMs', 'WheelCommandElapsedMs', ...
|
|||
|
|
'WheelCommandSuppressed', 'DiffSteerKp', 'DiffSteerKi', ...
|
|||
|
|
'DiffSteerKd', 'DiffSteerDeadZone', ...
|
|||
|
|
'DiffSteerRateFeedforwardGain'};
|
|||
|
|
|
|||
|
|
for wheelIndex = 1:numel(wheels)
|
|||
|
|
wheel = wheels(wheelIndex);
|
|||
|
|
required = [required, { ... %#ok<AGROW>
|
|||
|
|
wheel.TargetColumn, wheel.ActualColumn, ...
|
|||
|
|
wheel.ActualTimeColumn, wheel.SentLeftColumn, ...
|
|||
|
|
wheel.SentRightColumn, wheel.LeftLimitedColumn, ...
|
|||
|
|
wheel.RightLimitedColumn, wheel.PairLimitedColumn}];
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
missing = setdiff(required, snapshot.Properties.VariableNames);
|
|||
|
|
if ~isempty(missing)
|
|||
|
|
error('prepare_steering_iddata:MissingColumns', ...
|
|||
|
|
'snapshot CSV缺少字段:%s', strjoin(missing, ', '));
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function segments = processSnapshot( ...
|
|||
|
|
snapshot, sourceFile, transitionType, datasetCode, wheels, settings)
|
|||
|
|
%PROCESSSNAPSHOT Extract one iddata experiment for every wheel target step.
|
|||
|
|
elapsedMs = snapshot.ElapsedMs;
|
|||
|
|
finiteElapsed = elapsedMs(isfinite(elapsedMs));
|
|||
|
|
if isempty(finiteElapsed)
|
|||
|
|
error('prepare_steering_iddata:InvalidTime', ...
|
|||
|
|
'%s没有有效ElapsedMs。', sourceFile);
|
|||
|
|
end
|
|||
|
|
timeOriginMs = finiteElapsed(1);
|
|||
|
|
|
|||
|
|
kp = constantParameter(snapshot.DiffSteerKp, 'DiffSteerKp', sourceFile);
|
|||
|
|
ki = constantParameter(snapshot.DiffSteerKi, 'DiffSteerKi', sourceFile);
|
|||
|
|
kd = constantParameter(snapshot.DiffSteerKd, 'DiffSteerKd', sourceFile);
|
|||
|
|
deadZone = constantParameter( ...
|
|||
|
|
snapshot.DiffSteerDeadZone, 'DiffSteerDeadZone', sourceFile);
|
|||
|
|
feedforwardGain = constantParameter( ...
|
|||
|
|
snapshot.DiffSteerRateFeedforwardGain, ...
|
|||
|
|
'DiffSteerRateFeedforwardGain', sourceFile);
|
|||
|
|
|
|||
|
|
segments = emptySegmentArray();
|
|||
|
|
for wheelIndex = 1:numel(wheels)
|
|||
|
|
wheel = wheels(wheelIndex);
|
|||
|
|
wheelSegments = extractWheelSegments( ...
|
|||
|
|
snapshot, timeOriginMs, sourceFile, transitionType, ...
|
|||
|
|
datasetCode, wheel, settings, kp, ki, kd, ...
|
|||
|
|
deadZone, feedforwardGain);
|
|||
|
|
segments = [segments, wheelSegments]; %#ok<AGROW>
|
|||
|
|
|
|||
|
|
if numel(wheelSegments) ~= 20
|
|||
|
|
warning('prepare_steering_iddata:UnexpectedStepCount', ...
|
|||
|
|
'%s %s检测到%d次切换,预期为20次。', ...
|
|||
|
|
sourceFile, wheel.Code, numel(wheelSegments));
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function segments = extractWheelSegments( ...
|
|||
|
|
snapshot, timeOriginMs, sourceFile, transitionType, datasetCode, ...
|
|||
|
|
wheel, settings, kp, ki, kd, deadZone, feedforwardGain)
|
|||
|
|
%EXTRACTWHEELSEGMENTS Align and split one steering module's time series.
|
|||
|
|
targetTime = (snapshot.ControlElapsedMs - timeOriginMs) / 1000;
|
|||
|
|
targetAngle = snapshot.(wheel.TargetColumn);
|
|||
|
|
[targetTime, targetAngle] = compactTimedSignal(targetTime, targetAngle);
|
|||
|
|
|
|||
|
|
commandTime = (snapshot.WheelCommandElapsedMs - timeOriginMs) / 1000;
|
|||
|
|
uDiff = (snapshot.(wheel.SentRightColumn) - ...
|
|||
|
|
snapshot.(wheel.SentLeftColumn)) / 2;
|
|||
|
|
[commandTime, uDiff] = compactTimedSignal(commandTime, uDiff);
|
|||
|
|
|
|||
|
|
actualTime = (snapshot.(wheel.ActualTimeColumn) - timeOriginMs) / 1000;
|
|||
|
|
actualAngle = snapshot.(wheel.ActualColumn);
|
|||
|
|
[actualTime, actualAngle] = compactTimedSignal(actualTime, actualAngle);
|
|||
|
|
|
|||
|
|
if numel(targetTime) < 2 || numel(commandTime) < 2 || ...
|
|||
|
|
numel(actualTime) < 2
|
|||
|
|
warning('prepare_steering_iddata:InsufficientSignal', ...
|
|||
|
|
'%s %s的时间序列不足,已跳过。', sourceFile, wheel.Code);
|
|||
|
|
segments = emptySegmentArray();
|
|||
|
|
return;
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
rawEventIndices = find( ...
|
|||
|
|
abs(diff(targetAngle)) >= settings.MinimumTargetStepDegrees) + 1;
|
|||
|
|
eventIndices = suppressNearbyEvents( ...
|
|||
|
|
rawEventIndices, targetTime, settings.NextStepGuardSeconds);
|
|||
|
|
segments = emptySegmentArray();
|
|||
|
|
|
|||
|
|
for eventNumber = 1:numel(eventIndices)
|
|||
|
|
targetIndex = eventIndices(eventNumber);
|
|||
|
|
eventTime = targetTime(targetIndex);
|
|||
|
|
targetStep = targetAngle(targetIndex) - targetAngle(targetIndex - 1);
|
|||
|
|
|
|||
|
|
segmentStart = eventTime - settings.PreStepSeconds;
|
|||
|
|
segmentEnd = eventTime + settings.PostStepSeconds;
|
|||
|
|
if eventNumber < numel(eventIndices)
|
|||
|
|
nextEventTime = targetTime(eventIndices(eventNumber + 1));
|
|||
|
|
segmentEnd = min( ...
|
|||
|
|
segmentEnd, nextEventTime - settings.NextStepGuardSeconds);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
commonStart = max([ ...
|
|||
|
|
segmentStart, targetTime(1), commandTime(1), actualTime(1)]);
|
|||
|
|
commonEnd = min([ ...
|
|||
|
|
segmentEnd, targetTime(end), commandTime(end), actualTime(end)]);
|
|||
|
|
firstGridIndex = ceil(commonStart / settings.SampleTimeSeconds);
|
|||
|
|
lastGridIndex = floor(commonEnd / settings.SampleTimeSeconds);
|
|||
|
|
commonTime = (firstGridIndex:lastGridIndex)' * ...
|
|||
|
|
settings.SampleTimeSeconds;
|
|||
|
|
|
|||
|
|
if numel(commonTime) < settings.MinimumSegmentSamples
|
|||
|
|
warning('prepare_steering_iddata:ShortSegment', ...
|
|||
|
|
'%s %s第%d次切换数据过短,已跳过。', ...
|
|||
|
|
sourceFile, wheel.Code, eventNumber);
|
|||
|
|
continue;
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
targetResampled = interp1( ...
|
|||
|
|
targetTime, targetAngle, commonTime, 'previous');
|
|||
|
|
inputResampled = interp1( ...
|
|||
|
|
commandTime, uDiff, commonTime, 'previous');
|
|||
|
|
outputResampled = interp1( ...
|
|||
|
|
actualTime, actualAngle, commonTime, 'linear');
|
|||
|
|
relativeTime = commonTime - eventTime;
|
|||
|
|
|
|||
|
|
baselineMask = relativeTime >= -settings.BaselineSeconds & ...
|
|||
|
|
relativeTime < -settings.SampleTimeSeconds / 2;
|
|||
|
|
if ~any(baselineMask)
|
|||
|
|
baselineMask = relativeTime < 0;
|
|||
|
|
end
|
|||
|
|
if ~any(baselineMask)
|
|||
|
|
warning('prepare_steering_iddata:MissingBaseline', ...
|
|||
|
|
'%s %s第%d次切换没有切换前基线,已跳过。', ...
|
|||
|
|
sourceFile, wheel.Code, eventNumber);
|
|||
|
|
continue;
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
inputBaseline = finiteMedian(inputResampled(baselineMask));
|
|||
|
|
outputBaseline = finiteMedian(outputResampled(baselineMask));
|
|||
|
|
targetBaseline = finiteMedian(targetResampled(baselineMask));
|
|||
|
|
inputDelta = inputResampled - inputBaseline;
|
|||
|
|
outputDelta = outputResampled - outputBaseline;
|
|||
|
|
targetDelta = targetResampled - targetBaseline;
|
|||
|
|
|
|||
|
|
valid = isfinite(relativeTime) & isfinite(inputDelta) & ...
|
|||
|
|
isfinite(outputDelta) & isfinite(targetDelta);
|
|||
|
|
if nnz(valid) < settings.MinimumSegmentSamples
|
|||
|
|
warning('prepare_steering_iddata:InvalidSegment', ...
|
|||
|
|
'%s %s第%d次切换有效样本不足,已跳过。', ...
|
|||
|
|
sourceFile, wheel.Code, eventNumber);
|
|||
|
|
continue;
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
relativeTime = relativeTime(valid);
|
|||
|
|
inputDelta = inputDelta(valid);
|
|||
|
|
outputDelta = outputDelta(valid);
|
|||
|
|
targetDelta = targetDelta(valid);
|
|||
|
|
|
|||
|
|
if segmentContainsUnsafeCommand( ...
|
|||
|
|
snapshot, timeOriginMs, commonStart, commonEnd, wheel)
|
|||
|
|
warning('prepare_steering_iddata:UnsafeCommandSegment', ...
|
|||
|
|
['%s %s第%d次切换出现命令限幅或安全抑制,' ...
|
|||
|
|
'为保持线性辨识条件已跳过。'], ...
|
|||
|
|
sourceFile, wheel.Code, eventNumber);
|
|||
|
|
continue;
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
experimentName = sprintf('%s_%s_%s_E%02d', ...
|
|||
|
|
datasetCode, transitionType, wheel.Code, eventNumber);
|
|||
|
|
segment = struct();
|
|||
|
|
segment.Dataset = datasetCode;
|
|||
|
|
segment.Wheel = wheel.Code;
|
|||
|
|
segment.SourceFile = sourceFile;
|
|||
|
|
segment.TransitionType = transitionType;
|
|||
|
|
segment.EventIndex = eventNumber;
|
|||
|
|
segment.ExperimentName = experimentName;
|
|||
|
|
segment.EventTimeSeconds = eventTime;
|
|||
|
|
segment.TargetStepDegrees = targetStep;
|
|||
|
|
segment.Kp = kp;
|
|||
|
|
segment.Ki = ki;
|
|||
|
|
segment.Kd = kd;
|
|||
|
|
segment.DeadZoneDegrees = deadZone;
|
|||
|
|
segment.FeedforwardGain = feedforwardGain;
|
|||
|
|
segment.InputBaselineMps = inputBaseline;
|
|||
|
|
segment.OutputBaselineDegrees = outputBaseline;
|
|||
|
|
segment.TimeSeconds = relativeTime;
|
|||
|
|
segment.UDiffMps = inputDelta;
|
|||
|
|
segment.ActualAngleDeltaDegrees = outputDelta;
|
|||
|
|
segment.TargetAngleDeltaDegrees = targetDelta;
|
|||
|
|
segments(end + 1) = segment; %#ok<AGROW>
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function [time, value] = compactTimedSignal(time, value)
|
|||
|
|
%COMPACTTIMEDSIGNAL Remove missing data and keep the last value per timestamp.
|
|||
|
|
valid = isfinite(time) & isfinite(value);
|
|||
|
|
time = time(valid);
|
|||
|
|
value = value(valid);
|
|||
|
|
[time, order] = sort(time);
|
|||
|
|
value = value(order);
|
|||
|
|
[time, uniqueIndices] = unique(time, 'last');
|
|||
|
|
value = value(uniqueIndices);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function eventIndices = suppressNearbyEvents(rawIndices, time, minimumGap)
|
|||
|
|
%SUPPRESSNEARBYEVENTS Treat rapid intermediate target updates as one step.
|
|||
|
|
eventIndices = zeros(0, 1);
|
|||
|
|
for index = 1:numel(rawIndices)
|
|||
|
|
candidate = rawIndices(index);
|
|||
|
|
if isempty(eventIndices) || ...
|
|||
|
|
time(candidate) - time(eventIndices(end)) >= minimumGap
|
|||
|
|
eventIndices(end + 1, 1) = candidate; %#ok<AGROW>
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function unsafe = segmentContainsUnsafeCommand( ...
|
|||
|
|
snapshot, timeOriginMs, startTime, endTime, wheel)
|
|||
|
|
%SEGMENTCONTAINSUNSAFECOMMAND Reject saturated or suppressed responses.
|
|||
|
|
snapshotTime = (snapshot.ElapsedMs - timeOriginMs) / 1000;
|
|||
|
|
inSegment = snapshotTime >= startTime & snapshotTime <= endTime;
|
|||
|
|
unsafe = any(snapshot.WheelCommandSuppressed(inSegment) ~= 0) || ...
|
|||
|
|
any(snapshot.(wheel.LeftLimitedColumn)(inSegment) ~= 0) || ...
|
|||
|
|
any(snapshot.(wheel.RightLimitedColumn)(inSegment) ~= 0) || ...
|
|||
|
|
any(snapshot.(wheel.PairLimitedColumn)(inSegment) ~= 0);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function value = constantParameter(values, parameterName, sourceFile)
|
|||
|
|
%CONSTANTPARAMETER Verify that one experiment did not change configuration.
|
|||
|
|
finiteValues = values(isfinite(values));
|
|||
|
|
if isempty(finiteValues)
|
|||
|
|
error('prepare_steering_iddata:MissingParameter', ...
|
|||
|
|
'%s没有有效的%s。', sourceFile, parameterName);
|
|||
|
|
end
|
|||
|
|
value = finiteMedian(finiteValues);
|
|||
|
|
tolerance = max(1e-9, abs(value) * 1e-6);
|
|||
|
|
if any(abs(finiteValues - value) > tolerance)
|
|||
|
|
error('prepare_steering_iddata:ChangingParameter', ...
|
|||
|
|
'%s中的%s在记录期间发生变化,不能作为单一实验。', ...
|
|||
|
|
sourceFile, parameterName);
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function value = finiteMedian(values)
|
|||
|
|
%FINITEMEDIAN Return the median after removing non-finite samples.
|
|||
|
|
values = values(isfinite(values));
|
|||
|
|
if isempty(values)
|
|||
|
|
value = NaN;
|
|||
|
|
else
|
|||
|
|
value = median(values);
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function data = buildIdData(segments, wheelCode, inputKind, settings)
|
|||
|
|
%BUILDIDDATA Build one multi-experiment iddata object for one steering module.
|
|||
|
|
selected = segments(strcmp({segments.Wheel}, wheelCode));
|
|||
|
|
if isempty(selected)
|
|||
|
|
error('prepare_steering_iddata:MissingWheelData', ...
|
|||
|
|
'没有找到%s舵轮的%s数据。', wheelCode, inputKind);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
outputs = cell(1, numel(selected));
|
|||
|
|
inputs = cell(1, numel(selected));
|
|||
|
|
experimentNames = cell(1, numel(selected));
|
|||
|
|
for index = 1:numel(selected)
|
|||
|
|
outputs{index} = selected(index).ActualAngleDeltaDegrees(:);
|
|||
|
|
if strcmp(inputKind, 'plant')
|
|||
|
|
inputs{index} = selected(index).UDiffMps(:);
|
|||
|
|
else
|
|||
|
|
inputs{index} = selected(index).TargetAngleDeltaDegrees(:);
|
|||
|
|
end
|
|||
|
|
experimentNames{index} = selected(index).ExperimentName;
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
data = iddata(outputs, inputs, settings.SampleTimeSeconds);
|
|||
|
|
data.ExperimentName = experimentNames;
|
|||
|
|
data.OutputName = {'actualSteeringAngleDelta'};
|
|||
|
|
data.OutputUnit = {'deg'};
|
|||
|
|
data.TimeUnit = 'seconds';
|
|||
|
|
if strcmp(inputKind, 'plant')
|
|||
|
|
data.Name = sprintf('%s steering plant: uDiff to angle', wheelCode);
|
|||
|
|
data.InputName = {'uDiff'};
|
|||
|
|
data.InputUnit = {'m/s'};
|
|||
|
|
else
|
|||
|
|
data.Name = sprintf('%s existing closed loop: target to angle', wheelCode);
|
|||
|
|
data.InputName = {'targetSteeringAngleDelta'};
|
|||
|
|
data.InputUnit = {'deg'};
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function metadata = buildMetadataTable(segments)
|
|||
|
|
%BUILDMETADATATABLE Create one row of traceability data per step response.
|
|||
|
|
count = numel(segments);
|
|||
|
|
dataset = cell(count, 1);
|
|||
|
|
wheel = cell(count, 1);
|
|||
|
|
sourceFile = cell(count, 1);
|
|||
|
|
transitionType = cell(count, 1);
|
|||
|
|
experimentName = cell(count, 1);
|
|||
|
|
eventIndex = zeros(count, 1);
|
|||
|
|
eventTimeSeconds = zeros(count, 1);
|
|||
|
|
targetStepDegrees = zeros(count, 1);
|
|||
|
|
kp = zeros(count, 1);
|
|||
|
|
ki = zeros(count, 1);
|
|||
|
|
kd = zeros(count, 1);
|
|||
|
|
deadZoneDegrees = zeros(count, 1);
|
|||
|
|
feedforwardGain = zeros(count, 1);
|
|||
|
|
inputBaselineMps = zeros(count, 1);
|
|||
|
|
outputBaselineDegrees = zeros(count, 1);
|
|||
|
|
sampleCount = zeros(count, 1);
|
|||
|
|
|
|||
|
|
for index = 1:count
|
|||
|
|
segment = segments(index);
|
|||
|
|
dataset{index} = segment.Dataset;
|
|||
|
|
wheel{index} = segment.Wheel;
|
|||
|
|
sourceFile{index} = segment.SourceFile;
|
|||
|
|
transitionType{index} = segment.TransitionType;
|
|||
|
|
experimentName{index} = segment.ExperimentName;
|
|||
|
|
eventIndex(index) = segment.EventIndex;
|
|||
|
|
eventTimeSeconds(index) = segment.EventTimeSeconds;
|
|||
|
|
targetStepDegrees(index) = segment.TargetStepDegrees;
|
|||
|
|
kp(index) = segment.Kp;
|
|||
|
|
ki(index) = segment.Ki;
|
|||
|
|
kd(index) = segment.Kd;
|
|||
|
|
deadZoneDegrees(index) = segment.DeadZoneDegrees;
|
|||
|
|
feedforwardGain(index) = segment.FeedforwardGain;
|
|||
|
|
inputBaselineMps(index) = segment.InputBaselineMps;
|
|||
|
|
outputBaselineDegrees(index) = segment.OutputBaselineDegrees;
|
|||
|
|
sampleCount(index) = numel(segment.TimeSeconds);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
metadata = table( ...
|
|||
|
|
dataset, wheel, sourceFile, transitionType, experimentName, ...
|
|||
|
|
eventIndex, eventTimeSeconds, targetStepDegrees, ...
|
|||
|
|
kp, ki, kd, deadZoneDegrees, feedforwardGain, ...
|
|||
|
|
inputBaselineMps, outputBaselineDegrees, sampleCount, ...
|
|||
|
|
'VariableNames', { ...
|
|||
|
|
'Dataset', 'Wheel', 'SourceFile', 'TransitionType', ...
|
|||
|
|
'ExperimentName', 'EventIndex', 'EventTimeSeconds', ...
|
|||
|
|
'TargetStepDegrees', 'Kp', 'Ki', 'Kd', ...
|
|||
|
|
'DeadZoneDegrees', 'FeedforwardGain', ...
|
|||
|
|
'InputBaselineMps', 'OutputBaselineDegrees', 'SampleCount'});
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function samples = buildSampleTable(segments)
|
|||
|
|
%BUILDSAMPLETABLE Export a tidy long-form table for manual inspection.
|
|||
|
|
totalSamples = sum(arrayfun( ...
|
|||
|
|
@(segment) numel(segment.TimeSeconds), segments));
|
|||
|
|
dataset = cell(totalSamples, 1);
|
|||
|
|
wheel = cell(totalSamples, 1);
|
|||
|
|
experimentName = cell(totalSamples, 1);
|
|||
|
|
sourceFile = cell(totalSamples, 1);
|
|||
|
|
transitionType = cell(totalSamples, 1);
|
|||
|
|
eventIndex = zeros(totalSamples, 1);
|
|||
|
|
timeSeconds = zeros(totalSamples, 1);
|
|||
|
|
uDiffMps = zeros(totalSamples, 1);
|
|||
|
|
actualAngleDeltaDegrees = zeros(totalSamples, 1);
|
|||
|
|
targetAngleDeltaDegrees = zeros(totalSamples, 1);
|
|||
|
|
kp = zeros(totalSamples, 1);
|
|||
|
|
deadZoneDegrees = zeros(totalSamples, 1);
|
|||
|
|
|
|||
|
|
firstRow = 1;
|
|||
|
|
for index = 1:numel(segments)
|
|||
|
|
segment = segments(index);
|
|||
|
|
count = numel(segment.TimeSeconds);
|
|||
|
|
rows = (firstRow:(firstRow + count - 1))';
|
|||
|
|
dataset(rows) = repmat({segment.Dataset}, count, 1);
|
|||
|
|
wheel(rows) = repmat({segment.Wheel}, count, 1);
|
|||
|
|
experimentName(rows) = repmat( ...
|
|||
|
|
{segment.ExperimentName}, count, 1);
|
|||
|
|
sourceFile(rows) = repmat({segment.SourceFile}, count, 1);
|
|||
|
|
transitionType(rows) = repmat( ...
|
|||
|
|
{segment.TransitionType}, count, 1);
|
|||
|
|
eventIndex(rows) = segment.EventIndex;
|
|||
|
|
timeSeconds(rows) = segment.TimeSeconds;
|
|||
|
|
uDiffMps(rows) = segment.UDiffMps;
|
|||
|
|
actualAngleDeltaDegrees(rows) = segment.ActualAngleDeltaDegrees;
|
|||
|
|
targetAngleDeltaDegrees(rows) = segment.TargetAngleDeltaDegrees;
|
|||
|
|
kp(rows) = segment.Kp;
|
|||
|
|
deadZoneDegrees(rows) = segment.DeadZoneDegrees;
|
|||
|
|
firstRow = firstRow + count;
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
samples = table( ...
|
|||
|
|
dataset, wheel, experimentName, sourceFile, transitionType, ...
|
|||
|
|
eventIndex, timeSeconds, uDiffMps, ...
|
|||
|
|
actualAngleDeltaDegrees, targetAngleDeltaDegrees, ...
|
|||
|
|
kp, deadZoneDegrees, ...
|
|||
|
|
'VariableNames', { ...
|
|||
|
|
'Dataset', 'Wheel', 'ExperimentName', 'SourceFile', ...
|
|||
|
|
'TransitionType', 'EventIndex', 'TimeSeconds', ...
|
|||
|
|
'UDiffMps', 'ActualAngleDeltaDegrees', ...
|
|||
|
|
'TargetAngleDeltaDegrees', 'Kp', 'DeadZoneDegrees'});
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function segments = emptySegmentArray()
|
|||
|
|
%EMPTYSEGMENTARRAY Return an empty struct with the stable segment schema.
|
|||
|
|
template = struct( ...
|
|||
|
|
'Dataset', '', ...
|
|||
|
|
'Wheel', '', ...
|
|||
|
|
'SourceFile', '', ...
|
|||
|
|
'TransitionType', '', ...
|
|||
|
|
'EventIndex', 0, ...
|
|||
|
|
'ExperimentName', '', ...
|
|||
|
|
'EventTimeSeconds', 0, ...
|
|||
|
|
'TargetStepDegrees', 0, ...
|
|||
|
|
'Kp', 0, ...
|
|||
|
|
'Ki', 0, ...
|
|||
|
|
'Kd', 0, ...
|
|||
|
|
'DeadZoneDegrees', 0, ...
|
|||
|
|
'FeedforwardGain', 0, ...
|
|||
|
|
'InputBaselineMps', 0, ...
|
|||
|
|
'OutputBaselineDegrees', 0, ...
|
|||
|
|
'TimeSeconds', zeros(0, 1), ...
|
|||
|
|
'UDiffMps', zeros(0, 1), ...
|
|||
|
|
'ActualAngleDeltaDegrees', zeros(0, 1), ...
|
|||
|
|
'TargetAngleDeltaDegrees', zeros(0, 1));
|
|||
|
|
segments = template([]);
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function printSegmentCounts(estimationSegments, validationSegments, wheels)
|
|||
|
|
%PRINTSEGMENTCOUNTS Report the experiment count for every output iddata pair.
|
|||
|
|
for wheelIndex = 1:numel(wheels)
|
|||
|
|
wheelCode = wheels(wheelIndex).Code;
|
|||
|
|
estimationCount = nnz(strcmp( ...
|
|||
|
|
{estimationSegments.Wheel}, wheelCode));
|
|||
|
|
validationCount = nnz(strcmp( ...
|
|||
|
|
{validationSegments.Wheel}, wheelCode));
|
|||
|
|
fprintf(' %s:估计%d段,验证%d段\n', ...
|
|||
|
|
wheelCode, estimationCount, validationCount);
|
|||
|
|
end
|
|||
|
|
end
|
|||
|
|
|
|||
|
|
function requireFolder(folder)
|
|||
|
|
%REQUIREFOLDER Fail early when the expected experiment folder is absent.
|
|||
|
|
if ~exist(folder, 'dir')
|
|||
|
|
error('prepare_steering_iddata:MissingFolder', ...
|
|||
|
|
'找不到实验目录:%s', folder);
|
|||
|
|
end
|
|||
|
|
end
|