From 9e34c3e9ba8b636bd0d6b0dc6358dc94170c5d91 Mon Sep 17 00:00:00 2001 From: Bhupesh Cholake Date: Tue, 24 Mar 2026 13:58:05 +0530 Subject: [PATCH] Add SD-loaded data and pretrained model support --- PLSduino.cpp | 77 +++++++++- PLSduino.h | 12 ++ PLSduinoIO.cpp | 134 ++++++++++++++++ PLSduinoIO.h | 26 ++++ README.md | 26 +++- .../B.csv | 3 + .../meanX.csv | 1 + .../meanY.csv | 1 + .../predictX.csv | 5 + .../predict_from_pretrained_model_SDCARD.ino | 110 ++++++++++++++ .../read_XY_from_SDCARD.ino | 143 +++++++----------- 11 files changed, 440 insertions(+), 98 deletions(-) create mode 100644 PLSduinoIO.cpp create mode 100644 PLSduinoIO.h create mode 100644 examples/predict_from_pretrained_model_SDCARD/B.csv create mode 100644 examples/predict_from_pretrained_model_SDCARD/meanX.csv create mode 100644 examples/predict_from_pretrained_model_SDCARD/meanY.csv create mode 100644 examples/predict_from_pretrained_model_SDCARD/predictX.csv create mode 100644 examples/predict_from_pretrained_model_SDCARD/predict_from_pretrained_model_SDCARD.ino diff --git a/PLSduino.cpp b/PLSduino.cpp index ee731cd..7968026 100644 --- a/PLSduino.cpp +++ b/PLSduino.cpp @@ -24,15 +24,73 @@ PLS::PLS(int brate) PLS::PLS( const MatrixXf &B, const MatrixXf &meanX, - const MatrixXf &meanY ) : B(B), mean0X(meanX), mean0Y(meanY) + const MatrixXf &meanY ) { - // nothing to do + setModel(B, meanX, meanY); } PLS::~PLS() { //Default destructor } + +bool PLS::isModelShapeValid( + const MatrixXf &B, + const MatrixXf &meanX, + const MatrixXf &meanY ) const +{ + if (B.rows() == 0 || B.cols() == 0) { + return false; + } + + if (meanX.rows() != 1 || meanY.rows() != 1) { + return false; + } + + if (meanX.cols() != B.rows() || meanY.cols() != B.cols()) { + return false; + } + + return true; +} + +void PLS::printModelShapeError( + const MatrixXf &B, + const MatrixXf &meanX, + const MatrixXf &meanY ) const +{ + Serial.println("Invalid model dimensions"); + Serial.print("B: "); + Serial.print(B.rows()); + Serial.print("x"); + Serial.println(B.cols()); + Serial.print("meanX: "); + Serial.print(meanX.rows()); + Serial.print("x"); + Serial.println(meanX.cols()); + Serial.print("meanY: "); + Serial.print(meanY.rows()); + Serial.print("x"); + Serial.println(meanY.cols()); + Serial.println("Expected meanX to be 1xB.rows and meanY to be 1xB.cols"); +} + +bool PLS::setModel( + const MatrixXf &B, + const MatrixXf &meanX, + const MatrixXf &meanY ) +{ + if (!isModelShapeValid(B, meanX, meanY)) { + printModelShapeError(B, meanX, meanY); + return false; + } + + this->B = B; + this->mean0X = meanX; + this->mean0Y = meanY; + return true; +} + void PLS::train( const MatrixXf &Xdata, const MatrixXf &Ydata, @@ -44,6 +102,11 @@ void PLS::train( return; } + if (Xdata.rows() == 0 || Xdata.cols() == 0 || Ydata.cols() == 0) { + Serial.println("X and Y must not be empty"); + return; + } + MatrixXf X, Y; X = Xdata; mean0X = X.colwise().mean(); @@ -116,6 +179,16 @@ void PLS::train( MatrixXf PLS::predict( const MatrixXf &v ) const { + if (B.rows() == 0 || B.cols() == 0) { + Serial.println("Cannot predict: model is not initialized"); + return MatrixXf(); + } + + if (v.cols() != B.rows()) { + Serial.println("Cannot predict: input column count does not match model"); + return MatrixXf(); + } + MatrixXf temp; MatrixXf result = MatrixXf::Zero(v.rows(),B.cols()); temp = v; diff --git a/PLSduino.h b/PLSduino.h index b193561..19d603c 100644 --- a/PLSduino.h +++ b/PLSduino.h @@ -18,12 +18,24 @@ class PLS inline void display( const char *name, const MatrixXf &value ); + bool isModelShapeValid( + const MatrixXf &B, + const MatrixXf &meanX, + const MatrixXf &meanY ) const; + void printModelShapeError( + const MatrixXf &B, + const MatrixXf &meanX, + const MatrixXf &meanY ) const; public: PLS( ); PLS(int brate ); PLS( const MatrixXf &B, const MatrixXf &meanX, const MatrixXf &meanY ); ~PLS(); + bool setModel( + const MatrixXf &B, + const MatrixXf &meanX, + const MatrixXf &meanY ); void train( const MatrixXf &Xdata, diff --git a/PLSduinoIO.cpp b/PLSduinoIO.cpp new file mode 100644 index 0000000..d50ec33 --- /dev/null +++ b/PLSduinoIO.cpp @@ -0,0 +1,134 @@ +/* + PLSduinoIO.cpp - CSV loading helpers for PLSduino. +*/ + +#include "PLSduinoIO.h" +#include + +namespace +{ + bool parseLine( + const String &rawLine, + int expectedColumns, + MatrixXf &row ) + { + String line = rawLine; + line.trim(); + if (line.length() == 0) { + return false; + } + + int columnCount = 1; + for (int i = 0; i < line.length(); ++i) { + if (line.charAt(i) == ',') { + columnCount++; + } + } + + if (expectedColumns > 0 && columnCount != expectedColumns) { + Serial.println("CSV parse error: inconsistent column count"); + return false; + } + + row.resize(1, columnCount); + int start = 0; + for (int col = 0; col < columnCount; ++col) { + int end = line.indexOf(',', start); + if (end < 0) { + end = line.length(); + } + + String token = line.substring(start, end); + token.trim(); + if (token.length() == 0) { + Serial.println("CSV parse error: empty value"); + return false; + } + + const char *buffer = token.c_str(); + char *endPtr = NULL; + float value = strtof(buffer, &endPtr); + if (endPtr == buffer || *endPtr != '\0') { + Serial.print("CSV parse error: invalid float '"); + Serial.print(token); + Serial.println("'"); + return false; + } + + row(0, col) = value; + start = end + 1; + } + + return true; + } +} + +bool PLSIO::loadMatrixFromCSV( + Stream &stream, + MatrixXf &matrix ) +{ + matrix.resize(0, 0); + int rowCount = 0; + int expectedColumns = -1; + + while (stream.available()) { + String line = stream.readStringUntil('\n'); + String trimmed = line; + trimmed.trim(); + if (trimmed.length() == 0) { + continue; + } + + MatrixXf row; + if (!parseLine(trimmed, expectedColumns, row)) { + matrix.resize(0, 0); + return false; + } + + if (expectedColumns < 0) { + expectedColumns = row.cols(); + matrix = row; + rowCount = 1; + continue; + } + + matrix.conservativeResize(rowCount + 1, expectedColumns); + matrix.row(rowCount) = row; + rowCount++; + } + + if (rowCount == 0) { + Serial.println("CSV parse error: no data found"); + return false; + } + + return true; +} + +bool PLSIO::loadModelFromCSV( + Stream &bStream, + Stream &meanXStream, + Stream &meanYStream, + PLS &pls ) +{ + MatrixXf B; + MatrixXf meanX; + MatrixXf meanY; + + if (!loadMatrixFromCSV(bStream, B)) { + Serial.println("Failed to load B matrix"); + return false; + } + + if (!loadMatrixFromCSV(meanXStream, meanX)) { + Serial.println("Failed to load meanX matrix"); + return false; + } + + if (!loadMatrixFromCSV(meanYStream, meanY)) { + Serial.println("Failed to load meanY matrix"); + return false; + } + + return pls.setModel(B, meanX, meanY); +} diff --git a/PLSduinoIO.h b/PLSduinoIO.h new file mode 100644 index 0000000..4a01f7f --- /dev/null +++ b/PLSduinoIO.h @@ -0,0 +1,26 @@ +/* + PLSduinoIO.h - CSV loading helpers for PLSduino. +*/ +#ifndef PLSduinoIO_h +#define PLSduinoIO_h + +#include "Arduino.h" +#include +#include "PLSduino.h" + +using Eigen::MatrixXf; + +namespace PLSIO +{ + bool loadMatrixFromCSV( + Stream &stream, + MatrixXf &matrix ); + + bool loadModelFromCSV( + Stream &bStream, + Stream &meanXStream, + Stream &meanYStream, + PLS &pls ); +} + +#endif diff --git a/README.md b/README.md index d77901d..967c0df 100644 --- a/README.md +++ b/README.md @@ -10,17 +10,31 @@ Partial Least Squares (PLS) is a statistical technique that helps to model relat Usage: ========== Download all the source files. -There is one example for the user to use. ->* **examples/basic_usage/basic_usage.ino**,this example shows a simple usage using predefined matrices -## TODO ->* **examples/read_XY_from_SD_card/read_from_XY_from_SD.ino**, this example shows how to read X (features matrice) and Y (response matrice) from a file saved in an SD card, for example. +There are multiple examples for common workflows. +>* **examples/basic_usage/basic_usage.ino**, this example shows simple training and prediction using predefined matrices. +>* **examples/read_XY_from_SDCARD/read_XY_from_SDCARD.ino**, this example shows how to read X (feature matrix) and Y (response matrix) from CSV files stored on an SD-backed filesystem. +>* **examples/predict_from_pretrained_model_SDCARD/predict_from_pretrained_model_SDCARD.ino**, this example shows how to load a pretrained model from CSV files and call `predict()` directly without running `train()`. Notice: ============ >1. This library depends on the Arduino Eigen/Dense library. ->2. Arduino/ESP32 are quite limited in computational capabilities. You can also load a pre-trained matrix B and use it to predict -Y (as long as you keep the dimensionality consistent). +>2. Arduino/ESP32 are quite limited in computational capabilities. For large matrices you can skip training on-device by loading a pretrained model and calling `predict()` directly. +>3. Prediction-only mode requires **all three** model components: `B`, `meanX`, and `meanY`. Loading `B` alone is not enough because the library centers X and restores the Y offset during prediction. + + +CSV format: +=============== +All CSV helpers expect comma-separated float values with one matrix row per line. +Blank lines are ignored. + +>* **X.csv**: `n_samples x n_features` +>* **Y.csv**: `n_samples x n_outputs` +>* **B.csv**: `n_features x n_outputs` +>* **meanX.csv**: `1 x n_features` +>* **meanY.csv**: `1 x n_outputs` + +The parser rejects ragged rows, empty values, and non-numeric values. Reference: diff --git a/examples/predict_from_pretrained_model_SDCARD/B.csv b/examples/predict_from_pretrained_model_SDCARD/B.csv new file mode 100644 index 0000000..2f4f5ea --- /dev/null +++ b/examples/predict_from_pretrained_model_SDCARD/B.csv @@ -0,0 +1,3 @@ +2.0 +3.0 +-1.0 diff --git a/examples/predict_from_pretrained_model_SDCARD/meanX.csv b/examples/predict_from_pretrained_model_SDCARD/meanX.csv new file mode 100644 index 0000000..ef03e40 --- /dev/null +++ b/examples/predict_from_pretrained_model_SDCARD/meanX.csv @@ -0,0 +1 @@ +0.0,0.0,0.0 diff --git a/examples/predict_from_pretrained_model_SDCARD/meanY.csv b/examples/predict_from_pretrained_model_SDCARD/meanY.csv new file mode 100644 index 0000000..ba66466 --- /dev/null +++ b/examples/predict_from_pretrained_model_SDCARD/meanY.csv @@ -0,0 +1 @@ +0.0 diff --git a/examples/predict_from_pretrained_model_SDCARD/predictX.csv b/examples/predict_from_pretrained_model_SDCARD/predictX.csv new file mode 100644 index 0000000..8085812 --- /dev/null +++ b/examples/predict_from_pretrained_model_SDCARD/predictX.csv @@ -0,0 +1,5 @@ +1.0,2.0,3.0 +4.0,5.0,6.0 +7.0,8.0,9.0 +10.0,11.0,12.0 +13.0,14.0,15.0 diff --git a/examples/predict_from_pretrained_model_SDCARD/predict_from_pretrained_model_SDCARD.ino b/examples/predict_from_pretrained_model_SDCARD/predict_from_pretrained_model_SDCARD.ino new file mode 100644 index 0000000..ce0039b --- /dev/null +++ b/examples/predict_from_pretrained_model_SDCARD/predict_from_pretrained_model_SDCARD.ino @@ -0,0 +1,110 @@ +/* +This code was tested on ESP32 S3 Vroom which has an +embedded SD card already. Other boards may need a +different SD initialization step. +*/ +#include "Arduino.h" +#include "FS.h" +#include "SD_MMC.h" +#include +#include +#include + +#define SD_MMC_CMD 38 // Please do not modify it. +#define SD_MMC_CLK 39 // Please do not modify it. +#define SD_MMC_D0 40 // Please do not modify it. + +int baudrate = 115200; +PLS pls(baudrate); + +bool readMatrix(fs::FS &fs, const char *path, MatrixXf &matrix); +bool readModel(fs::FS &fs, const char *bPath, const char *meanXPath, const char *meanYPath); +void printMatrix(const MatrixXf& mat); + +void setup() { + Serial.begin(baudrate); + + SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_D0); + int mounted = SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_DEFAULT, 5); + if (!mounted) { + Serial.printf("Card mount failed: %d\r\n", mounted); + return; + } + + if (!readModel(SD_MMC, "/B.csv", "/meanX.csv", "/meanY.csv")) { + return; + } + + MatrixXf X; + if (!readMatrix(SD_MMC, "/predictX.csv", X)) { + return; + } + + MatrixXf prediction = pls.predict(X); + if (prediction.size() == 0) { + return; + } + + Serial.println("Prediction input X"); + printMatrix(X); + Serial.println("Predicted Y"); + printMatrix(prediction); +} + +void loop() { +} + +bool readMatrix(fs::FS &fs, const char *path, MatrixXf &matrix) { + Serial.printf("Reading file: %s\n", path); + + File file = fs.open(path); + if (!file) { + Serial.println("Failed to open file for reading"); + return false; + } + + bool ok = PLSIO::loadMatrixFromCSV(file, matrix); + file.close(); + return ok; +} + +bool readModel(fs::FS &fs, const char *bPath, const char *meanXPath, const char *meanYPath) { + Serial.println("Loading pretrained model"); + + File bFile = fs.open(bPath); + if (!bFile) { + Serial.println("Failed to open B matrix file"); + return false; + } + + File meanXFile = fs.open(meanXPath); + if (!meanXFile) { + Serial.println("Failed to open meanX file"); + bFile.close(); + return false; + } + + File meanYFile = fs.open(meanYPath); + if (!meanYFile) { + Serial.println("Failed to open meanY file"); + bFile.close(); + meanXFile.close(); + return false; + } + + bool ok = PLSIO::loadModelFromCSV(bFile, meanXFile, meanYFile, pls); + bFile.close(); + meanXFile.close(); + meanYFile.close(); + return ok; +} + +void printMatrix(const MatrixXf& mat) { + for (int i = 0; i < mat.rows(); ++i) { + for (int j = 0; j < mat.cols(); ++j) { + Serial.print(mat(i, j), 6); + Serial.print("\t"); + } + Serial.println(); + } +} diff --git a/examples/read_XY_from_SDCARD/read_XY_from_SDCARD.ino b/examples/read_XY_from_SDCARD/read_XY_from_SDCARD.ino index 0c9e206..36dd116 100644 --- a/examples/read_XY_from_SDCARD/read_XY_from_SDCARD.ino +++ b/examples/read_XY_from_SDCARD/read_XY_from_SDCARD.ino @@ -1,122 +1,85 @@ /* -This code was tested on ESP32 S3 Vroom which has a -an embedded SD card already, other tests might be -needed, +This code was tested on ESP32 S3 Vroom which has an +embedded SD card already. Other boards may need a +different SD initialization step. */ #include "Arduino.h" #include "FS.h" #include "SD_MMC.h" #include #include +#include + +#define SD_MMC_CMD 38 // Please do not modify it. +#define SD_MMC_CLK 39 // Please do not modify it. +#define SD_MMC_D0 40 // Please do not modify it. -#define SD_MMC_CMD 38 //Please do not modify it. -#define SD_MMC_CLK 39 //Please do not modify it. -#define SD_MMC_D0 40 //Please do not modify it. -void printMatrix(const Eigen::MatrixXd& mat) ; -Eigen::MatrixXd readFromFile(fs::FS &fs, const char * path); -Eigen::MatrixXd result; int baudrate = 115200; PLS pls(baudrate); + +bool readMatrix(fs::FS &fs, const char *path, MatrixXf &matrix); +void printMatrix(const MatrixXf& mat); + void setup() { - Serial.begin(115200); + Serial.begin(baudrate); SD_MMC.setPins(SD_MMC_CLK, SD_MMC_CMD, SD_MMC_D0); - int error = SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_DEFAULT, 5); - if (!error) { - Serial.printf("Card Mount Failed: %d\r\n", error); + int mounted = SD_MMC.begin("/sdcard", true, true, SDMMC_FREQ_DEFAULT, 5); + if (!mounted) { + Serial.printf("Card mount failed: %d\r\n", mounted); return; } - Serial.println("Card Mount Success."); - Eigen::MatrixXd X = readFromFile(SD_MMC, "/toyX.csv"); - Eigen::MatrixXd Y = readFromFile(SD_MMC, "/toyY.csv"); - // Check if matrices are initialized - if(X.rows() != Y.rows()) { - Serial.println("One of the matrices is uninitialized!"); + + MatrixXf X; + MatrixXf Y; + if (!readMatrix(SD_MMC, "/toyX.csv", X) || !readMatrix(SD_MMC, "/toyY.csv", Y)) { + return; } + + if (X.rows() != Y.rows()) { + Serial.println("X and Y row counts do not match"); + return; + } + + Serial.println("Training X"); printMatrix(X); + Serial.println("Training Y"); printMatrix(Y); - pls.train(X, Y); + pls.train(X, Y); + MatrixXf prediction = pls.predict(X); + if (prediction.size() == 0) { + return; + } + Serial.println("Predicted Y"); + printMatrix(prediction); } void loop() { } -Eigen::MatrixXd readFromFile(fs::FS &fs, const char * path) { - Serial.printf("Reading file: %s\n", path); - File file = fs.open(path); - if (!file) { - Serial.println("Failed to open file for reading"); - return Eigen::MatrixXd(); // Return an empty matrix - } - - int ccount = 0; - int rcount = 0; - - // First pass: Count rows and columns - while (file.available()) { - String sval = file.readStringUntil('\n'); - if (sval.length() > 0) { - rcount++; - ccount += countCommas(sval) + 1; // Count columns based on commas - } - } - file.close(); // Close the file after counting - - if (rcount == 0) { - Serial.println("No data found in file"); - return Eigen::MatrixXd(); // Return an empty matrix - } +bool readMatrix(fs::FS &fs, const char *path, MatrixXf &matrix) { + Serial.printf("Reading file: %s\n", path); - int nrows = rcount; - int ncols = ccount / rcount; - - Eigen::MatrixXd X(nrows, ncols); - file = fs.open(path); // Reopen the file for reading data - - int i = 0; - while (file.available()) { - String sval = file.readStringUntil('\n'); - int j = 0; - String value; - for (int k = 0; k < sval.length(); k++) { - if (sval.charAt(k) == ',') { - X(i, j) = value.toDouble(); - value = ""; // Reset value for next column - j++; - } else { - value += sval.charAt(k); // Accumulate characters for the current value - } - } - // Add the last value after the loop - if (value.length() > 0) { - X(i, j) = value.toFloat(); - } - i++; - } - file.close(); // Close the file after reading - - return X; -} + File file = fs.open(path); + if (!file) { + Serial.println("Failed to open file for reading"); + return false; + } -int countCommas(const String& sval) { - int count = 0; - for (char c : sval) { - if (c == ',') { - count++; - } - } - return count; + bool ok = PLSIO::loadMatrixFromCSV(file, matrix); + file.close(); + return ok; } -void printMatrix(const Eigen::MatrixXd& mat) { - for (int i = 0; i < mat.rows(); ++i) { - for (int j = 0; j < mat.cols(); ++j) { - Serial.print(mat(i, j), 3); - Serial.print("\t"); - } - Serial.println(); +void printMatrix(const MatrixXf& mat) { + for (int i = 0; i < mat.rows(); ++i) { + for (int j = 0; j < mat.cols(); ++j) { + Serial.print(mat(i, j), 6); + Serial.print("\t"); } + Serial.println(); + } }