File indexing completed on 2024-12-22 03:35:49
0001 /* 0002 File : SpiceReader.cpp 0003 Project : LabPlot 0004 Description : Reading spice files 0005 -------------------------------------------------------------------- 0006 SPDX-FileCopyrightText: 2022 Martin Marmsoler <martin.marmsoler@gmail.com> 0007 SPDX-License-Identifier: GPL-2.0-or-later 0008 */ 0009 #include "SpiceReader.h" 0010 0011 #include "backend/lib/macros.h" 0012 0013 #include <QDataStream> 0014 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0015 #include <QStringDecoder> 0016 #else 0017 #include <QTextCodec> 0018 #endif 0019 0020 #include <cmath> 0021 0022 void SpiceFileReader::init() { 0023 bool ok; 0024 0025 mInitialized = true; 0026 mInfoString = QString(); 0027 0028 if (!mFile.isOpen() && !open()) 0029 return; 0030 0031 QTextStream stream(&mFile); 0032 0033 // Determine if ltspice or ngspice or none of both 0034 QByteArray l = mFile.readLine(); 0035 int pos = l.count(); 0036 if (!QLatin1String(l).startsWith(QLatin1String("Title:"))) { 0037 if (!convertLTSpiceBinary(l).startsWith(QLatin1String("Title:"))) 0038 return; 0039 mNgspice = false; 0040 mInfoString += convertLTSpiceBinary(l + mFile.read(1)); // because of utf16 end of line "\n 0x00" the 0x00 must be flushed 0041 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0042 stream.setEncoding(QStringConverter::Utf16); 0043 #else 0044 stream.setCodec(QTextCodec::codecForMib(1015)); 0045 #endif 0046 pos++; 0047 } else // title: removed trailing '\r' and '\n' 0048 addInfoStringLine(QLatin1String(l).trimmed()); 0049 0050 QString line = stream.readLine(); 0051 if (!line.startsWith(QLatin1String("Date:"))) 0052 return; 0053 mDatetime = QDateTime::fromString(line.split(QLatin1String("Date:"))[1].simplified()); // TODO: set format 0054 addInfoStringLine(line); 0055 0056 line = stream.readLine(); 0057 if (!line.startsWith(QLatin1String("Plotname:"))) 0058 return; 0059 mPlotName = line.split(QLatin1String("Plotname:"))[1].simplified(); 0060 mMode = plotNameToPlotMode(mPlotName); 0061 addInfoStringLine(line); 0062 0063 line = stream.readLine(); 0064 if (!line.startsWith(QLatin1String("Flags:"))) 0065 return; 0066 mFlags = parseFlags(line.split(QLatin1String("Flags:"))[1].simplified()); 0067 addInfoStringLine(line); 0068 0069 line = stream.readLine(); 0070 if (!line.startsWith(QLatin1String("No. Variables:"))) 0071 return; 0072 addInfoStringLine(line); 0073 int numberVariables = line.split(QLatin1String("No. Variables:"))[1].simplified().toInt(&ok); 0074 if (!ok) 0075 return; 0076 0077 line = stream.readLine(); 0078 if (!line.startsWith(QLatin1String("No. Points:"))) 0079 return; 0080 addInfoStringLine(line); 0081 mNumberPoints = line.split(QLatin1String("No. Points:"))[1].simplified().toInt(&ok); 0082 if (!ok) 0083 return; 0084 0085 if (!mNgspice) { 0086 line = stream.readLine(); 0087 if (!line.startsWith(QLatin1String("Offset:"))) // LTSpice specific 0088 return; 0089 addInfoStringLine(line); 0090 mOffset = line.split(QLatin1String("Offset:"))[1].simplified().toDouble(&ok); 0091 if (!ok) 0092 return; 0093 } 0094 0095 line = stream.readLine(); 0096 if (!mNgspice) { 0097 while (!line.startsWith(QLatin1String("Variables:")) && !stream.atEnd()) { 0098 auto list = line.split(QLatin1Char(':')); 0099 if (list.length() < 2) 0100 return; 0101 addInfoStringLine(line); 0102 mLtSpiceOptions.insert(list[0].simplified(), list[1].simplified()); 0103 line = stream.readLine(); 0104 } 0105 } 0106 0107 if (!line.startsWith(QLatin1String("Variables:"))) { 0108 DEBUG("SpiceReader: line does not start with the Variables key: " << line.toStdString()); 0109 return; 0110 } 0111 addInfoStringLine(line); 0112 0113 mVariables.resize(numberVariables); 0114 for (int i = 0; i < numberVariables; i++) { 0115 line = stream.readLine(); 0116 auto sl = line.split(QLatin1Char('\t')); 0117 if (sl.length() < 4) 0118 return; 0119 auto index = sl.at(1).toInt(&ok); 0120 if (!ok) 0121 return; 0122 mVariables[i] = {index, sl[2], sl[3]}; 0123 addInfoStringLine(line); 0124 } 0125 0126 line = stream.readLine(); 0127 mBinary = line.startsWith(QStringLiteral("Binary")); 0128 0129 pos += stream.pos(); 0130 // mFile must be reset and then seeked, because above the stream is used to read and then QFile fails 0131 // when mixing them together 0132 // Set pos to first databyte 0133 mFile.reset(); 0134 mFile.seek(pos); 0135 0136 mValid = true; 0137 } 0138 0139 bool SpiceFileReader::open() { 0140 if (!mFile.open(QIODevice::ReadOnly)) { 0141 DEBUG("Failed to open the file " << STDSTRING(mFilename)); 0142 return false; 0143 } 0144 return true; 0145 } 0146 0147 int SpiceFileReader::readData(std::vector<void*>& data, int skipLines, int maxLines) { 0148 if (!mInitialized) 0149 init(); 0150 0151 const bool isComplex = !isReal(); 0152 const int numberValuesPerVariable = 1 + isComplex; 0153 0154 if (data.size() < (uint)(mVariables.count() * numberValuesPerVariable)) 0155 return 0; 0156 0157 // Assumption: if not a nullptr is in the array, 0158 // it is a valid pointer to an array 0159 for (uint i = 0; i < data.size(); i++) { 0160 if (data.at(i) == nullptr) 0161 return 0; 0162 } 0163 0164 int linesRead = 0; 0165 if (mBinary) { 0166 // NgSpice: All data are 64bit 0167 // LtSpice: AC (complex): all data are 64bit 0168 // LtSpice: Transient: time is 64bit, y data is 32bit 0169 // LtSpice: Transient (double flag set): all data are 64bit 0170 const int yDataBytes = mNgspice ? 8 : ((isComplex | isDouble()) + 1) * 4; // in ltspice the values are stored normaly as single precision 0171 // the lines multiplied with the number of bytes per lines gives the number of bytes to read 0172 const int lineBytes = (8 + yDataBytes * (mVariables.count() - 1)) * numberValuesPerVariable; 0173 int patchesCount = 0; 0174 if (skipLines > 0) 0175 mFile.read(skipLines * lineBytes); 0176 while (!mFile.atEnd()) { 0177 const QByteArray ba = mFile.read(mNumberLines * lineBytes); 0178 const char* binary = ba.data(); 0179 const int length = ba.length(); 0180 if (length % lineBytes != 0) { 0181 DEBUG(Q_FUNC_INFO << ": The data is corrupted") 0182 return 0; 0183 } 0184 const int readLines = (int)(length / lineBytes); 0185 const int patchesIndexOffset = patchesCount * mNumberLines; 0186 0187 for (int l = 0; l < std::min(readLines, mNumberLines); l++) { 0188 // time / frequency real part 0189 const int lineNumber = l * lineBytes; 0190 double value; 0191 memcpy(&value, &binary[lineNumber], 8); 0192 0193 if (!mNgspice && mLTSpiceBug) { 0194 // Bug in the ltspice binary raw format 0195 // For more information see MR !108 0196 value = std::fabs(value); 0197 } 0198 0199 (*static_cast<QVector<double>*>(data[0]))[patchesIndexOffset + l] = value; 0200 0201 if (isComplex) { 0202 // time / frequency imaginary part 0203 memcpy(&value, &binary[lineNumber + 1 * 8], 8); 0204 (*static_cast<QVector<double>*>(data[1]))[patchesIndexOffset + l] = value; 0205 } 0206 0207 for (int i = numberValuesPerVariable; i < mVariables.count() * numberValuesPerVariable; i++) { 0208 const int lineIndex = 8 * numberValuesPerVariable + (i - numberValuesPerVariable) * yDataBytes; 0209 if (lineIndex % (numberValuesPerVariable * 4) != 0) 0210 return linesRead; 0211 if (yDataBytes == 4) { 0212 float f = 0; 0213 memcpy(&f, &binary[lineNumber + lineIndex], yDataBytes); 0214 value = f; 0215 } else { 0216 memcpy(&value, &binary[lineNumber + lineIndex], yDataBytes); 0217 } 0218 (*static_cast<QVector<double>*>(data[i]))[patchesIndexOffset + l] = value; 0219 } 0220 linesRead++; 0221 if (maxLines > 0 && linesRead >= maxLines) 0222 return linesRead; 0223 } 0224 patchesCount++; 0225 } 0226 } else { // Ascii 0227 QTextStream stream(&mFile); 0228 if (!mNgspice) 0229 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0230 stream.setEncoding(QStringConverter::Utf16); 0231 #else 0232 stream.setCodec(QTextCodec::codecForMib(1015)); 0233 #endif 0234 0235 for (int s = 0; s < skipLines; s++) { 0236 for (int i = 0; i < mVariables.count(); i++) 0237 stream.readLine(); 0238 stream.readLine(); // read empty line 0239 } 0240 0241 QString line; 0242 QLocale locale(QLocale::C); 0243 bool isNumber(false); 0244 linesRead = 0; // indexes the position in the vector(column). Because of the continue the loop index cannot be used 0245 const int points = maxLines > 0 ? std::min(mNumberPoints - skipLines, maxLines) : mNumberPoints - skipLines; 0246 for (int l = 0; l < points; l++) { 0247 for (int j = 0; j < mVariables.count(); j++) { 0248 line = stream.readLine(); 0249 QStringList tokens = line.split(QLatin1Char('\t')); 0250 0251 // skip lines that don't contain the proper number of tokens (wrong format, corrupted file) 0252 if (tokens.size() < 2) 0253 continue; 0254 0255 QString valueString = tokens.at(1).simplified(); // string containing the value(s), 0 is the index of the data 0256 if (isComplex) { 0257 QStringList realImgTokens = valueString.split(QLatin1Char(',')); 0258 if (realImgTokens.size() == 2) { // sanity check to make sure we really have both parts 0259 // real part 0260 double value = locale.toDouble(realImgTokens.at(0), &isNumber); 0261 static_cast<QVector<double>*>(data[2 * j])->operator[](linesRead) = (isNumber ? value : NAN); 0262 0263 // imaginary part 0264 value = locale.toDouble(realImgTokens.at(1), &isNumber); 0265 static_cast<QVector<double>*>(data[2 * j + 1])->operator[](linesRead) = (isNumber ? value : NAN); 0266 } 0267 } else { 0268 const double value = locale.toDouble(valueString, &isNumber); 0269 auto* v = static_cast<QVector<double>*>(data[j]); 0270 v->operator[](linesRead) = (isNumber ? value : NAN); 0271 } 0272 } 0273 linesRead++; 0274 stream.readLine(); // read the empty line between every dataset 0275 if (maxLines > 0 && linesRead >= maxLines) 0276 return linesRead; 0277 } 0278 } 0279 return linesRead; 0280 } 0281 0282 QString SpiceFileReader::convertLTSpiceBinary(const QByteArray& s) { 0283 #if QT_VERSION >= QT_VERSION_CHECK(6, 0, 0) 0284 auto toUtf16 = QStringDecoder(QStringDecoder::Utf16); 0285 return toUtf16(s); 0286 #else 0287 // (1015 is UTF-16, 1014 is UTF-16LE, 1013 is UTF-16BE, 106 is UTF-8) 0288 // https://stackoverflow.com/questions/14131127/qbytearray-to-qstring 0289 return QTextCodec::codecForMib(1015)->toUnicode(s); 0290 #endif 0291 } 0292 0293 SpiceFileReader::PlotMode SpiceFileReader::plotNameToPlotMode(const QString& name) { 0294 mLTSpiceBug = true; 0295 if (name.contains(QLatin1String("Transient"))) 0296 return PlotMode::Transient; 0297 else if (name.contains(QLatin1String("FFT"))) 0298 return PlotMode::FFT; 0299 else if (name.contains(QLatin1String("DC"))) { 0300 mLTSpiceBug = false; 0301 return PlotMode::DC; 0302 } else if (name.contains(QLatin1String("AC"))) 0303 return PlotMode::AC; 0304 else if (name.contains(QLatin1String("Noise"))) 0305 return PlotMode::Noise; 0306 0307 return PlotMode::Unknown; 0308 } 0309 0310 int SpiceFileReader::parseFlags(const QString& s) { 0311 // real, forward, double, complex 0312 0313 auto sl = s.split(QLatin1Char(' ')); 0314 int value = 0; 0315 0316 value |= sl.contains(QLatin1String("real")) ? Flags::real : 0; 0317 value |= sl.contains(QLatin1String("complex")) ? (value & ~Flags::real) : Flags::real; // TODO: check that real and complex are not in the same data 0318 value |= sl.contains(QLatin1String("forward")) ? Flags::forward : 0; 0319 value |= sl.contains(QLatin1String("log")) ? Flags::log : 0; 0320 value |= sl.contains(QLatin1String("double")) ? Flags::yDouble : 0; 0321 return value; 0322 }