Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 75 additions & 2 deletions PLSduino.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down
12 changes: 12 additions & 0 deletions PLSduino.h
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
134 changes: 134 additions & 0 deletions PLSduinoIO.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
/*
PLSduinoIO.cpp - CSV loading helpers for PLSduino.
*/

#include "PLSduinoIO.h"
#include <stdlib.h>

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);
}
26 changes: 26 additions & 0 deletions PLSduinoIO.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
/*
PLSduinoIO.h - CSV loading helpers for PLSduino.
*/
#ifndef PLSduinoIO_h
#define PLSduinoIO_h

#include "Arduino.h"
#include <ArduinoEigenDense.h>
#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
26 changes: 20 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 3 additions & 0 deletions examples/predict_from_pretrained_model_SDCARD/B.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
2.0
3.0
-1.0
1 change: 1 addition & 0 deletions examples/predict_from_pretrained_model_SDCARD/meanX.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0,0.0,0.0
1 change: 1 addition & 0 deletions examples/predict_from_pretrained_model_SDCARD/meanY.csv
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
0.0
5 changes: 5 additions & 0 deletions examples/predict_from_pretrained_model_SDCARD/predictX.csv
Original file line number Diff line number Diff line change
@@ -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
Loading