Files
ram-remanence-tester/plotter.py
Krystian Hebel 53a45193ab plotter.py: add grid every 8 bits
This also changed number of labels produced, which wasn't planned, but
maybe it does make it cleaner.

Signed-off-by: Krystian Hebel <krystian.hebel@3mdeb.com>
2025-01-23 15:04:56 +01:00

229 lines
8.4 KiB
Python
Executable File

#!/usr/bin/env python3
import csv
import os
import argparse
import tempfile
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.ticker import FuncFormatter
from odf.opendocument import OpenDocumentSpreadsheet
from odf.table import Table, TableRow, TableCell
from odf.text import P
from odf.draw import Frame, Image
import PIL.Image
def generate_bar_chart(data, temp_dir, file_stem, save_pngs, output_folder, total_bits):
"""
Create a bar chart with a secondary Y-axis (percentage scale) on the right.
Save it temporarily and optionally save as a standalone .png file in output_folder/chart_pngs/.
"""
bits = [int(row[0]) for row in data]
values_0to1 = [int(row[1]) for row in data]
values_1to0 = [int(row[2]) for row in data]
averages = [float(row[3]) for row in data]
bar_width = 0.2
x_positions = np.arange(len(bits))
fig_width = max(10, len(bits) * 0.2)
fig, ax1 = plt.subplots(figsize=(fig_width, 8))
ax1.bar(x_positions + 1.5 * bar_width, values_0to1, bar_width, label="0to1", color="tab:blue")
ax1.bar(x_positions + 2.5 * bar_width, values_1to0, bar_width, label="1to0", color="tab:orange")
ax1.bar(x_positions + 3.5 * bar_width, averages, bar_width, label="average", color="tab:green")
ax1.legend(loc="upper left")
ax1.set_ylabel("Absolute Value (bits switched)", fontsize=14, labelpad=10)
ax1.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{int(x):,}"))
ax1.set_xlabel("Bit position on data bus", fontsize=16, labelpad=10)
ax1.tick_params(axis='x', labelsize=10)
ax1.tick_params(axis='y', labelsize=10)
ax2 = ax1.twinx()
ax2.set_ylabel("Percentage of Total Memory\n (Applies to average only) ", fontsize=14, labelpad=10)
ax2.set_ylim(0, ax1.get_ylim()[1]/(total_bits/128) * 100) # 128, not 64, to include the average in calculations
if ax2.get_ylim()[1] > 35:
ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x:.0f}%"))
elif ax2.get_ylim()[1] > 10:
ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x:.1f}%"))
else:
ax2.yaxis.set_major_formatter(FuncFormatter(lambda x, _: f"{x:.2f}%"))
ax2.tick_params(axis='y', labelsize=10)
ax2.grid(True)
ax1.set_xticks(x_positions)
ax1.set_xticklabels(bits, rotation=-45, ha="left")
ax1.set_xlim([0, len(bits)])
ax1.set_xticks(ax1.get_xticks()[::8])
ax1.grid(True, axis='x')
plt.tight_layout()
chart_path = os.path.join(temp_dir, f"{file_stem}.png")
plt.savefig(chart_path)
if save_pngs:
png_output_folder = os.path.join(output_folder, "chart_pngs")
os.makedirs(png_output_folder, exist_ok=True)
png_output_path = os.path.join(png_output_folder, f"{file_stem}.png")
plt.savefig(png_output_path)
print(f"Standalone PNG saved to: {png_output_path}")
plt.close()
return chart_path
def write_to_ods(ods_doc, sheet_name, data, chart_path, total_flipped_bits, total_bits):
"""
Add a sheet to the ODS file with the given data and embed the chart at the top in a new column.
The chart is resized to 80% of its original size while preserving the aspect ratio.
"""
table = Table(name=sheet_name)
# Add the table with data
for row in data:
table_row = TableRow()
for cell in row:
table_cell = TableCell()
table_cell.addElement(P(text=str(cell)))
table_row.addElement(table_cell)
table.addElement(table_row)
# Ensure there are at least 69 rows
while len(table.childNodes) < 69:
table.addElement(TableRow())
# Add "Average percentage" to C68
row_68 = table.childNodes[67] # Row 68 (0-indexed)
c68_cell = TableCell()
c68_cell.addElement(P(text="Average percentage"))
row_68.addElement(c68_cell)
# Add the average percentage to C69
row_69 = table.childNodes[68] # Row 69 (0-indexed)
c69_cell = TableCell()
if total_bits > 0:
avg_percentage = (total_flipped_bits / total_bits) * 100
c69_cell.addElement(P(text=f"{avg_percentage:.2f}%"))
row_69.addElement(c69_cell)
# Embed the chart as an image in the top row, new column
if os.path.exists(chart_path):
relative_path = ods_doc.addPicture(chart_path)
with PIL.Image.open(chart_path) as img:
img_width, img_height = img.size
scale_factor = 0.7
scaled_width_cm = (img_width * scale_factor) / 96 * 2.54
scaled_height_cm = (img_height * scale_factor) / 96 * 2.54
frame = Frame(
width=f"{scaled_width_cm:.2f}cm",
height=f"{scaled_height_cm:.2f}cm",
x="0cm",
y="0cm",
)
image = Image(href=relative_path, type="simple")
frame.addElement(image)
chart_row = table.firstChild
if not chart_row:
chart_row = TableRow()
table.addElement(chart_row)
chart_cell = TableCell()
chart_cell.addElement(frame)
chart_row.addElement(chart_cell)
ods_doc.spreadsheet.addElement(table)
def process_csv(input_csv, ods_doc, temp_dir, save_pngs, output_folder):
file_stem = os.path.splitext(os.path.basename(input_csv))[0]
with open(input_csv, 'r', encoding='utf-8') as f_in:
reader = csv.reader(f_in, delimiter=',')
rows = list(reader)
product_name, temperature, time = None, None, None
for row in rows:
if len(row) > 1:
if row[0].strip() == "ProductName":
product_name = row[1].strip()
elif row[0].strip() == "Temperature":
temperature = float(row[1].strip())
elif row[0].strip() == "Time":
time = float(row[1].strip())
if product_name and temperature is not None and time is not None:
sheet_name = f"{file_stem}_time_{time}_temp_{temperature}"
else:
sheet_name = file_stem
try:
total_flipped_bits = int(rows[68][0].strip().lstrip("'"))
total_bits = int(rows[68][1].strip().lstrip("'"))
except (IndexError, ValueError):
print(f"Warning: Missing or invalid 'Different bits'/'Total compared bits' in {input_csv}.")
total_flipped_bits = 0
total_bits = 1
processed_rows = []
header_found = False
for row in rows:
if not row:
processed_rows.append(row)
continue
if not header_found and len(row) >= 3 and row[0].strip() == "Bit":
row.append("average")
header_found = True
else:
try:
avg_val = (int(row[1].strip()) + int(row[2].strip())) / 2
row.append(f"{avg_val:.1f}")
except (ValueError, IndexError):
pass
processed_rows.append(row)
numeric_data = [row for row in processed_rows if len(row) >= 4 and row[0].isdigit()]
chart_path = generate_bar_chart(numeric_data, temp_dir, sheet_name, save_pngs, output_folder, total_bits)
write_to_ods(ods_doc, sheet_name, processed_rows, chart_path, total_flipped_bits, total_bits)
return product_name, temperature, time
def main():
parser = argparse.ArgumentParser(description="Process CSV files and generate ODS file with optional PNGs.")
parser.add_argument("input_folder", help="Folder containing the CSV files to process.")
parser.add_argument("--output-folder", required=True, help="Folder to save the ODS file and PNGs.")
parser.add_argument("--save-pngs", action="store_true", help="Save standalone PNG files.")
args = parser.parse_args()
input_folder = args.input_folder
output_folder = args.output_folder
save_pngs = args.save_pngs
os.makedirs(output_folder, exist_ok=True)
with tempfile.TemporaryDirectory() as temp_dir:
ods_name = "processed_data"
product_name_found = False
ods_doc = OpenDocumentSpreadsheet()
for file_name in sorted(os.listdir(input_folder)):
input_path = os.path.join(input_folder, file_name)
if os.path.isfile(input_path) and file_name.endswith(".csv"):
product_name, temperature, time = process_csv(input_path, ods_doc, temp_dir, save_pngs, output_folder)
if product_name and not product_name_found:
ods_name = product_name
product_name_found = True
ods_file_path = os.path.join(output_folder, f"{ods_name}.ods")
ods_doc.save(ods_file_path)
print(f"All processed data saved to: {ods_file_path}")
if __name__ == "__main__":
main()