Files

652 lines
27 KiB
Matlab
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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