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 }