优化差速转舵并增加纵向辨识测试

This commit is contained in:
2026-08-27 12:39:22 +08:00
parent 17092c2766
commit 1c5181b327
24 changed files with 1748 additions and 266 deletions
@@ -0,0 +1,651 @@
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