c2d-utils 0.0.1__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.
- c2d_utils-0.0.1/.gitignore +48 -0
- c2d_utils-0.0.1/LICENSE +21 -0
- c2d_utils-0.0.1/Makefile +23 -0
- c2d_utils-0.0.1/PKG-INFO +167 -0
- c2d_utils-0.0.1/README.md +151 -0
- c2d_utils-0.0.1/pyproject.toml +28 -0
- c2d_utils-0.0.1/requirements.txt +1 -0
- c2d_utils-0.0.1/src/c2d_utils/__init__.py +3 -0
- c2d_utils-0.0.1/src/c2d_utils/ctd_env.py +144 -0
- c2d_utils-0.0.1/src/c2d_utils/reader.py +128 -0
- c2d_utils-0.0.1/src/c2d_utils/reporting.py +288 -0
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
*.so
|
|
5
|
+
build/
|
|
6
|
+
develop-eggs/
|
|
7
|
+
dist/
|
|
8
|
+
downloads/
|
|
9
|
+
eggs/
|
|
10
|
+
.eggs/
|
|
11
|
+
lib/
|
|
12
|
+
lib64/
|
|
13
|
+
parts/
|
|
14
|
+
sdist/
|
|
15
|
+
var/
|
|
16
|
+
wheels/
|
|
17
|
+
share/python-wheels/
|
|
18
|
+
*.egg-info/
|
|
19
|
+
.installed.cfg
|
|
20
|
+
*.egg
|
|
21
|
+
MANIFEST
|
|
22
|
+
pip-log.txt
|
|
23
|
+
pip-delete-this-directory.txt
|
|
24
|
+
htmlcov/
|
|
25
|
+
.tox/
|
|
26
|
+
.nox/
|
|
27
|
+
.coverage
|
|
28
|
+
.coverage.*
|
|
29
|
+
.cache
|
|
30
|
+
nosetests.xml
|
|
31
|
+
coverage.xml
|
|
32
|
+
*.cover
|
|
33
|
+
*.py,cover
|
|
34
|
+
.hypothesis/
|
|
35
|
+
.pytest_cache/
|
|
36
|
+
cover/
|
|
37
|
+
test-results.txt
|
|
38
|
+
.env
|
|
39
|
+
.venv
|
|
40
|
+
env/
|
|
41
|
+
venv/
|
|
42
|
+
ENV/
|
|
43
|
+
env.bak/
|
|
44
|
+
venv.bak/
|
|
45
|
+
.vscode/
|
|
46
|
+
.idea/
|
|
47
|
+
*.swp
|
|
48
|
+
*.swo
|
c2d_utils-0.0.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TU Wien
|
|
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.
|
c2d_utils-0.0.1/Makefile
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
.PHONY: init install test build upload clean
|
|
2
|
+
|
|
3
|
+
init: clean
|
|
4
|
+
pip install -r requirements.txt
|
|
5
|
+
|
|
6
|
+
create:
|
|
7
|
+
python3 -m venv .venv
|
|
8
|
+
|
|
9
|
+
install:
|
|
10
|
+
pip install -e .
|
|
11
|
+
|
|
12
|
+
test:
|
|
13
|
+
PYTHONPATH=src pytest tests/
|
|
14
|
+
|
|
15
|
+
build: init
|
|
16
|
+
python3 -m build
|
|
17
|
+
|
|
18
|
+
upload: build
|
|
19
|
+
python3 -m twine upload dist/*
|
|
20
|
+
|
|
21
|
+
clean:
|
|
22
|
+
rm -rf dist
|
|
23
|
+
find . -type d -name '__pycache__' -exec rm -rf {} +
|
c2d_utils-0.0.1/PKG-INFO
ADDED
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: c2d-utils
|
|
3
|
+
Version: 0.0.1
|
|
4
|
+
Summary: A shared infrastructure library for Pontus-X / C2D algorithms.
|
|
5
|
+
Author: TU Wien
|
|
6
|
+
License: MIT
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Requires-Python: >=3.10
|
|
9
|
+
Requires-Dist: markdown
|
|
10
|
+
Requires-Dist: matplotlib
|
|
11
|
+
Requires-Dist: numpy
|
|
12
|
+
Requires-Dist: pandas
|
|
13
|
+
Requires-Dist: tables
|
|
14
|
+
Requires-Dist: xhtml2pdf
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
|
|
17
|
+
# C2D-Utils
|
|
18
|
+
|
|
19
|
+
A shared infrastructure and data-access SDK for Pontus-X / Compute-to-Data (C2D) manufacturing analytics and milling diagnostics algorithms.
|
|
20
|
+
|
|
21
|
+
## Features
|
|
22
|
+
* **C2D Environment Context**: Resolves Pontus-X DID input directories, handles consumer customization payloads, auto-creates output directories, and automates parameter logging.
|
|
23
|
+
* **HDF5 Parsing Engine**: Extracts high-frequency vibration streams, sensor calibration tables, and base64 camera blob nodes.
|
|
24
|
+
* **Universal PDF Reporting**: Renders Markdown/HTML documents to A4 PDFs with sleek slate-and-blue stylesheets and fixed-width 4-column parameters tables.
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
## Installation
|
|
29
|
+
To install the package locally in editable development mode:
|
|
30
|
+
```bash
|
|
31
|
+
pip install -e .
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
## Local Development Flow
|
|
37
|
+
|
|
38
|
+
When building and testing algorithms locally, you override the default Ocean C2D directories by passing manual arguments to your CLI script:
|
|
39
|
+
|
|
40
|
+
* **`--sample <path>`**: The path to a local HDF5 file or directory of datasets.
|
|
41
|
+
* **`--out <path>`**: The directory where outputs (plots, CSVs, and PDF reports) are saved. If the specified directory does not exist, `C2DContext` will automatically create it (including all parent paths) on instantiation.
|
|
42
|
+
|
|
43
|
+
### Execution Example
|
|
44
|
+
```bash
|
|
45
|
+
# Run single report on a local file and output to outputs/single_run
|
|
46
|
+
python run_single_report.py --sample test-data/cirp_twm_1.hdf5 --out outputs/single_run
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## Usage Examples
|
|
52
|
+
|
|
53
|
+
### 1. Manual CLI Definition & C2D Context Resolution
|
|
54
|
+
Define your command line flags manually, and instantiate `C2DContext`. The library dynamically checks the `DIDS` environment array. If found, it targets the C2D mounted dataset `/data/inputs/{DID}/0`. Otherwise, it falls back to your local `--sample` input path.
|
|
55
|
+
|
|
56
|
+
```python
|
|
57
|
+
import argparse
|
|
58
|
+
import sys
|
|
59
|
+
import logging
|
|
60
|
+
from c2d_utils.ctd_env import C2DContext
|
|
61
|
+
|
|
62
|
+
logger = logging.getLogger("ocean_c2d.run_single_report")
|
|
63
|
+
|
|
64
|
+
def main():
|
|
65
|
+
parser = argparse.ArgumentParser(description="HDF5 Process Analytics - Single Dataset Report Generator")
|
|
66
|
+
parser.add_argument(
|
|
67
|
+
'--log',
|
|
68
|
+
default='INFO',
|
|
69
|
+
help='Set the logging level. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL'
|
|
70
|
+
)
|
|
71
|
+
parser.add_argument(
|
|
72
|
+
'--sample',
|
|
73
|
+
default=None,
|
|
74
|
+
help='Run sample file.'
|
|
75
|
+
)
|
|
76
|
+
parser.add_argument(
|
|
77
|
+
'--out',
|
|
78
|
+
default="/data/outputs",
|
|
79
|
+
help='Output directory.'
|
|
80
|
+
)
|
|
81
|
+
parser.add_argument("--window_size", type=int, default=None, help="Window size for RMS")
|
|
82
|
+
parser.add_argument("--step_size", type=int, default=None, help="Step size for RMS")
|
|
83
|
+
parser.add_argument("--draft_size", type=int, default=None, help="Draft size for zoom")
|
|
84
|
+
|
|
85
|
+
args, unknown = parser.parse_known_args()
|
|
86
|
+
C2DContext.setup_logging(args.log)
|
|
87
|
+
|
|
88
|
+
logger.info("=================================================")
|
|
89
|
+
logger.info("*** HDF5 Single Dataset Analytics Tool ***")
|
|
90
|
+
logger.info("=================================================")
|
|
91
|
+
|
|
92
|
+
# Initialize environment context:
|
|
93
|
+
# 1. Resolves Pontus-X or local directories.
|
|
94
|
+
# 2. Automatically runs `output_dir.mkdir(parents=True, exist_ok=True)`.
|
|
95
|
+
# 3. Logs a clean parameters summary to the console.
|
|
96
|
+
ctx = C2DContext(args)
|
|
97
|
+
|
|
98
|
+
# Access resolved variables
|
|
99
|
+
print(f"Loading files from: {ctx.input_file}")
|
|
100
|
+
print(f"Writing results to: {ctx.output_dir}")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### 2. Reading HDF5 Sensor Streams & Camera Blobs
|
|
104
|
+
Parse the acceleration tables, raw calibration properties, and base64 inspection frames from an HDF5 database:
|
|
105
|
+
|
|
106
|
+
```python
|
|
107
|
+
from pathlib import Path
|
|
108
|
+
from c2d_utils.reader import get_file_data, save_extracted_images
|
|
109
|
+
|
|
110
|
+
# Parse HDF5 structure
|
|
111
|
+
content = get_file_data(Path("measurement.hdf5"))
|
|
112
|
+
|
|
113
|
+
# Access high-frequency vibration signals
|
|
114
|
+
acceleration_df = content.acceleration_df
|
|
115
|
+
x_signal = acceleration_df["x"].values
|
|
116
|
+
timestamps = acceleration_df["timestamp"].values
|
|
117
|
+
|
|
118
|
+
# Decode and write camera frame blobs to output folder
|
|
119
|
+
save_extracted_images(content.pictures, Path("output/images"))
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### 3. Reporting Functions & PDF Compilation
|
|
123
|
+
The `c2d-utils` reporting module provides two core helper functions to compile professional, print-ready reports:
|
|
124
|
+
|
|
125
|
+
#### A. `ReportGenerator.build_params_table(data_dict, keys_to_include, table_title)`
|
|
126
|
+
Formats a raw dictionary into a structured, 4-column parameter table. The table is automatically split to fit within A4 boundaries (columns 1 & 2 show the first half, columns 3 & 4 show the second half with the same titles):
|
|
127
|
+
* `data_dict`: Raw data dictionary containing the parameters.
|
|
128
|
+
* `keys_to_include`: A list of strings/keys to select from `data_dict`.
|
|
129
|
+
* `table_title`: The display header for the table.
|
|
130
|
+
|
|
131
|
+
#### B. `ReportGenerator.render_markdown_to_pdf(markdown_text, output_pdf_path, base_dir)`
|
|
132
|
+
Compiles markdown syntax and embedded HTML tables/images into an A4 PDF document using a modern slate-and-blue design scheme:
|
|
133
|
+
* `markdown_text`: The string content in Markdown format.
|
|
134
|
+
* `output_pdf_path`: The `Path` where the final `.pdf` file will be saved.
|
|
135
|
+
* `base_dir`: The directory to resolve relative paths of referenced asset images (e.g. graphs, snapshots).
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
from pathlib import Path
|
|
139
|
+
from c2d_utils.reporting import ReportGenerator
|
|
140
|
+
|
|
141
|
+
# 1. Build a 4-column parameter table
|
|
142
|
+
raw_params = {
|
|
143
|
+
"Cutter Diameter": "10 mm",
|
|
144
|
+
"Spindle Speed": "2546 RPM",
|
|
145
|
+
"Tooth Count": "2",
|
|
146
|
+
"Max Depth": "1.5 mm"
|
|
147
|
+
}
|
|
148
|
+
keys = ["Cutter Diameter", "Spindle Speed", "Tooth Count", "Max Depth"]
|
|
149
|
+
param_table_html = ReportGenerator.build_params_table(raw_params, keys, "Tool Specifications")
|
|
150
|
+
|
|
151
|
+
# 2. Construct Markdown report layout
|
|
152
|
+
report_markdown = f"""
|
|
153
|
+
# Process Diagnostics Report
|
|
154
|
+
|
|
155
|
+
{param_table_html}
|
|
156
|
+
|
|
157
|
+
## Vibration Profile
|
|
158
|
+
<img class="graph" src="vibration_analysis.png" />
|
|
159
|
+
"""
|
|
160
|
+
|
|
161
|
+
# 3. Render Markdown layout to PDF
|
|
162
|
+
success = ReportGenerator.render_markdown_to_pdf(
|
|
163
|
+
markdown_text=report_markdown,
|
|
164
|
+
output_pdf_path=Path("outputs/single_run/result.pdf"),
|
|
165
|
+
base_dir=Path("outputs/single_run")
|
|
166
|
+
)
|
|
167
|
+
```
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
# C2D-Utils
|
|
2
|
+
|
|
3
|
+
A shared infrastructure and data-access SDK for Pontus-X / Compute-to-Data (C2D) manufacturing analytics and milling diagnostics algorithms.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
* **C2D Environment Context**: Resolves Pontus-X DID input directories, handles consumer customization payloads, auto-creates output directories, and automates parameter logging.
|
|
7
|
+
* **HDF5 Parsing Engine**: Extracts high-frequency vibration streams, sensor calibration tables, and base64 camera blob nodes.
|
|
8
|
+
* **Universal PDF Reporting**: Renders Markdown/HTML documents to A4 PDFs with sleek slate-and-blue stylesheets and fixed-width 4-column parameters tables.
|
|
9
|
+
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
## Installation
|
|
13
|
+
To install the package locally in editable development mode:
|
|
14
|
+
```bash
|
|
15
|
+
pip install -e .
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## Local Development Flow
|
|
21
|
+
|
|
22
|
+
When building and testing algorithms locally, you override the default Ocean C2D directories by passing manual arguments to your CLI script:
|
|
23
|
+
|
|
24
|
+
* **`--sample <path>`**: The path to a local HDF5 file or directory of datasets.
|
|
25
|
+
* **`--out <path>`**: The directory where outputs (plots, CSVs, and PDF reports) are saved. If the specified directory does not exist, `C2DContext` will automatically create it (including all parent paths) on instantiation.
|
|
26
|
+
|
|
27
|
+
### Execution Example
|
|
28
|
+
```bash
|
|
29
|
+
# Run single report on a local file and output to outputs/single_run
|
|
30
|
+
python run_single_report.py --sample test-data/cirp_twm_1.hdf5 --out outputs/single_run
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## Usage Examples
|
|
36
|
+
|
|
37
|
+
### 1. Manual CLI Definition & C2D Context Resolution
|
|
38
|
+
Define your command line flags manually, and instantiate `C2DContext`. The library dynamically checks the `DIDS` environment array. If found, it targets the C2D mounted dataset `/data/inputs/{DID}/0`. Otherwise, it falls back to your local `--sample` input path.
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
import argparse
|
|
42
|
+
import sys
|
|
43
|
+
import logging
|
|
44
|
+
from c2d_utils.ctd_env import C2DContext
|
|
45
|
+
|
|
46
|
+
logger = logging.getLogger("ocean_c2d.run_single_report")
|
|
47
|
+
|
|
48
|
+
def main():
|
|
49
|
+
parser = argparse.ArgumentParser(description="HDF5 Process Analytics - Single Dataset Report Generator")
|
|
50
|
+
parser.add_argument(
|
|
51
|
+
'--log',
|
|
52
|
+
default='INFO',
|
|
53
|
+
help='Set the logging level. Options: DEBUG, INFO, WARNING, ERROR, CRITICAL'
|
|
54
|
+
)
|
|
55
|
+
parser.add_argument(
|
|
56
|
+
'--sample',
|
|
57
|
+
default=None,
|
|
58
|
+
help='Run sample file.'
|
|
59
|
+
)
|
|
60
|
+
parser.add_argument(
|
|
61
|
+
'--out',
|
|
62
|
+
default="/data/outputs",
|
|
63
|
+
help='Output directory.'
|
|
64
|
+
)
|
|
65
|
+
parser.add_argument("--window_size", type=int, default=None, help="Window size for RMS")
|
|
66
|
+
parser.add_argument("--step_size", type=int, default=None, help="Step size for RMS")
|
|
67
|
+
parser.add_argument("--draft_size", type=int, default=None, help="Draft size for zoom")
|
|
68
|
+
|
|
69
|
+
args, unknown = parser.parse_known_args()
|
|
70
|
+
C2DContext.setup_logging(args.log)
|
|
71
|
+
|
|
72
|
+
logger.info("=================================================")
|
|
73
|
+
logger.info("*** HDF5 Single Dataset Analytics Tool ***")
|
|
74
|
+
logger.info("=================================================")
|
|
75
|
+
|
|
76
|
+
# Initialize environment context:
|
|
77
|
+
# 1. Resolves Pontus-X or local directories.
|
|
78
|
+
# 2. Automatically runs `output_dir.mkdir(parents=True, exist_ok=True)`.
|
|
79
|
+
# 3. Logs a clean parameters summary to the console.
|
|
80
|
+
ctx = C2DContext(args)
|
|
81
|
+
|
|
82
|
+
# Access resolved variables
|
|
83
|
+
print(f"Loading files from: {ctx.input_file}")
|
|
84
|
+
print(f"Writing results to: {ctx.output_dir}")
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 2. Reading HDF5 Sensor Streams & Camera Blobs
|
|
88
|
+
Parse the acceleration tables, raw calibration properties, and base64 inspection frames from an HDF5 database:
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from pathlib import Path
|
|
92
|
+
from c2d_utils.reader import get_file_data, save_extracted_images
|
|
93
|
+
|
|
94
|
+
# Parse HDF5 structure
|
|
95
|
+
content = get_file_data(Path("measurement.hdf5"))
|
|
96
|
+
|
|
97
|
+
# Access high-frequency vibration signals
|
|
98
|
+
acceleration_df = content.acceleration_df
|
|
99
|
+
x_signal = acceleration_df["x"].values
|
|
100
|
+
timestamps = acceleration_df["timestamp"].values
|
|
101
|
+
|
|
102
|
+
# Decode and write camera frame blobs to output folder
|
|
103
|
+
save_extracted_images(content.pictures, Path("output/images"))
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### 3. Reporting Functions & PDF Compilation
|
|
107
|
+
The `c2d-utils` reporting module provides two core helper functions to compile professional, print-ready reports:
|
|
108
|
+
|
|
109
|
+
#### A. `ReportGenerator.build_params_table(data_dict, keys_to_include, table_title)`
|
|
110
|
+
Formats a raw dictionary into a structured, 4-column parameter table. The table is automatically split to fit within A4 boundaries (columns 1 & 2 show the first half, columns 3 & 4 show the second half with the same titles):
|
|
111
|
+
* `data_dict`: Raw data dictionary containing the parameters.
|
|
112
|
+
* `keys_to_include`: A list of strings/keys to select from `data_dict`.
|
|
113
|
+
* `table_title`: The display header for the table.
|
|
114
|
+
|
|
115
|
+
#### B. `ReportGenerator.render_markdown_to_pdf(markdown_text, output_pdf_path, base_dir)`
|
|
116
|
+
Compiles markdown syntax and embedded HTML tables/images into an A4 PDF document using a modern slate-and-blue design scheme:
|
|
117
|
+
* `markdown_text`: The string content in Markdown format.
|
|
118
|
+
* `output_pdf_path`: The `Path` where the final `.pdf` file will be saved.
|
|
119
|
+
* `base_dir`: The directory to resolve relative paths of referenced asset images (e.g. graphs, snapshots).
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from pathlib import Path
|
|
123
|
+
from c2d_utils.reporting import ReportGenerator
|
|
124
|
+
|
|
125
|
+
# 1. Build a 4-column parameter table
|
|
126
|
+
raw_params = {
|
|
127
|
+
"Cutter Diameter": "10 mm",
|
|
128
|
+
"Spindle Speed": "2546 RPM",
|
|
129
|
+
"Tooth Count": "2",
|
|
130
|
+
"Max Depth": "1.5 mm"
|
|
131
|
+
}
|
|
132
|
+
keys = ["Cutter Diameter", "Spindle Speed", "Tooth Count", "Max Depth"]
|
|
133
|
+
param_table_html = ReportGenerator.build_params_table(raw_params, keys, "Tool Specifications")
|
|
134
|
+
|
|
135
|
+
# 2. Construct Markdown report layout
|
|
136
|
+
report_markdown = f"""
|
|
137
|
+
# Process Diagnostics Report
|
|
138
|
+
|
|
139
|
+
{param_table_html}
|
|
140
|
+
|
|
141
|
+
## Vibration Profile
|
|
142
|
+
<img class="graph" src="vibration_analysis.png" />
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
# 3. Render Markdown layout to PDF
|
|
146
|
+
success = ReportGenerator.render_markdown_to_pdf(
|
|
147
|
+
markdown_text=report_markdown,
|
|
148
|
+
output_pdf_path=Path("outputs/single_run/result.pdf"),
|
|
149
|
+
base_dir=Path("outputs/single_run")
|
|
150
|
+
)
|
|
151
|
+
```
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "c2d-utils"
|
|
7
|
+
version = "0.0.1"
|
|
8
|
+
description = "A shared infrastructure library for Pontus-X / C2D algorithms."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = { text = "MIT" }
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "TU Wien" }
|
|
13
|
+
]
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
dependencies = [
|
|
16
|
+
"numpy",
|
|
17
|
+
"pandas",
|
|
18
|
+
"tables",
|
|
19
|
+
"matplotlib",
|
|
20
|
+
"xhtml2pdf",
|
|
21
|
+
"markdown"
|
|
22
|
+
]
|
|
23
|
+
|
|
24
|
+
[tool.hatch.build.targets.sdist]
|
|
25
|
+
exclude = [
|
|
26
|
+
"/.venv",
|
|
27
|
+
"/dist"
|
|
28
|
+
]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
hatchling
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import sys
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, Any, List
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("c2d_utils.ctd_env")
|
|
9
|
+
|
|
10
|
+
class C2DContext:
|
|
11
|
+
"""Manages the Ocean Compute-to-Data (C2D) environment, directory structures,
|
|
12
|
+
and custom consumer parameters payload resolving.
|
|
13
|
+
"""
|
|
14
|
+
def __init__(self, args=None, sample: str = None, out: str = None,
|
|
15
|
+
window: int = None, step: int = None, draft: int = None,
|
|
16
|
+
compliance_only: bool = False):
|
|
17
|
+
|
|
18
|
+
# Extract from args if provided, supporting both window and window_size variants
|
|
19
|
+
args_sample = getattr(args, "sample", None) if args else None
|
|
20
|
+
args_out = getattr(args, "out", None) if args else None
|
|
21
|
+
args_window = getattr(args, "window_size", None) if args else None
|
|
22
|
+
if args_window is None and args:
|
|
23
|
+
args_window = getattr(args, "window", None)
|
|
24
|
+
args_step = getattr(args, "step_size", None) if args else None
|
|
25
|
+
if args_step is None and args:
|
|
26
|
+
args_step = getattr(args, "step", None)
|
|
27
|
+
args_draft = getattr(args, "draft_size", None) if args else None
|
|
28
|
+
if args_draft is None and args:
|
|
29
|
+
args_draft = getattr(args, "draft", None)
|
|
30
|
+
args_compliance_only = getattr(args, "compliance_only", False) if args else False
|
|
31
|
+
|
|
32
|
+
# Resolve values (explicit constructor kwargs override args namespace fields)
|
|
33
|
+
sample = sample if sample is not None else args_sample
|
|
34
|
+
out = out if out is not None else (args_out if args_out is not None else "/data/outputs")
|
|
35
|
+
window = window if window is not None else args_window
|
|
36
|
+
step = step if step is not None else args_step
|
|
37
|
+
draft = draft if draft is not None else args_draft
|
|
38
|
+
compliance_only = compliance_only or args_compliance_only
|
|
39
|
+
|
|
40
|
+
# 1. Resolve Input File / Directory (DIDS environment variable takes precedence)
|
|
41
|
+
if os.environ.get("DIDS"):
|
|
42
|
+
try:
|
|
43
|
+
import json
|
|
44
|
+
dids = json.loads(os.environ.get("DIDS"))
|
|
45
|
+
if dids:
|
|
46
|
+
self.input_file = Path(f"/data/inputs/{dids[0]}/0")
|
|
47
|
+
else:
|
|
48
|
+
self.input_file = Path(os.environ.get("INPUT_DIR", "/data/inputs"))
|
|
49
|
+
except Exception:
|
|
50
|
+
self.input_file = Path(os.environ.get("INPUT_DIR", "/data/inputs"))
|
|
51
|
+
elif sample:
|
|
52
|
+
self.input_file = Path(sample)
|
|
53
|
+
else:
|
|
54
|
+
self.input_file = Path(os.environ.get("INPUT_DIR", "/data/inputs"))
|
|
55
|
+
|
|
56
|
+
# 2. Resolve Output Directory
|
|
57
|
+
self.output_dir = Path(out)
|
|
58
|
+
self.output_dir.mkdir(parents=True, exist_ok=True)
|
|
59
|
+
|
|
60
|
+
# 3. Load Pontus-X / Ocean Custom Parameters (algoCustomData.json)
|
|
61
|
+
self.custom_params = {}
|
|
62
|
+
custom_data_dir = self.input_file if self.input_file.is_dir() else self.input_file.parent
|
|
63
|
+
custom_data_file = custom_data_dir / "algoCustomData.json"
|
|
64
|
+
|
|
65
|
+
if custom_data_file.exists():
|
|
66
|
+
try:
|
|
67
|
+
with open(custom_data_file, "r") as f:
|
|
68
|
+
self.custom_params = json.load(f)
|
|
69
|
+
logger.info(f"Loaded custom C2D parameters from {custom_data_file}")
|
|
70
|
+
except Exception as e:
|
|
71
|
+
logger.warning(f"Failed to load custom parameters from {custom_data_file}: {e}")
|
|
72
|
+
|
|
73
|
+
# 4. Resolve parameters with overrides and fallbacks using matching names
|
|
74
|
+
self.window = self._resolve_param("window", window, 10000)
|
|
75
|
+
self.step = self._resolve_param("step", step, 2000)
|
|
76
|
+
self.draft = self._resolve_param("draft", draft, 2000)
|
|
77
|
+
|
|
78
|
+
# 5. Resolve compliance-only mode
|
|
79
|
+
self.compliance_only = (
|
|
80
|
+
compliance_only or
|
|
81
|
+
os.environ.get("COMPLIANCE_ONLY", "").lower() in ("true", "1", "yes")
|
|
82
|
+
)
|
|
83
|
+
self.log_params()
|
|
84
|
+
|
|
85
|
+
def log_params(self):
|
|
86
|
+
"""Log the resolved C2D context parameters."""
|
|
87
|
+
logger.info("=================================================")
|
|
88
|
+
logger.info(f"Resolved Input File: {self.input_file}")
|
|
89
|
+
logger.info(f"Resolved Output Dir: {self.output_dir}")
|
|
90
|
+
logger.info(f"Rolling Window Size: {self.window}")
|
|
91
|
+
logger.info(f"Rolling Step Size: {self.step}")
|
|
92
|
+
logger.info(f"Draft Sample Limit: {self.draft}")
|
|
93
|
+
logger.info(f"Compliance Only Mode: {self.compliance_only}")
|
|
94
|
+
logger.info("=================================================")
|
|
95
|
+
|
|
96
|
+
def _resolve_param(self, name: str, override: int, default: int) -> int:
|
|
97
|
+
if override is not None:
|
|
98
|
+
return override
|
|
99
|
+
val = self.custom_params.get(name, os.environ.get(name.upper(), default))
|
|
100
|
+
try:
|
|
101
|
+
return int(val)
|
|
102
|
+
except (ValueError, TypeError):
|
|
103
|
+
logger.warning(f"Could not convert parameter {name}='{val}' to integer. Using default {default}")
|
|
104
|
+
return default
|
|
105
|
+
|
|
106
|
+
@property
|
|
107
|
+
def hdf5_files(self) -> List[Path]:
|
|
108
|
+
"""Discovers and filters valid HDF5 files based on their magic bytes."""
|
|
109
|
+
if self.input_file.is_file():
|
|
110
|
+
return [self.input_file] if self._is_hdf5_file(self.input_file) else []
|
|
111
|
+
|
|
112
|
+
if not self.input_file.exists():
|
|
113
|
+
return []
|
|
114
|
+
|
|
115
|
+
candidates = []
|
|
116
|
+
for p in self.input_file.rglob("*"):
|
|
117
|
+
if p.is_file() and self._is_hdf5_file(p):
|
|
118
|
+
candidates.append(p)
|
|
119
|
+
return sorted(candidates)
|
|
120
|
+
|
|
121
|
+
def _is_hdf5_file(self, filepath: Path) -> bool:
|
|
122
|
+
"""Check if file has HDF5 magic number (\x89HDF\r\n\x1a\n)."""
|
|
123
|
+
try:
|
|
124
|
+
if not filepath.is_file():
|
|
125
|
+
return False
|
|
126
|
+
with open(filepath, "rb") as f:
|
|
127
|
+
header = f.read(8)
|
|
128
|
+
return header == b"\x89HDF\r\n\x1a\n"
|
|
129
|
+
except Exception:
|
|
130
|
+
return False
|
|
131
|
+
|
|
132
|
+
@staticmethod
|
|
133
|
+
def setup_logging(log_level: str = "INFO"):
|
|
134
|
+
"""Setup logger formatting."""
|
|
135
|
+
numeric_level = getattr(logging, log_level.upper(), None)
|
|
136
|
+
if not isinstance(numeric_level, int):
|
|
137
|
+
numeric_level = logging.INFO
|
|
138
|
+
|
|
139
|
+
logging.basicConfig(
|
|
140
|
+
level=numeric_level,
|
|
141
|
+
format="%(asctime)s %(levelname)-8s %(message)s",
|
|
142
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
143
|
+
force=True
|
|
144
|
+
)
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import json
|
|
3
|
+
import base64
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict, List, Any
|
|
7
|
+
import pandas as pd
|
|
8
|
+
|
|
9
|
+
logger = logging.getLogger("c2d_utils.reader")
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class HDF5NodeInfo:
|
|
13
|
+
name: str
|
|
14
|
+
path: str
|
|
15
|
+
type: str
|
|
16
|
+
attributes: Dict[str, Any]
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ParsedHDF5FileContent:
|
|
20
|
+
acceleration_df: pd.DataFrame
|
|
21
|
+
sensor_df: pd.DataFrame
|
|
22
|
+
acceleration_meta: HDF5NodeInfo
|
|
23
|
+
pictures: Dict[str, List[str]]
|
|
24
|
+
|
|
25
|
+
class MetadataPrefix:
|
|
26
|
+
PRE = "pre_metadata"
|
|
27
|
+
POST = "post_metadata"
|
|
28
|
+
|
|
29
|
+
def get_node_names(hdf5_file_handle) -> List[str]:
|
|
30
|
+
"""Return the path names of all root-level nodes."""
|
|
31
|
+
return [node._v_pathname for node in hdf5_file_handle.list_nodes("/")]
|
|
32
|
+
|
|
33
|
+
def get_picture_node_names(hdf5_file_handle) -> List[str]:
|
|
34
|
+
"""Return names of nodes that contain base64 picture data."""
|
|
35
|
+
return [name for name in get_node_names(hdf5_file_handle) if "pictures" in name]
|
|
36
|
+
|
|
37
|
+
def parse_json_if_possible(val):
|
|
38
|
+
"""Decode JSON from a string or bytes value, or return the value unchanged."""
|
|
39
|
+
if isinstance(val, (bytes, bytearray)):
|
|
40
|
+
try:
|
|
41
|
+
text = val.decode("utf-8")
|
|
42
|
+
except UnicodeDecodeError:
|
|
43
|
+
return val
|
|
44
|
+
elif isinstance(val, str):
|
|
45
|
+
text = val
|
|
46
|
+
else:
|
|
47
|
+
return val
|
|
48
|
+
try:
|
|
49
|
+
return json.loads(text)
|
|
50
|
+
except json.JSONDecodeError:
|
|
51
|
+
return val
|
|
52
|
+
|
|
53
|
+
def node_to_dict(node) -> HDF5NodeInfo:
|
|
54
|
+
"""Convert an HDF5 node's attribute store into a serialisable dataclass."""
|
|
55
|
+
info = HDF5NodeInfo(name=node._v_name, path=node._v_pathname,
|
|
56
|
+
type=node.__class__.__name__, attributes={})
|
|
57
|
+
for key in node._v_attrs._f_list(attrset="all"):
|
|
58
|
+
raw = node._v_attrs[key]
|
|
59
|
+
if hasattr(raw, "tolist"):
|
|
60
|
+
pyval = raw.tolist()
|
|
61
|
+
elif hasattr(raw, "item"):
|
|
62
|
+
pyval = raw.item()
|
|
63
|
+
else:
|
|
64
|
+
pyval = raw
|
|
65
|
+
info.attributes[key] = parse_json_if_possible(pyval)
|
|
66
|
+
return info
|
|
67
|
+
|
|
68
|
+
def get_file_data(file_path: Path) -> ParsedHDF5FileContent:
|
|
69
|
+
"""Read and parse the structured data from an HDF5 measurement file."""
|
|
70
|
+
import tables
|
|
71
|
+
from tables import NoSuchNodeError
|
|
72
|
+
logger.info(f"Opening and parsing HDF5 file: {file_path.name}")
|
|
73
|
+
|
|
74
|
+
with tables.open_file(str(file_path), mode="r") as file_handle:
|
|
75
|
+
picture_node_names = get_picture_node_names(file_handle)
|
|
76
|
+
pictures: Dict[str, List[str]] = {}
|
|
77
|
+
for node_name in picture_node_names:
|
|
78
|
+
node = file_handle.get_node(node_name)
|
|
79
|
+
assert isinstance(node, tables.Array)
|
|
80
|
+
pictures[node_name.removeprefix("/")] = [
|
|
81
|
+
img.decode("utf-8") for img in node.read().tolist()
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
accel_node = file_handle.get_node("/acceleration")
|
|
86
|
+
assert isinstance(accel_node, tables.Table)
|
|
87
|
+
acceleration_df = pd.DataFrame.from_records(accel_node.read())
|
|
88
|
+
acceleration_meta = node_to_dict(accel_node)
|
|
89
|
+
except NoSuchNodeError:
|
|
90
|
+
logger.warning("No /acceleration table found in HDF5 file.")
|
|
91
|
+
acceleration_df = pd.DataFrame()
|
|
92
|
+
acceleration_meta = HDF5NodeInfo(name="acceleration", path="/acceleration", type="Table", attributes={})
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
sensor_node = file_handle.get_node("/sensors")
|
|
96
|
+
assert isinstance(sensor_node, tables.Table)
|
|
97
|
+
sensor_df = pd.DataFrame.from_records(sensor_node.read())
|
|
98
|
+
except NoSuchNodeError:
|
|
99
|
+
logger.warning("No /sensors table found in HDF5 file.")
|
|
100
|
+
sensor_df = pd.DataFrame()
|
|
101
|
+
|
|
102
|
+
return ParsedHDF5FileContent(
|
|
103
|
+
acceleration_df=acceleration_df,
|
|
104
|
+
sensor_df=sensor_df,
|
|
105
|
+
acceleration_meta=acceleration_meta,
|
|
106
|
+
pictures=pictures,
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
def save_extracted_images(pictures: Dict[str, List[str]], images_dir: Path):
|
|
110
|
+
"""Decode and save base64 image strings as PNG / JPEG files."""
|
|
111
|
+
if not pictures:
|
|
112
|
+
logger.info("No pictures found to extract.")
|
|
113
|
+
return
|
|
114
|
+
|
|
115
|
+
logger.info(f"Extracting {len(pictures)} categories of images to {images_dir}")
|
|
116
|
+
for category, image_list in pictures.items():
|
|
117
|
+
category_dir = images_dir / category
|
|
118
|
+
category_dir.mkdir(parents=True, exist_ok=True)
|
|
119
|
+
for idx, img_data in enumerate(image_list):
|
|
120
|
+
try:
|
|
121
|
+
if "," in img_data:
|
|
122
|
+
header, data = img_data.split(",", 1)
|
|
123
|
+
ext = "jpg" if "jpeg" in header else "png"
|
|
124
|
+
else:
|
|
125
|
+
data, ext = img_data, "png"
|
|
126
|
+
(category_dir / f"{idx}.{ext}").write_bytes(base64.b64decode(data))
|
|
127
|
+
except Exception as e:
|
|
128
|
+
logger.error(f"Failed to save image {category}/{idx}: {e}")
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import numpy as np
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import matplotlib.pyplot as plt
|
|
4
|
+
import logging
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import List, Dict, Any
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger("c2d_utils.reporting")
|
|
9
|
+
|
|
10
|
+
class ReportGenerator:
|
|
11
|
+
"""Common reporting class to ensure consistent styling and formatting across all PDF/HTML reports."""
|
|
12
|
+
|
|
13
|
+
@staticmethod
|
|
14
|
+
def get_common_css() -> str:
|
|
15
|
+
"""Returns the common CSS stylesheet used across all reports."""
|
|
16
|
+
return """
|
|
17
|
+
@page {
|
|
18
|
+
size: A4;
|
|
19
|
+
margin: 1.8cm;
|
|
20
|
+
}
|
|
21
|
+
body {
|
|
22
|
+
font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif;
|
|
23
|
+
font-size: 9.5px;
|
|
24
|
+
color: #334155;
|
|
25
|
+
line-height: 1.6;
|
|
26
|
+
}
|
|
27
|
+
h1 {
|
|
28
|
+
color: #0f172a;
|
|
29
|
+
font-size: 22pt;
|
|
30
|
+
font-weight: 700;
|
|
31
|
+
margin-top: 0px;
|
|
32
|
+
margin-bottom: 8px;
|
|
33
|
+
border-bottom: 2px solid #3b82f6;
|
|
34
|
+
padding-bottom: 6px;
|
|
35
|
+
}
|
|
36
|
+
h2 {
|
|
37
|
+
color: #1e293b;
|
|
38
|
+
font-size: 13pt;
|
|
39
|
+
font-weight: 600;
|
|
40
|
+
margin-top: 18px;
|
|
41
|
+
margin-bottom: 8px;
|
|
42
|
+
border-bottom: 1px solid #e2e8f0;
|
|
43
|
+
padding-bottom: 4px;
|
|
44
|
+
}
|
|
45
|
+
h3, h4 {
|
|
46
|
+
color: #2563eb;
|
|
47
|
+
font-size: 10.5pt;
|
|
48
|
+
font-weight: 600;
|
|
49
|
+
margin-top: 14px;
|
|
50
|
+
margin-bottom: 4px;
|
|
51
|
+
}
|
|
52
|
+
table {
|
|
53
|
+
width: 100%;
|
|
54
|
+
border-collapse: collapse;
|
|
55
|
+
margin-top: 10px;
|
|
56
|
+
margin-bottom: 12px;
|
|
57
|
+
font-size: 8px;
|
|
58
|
+
}
|
|
59
|
+
th {
|
|
60
|
+
background-color: #f1f5f9;
|
|
61
|
+
color: #1e293b;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
text-align: left;
|
|
64
|
+
padding: 5px 7px;
|
|
65
|
+
border: 1px solid #cbd5e1;
|
|
66
|
+
}
|
|
67
|
+
td {
|
|
68
|
+
border: 1px solid #cbd5e1;
|
|
69
|
+
padding: 4px 7px;
|
|
70
|
+
color: #334155;
|
|
71
|
+
}
|
|
72
|
+
tr:nth-child(even) {
|
|
73
|
+
background-color: #f8fafc;
|
|
74
|
+
}
|
|
75
|
+
.note {
|
|
76
|
+
background-color: #f8fafc;
|
|
77
|
+
border-left: 4px solid #3b82f6;
|
|
78
|
+
padding: 10px;
|
|
79
|
+
margin-bottom: 12px;
|
|
80
|
+
font-size: 9px;
|
|
81
|
+
color: #475569;
|
|
82
|
+
}
|
|
83
|
+
pre {
|
|
84
|
+
background: #f1f5f9;
|
|
85
|
+
padding: 8px;
|
|
86
|
+
font-size: 8px;
|
|
87
|
+
border-left: 3px solid #3b82f6;
|
|
88
|
+
margin-top: 10px;
|
|
89
|
+
margin-bottom: 10px;
|
|
90
|
+
color: #334155;
|
|
91
|
+
}
|
|
92
|
+
img {
|
|
93
|
+
display: block;
|
|
94
|
+
margin-left: auto;
|
|
95
|
+
margin-right: auto;
|
|
96
|
+
margin-top: 10px;
|
|
97
|
+
margin-bottom: 10px;
|
|
98
|
+
}
|
|
99
|
+
img.graph {
|
|
100
|
+
width: 520px;
|
|
101
|
+
}
|
|
102
|
+
img.snapshot-img {
|
|
103
|
+
width: 250px;
|
|
104
|
+
}
|
|
105
|
+
.page-break {
|
|
106
|
+
page-break-before: always;
|
|
107
|
+
}
|
|
108
|
+
.chart {
|
|
109
|
+
text-align: center;
|
|
110
|
+
margin: 15px 0;
|
|
111
|
+
}
|
|
112
|
+
.chart h3 {
|
|
113
|
+
margin-bottom: 5px;
|
|
114
|
+
}
|
|
115
|
+
.chart img {
|
|
116
|
+
width: 350px;
|
|
117
|
+
}
|
|
118
|
+
ul, ol {
|
|
119
|
+
margin-top: 5px;
|
|
120
|
+
margin-bottom: 10px;
|
|
121
|
+
padding-left: 20px;
|
|
122
|
+
}
|
|
123
|
+
li {
|
|
124
|
+
margin-bottom: 3px;
|
|
125
|
+
}
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def build_html_document(cls, body_content: str, title: str = "Report") -> str:
|
|
130
|
+
"""Wraps body content in a styled HTML document template."""
|
|
131
|
+
return f"""<!DOCTYPE html>
|
|
132
|
+
<html>
|
|
133
|
+
<head>
|
|
134
|
+
<meta charset="utf-8">
|
|
135
|
+
<title>{title}</title>
|
|
136
|
+
<style>
|
|
137
|
+
{cls.get_common_css()}
|
|
138
|
+
</style>
|
|
139
|
+
</head>
|
|
140
|
+
<body>
|
|
141
|
+
{body_content}
|
|
142
|
+
</body>
|
|
143
|
+
</html>"""
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def format_meta_val(v) -> str:
|
|
147
|
+
"""Recursively formats metadata dictionary objects to readable strings."""
|
|
148
|
+
if isinstance(v, dict):
|
|
149
|
+
if "value" in v and "unit" in v:
|
|
150
|
+
return f"{v['value']} {v['unit']}"
|
|
151
|
+
return ", ".join(f"{k}: {ReportGenerator.format_meta_val(val)}" for k, val in v.items())
|
|
152
|
+
return str(v)
|
|
153
|
+
|
|
154
|
+
@staticmethod
|
|
155
|
+
def build_params_table(params: Dict[str, Any], keys_list: List[str], title_header: str) -> str:
|
|
156
|
+
"""Build a 4-column HTML table from a list of keys, side-by-side (Attributes and Values)."""
|
|
157
|
+
rows = []
|
|
158
|
+
for k in sorted(keys_list):
|
|
159
|
+
if k in params:
|
|
160
|
+
key_name = k.replace('_', ' ').title()
|
|
161
|
+
if k.startswith("tool_r_at_"):
|
|
162
|
+
pos = k.split("_")[-1]
|
|
163
|
+
key_name = f"Tool Radius at {pos}mm"
|
|
164
|
+
rows.append((key_name, ReportGenerator.format_meta_val(params[k])))
|
|
165
|
+
|
|
166
|
+
if not rows:
|
|
167
|
+
return ""
|
|
168
|
+
|
|
169
|
+
n = len(rows)
|
|
170
|
+
half = (n + 1) // 2
|
|
171
|
+
left_half = rows[:half]
|
|
172
|
+
right_half = rows[half:]
|
|
173
|
+
|
|
174
|
+
while len(right_half) < len(left_half):
|
|
175
|
+
right_half.append(("", ""))
|
|
176
|
+
|
|
177
|
+
# Build HTML table with fixed widths to prevent right overflow
|
|
178
|
+
html = f"<h4>{title_header}</h4>\n"
|
|
179
|
+
html += '<table style="width: 100%; table-layout: fixed; border-collapse: collapse; margin-top: 10px; margin-bottom: 15px;">\n'
|
|
180
|
+
html += ' <thead>\n'
|
|
181
|
+
html += ' <tr>\n'
|
|
182
|
+
html += ' <th style="width: 25%; background-color: #006699; color: white; font-weight: bold; padding: 4px 6px; border: 1px solid #D0D0D0;">Attribute</th>\n'
|
|
183
|
+
html += ' <th style="width: 25%; background-color: #006699; color: white; font-weight: bold; padding: 4px 6px; border: 1px solid #D0D0D0;">Value</th>\n'
|
|
184
|
+
html += ' <th style="width: 25%; background-color: #006699; color: white; font-weight: bold; padding: 4px 6px; border: 1px solid #D0D0D0;">Attribute</th>\n'
|
|
185
|
+
html += ' <th style="width: 25%; background-color: #006699; color: white; font-weight: bold; padding: 4px 6px; border: 1px solid #D0D0D0;">Value</th>\n'
|
|
186
|
+
html += ' </tr>\n'
|
|
187
|
+
html += ' </thead>\n'
|
|
188
|
+
html += ' <tbody>\n'
|
|
189
|
+
for left, right in zip(left_half, right_half):
|
|
190
|
+
html += ' <tr>\n'
|
|
191
|
+
html += f' <td style="width: 25%; padding: 4px 6px; border: 1px solid #D0D0D0; word-wrap: break-word;">{left[0]}</td>\n'
|
|
192
|
+
html += f' <td style="width: 25%; padding: 4px 6px; border: 1px solid #D0D0D0; word-wrap: break-word;">{left[1]}</td>\n'
|
|
193
|
+
html += f' <td style="width: 25%; padding: 4px 6px; border: 1px solid #D0D0D0; word-wrap: break-word;">{right[0]}</td>\n'
|
|
194
|
+
html += f' <td style="width: 25%; padding: 4px 6px; border: 1px solid #D0D0D0; word-wrap: break-word;">{right[1]}</td>\n'
|
|
195
|
+
html += ' </tr>\n'
|
|
196
|
+
html += ' </tbody>\n'
|
|
197
|
+
html += '</table>\n'
|
|
198
|
+
return html
|
|
199
|
+
|
|
200
|
+
@classmethod
|
|
201
|
+
def render_markdown_to_pdf(cls, markdown_text: str, output_pdf_path: Path, base_dir: Path) -> bool:
|
|
202
|
+
"""Converts Markdown content to a styled PDF using xhtml2pdf."""
|
|
203
|
+
try:
|
|
204
|
+
import markdown as md_lib
|
|
205
|
+
from xhtml2pdf import pisa
|
|
206
|
+
|
|
207
|
+
# Convert markdown to html body
|
|
208
|
+
html_body = md_lib.markdown(markdown_text, extensions=["tables"])
|
|
209
|
+
html_document = cls.build_html_document(html_body, title="Report")
|
|
210
|
+
|
|
211
|
+
def link_callback(uri, rel):
|
|
212
|
+
if uri.startswith("http://") or uri.startswith("https://"):
|
|
213
|
+
return uri
|
|
214
|
+
resolved_path = base_dir / uri.lstrip("/")
|
|
215
|
+
if resolved_path.exists():
|
|
216
|
+
return str(resolved_path)
|
|
217
|
+
cwd_path = Path.cwd() / uri.lstrip("/")
|
|
218
|
+
if cwd_path.exists():
|
|
219
|
+
return str(cwd_path)
|
|
220
|
+
return uri
|
|
221
|
+
|
|
222
|
+
with open(output_pdf_path, "wb") as pdf_file:
|
|
223
|
+
pisa_status = pisa.CreatePDF(html_document, dest=pdf_file, link_callback=link_callback)
|
|
224
|
+
return not pisa_status.err
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Markdown PDF export failed: {e}")
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def export_html_to_pdf(html_content: str, output_path: str) -> bool:
|
|
230
|
+
"""Compile an HTML string into a PDF file using xhtml2pdf."""
|
|
231
|
+
try:
|
|
232
|
+
from xhtml2pdf import pisa
|
|
233
|
+
with open(output_path, "wb") as f_pdf:
|
|
234
|
+
pisa_status = pisa.CreatePDF(html_content, dest=f_pdf)
|
|
235
|
+
if pisa_status.err:
|
|
236
|
+
logger.error(f"xhtml2pdf error during rendering to {output_path}")
|
|
237
|
+
return False
|
|
238
|
+
return True
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Failed to generate PDF at {output_path}: {e}")
|
|
241
|
+
return False
|
|
242
|
+
|
|
243
|
+
def plot_feature_importances(importances: np.ndarray, features: list, output_path: str):
|
|
244
|
+
"""Plot feature importances as a horizontal bar chart."""
|
|
245
|
+
plt.figure(figsize=(8, 4.5))
|
|
246
|
+
sorted_idx = np.argsort(importances)
|
|
247
|
+
plt.barh(np.array(features)[sorted_idx], importances[sorted_idx], color="#1a73e8", edgecolor="none")
|
|
248
|
+
plt.xlabel("Mean Relative Importance")
|
|
249
|
+
plt.title("Feature Importance Ranking (Random Forest Regressor)")
|
|
250
|
+
plt.tight_layout()
|
|
251
|
+
plt.savefig(output_path, dpi=150)
|
|
252
|
+
plt.close()
|
|
253
|
+
|
|
254
|
+
def plot_model_comparison(targets: list, rf_maes: list, ridge_maes: list, output_path: str):
|
|
255
|
+
"""Plot Mean Absolute Error (MAE) comparisons between Ridge and Random Forest."""
|
|
256
|
+
plt.figure(figsize=(8, 4.5))
|
|
257
|
+
x_indices = np.arange(len(targets))
|
|
258
|
+
width = 0.35
|
|
259
|
+
plt.bar(x_indices - width/2, np.array(rf_maes) * 1000.0, width, label="Random Forest", color="#1a73e8")
|
|
260
|
+
plt.bar(x_indices + width/2, np.array(ridge_maes) * 1000.0, width, label="Ridge Regression", color="#dadce0")
|
|
261
|
+
plt.ylabel("Mean Absolute Error (μm)")
|
|
262
|
+
plt.xticks(x_indices, [f"{i}mm" for i in range(len(targets))])
|
|
263
|
+
plt.xlabel("Measurement Position from Spindle Tip")
|
|
264
|
+
plt.title("Prediction Error Comparison (MAE)")
|
|
265
|
+
plt.legend()
|
|
266
|
+
plt.tight_layout()
|
|
267
|
+
plt.savefig(output_path, dpi=150)
|
|
268
|
+
plt.close()
|
|
269
|
+
|
|
270
|
+
def plot_correlation_heatmap(df_clean: pd.DataFrame, features: list, targets: list, output_path: str):
|
|
271
|
+
"""Plot a Pearson correlation heatmap of features vs. targets."""
|
|
272
|
+
corr_matrix = df_clean[features + targets].corr().loc[features, targets]
|
|
273
|
+
plt.figure(figsize=(8, 5.5))
|
|
274
|
+
im = plt.imshow(corr_matrix, cmap="coolwarm", vmin=-1.0, vmax=1.0, aspect="auto")
|
|
275
|
+
plt.colorbar(im, label="Pearson Correlation Coeff.")
|
|
276
|
+
plt.yticks(np.arange(len(features)), features)
|
|
277
|
+
plt.xticks(np.arange(len(targets)), [f"{i}mm" for i in range(len(targets))])
|
|
278
|
+
plt.title("Feature-Target Correlation Matrix Heatmap")
|
|
279
|
+
|
|
280
|
+
# Annotate correlation values
|
|
281
|
+
for ii in range(len(features)):
|
|
282
|
+
for jj in range(len(targets)):
|
|
283
|
+
val = corr_matrix.iloc[ii, jj]
|
|
284
|
+
plt.text(jj, ii, f"{val:.2f}", ha="center", va="center", color="white" if abs(val) > 0.5 else "black", fontsize=8)
|
|
285
|
+
|
|
286
|
+
plt.tight_layout()
|
|
287
|
+
plt.savefig(output_path, dpi=150)
|
|
288
|
+
plt.close()
|