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 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 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 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 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 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