raksa 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- raksa-0.1.0/LICENSE +21 -0
- raksa-0.1.0/PKG-INFO +118 -0
- raksa-0.1.0/README.md +104 -0
- raksa-0.1.0/pyproject.toml +23 -0
- raksa-0.1.0/raksa.egg-info/PKG-INFO +118 -0
- raksa-0.1.0/raksa.egg-info/SOURCES.txt +8 -0
- raksa-0.1.0/raksa.egg-info/dependency_links.txt +1 -0
- raksa-0.1.0/raksa.egg-info/top_level.txt +1 -0
- raksa-0.1.0/raksa.py +733 -0
- raksa-0.1.0/setup.cfg +4 -0
raksa-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Noc Lab & Muhammad Ikhwan Fathulloh
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
raksa-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raksa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MicroPython client library for In-situ Analytics and WebSocket communication with FastAPI
|
|
5
|
+
Author: Noc Lab
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# Raksa ๐ก๏ธ
|
|
16
|
+
|
|
17
|
+
**Raksa** (derived from Sanskrit/Indonesian word for *Protector* or *Guardian*) is a high-persistence, self-contained MicroPython library built for the **Noc Lab** TinyML ecosystem as part of the **'Tiny Chip, Big Brain'** workshop. It serves as an end-to-end *In-situ Analytics* gateway, securing stable asynchronous telemetry data stream from edge devices (ESP32/RP2040) to a FastAPI cloud backend.
|
|
18
|
+
|
|
19
|
+
Leveraging native compiler execution (`@micropython.native`) for fast on-device inference and memory consolidations (`gc.collect()`), Raksa protects MicroPython devices from heap fragmentation during high-velocity TinyML operations.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Key Features
|
|
24
|
+
- ๐ **Persistent WebSockets**: Pure python RFC 6455 client implementation utilizing raw asynchronous `uasyncio` streams without external footprint overhead.
|
|
25
|
+
- โก **High-Speed In-situ Analytics**: Edge inference calculations optimized directly via `@micropython.native` decorators to ensure minimal loop latency.
|
|
26
|
+
- ๐งน **Heap Consolidation**: Garbage collection executed dynamically inside data sync cycles to prevent RAM fragmentation on constrained hardware.
|
|
27
|
+
- ๐ฎ๐ฉ **Nusantara Resiliency**: Promotes self-reliance, stability, and secure data guardianship for resource-constrained edge-to-cloud architectures.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation via `mip`
|
|
32
|
+
|
|
33
|
+
### 1. Using `mpremote` (Recommended)
|
|
34
|
+
Connect your microcontroller board to your pc via USB/Serial and run:
|
|
35
|
+
```bash
|
|
36
|
+
mpremote mip install github:Muhammad-Ikhwan-Fathulloh/Raksa
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Directly in MicroPython REPL
|
|
40
|
+
Ensure your microcontroller board has an active Wi-Fi connection, then run:
|
|
41
|
+
```python
|
|
42
|
+
import mip
|
|
43
|
+
mip.install("github:Muhammad-Ikhwan-Fathulloh/Raksa")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Minimalist Code Example (ESP32 / RP2040)
|
|
49
|
+
|
|
50
|
+
Below is an end-to-end usage code highlighting edge inference and data synchronization under 10 lines of functional code:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import uasyncio as asyncio
|
|
54
|
+
from raksa import RaksaClient
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
client = RaksaClient("ws://192.168.1.100:8000/ws/telemetry")
|
|
58
|
+
model = ([[0.5, -0.2], [0.1, 0.9]], [0.1, 0.0]) # TinyML weights & biases
|
|
59
|
+
inputs = [0.8, 1.5] # Raw sensor variables
|
|
60
|
+
|
|
61
|
+
pred = client.infer(model, inputs)
|
|
62
|
+
await client.sync({"model": "NocML_V1", "features": inputs, "prediction": pred})
|
|
63
|
+
|
|
64
|
+
asyncio.run(main())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Complete Examples & Machine Learning Features
|
|
70
|
+
|
|
71
|
+
Full production-ready examples are available under the [`examples/`](examples/) folder, covering both hardware-specific scenarios and internal, Scikit-Learn-compatible Machine Learning algorithms:
|
|
72
|
+
|
|
73
|
+
### Hardware-Connected Scenarios
|
|
74
|
+
| File | Scenario | Sensors |
|
|
75
|
+
| ------------------------------------------------------- | ------------------------------------------ | -------------------------- |
|
|
76
|
+
| [`main_edge.py`](examples/main_edge.py) | Quick-start minimalist (< 10 lines) | โ |
|
|
77
|
+
| [`esp32_wifi_sensor.py`](examples/esp32_wifi_sensor.py) | ADC analog sensor + 3-class classification | LDR / MQ-x / Potentiometer |
|
|
78
|
+
| [`esp32_dht_monitor.py`](examples/esp32_dht_monitor.py) | Temperature & humidity anomaly detection | DHT11 / DHT22 |
|
|
79
|
+
|
|
80
|
+
### On-Device Machine Learning (NocML Port)
|
|
81
|
+
| File | Algorithms Covered | API Classes |
|
|
82
|
+
| ------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------- |
|
|
83
|
+
| [`ml_preprocessing.py`](examples/ml_preprocessing.py) | Custom Data Scaling & Extensions | `MinMaxScaler`, `StandardScaler`, `PolynomialFeatures` |
|
|
84
|
+
| [`ml_classification.py`](examples/ml_classification.py) | Optimized Classifiers | `KNN`, `NaiveBayes`, `LogisticRegression`, `DecisionTreeClassifier` |
|
|
85
|
+
| [`ml_clustering.py`](examples/ml_clustering.py) | Unsupervised Grouping / Fit | `KMeans` |
|
|
86
|
+
| [`ml_forecasting.py`](examples/ml_forecasting.py) | Simple Time Series forecasting | `LinearForecaster` |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Machine Learning Reference (NocML Equivalent)
|
|
91
|
+
|
|
92
|
+
Raksa bundles a highly optimized port of the **NocML** C++ library, utilizing `@micropython.native` compiled mathematical loops to secure lightning-fast predictions directly on MicroPython edge boards without external dependencies or heavy resource bloat.
|
|
93
|
+
|
|
94
|
+
### Preprocessing
|
|
95
|
+
- **`MinMaxScaler(dims, min_vals, max_vals)`**: Rescales variables to a range `[0.0 - 1.0]`.
|
|
96
|
+
- **`StandardScaler(dims, means, stddevs)`**: Standardizes features using population mean and variance.
|
|
97
|
+
- **`PolynomialFeatures(degree)`**: Expands input dimension using combinations with replacement.
|
|
98
|
+
|
|
99
|
+
### Classification & Clustering
|
|
100
|
+
- **`KNN(training_data, labels, num_samples, dims, k=3)`**: Traditional classification using Euclidean distances.
|
|
101
|
+
- **`NaiveBayes(num_classes, dims, means, vars, priors)`**: Gaussian probabilistic classification.
|
|
102
|
+
- **`LogisticRegression(dims, weights, bias)`**: Fast binary classification with `predict()` and `predict_proba()`.
|
|
103
|
+
- **`DecisionTreeClassifier(nodes, num_nodes)`**: Node list traverse trees supporting dictionary, tuple, or custom node structures.
|
|
104
|
+
- **`KMeans(k, dims, centroids)`**: Clusters features into centroids. Supports `run(data, num_samples)` for on-device fitting.
|
|
105
|
+
|
|
106
|
+
### Forecasting
|
|
107
|
+
- **`LinearForecaster()`**: Computes simple linear regressive trends ($y=mx+c$) natively via `fit(x, y)` and `forecastNext()`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Acknowledgments & Credits
|
|
112
|
+
|
|
113
|
+
The machine learning capabilities contained in Raksa are ported from the [NocML C++ Library for Arduino](https://github.com/Nocturnailed-Community/NocML), developed by Muhammad Ikhwan Fathulloh. Special credits go to the original creators for providing the foundation of these resource-constrained edge execution algorithms.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Licenses & Copyrights
|
|
118
|
+
Developed by **Noc Lab** for nurturing TinyML literacy on hardware developers. Licensed under the MIT License.
|
raksa-0.1.0/README.md
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Raksa ๐ก๏ธ
|
|
2
|
+
|
|
3
|
+
**Raksa** (derived from Sanskrit/Indonesian word for *Protector* or *Guardian*) is a high-persistence, self-contained MicroPython library built for the **Noc Lab** TinyML ecosystem as part of the **'Tiny Chip, Big Brain'** workshop. It serves as an end-to-end *In-situ Analytics* gateway, securing stable asynchronous telemetry data stream from edge devices (ESP32/RP2040) to a FastAPI cloud backend.
|
|
4
|
+
|
|
5
|
+
Leveraging native compiler execution (`@micropython.native`) for fast on-device inference and memory consolidations (`gc.collect()`), Raksa protects MicroPython devices from heap fragmentation during high-velocity TinyML operations.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## Key Features
|
|
10
|
+
- ๐ **Persistent WebSockets**: Pure python RFC 6455 client implementation utilizing raw asynchronous `uasyncio` streams without external footprint overhead.
|
|
11
|
+
- โก **High-Speed In-situ Analytics**: Edge inference calculations optimized directly via `@micropython.native` decorators to ensure minimal loop latency.
|
|
12
|
+
- ๐งน **Heap Consolidation**: Garbage collection executed dynamically inside data sync cycles to prevent RAM fragmentation on constrained hardware.
|
|
13
|
+
- ๐ฎ๐ฉ **Nusantara Resiliency**: Promotes self-reliance, stability, and secure data guardianship for resource-constrained edge-to-cloud architectures.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Installation via `mip`
|
|
18
|
+
|
|
19
|
+
### 1. Using `mpremote` (Recommended)
|
|
20
|
+
Connect your microcontroller board to your pc via USB/Serial and run:
|
|
21
|
+
```bash
|
|
22
|
+
mpremote mip install github:Muhammad-Ikhwan-Fathulloh/Raksa
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
### 2. Directly in MicroPython REPL
|
|
26
|
+
Ensure your microcontroller board has an active Wi-Fi connection, then run:
|
|
27
|
+
```python
|
|
28
|
+
import mip
|
|
29
|
+
mip.install("github:Muhammad-Ikhwan-Fathulloh/Raksa")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## Minimalist Code Example (ESP32 / RP2040)
|
|
35
|
+
|
|
36
|
+
Below is an end-to-end usage code highlighting edge inference and data synchronization under 10 lines of functional code:
|
|
37
|
+
|
|
38
|
+
```python
|
|
39
|
+
import uasyncio as asyncio
|
|
40
|
+
from raksa import RaksaClient
|
|
41
|
+
|
|
42
|
+
async def main():
|
|
43
|
+
client = RaksaClient("ws://192.168.1.100:8000/ws/telemetry")
|
|
44
|
+
model = ([[0.5, -0.2], [0.1, 0.9]], [0.1, 0.0]) # TinyML weights & biases
|
|
45
|
+
inputs = [0.8, 1.5] # Raw sensor variables
|
|
46
|
+
|
|
47
|
+
pred = client.infer(model, inputs)
|
|
48
|
+
await client.sync({"model": "NocML_V1", "features": inputs, "prediction": pred})
|
|
49
|
+
|
|
50
|
+
asyncio.run(main())
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
|
+
## Complete Examples & Machine Learning Features
|
|
56
|
+
|
|
57
|
+
Full production-ready examples are available under the [`examples/`](examples/) folder, covering both hardware-specific scenarios and internal, Scikit-Learn-compatible Machine Learning algorithms:
|
|
58
|
+
|
|
59
|
+
### Hardware-Connected Scenarios
|
|
60
|
+
| File | Scenario | Sensors |
|
|
61
|
+
| ------------------------------------------------------- | ------------------------------------------ | -------------------------- |
|
|
62
|
+
| [`main_edge.py`](examples/main_edge.py) | Quick-start minimalist (< 10 lines) | โ |
|
|
63
|
+
| [`esp32_wifi_sensor.py`](examples/esp32_wifi_sensor.py) | ADC analog sensor + 3-class classification | LDR / MQ-x / Potentiometer |
|
|
64
|
+
| [`esp32_dht_monitor.py`](examples/esp32_dht_monitor.py) | Temperature & humidity anomaly detection | DHT11 / DHT22 |
|
|
65
|
+
|
|
66
|
+
### On-Device Machine Learning (NocML Port)
|
|
67
|
+
| File | Algorithms Covered | API Classes |
|
|
68
|
+
| ------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------- |
|
|
69
|
+
| [`ml_preprocessing.py`](examples/ml_preprocessing.py) | Custom Data Scaling & Extensions | `MinMaxScaler`, `StandardScaler`, `PolynomialFeatures` |
|
|
70
|
+
| [`ml_classification.py`](examples/ml_classification.py) | Optimized Classifiers | `KNN`, `NaiveBayes`, `LogisticRegression`, `DecisionTreeClassifier` |
|
|
71
|
+
| [`ml_clustering.py`](examples/ml_clustering.py) | Unsupervised Grouping / Fit | `KMeans` |
|
|
72
|
+
| [`ml_forecasting.py`](examples/ml_forecasting.py) | Simple Time Series forecasting | `LinearForecaster` |
|
|
73
|
+
|
|
74
|
+
---
|
|
75
|
+
|
|
76
|
+
## Machine Learning Reference (NocML Equivalent)
|
|
77
|
+
|
|
78
|
+
Raksa bundles a highly optimized port of the **NocML** C++ library, utilizing `@micropython.native` compiled mathematical loops to secure lightning-fast predictions directly on MicroPython edge boards without external dependencies or heavy resource bloat.
|
|
79
|
+
|
|
80
|
+
### Preprocessing
|
|
81
|
+
- **`MinMaxScaler(dims, min_vals, max_vals)`**: Rescales variables to a range `[0.0 - 1.0]`.
|
|
82
|
+
- **`StandardScaler(dims, means, stddevs)`**: Standardizes features using population mean and variance.
|
|
83
|
+
- **`PolynomialFeatures(degree)`**: Expands input dimension using combinations with replacement.
|
|
84
|
+
|
|
85
|
+
### Classification & Clustering
|
|
86
|
+
- **`KNN(training_data, labels, num_samples, dims, k=3)`**: Traditional classification using Euclidean distances.
|
|
87
|
+
- **`NaiveBayes(num_classes, dims, means, vars, priors)`**: Gaussian probabilistic classification.
|
|
88
|
+
- **`LogisticRegression(dims, weights, bias)`**: Fast binary classification with `predict()` and `predict_proba()`.
|
|
89
|
+
- **`DecisionTreeClassifier(nodes, num_nodes)`**: Node list traverse trees supporting dictionary, tuple, or custom node structures.
|
|
90
|
+
- **`KMeans(k, dims, centroids)`**: Clusters features into centroids. Supports `run(data, num_samples)` for on-device fitting.
|
|
91
|
+
|
|
92
|
+
### Forecasting
|
|
93
|
+
- **`LinearForecaster()`**: Computes simple linear regressive trends ($y=mx+c$) natively via `fit(x, y)` and `forecastNext()`.
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Acknowledgments & Credits
|
|
98
|
+
|
|
99
|
+
The machine learning capabilities contained in Raksa are ported from the [NocML C++ Library for Arduino](https://github.com/Nocturnailed-Community/NocML), developed by Muhammad Ikhwan Fathulloh. Special credits go to the original creators for providing the foundation of these resource-constrained edge execution algorithms.
|
|
100
|
+
|
|
101
|
+
---
|
|
102
|
+
|
|
103
|
+
## Licenses & Copyrights
|
|
104
|
+
Developed by **Noc Lab** for nurturing TinyML literacy on hardware developers. Licensed under the MIT License.
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0.0"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "raksa"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "MicroPython client library for In-situ Analytics and WebSocket communication with FastAPI"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.7"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Noc Lab"}
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: Implementation :: MicroPython",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
]
|
|
20
|
+
dependencies = []
|
|
21
|
+
|
|
22
|
+
[tool.setuptools]
|
|
23
|
+
py-modules = ["raksa"]
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: raksa
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: MicroPython client library for In-situ Analytics and WebSocket communication with FastAPI
|
|
5
|
+
Author: Noc Lab
|
|
6
|
+
License: MIT
|
|
7
|
+
Classifier: Programming Language :: Python :: 3
|
|
8
|
+
Classifier: Programming Language :: Python :: Implementation :: MicroPython
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Requires-Python: >=3.7
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Dynamic: license-file
|
|
14
|
+
|
|
15
|
+
# Raksa ๐ก๏ธ
|
|
16
|
+
|
|
17
|
+
**Raksa** (derived from Sanskrit/Indonesian word for *Protector* or *Guardian*) is a high-persistence, self-contained MicroPython library built for the **Noc Lab** TinyML ecosystem as part of the **'Tiny Chip, Big Brain'** workshop. It serves as an end-to-end *In-situ Analytics* gateway, securing stable asynchronous telemetry data stream from edge devices (ESP32/RP2040) to a FastAPI cloud backend.
|
|
18
|
+
|
|
19
|
+
Leveraging native compiler execution (`@micropython.native`) for fast on-device inference and memory consolidations (`gc.collect()`), Raksa protects MicroPython devices from heap fragmentation during high-velocity TinyML operations.
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## Key Features
|
|
24
|
+
- ๐ **Persistent WebSockets**: Pure python RFC 6455 client implementation utilizing raw asynchronous `uasyncio` streams without external footprint overhead.
|
|
25
|
+
- โก **High-Speed In-situ Analytics**: Edge inference calculations optimized directly via `@micropython.native` decorators to ensure minimal loop latency.
|
|
26
|
+
- ๐งน **Heap Consolidation**: Garbage collection executed dynamically inside data sync cycles to prevent RAM fragmentation on constrained hardware.
|
|
27
|
+
- ๐ฎ๐ฉ **Nusantara Resiliency**: Promotes self-reliance, stability, and secure data guardianship for resource-constrained edge-to-cloud architectures.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## Installation via `mip`
|
|
32
|
+
|
|
33
|
+
### 1. Using `mpremote` (Recommended)
|
|
34
|
+
Connect your microcontroller board to your pc via USB/Serial and run:
|
|
35
|
+
```bash
|
|
36
|
+
mpremote mip install github:Muhammad-Ikhwan-Fathulloh/Raksa
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### 2. Directly in MicroPython REPL
|
|
40
|
+
Ensure your microcontroller board has an active Wi-Fi connection, then run:
|
|
41
|
+
```python
|
|
42
|
+
import mip
|
|
43
|
+
mip.install("github:Muhammad-Ikhwan-Fathulloh/Raksa")
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## Minimalist Code Example (ESP32 / RP2040)
|
|
49
|
+
|
|
50
|
+
Below is an end-to-end usage code highlighting edge inference and data synchronization under 10 lines of functional code:
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import uasyncio as asyncio
|
|
54
|
+
from raksa import RaksaClient
|
|
55
|
+
|
|
56
|
+
async def main():
|
|
57
|
+
client = RaksaClient("ws://192.168.1.100:8000/ws/telemetry")
|
|
58
|
+
model = ([[0.5, -0.2], [0.1, 0.9]], [0.1, 0.0]) # TinyML weights & biases
|
|
59
|
+
inputs = [0.8, 1.5] # Raw sensor variables
|
|
60
|
+
|
|
61
|
+
pred = client.infer(model, inputs)
|
|
62
|
+
await client.sync({"model": "NocML_V1", "features": inputs, "prediction": pred})
|
|
63
|
+
|
|
64
|
+
asyncio.run(main())
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Complete Examples & Machine Learning Features
|
|
70
|
+
|
|
71
|
+
Full production-ready examples are available under the [`examples/`](examples/) folder, covering both hardware-specific scenarios and internal, Scikit-Learn-compatible Machine Learning algorithms:
|
|
72
|
+
|
|
73
|
+
### Hardware-Connected Scenarios
|
|
74
|
+
| File | Scenario | Sensors |
|
|
75
|
+
| ------------------------------------------------------- | ------------------------------------------ | -------------------------- |
|
|
76
|
+
| [`main_edge.py`](examples/main_edge.py) | Quick-start minimalist (< 10 lines) | โ |
|
|
77
|
+
| [`esp32_wifi_sensor.py`](examples/esp32_wifi_sensor.py) | ADC analog sensor + 3-class classification | LDR / MQ-x / Potentiometer |
|
|
78
|
+
| [`esp32_dht_monitor.py`](examples/esp32_dht_monitor.py) | Temperature & humidity anomaly detection | DHT11 / DHT22 |
|
|
79
|
+
|
|
80
|
+
### On-Device Machine Learning (NocML Port)
|
|
81
|
+
| File | Algorithms Covered | API Classes |
|
|
82
|
+
| ------------------------------------------------------- | -------------------------------- | ------------------------------------------------------------------- |
|
|
83
|
+
| [`ml_preprocessing.py`](examples/ml_preprocessing.py) | Custom Data Scaling & Extensions | `MinMaxScaler`, `StandardScaler`, `PolynomialFeatures` |
|
|
84
|
+
| [`ml_classification.py`](examples/ml_classification.py) | Optimized Classifiers | `KNN`, `NaiveBayes`, `LogisticRegression`, `DecisionTreeClassifier` |
|
|
85
|
+
| [`ml_clustering.py`](examples/ml_clustering.py) | Unsupervised Grouping / Fit | `KMeans` |
|
|
86
|
+
| [`ml_forecasting.py`](examples/ml_forecasting.py) | Simple Time Series forecasting | `LinearForecaster` |
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Machine Learning Reference (NocML Equivalent)
|
|
91
|
+
|
|
92
|
+
Raksa bundles a highly optimized port of the **NocML** C++ library, utilizing `@micropython.native` compiled mathematical loops to secure lightning-fast predictions directly on MicroPython edge boards without external dependencies or heavy resource bloat.
|
|
93
|
+
|
|
94
|
+
### Preprocessing
|
|
95
|
+
- **`MinMaxScaler(dims, min_vals, max_vals)`**: Rescales variables to a range `[0.0 - 1.0]`.
|
|
96
|
+
- **`StandardScaler(dims, means, stddevs)`**: Standardizes features using population mean and variance.
|
|
97
|
+
- **`PolynomialFeatures(degree)`**: Expands input dimension using combinations with replacement.
|
|
98
|
+
|
|
99
|
+
### Classification & Clustering
|
|
100
|
+
- **`KNN(training_data, labels, num_samples, dims, k=3)`**: Traditional classification using Euclidean distances.
|
|
101
|
+
- **`NaiveBayes(num_classes, dims, means, vars, priors)`**: Gaussian probabilistic classification.
|
|
102
|
+
- **`LogisticRegression(dims, weights, bias)`**: Fast binary classification with `predict()` and `predict_proba()`.
|
|
103
|
+
- **`DecisionTreeClassifier(nodes, num_nodes)`**: Node list traverse trees supporting dictionary, tuple, or custom node structures.
|
|
104
|
+
- **`KMeans(k, dims, centroids)`**: Clusters features into centroids. Supports `run(data, num_samples)` for on-device fitting.
|
|
105
|
+
|
|
106
|
+
### Forecasting
|
|
107
|
+
- **`LinearForecaster()`**: Computes simple linear regressive trends ($y=mx+c$) natively via `fit(x, y)` and `forecastNext()`.
|
|
108
|
+
|
|
109
|
+
---
|
|
110
|
+
|
|
111
|
+
## Acknowledgments & Credits
|
|
112
|
+
|
|
113
|
+
The machine learning capabilities contained in Raksa are ported from the [NocML C++ Library for Arduino](https://github.com/Nocturnailed-Community/NocML), developed by Muhammad Ikhwan Fathulloh. Special credits go to the original creators for providing the foundation of these resource-constrained edge execution algorithms.
|
|
114
|
+
|
|
115
|
+
---
|
|
116
|
+
|
|
117
|
+
## Licenses & Copyrights
|
|
118
|
+
Developed by **Noc Lab** for nurturing TinyML literacy on hardware developers. Licensed under the MIT License.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
raksa
|
raksa-0.1.0/raksa.py
ADDED
|
@@ -0,0 +1,733 @@
|
|
|
1
|
+
# raksa.py
|
|
2
|
+
# MicroPython Client Library for In-situ Analytics and WebSocket communication.
|
|
3
|
+
# Part of the 'Noc Lab' TinyML ecosystem for the 'Tiny Chip, Big Brain' workshop.
|
|
4
|
+
# Developed with Nusantara values: Raksa = Protection, Guard, Reliability.
|
|
5
|
+
|
|
6
|
+
import gc
|
|
7
|
+
|
|
8
|
+
try:
|
|
9
|
+
import ustruct as struct
|
|
10
|
+
except ImportError:
|
|
11
|
+
import struct
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
import uasyncio as asyncio
|
|
15
|
+
except ImportError:
|
|
16
|
+
import asyncio
|
|
17
|
+
|
|
18
|
+
try:
|
|
19
|
+
import urandom
|
|
20
|
+
except ImportError:
|
|
21
|
+
import random as urandom
|
|
22
|
+
|
|
23
|
+
# Dual compatibility for CPython and MicroPython platforms
|
|
24
|
+
try:
|
|
25
|
+
import micropython
|
|
26
|
+
except ImportError:
|
|
27
|
+
class micropython:
|
|
28
|
+
@staticmethod
|
|
29
|
+
def native(f):
|
|
30
|
+
return f
|
|
31
|
+
@staticmethod
|
|
32
|
+
def viper(f):
|
|
33
|
+
return f
|
|
34
|
+
|
|
35
|
+
# Standard Base64 characters helper for pure Python/MicroPython compilation
|
|
36
|
+
_B64_CHARS = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
|
|
37
|
+
|
|
38
|
+
@micropython.native
|
|
39
|
+
def _b64encode(b: bytes) -> str:
|
|
40
|
+
"""Encode bytes to base64 string without external dependencies."""
|
|
41
|
+
res = ""
|
|
42
|
+
n = len(b)
|
|
43
|
+
for i in range(0, n, 3):
|
|
44
|
+
chunk = b[i:i+3]
|
|
45
|
+
pad = 3 - len(chunk)
|
|
46
|
+
if pad == 1:
|
|
47
|
+
chunk = chunk + b'\x00'
|
|
48
|
+
elif pad == 2:
|
|
49
|
+
chunk = chunk + b'\x00\x00'
|
|
50
|
+
|
|
51
|
+
val = (chunk[0] << 16) | (chunk[1] << 8) | chunk[2]
|
|
52
|
+
res += _B64_CHARS[(val >> 18) & 63]
|
|
53
|
+
res += _B64_CHARS[(val >> 12) & 63]
|
|
54
|
+
res += _B64_CHARS[(val >> 6) & 63] if pad < 2 else "="
|
|
55
|
+
res += _B64_CHARS[val & 63] if pad < 1 else "="
|
|
56
|
+
return res
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class RaksaClient:
|
|
60
|
+
"""
|
|
61
|
+
RaksaClient: Handles persistent, asynchronous WebSocket connections on MicroPython.
|
|
62
|
+
Manages client handshake, data serialization, memory optimization, and fast In-situ Analytics.
|
|
63
|
+
"""
|
|
64
|
+
def __init__(self, uri: str, reconnect_delay: int = 5):
|
|
65
|
+
self.uri = uri
|
|
66
|
+
self.reconnect_delay = reconnect_delay
|
|
67
|
+
self.host = ""
|
|
68
|
+
self.port = 80
|
|
69
|
+
self.path = "/"
|
|
70
|
+
self.ssl = False
|
|
71
|
+
self.reader = None
|
|
72
|
+
self.writer = None
|
|
73
|
+
self._connected = False
|
|
74
|
+
self._lock = asyncio.Lock()
|
|
75
|
+
|
|
76
|
+
self._parse_uri(uri)
|
|
77
|
+
|
|
78
|
+
def _parse_uri(self, uri: str):
|
|
79
|
+
"""Parse incoming ws:// or wss:// link into connection details."""
|
|
80
|
+
if not (uri.startswith("ws://") or uri.startswith("wss://")):
|
|
81
|
+
raise ValueError("URL must start with ws:// or wss://")
|
|
82
|
+
|
|
83
|
+
ssl = uri.startswith("wss://")
|
|
84
|
+
self.ssl = ssl
|
|
85
|
+
self.port = 443 if ssl else 80
|
|
86
|
+
|
|
87
|
+
url_path = uri.split("://", 1)[1]
|
|
88
|
+
if "/" in url_path:
|
|
89
|
+
host_port, path = url_path.split("/", 1)
|
|
90
|
+
self.path = "/" + path
|
|
91
|
+
else:
|
|
92
|
+
host_port = url_path
|
|
93
|
+
self.path = "/"
|
|
94
|
+
|
|
95
|
+
if ":" in host_port:
|
|
96
|
+
host, port = host_port.split(":", 1)
|
|
97
|
+
self.host = host
|
|
98
|
+
self.port = int(port)
|
|
99
|
+
else:
|
|
100
|
+
self.host = host_port
|
|
101
|
+
|
|
102
|
+
async def connect(self) -> bool:
|
|
103
|
+
"""Establish WebSocket connection with the cloud server."""
|
|
104
|
+
async with self._lock:
|
|
105
|
+
if self._connected:
|
|
106
|
+
return True
|
|
107
|
+
|
|
108
|
+
print(f"[RAKSA] Menghubungkan ke backend di {self.host}:{self.port} ...")
|
|
109
|
+
try:
|
|
110
|
+
# Async TCP socket connection
|
|
111
|
+
self.reader, self.writer = await asyncio.open_connection(self.host, self.port)
|
|
112
|
+
|
|
113
|
+
# Perform handshakes manually to keep codebase extremely tiny
|
|
114
|
+
# Generate random 16-byte base64 key
|
|
115
|
+
raw_key = bytes([urandom.getrandbits(8) for _ in range(16)])
|
|
116
|
+
ws_key = _b64encode(raw_key)
|
|
117
|
+
|
|
118
|
+
handshake = (
|
|
119
|
+
f"GET {self.path} HTTP/1.1\r\n"
|
|
120
|
+
f"Host: {self.host}:{self.port}\r\n"
|
|
121
|
+
"Upgrade: websocket\r\n"
|
|
122
|
+
"Connection: Upgrade\r\n"
|
|
123
|
+
f"Sec-WebSocket-Key: {ws_key}\r\n"
|
|
124
|
+
"Sec-WebSocket-Version: 13\r\n\r\n"
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
self.writer.write(handshake.encode("utf-8"))
|
|
128
|
+
await self.writer.drain()
|
|
129
|
+
|
|
130
|
+
# Verify switching protocols status line
|
|
131
|
+
line = await self.reader.readline()
|
|
132
|
+
if not line:
|
|
133
|
+
raise Exception("Koneksi ditutup secara tidak terduga oleh server.")
|
|
134
|
+
|
|
135
|
+
status_line = line.decode("utf-8")
|
|
136
|
+
if "101" not in status_line:
|
|
137
|
+
raise Exception(f"Gagal melakukan upgrade websocket: {status_line.strip()}")
|
|
138
|
+
|
|
139
|
+
# Consume response headers until double CR/LF
|
|
140
|
+
while True:
|
|
141
|
+
line = await self.reader.readline()
|
|
142
|
+
if line == b"\r\n" or not line:
|
|
143
|
+
break
|
|
144
|
+
|
|
145
|
+
self._connected = True
|
|
146
|
+
print("[RAKSA] Koneksi persisten berhasil dijalankan. Aliran data aman dan stabil.")
|
|
147
|
+
gc.collect()
|
|
148
|
+
return True
|
|
149
|
+
except Exception as e:
|
|
150
|
+
print(f"[RAKSA] Gagal menghubungkan: {e}")
|
|
151
|
+
self._connected = False
|
|
152
|
+
self.writer = None
|
|
153
|
+
self.reader = None
|
|
154
|
+
return False
|
|
155
|
+
|
|
156
|
+
async def sync(self, payload) -> bool:
|
|
157
|
+
"""
|
|
158
|
+
Sends telemetry/sensor payloads asynchronously via WebSocket.
|
|
159
|
+
Invokes memory clean-up automatically in the cycles to avoid RAM fragmentation.
|
|
160
|
+
"""
|
|
161
|
+
if not self._connected:
|
|
162
|
+
success = await self.connect()
|
|
163
|
+
if not success:
|
|
164
|
+
return False
|
|
165
|
+
|
|
166
|
+
# Parse dict to JSON string if applicable
|
|
167
|
+
if isinstance(payload, dict):
|
|
168
|
+
import json
|
|
169
|
+
try:
|
|
170
|
+
payload = json.dumps(payload)
|
|
171
|
+
except Exception as e:
|
|
172
|
+
print(f"[RAKSA] Kesalahan serialisasi JSON: {e}")
|
|
173
|
+
return False
|
|
174
|
+
|
|
175
|
+
if not isinstance(payload, str):
|
|
176
|
+
payload = str(payload)
|
|
177
|
+
|
|
178
|
+
async with self._lock:
|
|
179
|
+
try:
|
|
180
|
+
# WebSocket Frame specification
|
|
181
|
+
data_bytes = payload.encode("utf-8")
|
|
182
|
+
length = len(data_bytes)
|
|
183
|
+
|
|
184
|
+
header = bytearray([0x81]) # Text frame FIN
|
|
185
|
+
|
|
186
|
+
# Client to Server must be masked
|
|
187
|
+
if length <= 125:
|
|
188
|
+
header.append(0x80 | length)
|
|
189
|
+
elif length <= 65535:
|
|
190
|
+
header.append(0x80 | 126)
|
|
191
|
+
header.extend(struct.pack(">H", length))
|
|
192
|
+
else:
|
|
193
|
+
header.append(0x80 | 127)
|
|
194
|
+
header.extend(struct.pack(">Q", length))
|
|
195
|
+
|
|
196
|
+
# Generate random 4-byte mask
|
|
197
|
+
mask_key = bytes([urandom.getrandbits(8) for _ in range(4)])
|
|
198
|
+
header.extend(mask_key)
|
|
199
|
+
|
|
200
|
+
masked_payload = self._mask_payload(data_bytes, mask_key)
|
|
201
|
+
|
|
202
|
+
# Send frame header and payload
|
|
203
|
+
self.writer.write(header)
|
|
204
|
+
self.writer.write(masked_payload)
|
|
205
|
+
await self.writer.drain()
|
|
206
|
+
|
|
207
|
+
# Memory consolidation
|
|
208
|
+
masked_payload = None
|
|
209
|
+
header = None
|
|
210
|
+
gc.collect() # Force execution of gc to maintain clean heap
|
|
211
|
+
return True
|
|
212
|
+
|
|
213
|
+
except Exception as e:
|
|
214
|
+
print(f"[RAKSA] Pengiriman gagal: {e}. Mencoba memulihkan...")
|
|
215
|
+
self._connected = False
|
|
216
|
+
self.writer = None
|
|
217
|
+
self.reader = None
|
|
218
|
+
gc.collect()
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
@micropython.native
|
|
222
|
+
def _mask_payload(self, data: bytes, mask: bytes) -> bytes:
|
|
223
|
+
"""XOR mask payload for client packets. Native acceleration applied."""
|
|
224
|
+
n = len(data)
|
|
225
|
+
m = bytearray(n)
|
|
226
|
+
for i in range(n):
|
|
227
|
+
m[i] = data[i] ^ mask[i % 4]
|
|
228
|
+
return bytes(m)
|
|
229
|
+
|
|
230
|
+
@micropython.native
|
|
231
|
+
def infer(self, model, data) -> float:
|
|
232
|
+
"""
|
|
233
|
+
Process In-situ Analytics of data relative to a local ML weight model.
|
|
234
|
+
Uses MicroPython `@micropython.native` decorator for maximum computation speed.
|
|
235
|
+
|
|
236
|
+
Args:
|
|
237
|
+
model: A model configuration, standard callable, or a tuple of (weights, biases) matrix.
|
|
238
|
+
data: Data feature vector input (list).
|
|
239
|
+
|
|
240
|
+
Returns:
|
|
241
|
+
Computed inference result.
|
|
242
|
+
"""
|
|
243
|
+
# Feature Matrix Multiply Inference Optimization
|
|
244
|
+
# weights: list of float lists (outputs x inputs), biases: list of float values (outputs)
|
|
245
|
+
if isinstance(model, tuple) and len(model) == 2:
|
|
246
|
+
weights = model[0]
|
|
247
|
+
biases = model[1]
|
|
248
|
+
if isinstance(weights, list) and isinstance(biases, list) and isinstance(data, list):
|
|
249
|
+
n_out = len(weights)
|
|
250
|
+
n_in = len(data)
|
|
251
|
+
|
|
252
|
+
if n_out > 0 and len(weights[0]) == n_in:
|
|
253
|
+
results = [0.0] * n_out
|
|
254
|
+
for i in range(n_out):
|
|
255
|
+
row = weights[i]
|
|
256
|
+
val = biases[i]
|
|
257
|
+
for j in range(n_in):
|
|
258
|
+
val += row[j] * data[j]
|
|
259
|
+
# Apply ReLU Activation
|
|
260
|
+
if val < 0.0:
|
|
261
|
+
results[i] = 0.0
|
|
262
|
+
else:
|
|
263
|
+
results[i] = val
|
|
264
|
+
return results
|
|
265
|
+
|
|
266
|
+
# Standard model execution fallback checks
|
|
267
|
+
if hasattr(model, 'predict') and callable(model.predict):
|
|
268
|
+
return model.predict(data)
|
|
269
|
+
|
|
270
|
+
if callable(model):
|
|
271
|
+
return model(data)
|
|
272
|
+
|
|
273
|
+
# Simulative linear TinyML model inference calculation fallback
|
|
274
|
+
val_sum = 0.0
|
|
275
|
+
n_features = len(data)
|
|
276
|
+
for i in range(n_features):
|
|
277
|
+
val_sum += data[i] * 0.45
|
|
278
|
+
val_sum += 0.05
|
|
279
|
+
return val_sum
|
|
280
|
+
|
|
281
|
+
async def recv(self) -> str:
|
|
282
|
+
"""
|
|
283
|
+
Receives a single WebSocket frame asynchronously and returns its text payload.
|
|
284
|
+
Handles standard WebSocket framing including extended 16-bit and 64-bit lengths.
|
|
285
|
+
"""
|
|
286
|
+
if not self._connected:
|
|
287
|
+
raise Exception("[RAKSA] Client tidak terhubung.")
|
|
288
|
+
|
|
289
|
+
try:
|
|
290
|
+
# Read first 2 bytes (header)
|
|
291
|
+
header = await self.reader.readexactly(2)
|
|
292
|
+
opcode = header[0] & 0x0f
|
|
293
|
+
has_mask = header[1] & 0x80
|
|
294
|
+
length = header[1] & 0x7f
|
|
295
|
+
|
|
296
|
+
# Close frame or ping/pong handlers
|
|
297
|
+
if opcode == 0x08: # CLOSE
|
|
298
|
+
await self.close()
|
|
299
|
+
return ""
|
|
300
|
+
elif opcode == 0x09: # PING
|
|
301
|
+
# Send PONG frame
|
|
302
|
+
await self._send_pong()
|
|
303
|
+
return await self.recv()
|
|
304
|
+
elif opcode == 0x0a: # PONG
|
|
305
|
+
return await self.recv()
|
|
306
|
+
|
|
307
|
+
# Parse extended lengths
|
|
308
|
+
if length == 126:
|
|
309
|
+
ext_len_bytes = await self.reader.readexactly(2)
|
|
310
|
+
length = struct.unpack(">H", ext_len_bytes)[0]
|
|
311
|
+
elif length == 127:
|
|
312
|
+
ext_len_bytes = await self.reader.readexactly(8)
|
|
313
|
+
length = struct.unpack(">Q", ext_len_bytes)[0]
|
|
314
|
+
|
|
315
|
+
# Read mask key if server masked it (though servers shouldn't mask)
|
|
316
|
+
if has_mask:
|
|
317
|
+
mask_key = await self.reader.readexactly(4)
|
|
318
|
+
|
|
319
|
+
# Read payload body
|
|
320
|
+
body = await self.reader.readexactly(length)
|
|
321
|
+
|
|
322
|
+
if has_mask:
|
|
323
|
+
body = self._mask_payload(body, mask_key)
|
|
324
|
+
|
|
325
|
+
# Decode and execute GC
|
|
326
|
+
res = body.decode("utf-8")
|
|
327
|
+
body = None
|
|
328
|
+
gc.collect()
|
|
329
|
+
return res
|
|
330
|
+
|
|
331
|
+
except Exception as e:
|
|
332
|
+
print(f"[RAKSA] Koneksi terputus saat membaca data: {e}")
|
|
333
|
+
self._connected = False
|
|
334
|
+
self.writer = None
|
|
335
|
+
self.reader = None
|
|
336
|
+
gc.collect()
|
|
337
|
+
raise e
|
|
338
|
+
|
|
339
|
+
async def _send_pong(self):
|
|
340
|
+
"""Send a WebSocket Pong frame in response to Ping."""
|
|
341
|
+
async with self._lock:
|
|
342
|
+
if self.writer:
|
|
343
|
+
try:
|
|
344
|
+
self.writer.write(b'\x8a\x00') # FIN + PONG, length 0
|
|
345
|
+
await self.writer.drain()
|
|
346
|
+
except:
|
|
347
|
+
pass
|
|
348
|
+
|
|
349
|
+
async def close(self):
|
|
350
|
+
"""Close connection cleanly and run gc.collect."""
|
|
351
|
+
async with self._lock:
|
|
352
|
+
if self.writer:
|
|
353
|
+
try:
|
|
354
|
+
self.writer.close()
|
|
355
|
+
await self.writer.wait_closed()
|
|
356
|
+
except:
|
|
357
|
+
pass
|
|
358
|
+
self.writer = None
|
|
359
|
+
self.reader = None
|
|
360
|
+
self._connected = False
|
|
361
|
+
gc.collect()
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
# โโ Preprocessing & Machine Learning Features (NocML inspired) โโโโโโโโโโโโโโโ
|
|
365
|
+
|
|
366
|
+
class MinMaxScaler:
|
|
367
|
+
def __init__(self, dims, min_vals, max_vals):
|
|
368
|
+
self.dims = dims
|
|
369
|
+
self.min_vals = list(min_vals)
|
|
370
|
+
self.max_vals = list(max_vals)
|
|
371
|
+
# Precompute denominators to avoid division by zero and speed up math
|
|
372
|
+
self.ranges = [1.0] * dims
|
|
373
|
+
for i in range(dims):
|
|
374
|
+
r = self.max_vals[i] - self.min_vals[i]
|
|
375
|
+
self.ranges[i] = r if r != 0.0 else 1.0
|
|
376
|
+
|
|
377
|
+
@micropython.native
|
|
378
|
+
def transform(self, x, out=None):
|
|
379
|
+
if out is None:
|
|
380
|
+
out = [0.0] * self.dims
|
|
381
|
+
for i in range(self.dims):
|
|
382
|
+
out[i] = (x[i] - self.min_vals[i]) / self.ranges[i]
|
|
383
|
+
return out
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
class StandardScaler:
|
|
387
|
+
def __init__(self, dims, means, stddevs):
|
|
388
|
+
self.dims = dims
|
|
389
|
+
self.means = list(means)
|
|
390
|
+
self.stddevs = list(stddevs)
|
|
391
|
+
# Avoid division by zero
|
|
392
|
+
self.scales = [1.0] * dims
|
|
393
|
+
for i in range(dims):
|
|
394
|
+
s = self.stddevs[i]
|
|
395
|
+
self.scales[i] = s if s != 0.0 else 1.0
|
|
396
|
+
|
|
397
|
+
@micropython.native
|
|
398
|
+
def transform(self, x, out=None):
|
|
399
|
+
if out is None:
|
|
400
|
+
out = [0.0] * self.dims
|
|
401
|
+
for i in range(self.dims):
|
|
402
|
+
out[i] = (x[i] - self.means[i]) / self.scales[i]
|
|
403
|
+
return out
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
class PolynomialFeatures:
|
|
407
|
+
def __init__(self, degree):
|
|
408
|
+
self.degree = degree
|
|
409
|
+
self._cache = {}
|
|
410
|
+
|
|
411
|
+
def _get_combinations(self, dims):
|
|
412
|
+
if dims in self._cache:
|
|
413
|
+
return self._cache[dims]
|
|
414
|
+
|
|
415
|
+
# Generate combinations with replacement manually without itertools
|
|
416
|
+
combos = []
|
|
417
|
+
for d in range(self.degree + 1):
|
|
418
|
+
combos.extend(self._cvw(list(range(dims)), d))
|
|
419
|
+
self._cache[dims] = combos
|
|
420
|
+
return combos
|
|
421
|
+
|
|
422
|
+
def _cvw(self, items, r):
|
|
423
|
+
# Helper for combinations with replacement
|
|
424
|
+
if r == 0:
|
|
425
|
+
return [()]
|
|
426
|
+
if not items:
|
|
427
|
+
return []
|
|
428
|
+
n = len(items)
|
|
429
|
+
indices = [0] * r
|
|
430
|
+
results = [tuple(items[i] for i in indices)]
|
|
431
|
+
while True:
|
|
432
|
+
for i in reversed(range(r)):
|
|
433
|
+
if indices[i] != n - 1:
|
|
434
|
+
break
|
|
435
|
+
else:
|
|
436
|
+
return results
|
|
437
|
+
indices[i:] = [indices[i] + 1] * (r - i)
|
|
438
|
+
results.append(tuple(items[k] for k in indices))
|
|
439
|
+
|
|
440
|
+
@micropython.native
|
|
441
|
+
def transform(self, x, dims=None, out=None):
|
|
442
|
+
if dims is None:
|
|
443
|
+
dims = len(x)
|
|
444
|
+
combos = self._get_combinations(dims)
|
|
445
|
+
|
|
446
|
+
n_out = len(combos)
|
|
447
|
+
if out is None:
|
|
448
|
+
out = [0.0] * n_out
|
|
449
|
+
|
|
450
|
+
for i in range(n_out):
|
|
451
|
+
combo = combos[i]
|
|
452
|
+
val = 1.0
|
|
453
|
+
for idx in combo:
|
|
454
|
+
val *= x[idx]
|
|
455
|
+
out[i] = val
|
|
456
|
+
return out
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
class LinearForecaster:
|
|
460
|
+
def __init__(self):
|
|
461
|
+
self.m = 0.0
|
|
462
|
+
self.c = 0.0
|
|
463
|
+
self.last_x = 0.0
|
|
464
|
+
self.step = 1.0
|
|
465
|
+
|
|
466
|
+
def fit(self, x, y):
|
|
467
|
+
n = len(x)
|
|
468
|
+
if n == 0:
|
|
469
|
+
return
|
|
470
|
+
sum_x = sum(x)
|
|
471
|
+
sum_y = sum(y)
|
|
472
|
+
mean_x = sum_x / n
|
|
473
|
+
mean_y = sum_y / n
|
|
474
|
+
|
|
475
|
+
num = 0.0
|
|
476
|
+
den = 0.0
|
|
477
|
+
for i in range(n):
|
|
478
|
+
dx = x[i] - mean_x
|
|
479
|
+
num += dx * (y[i] - mean_y)
|
|
480
|
+
den += dx * dx
|
|
481
|
+
|
|
482
|
+
if den != 0.0:
|
|
483
|
+
self.m = num / den
|
|
484
|
+
else:
|
|
485
|
+
self.m = 0.0
|
|
486
|
+
self.c = mean_y - self.m * mean_x
|
|
487
|
+
|
|
488
|
+
self.last_x = x[-1]
|
|
489
|
+
self.step = (x[-1] - x[0]) / (n - 1) if n > 1 else 1.0
|
|
490
|
+
|
|
491
|
+
def predict(self, x):
|
|
492
|
+
return self.m * x + self.c
|
|
493
|
+
|
|
494
|
+
def forecastNext(self):
|
|
495
|
+
return self.m * (self.last_x + self.step) + self.c
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
class KNN:
|
|
499
|
+
def __init__(self, training_data, labels, num_samples, dims, k=3):
|
|
500
|
+
self.training_data = list(training_data)
|
|
501
|
+
self.labels = list(labels)
|
|
502
|
+
self.num_samples = num_samples
|
|
503
|
+
self.dims = dims
|
|
504
|
+
self.k = k
|
|
505
|
+
self.is_flat = len(self.training_data) == num_samples * dims
|
|
506
|
+
|
|
507
|
+
@micropython.native
|
|
508
|
+
def predict(self, x):
|
|
509
|
+
dists = [0.0] * self.num_samples
|
|
510
|
+
if self.is_flat:
|
|
511
|
+
for i in range(self.num_samples):
|
|
512
|
+
d2 = 0.0
|
|
513
|
+
offset = i * self.dims
|
|
514
|
+
for j in range(self.dims):
|
|
515
|
+
diff = x[j] - self.training_data[offset + j]
|
|
516
|
+
d2 += diff * diff
|
|
517
|
+
dists[i] = d2
|
|
518
|
+
else:
|
|
519
|
+
for i in range(self.num_samples):
|
|
520
|
+
d2 = 0.0
|
|
521
|
+
row = self.training_data[i]
|
|
522
|
+
for j in range(self.dims):
|
|
523
|
+
diff = x[j] - row[j]
|
|
524
|
+
d2 += diff * diff
|
|
525
|
+
dists[i] = d2
|
|
526
|
+
|
|
527
|
+
pairs = []
|
|
528
|
+
for i in range(self.num_samples):
|
|
529
|
+
pairs.append((dists[i], self.labels[i]))
|
|
530
|
+
pairs.sort()
|
|
531
|
+
|
|
532
|
+
votes = {}
|
|
533
|
+
for i in range(self.k):
|
|
534
|
+
lbl = pairs[i][1]
|
|
535
|
+
votes[lbl] = votes.get(lbl, 0) + 1
|
|
536
|
+
|
|
537
|
+
max_votes = -1
|
|
538
|
+
winner = None
|
|
539
|
+
for lbl, count in votes.items():
|
|
540
|
+
if count > max_votes:
|
|
541
|
+
max_votes = count
|
|
542
|
+
winner = lbl
|
|
543
|
+
return winner
|
|
544
|
+
|
|
545
|
+
|
|
546
|
+
class NaiveBayes:
|
|
547
|
+
def __init__(self, num_classes, dims, means, vars, priors):
|
|
548
|
+
self.num_classes = num_classes
|
|
549
|
+
self.dims = dims
|
|
550
|
+
self.means = list(means)
|
|
551
|
+
self.vars = list(vars)
|
|
552
|
+
self.priors = list(priors)
|
|
553
|
+
|
|
554
|
+
@micropython.native
|
|
555
|
+
def predict(self, x):
|
|
556
|
+
best_class = 0
|
|
557
|
+
max_log_prob = -1e9
|
|
558
|
+
import math
|
|
559
|
+
|
|
560
|
+
for c in range(self.num_classes):
|
|
561
|
+
log_prob = math.log(self.priors[c])
|
|
562
|
+
for j in range(self.dims):
|
|
563
|
+
idx = c * self.dims + j
|
|
564
|
+
mean_val = self.means[idx]
|
|
565
|
+
var_val = self.vars[idx]
|
|
566
|
+
if var_val <= 0.0:
|
|
567
|
+
var_val = 1e-9
|
|
568
|
+
diff = x[j] - mean_val
|
|
569
|
+
log_prob += -0.5 * math.log(6.283185307179586 * var_val) - (diff * diff) / (2.0 * var_val)
|
|
570
|
+
|
|
571
|
+
if log_prob > max_log_prob:
|
|
572
|
+
max_log_prob = log_prob
|
|
573
|
+
best_class = c
|
|
574
|
+
return best_class
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
class LogisticRegression:
|
|
578
|
+
def __init__(self, dims, weights, bias):
|
|
579
|
+
self.dims = dims
|
|
580
|
+
self.weights = list(weights)
|
|
581
|
+
self.bias = bias
|
|
582
|
+
|
|
583
|
+
@micropython.native
|
|
584
|
+
def predict(self, x):
|
|
585
|
+
z = self.bias
|
|
586
|
+
for i in range(self.dims):
|
|
587
|
+
z += self.weights[i] * x[i]
|
|
588
|
+
return 1 if z >= 0.0 else 0
|
|
589
|
+
|
|
590
|
+
def predict_proba(self, x):
|
|
591
|
+
z = self.bias
|
|
592
|
+
for i in range(self.dims):
|
|
593
|
+
z += self.weights[i] * x[i]
|
|
594
|
+
import math
|
|
595
|
+
try:
|
|
596
|
+
return 1.0 / (1.0 + math.exp(-z))
|
|
597
|
+
except OverflowError:
|
|
598
|
+
return 0.0 if z < 0 else 1.0
|
|
599
|
+
|
|
600
|
+
|
|
601
|
+
class DecisionTreeClassifier:
|
|
602
|
+
def __init__(self, nodes, num_nodes=None):
|
|
603
|
+
self.nodes = list(nodes)
|
|
604
|
+
self.num_nodes = len(self.nodes) if num_nodes is None else num_nodes
|
|
605
|
+
|
|
606
|
+
@micropython.native
|
|
607
|
+
def predict(self, x):
|
|
608
|
+
curr_idx = 0
|
|
609
|
+
while curr_idx < self.num_nodes:
|
|
610
|
+
node = self.nodes[curr_idx]
|
|
611
|
+
if isinstance(node, dict):
|
|
612
|
+
feat = node.get("feature", -1)
|
|
613
|
+
if feat == -1: feat = node.get("feature_idx", -1)
|
|
614
|
+
threshold = node.get("threshold", 0.0)
|
|
615
|
+
left = node.get("left", -1)
|
|
616
|
+
if left == -1: left = node.get("left_child", -1)
|
|
617
|
+
right = node.get("right", -1)
|
|
618
|
+
if right == -1: right = node.get("right_child", -1)
|
|
619
|
+
val = node.get("value", -1)
|
|
620
|
+
elif isinstance(node, (list, tuple)):
|
|
621
|
+
feat = int(node[0])
|
|
622
|
+
threshold = float(node[1])
|
|
623
|
+
left = int(node[2])
|
|
624
|
+
right = int(node[3])
|
|
625
|
+
val = int(node[4])
|
|
626
|
+
else:
|
|
627
|
+
feat = getattr(node, "feature", -1)
|
|
628
|
+
if feat == -1: feat = getattr(node, "feature_idx", -1)
|
|
629
|
+
threshold = getattr(node, "threshold", 0.0)
|
|
630
|
+
left = getattr(node, "left", -1)
|
|
631
|
+
if left == -1: left = getattr(node, "left_child", -1)
|
|
632
|
+
right = getattr(node, "right", -1)
|
|
633
|
+
if right == -1: right = getattr(node, "right_child", -1)
|
|
634
|
+
val = getattr(node, "value", -1)
|
|
635
|
+
|
|
636
|
+
if feat < 0:
|
|
637
|
+
return val
|
|
638
|
+
if x[feat] <= threshold:
|
|
639
|
+
curr_idx = left
|
|
640
|
+
else:
|
|
641
|
+
curr_idx = right
|
|
642
|
+
return -1
|
|
643
|
+
|
|
644
|
+
|
|
645
|
+
class KMeans:
|
|
646
|
+
def __init__(self, k, dims, centroids=None):
|
|
647
|
+
self.k = k
|
|
648
|
+
self.dims = dims
|
|
649
|
+
if centroids is not None:
|
|
650
|
+
self.centroids = list(centroids)
|
|
651
|
+
self.is_flat = len(self.centroids) == k * dims
|
|
652
|
+
else:
|
|
653
|
+
self.centroids = [0.0] * (k * dims)
|
|
654
|
+
self.is_flat = True
|
|
655
|
+
|
|
656
|
+
@micropython.native
|
|
657
|
+
def predict(self, x):
|
|
658
|
+
best_k = 0
|
|
659
|
+
min_d2 = 1e9
|
|
660
|
+
for i in range(self.k):
|
|
661
|
+
d2 = 0.0
|
|
662
|
+
if self.is_flat:
|
|
663
|
+
offset = i * self.dims
|
|
664
|
+
for j in range(self.dims):
|
|
665
|
+
diff = x[j] - self.centroids[offset + j]
|
|
666
|
+
d2 += diff * diff
|
|
667
|
+
else:
|
|
668
|
+
row = self.centroids[i]
|
|
669
|
+
for j in range(self.dims):
|
|
670
|
+
diff = x[j] - row[j]
|
|
671
|
+
d2 += diff * diff
|
|
672
|
+
if d2 < min_d2:
|
|
673
|
+
min_d2 = d2
|
|
674
|
+
best_k = i
|
|
675
|
+
return best_k
|
|
676
|
+
|
|
677
|
+
def run(self, data, num_samples, assignments=None, max_iters=10):
|
|
678
|
+
data = list(data)
|
|
679
|
+
is_data_flat = len(data) == num_samples * self.dims
|
|
680
|
+
if assignments is None:
|
|
681
|
+
assignments = [0] * num_samples
|
|
682
|
+
|
|
683
|
+
total_zeros = True
|
|
684
|
+
for val in self.centroids:
|
|
685
|
+
if val != 0.0:
|
|
686
|
+
total_zeros = False
|
|
687
|
+
break
|
|
688
|
+
if total_zeros:
|
|
689
|
+
for i in range(self.k):
|
|
690
|
+
sample_idx = i % num_samples
|
|
691
|
+
if is_data_flat:
|
|
692
|
+
for j in range(self.dims):
|
|
693
|
+
self.centroids[i * self.dims + j] = data[sample_idx * self.dims + j]
|
|
694
|
+
else:
|
|
695
|
+
self.centroids[i] = list(data[sample_idx])
|
|
696
|
+
|
|
697
|
+
for _ in range(max_iters):
|
|
698
|
+
changed = False
|
|
699
|
+
for i in range(num_samples):
|
|
700
|
+
if is_data_flat:
|
|
701
|
+
x = data[i * self.dims : (i + 1) * self.dims]
|
|
702
|
+
else:
|
|
703
|
+
x = data[i]
|
|
704
|
+
old_k = assignments[i]
|
|
705
|
+
new_k = self.predict(x)
|
|
706
|
+
if old_k != new_k:
|
|
707
|
+
assignments[i] = new_k
|
|
708
|
+
changed = True
|
|
709
|
+
if not changed:
|
|
710
|
+
break
|
|
711
|
+
|
|
712
|
+
cnt = [0] * self.k
|
|
713
|
+
sums = [0.0] * (self.k * self.dims)
|
|
714
|
+
for i in range(num_samples):
|
|
715
|
+
cid = assignments[i]
|
|
716
|
+
cnt[cid] += 1
|
|
717
|
+
if is_data_flat:
|
|
718
|
+
for j in range(self.dims):
|
|
719
|
+
sums[cid * self.dims + j] += data[i * self.dims + j]
|
|
720
|
+
else:
|
|
721
|
+
for j in range(self.dims):
|
|
722
|
+
sums[cid * self.dims + j] += data[i][j]
|
|
723
|
+
|
|
724
|
+
for cid in range(self.k):
|
|
725
|
+
n_pts = cnt[cid]
|
|
726
|
+
if n_pts > 0:
|
|
727
|
+
for j in range(self.dims):
|
|
728
|
+
val = sums[cid * self.dims + j] / n_pts
|
|
729
|
+
if self.is_flat:
|
|
730
|
+
self.centroids[cid * self.dims + j] = val
|
|
731
|
+
else:
|
|
732
|
+
self.centroids[cid][j] = val
|
|
733
|
+
return assignments
|
raksa-0.1.0/setup.cfg
ADDED