// MATLAB `.mat`(v7.3 / HDF5)加载器。 // // 支持两种内容(详见 MatLoader.hpp 的说明): // * Simulink SDI / `SimulationData.Dataset` 导出:`#refs#` 里的元素图 + `#sigstream#` 的样本流; // * 普通 v7.3 变量:数值矩阵按"列 = 信号、行 = 采样"读取(时间轴 = 采样序号)。 #include "MatLoader.hpp" #include "MatH5.hpp" #include #include #include #include #include #include namespace jdi { namespace { /** @brief 一个 SDI 信号(或子数据集)的解析结果。 */ struct SdiElement { bool isSignal = false; bool isDataset = false; std::wstring name; std::vector children; /**< 子数据集元素的引用目标。 */ int dataStream = -1; /**< 数据流下标。 */ int timeStream = -1; /**< 时间流下标。 */ }; std::wstring TrimNumber(double value) { wchar_t buffer[64] = {}; if (value == static_cast(static_cast(value))) { std::swprintf(buffer, 64, L"%lld", static_cast(value)); } else { std::swprintf(buffer, 64, L"%g", value); } return buffer; } /** @brief 去掉文件名中的扩展名。 */ std::wstring StemOf(const std::wstring& path) { const std::size_t slash = path.find_last_of(L"\\/"); std::wstring name = (slash == std::wstring::npos) ? path : path.substr(slash + 1); const std::size_t dot = name.find_last_of(L'.'); if (dot != std::wstring::npos && dot > 0) { name.resize(dot); } return name; } /** @brief 读取一个「流」的全部 double 样本(`#sigstream#//#data#`)。 */ bool ReadStream(const MatHdf5& file, std::uint64_t streamObject, std::vector& out) { out.clear(); const std::uint64_t data = file.Member(streamObject, "#data#"); if (data == 0) { return false; } MatHdf5::Dataset dataset; if (!file.ReadDataset(data, dataset) || !dataset.ok) { return false; } // ⚠️ SDI 的 `#data#` 是**字节流**(`uint8` 数据集,长度 = 采样数 × 8), // 而不是 double 数据集:必须按 8 字节一组重新解释成 double(`#sampleSize#` 可印证)。 if (dataset.data.size() % sizeof(double) != 0) { return false; } const std::size_t count = dataset.data.size() / sizeof(double); out.resize(count); std::memcpy(out.data(), dataset.data.data(), count * sizeof(double)); return true; } /** @brief 解析 `#refs#` 里的一个元素(信号 / 子数据集)。 */ void ParseElement(const MatHdf5& file, std::uint64_t address, SdiElement& element) { element = SdiElement(); std::wstring elementType; file.ReadText(file.Member(address, "ElementType"), elementType); file.ReadText(file.Member(address, "Name"), element.name); const std::uint64_t values = file.Member(address, "Values"); if (elementType == L"dataset") { element.isDataset = true; if (values != 0) { file.ReadReferences(values, element.children); } return; } element.isSignal = true; if (values == 0) { return; } // 名字为空时退回 timeseries 自己的 Name。 if (element.name.empty()) { file.ReadText(file.Member(values, "Name"), element.name); } std::vector numbers; if (file.ReadNumbers(file.Member(values, "DataR2"), numbers) && !numbers.empty()) { element.dataStream = static_cast(numbers.front()); } numbers.clear(); if (file.ReadNumbers(file.Member(values, "TimeR2"), numbers) && !numbers.empty()) { element.timeStream = static_cast(numbers.front()); } } /** @brief 追加一条信号(时间 + 数值)到某个分组的时间轴块集合。 */ void AppendSignal(LoadedSource& source, const std::wstring& name, std::vector&& time, std::vector&& values) { if (time.empty() || values.empty()) { return; } TableBlock block; block.timeHeader = L"time"; DataTable& table = block.table; table.headers.push_back(L"time"); table.headers.push_back(name); table.rowCount = std::min(time.size(), values.size()); time.resize(table.rowCount); values.resize(table.rowCount); table.columns.push_back(std::move(time)); table.columns.push_back(std::move(values)); source.blocks.push_back(std::move(block)); } /** @brief SDI(`SimulationData.Dataset`)结构的分组读取。 */ bool LoadSdi(const MatHdf5& file, std::uint64_t refs, std::uint64_t sigstream, const std::wstring& stem, std::vector& groups, std::wstring& error) { // 1) 枚举 `#refs#` 里的全部元素。 std::map elements; for (const auto& member : file.Members(refs)) { if (file.IsDataset(member.second)) { continue; } SdiElement element; ParseElement(file, member.second, element); if (element.isSignal || element.isDataset) { elements[member.second] = std::move(element); } } if (elements.empty()) { error = L"文件中没有 Simulink 数据元素。"; return false; } // 2) 找出"根"数据集:没有被任何其它数据集当作子元素引用的那些。 std::set childAddresses; for (const auto& entry : elements) { for (const std::uint64_t child : entry.second.children) { childAddresses.insert(child); } } std::vector roots; for (const auto& entry : elements) { if (!entry.second.isDataset) { continue; } if (childAddresses.count(entry.first) == 0) { roots.push_back(entry.first); } } if (roots.empty()) { // 没有任何子数据集关系:所有信号归入同一组。 for (const auto& entry : elements) { if (entry.second.isSignal) { roots.push_back(entry.first); } } } // 3) 深度优先遍历:路径上的数据集名字用 `-` 连接成分组名。 struct Pending { std::uint64_t address = 0; std::vector path; int depth = 0; }; std::vector stack; for (const std::uint64_t root : roots) { const auto found = elements.find(root); if (found == elements.end()) { continue; } Pending pending; pending.address = root; if (found->second.isDataset && !found->second.name.empty()) { pending.path.push_back(found->second.name); } stack.push_back(std::move(pending)); } std::map groupIndex; std::vector> streams; // 流编号 → 样本 std::vector streamCached; const auto streamFor = [&](int index) -> const std::vector* { if (index < 0) { return nullptr; } if (streams.size() <= static_cast(index)) { streams.resize(static_cast(index) + 1); streamCached.resize(static_cast(index) + 1, false); } if (!streamCached[static_cast(index)]) { const std::uint64_t streamObject = file.Member(sigstream, std::to_string(index)); if (streamObject == 0 || !ReadStream(file, streamObject, streams[static_cast(index)])) { streams[static_cast(index)].clear(); } streamCached[static_cast(index)] = true; } return streams[static_cast(index)].empty() ? nullptr : &streams[static_cast(index)]; }; int unnamedSignals = 0; while (!stack.empty()) { Pending pending = std::move(stack.back()); stack.pop_back(); const auto found = elements.find(pending.address); if (found == elements.end()) { continue; } const SdiElement& element = found->second; if (element.isDataset) { for (const std::uint64_t child : element.children) { Pending next; next.address = child; next.path = pending.path; const auto childElement = elements.find(child); if (childElement != elements.end() && childElement->second.isDataset && !childElement->second.name.empty()) { next.path.push_back(childElement->second.name); } stack.push_back(std::move(next)); } continue; } if (!element.isSignal) { continue; } const std::vector* time = streamFor(element.timeStream); const std::vector* values = streamFor(element.dataStream); if (values == nullptr) { continue; } std::vector timeCopy; if (time != nullptr && time->size() == values->size()) { timeCopy = *time; } else { // 时间流缺失/长度不符:退化为采样序号(秒),保证信号仍可显示。 timeCopy.resize(values->size()); for (std::size_t index = 0; index < values->size(); ++index) { timeCopy[index] = static_cast(index); } } std::wstring groupName; for (const std::wstring& piece : pending.path) { if (!groupName.empty()) { groupName += L"-"; } groupName += piece; } if (groupName.empty()) { groupName = stem; } std::wstring signalName = element.name; if (signalName.empty()) { signalName = L"信号" + std::to_wstring(++unnamedSignals); } std::size_t index = 0; const auto existing = groupIndex.find(groupName); if (existing == groupIndex.end()) { LoadedSourceGroup group; group.name = groupName; group.source.sourceName = groupName; groups.push_back(std::move(group)); index = groups.size() - 1; groupIndex.emplace(groupName, index); } else { index = existing->second; } AppendSignal(groups[index].source, signalName, std::move(timeCopy), std::vector(*values)); } if (groups.empty()) { error = L"文件中没有可用的信号数据。"; return false; } return true; } /** @brief 普通 v7.3 变量:数值矩阵 / 结构体。 */ void LoadPlainObject(const MatHdf5& file, std::uint64_t address, const std::wstring& name, LoadedSource& source) { if (file.IsDataset(address)) { std::vector numbers; if (!file.ReadNumbers(address, numbers) || numbers.empty()) { return; } MatHdf5::Dataset dataset; file.ReadDataset(address, dataset); std::size_t rows = 1; std::size_t columns = 1; if (dataset.dims.size() >= 2) { rows = dataset.dims[0]; columns = dataset.dims[1]; } else if (dataset.dims.size() == 1) { rows = dataset.dims[0]; } if (rows == 0 || columns == 0 || rows * columns != numbers.size()) { return; } // 列 = 信号、行 = 采样(与 MATLAB 的列优先存储一致)。 for (std::size_t column = 0; column < columns; ++column) { std::vector values(rows); for (std::size_t row = 0; row < rows; ++row) { values[row] = numbers[column * rows + row]; } std::vector time(rows); for (std::size_t row = 0; row < rows; ++row) { time[row] = static_cast(row); } const std::wstring signalName = (columns == 1) ? name : (name + L"(" + TrimNumber(static_cast(column + 1)) + L")"); AppendSignal(source, signalName, std::move(time), std::move(values)); } return; } // 结构体:字段各自作为信号(子结构体递归)。 for (const auto& member : file.Members(address)) { if (member.first.empty()) { continue; } std::wstring childName(member.first.begin(), member.first.end()); LoadPlainObject(file, member.second, childName, source); } } } // namespace bool LoadMatSourceGroups(const std::wstring& filePath, std::vector& groups, std::wstring& error, const LoadProgress& progress) { error.clear(); groups.clear(); MatHdf5 file; if (!file.Open(filePath, error)) { return false; } const std::wstring stem = StemOf(filePath); const std::uint64_t refs = file.Member(file.Root(), "#refs#"); const std::uint64_t sigstream = file.Member(file.Root(), "#sigstream#"); if (refs != 0 && sigstream != 0) { if (!LoadSdi(file, refs, sigstream, stem, groups, error)) { return false; } } else { LoadedSourceGroup group; group.source.sourceName = stem; for (const auto& member : file.Members(file.Root())) { if (member.first.empty() || member.first[0] == '#') { continue; } std::wstring name(member.first.begin(), member.first.end()); LoadPlainObject(file, member.second, name, group.source); } if (!group.source.HasData()) { error = L".mat 文件中没有可用的数值数据。"; return false; } groups.push_back(std::move(group)); } if (progress) { LoadProgressInfo info; info.ratio = 1.0; progress(info); } return true; } } // namespace jdi