# This file is part of HYLOA - HYsteresis LOop Analyzer.
# Copyright (C) 2024 Francesco Zeno Costanzo
# HYLOA is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
# HYLOA is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
# You should have received a copy of the GNU General Public License
# along with HYLOA. If not, see <https://www.gnu.org/licenses/>.
"""
Code to save a session (i.e. alla dta loaded all plot crated and so on) in .pkl files
"""
import os
import sys
import gzip
import pickle
import logging
from datetime import datetime
import numpy as np
import pandas as pd
from PyQt5.QtCore import QTimer
from PyQt5.QtWidgets import (
QMessageBox, QFileDialog
)
from hyloa.utils.logging_setup import setup_logging
from hyloa.utils.df_serial import DataFrameSerializer
from hyloa.gui.plot_window import PlotControlWidget
from hyloa.gui.worksheet import WorksheetWindow
[docs]
def save_current_session(app_instance, parent_widget=None):
'''
Save the current session to a HYLOA session file (.hyloa)
Session files are intended to be opened only by HYLOA.
Backward compatibility is guaranteed across minor versions.
Parameters
----------
app_instance : MainApp
Main application instance containing the session data.
parent_widget : QWidget or None
Optional parent for the dialog windows.
'''
if app_instance.logger is None:
QMessageBox.critical(parent_widget, "Error", "Cannot start analysis without starting log.")
return
file_path, _ = QFileDialog.getSaveFileName(
parent_widget,
"Save session",
"",
"HYLOA Session (*.hyloa)"
)
if not file_path:
QMessageBox.warning(parent_widget, "Warning", "No file selected for saving.")
return
if not file_path.lower().endswith(".hyloa"):
file_path += ".hyloa"
try:
# Serialize all dataframes and header lines to ensure cross-platform compatibility
serialized_dataframes = [
DataFrameSerializer.serialize(df) for df in app_instance.dataframes
]
serialized_header_lines = [
DataFrameSerializer.serialize(df) for df in app_instance.header_lines
]
# Build the dictionary for saving data
session_data = {
"__hyloa__": True,
"file_format_version": 2,
"serialization_format": "simple dict, json-compatible",
"app_version": getattr(app_instance, "version", "unknown"),
"created_on": datetime.now().isoformat(),
"python_version": f"{sys.version_info.major}.{sys.version_info.minor}",
"numpy_version": np.__version__,
"pandas_version": pd.__version__,
"platform": sys.platform,
# To avoid pandas dataframe serialization
"dataframes" : serialized_dataframes,
"header_lines": serialized_header_lines,
"logger_path" : app_instance.logger_path,
"log_filename": os.path.basename(app_instance.logger_path),
"fit_results" : app_instance.fit_results,
"number_plots": app_instance.number_plots,
"plot_widgets": {
idx: {
"selected_pairs": [
(
f_combo.currentText(),
x_combo.currentText(),
y_combo.currentText()
)
for f_combo, x_combo, y_combo in widget.selected_pairs
],
"plot_customizations": widget.plot_customizations.copy()
}
for idx, widget in app_instance.plot_widgets.items()
},
"plot_names": {
idx : name for idx, name in app_instance.plot_names.items()
},
"plot_control_pos": (app_instance.plot_control_subwindow.pos().x(),
app_instance.plot_control_subwindow.pos().y()
),
"tab_order": list(app_instance.plot_widgets.keys()),
"plot_windows_geometry": {
idx: {
"x": fig_sub.x(),
"y": fig_sub.y(),
"width": fig_sub.width(),
"height": fig_sub.height(),
"minimized": fig_sub.isMinimized()
}
for idx, fig_sub in app_instance.figure_subwindows.items()
},
"worksheets": {
idx: {
"name": app_instance.worksheet_names.get(idx, f"Worksheet {idx}"),
"content": app_instance.worksheet_windows[idx].to_session_data()
}
for idx in app_instance.worksheet_windows
},
}
with gzip.open(file_path, "wb") as f:
pickle.dump(session_data, f, protocol=pickle.HIGHEST_PROTOCOL)
QMessageBox.information(parent_widget, "Session saved",
f"Session saved in file:\n{file_path}")
except Exception as e:
QMessageBox.critical(parent_widget, "Error", f"Error while saving:\n{e}")
[docs]
def load_previous_session(app_instance, parent_widget=None):
'''
Load a previously saved session from a .hyloa file.
Parameters
----------
app_instance : MainApp
Main application instance to load the session into.
parent_widget : QWidget or None
Optional parent for the dialog windows.
'''
options = QFileDialog.Options()
file_path, _ = QFileDialog.getOpenFileName(
parent_widget,
"Load session",
"",
"HYLOA Session (*.hyloa);;Legacy Pickle (*.pkl)",
options=options
)
if not file_path:
QMessageBox.warning(parent_widget, "Error", "No file selected for session loading.")
return
try:
try:
with gzip.open(file_path, "rb") as f:
session_data = pickle.load(f)
except OSError:
# --- fallback to legacy pickle ---
with open(file_path, "rb") as f:
session_data = pickle.load(f)
# Reload attributes of main app instance
saved_dfs = session_data.get("dataframes", [])
app_instance.dataframes = []
for saved_df in saved_dfs:
# Handle both old (direct DataFrame) and new (serialized dict) formats
if isinstance(saved_df, pd.DataFrame):
# Legacy format: direct pickle of DataFrame
df = saved_df
elif isinstance(saved_df, dict) and "columns" in saved_df:
# New format: use DataFrameSerializer
df = DataFrameSerializer.deserialize(saved_df)
else:
continue
app_instance.dataframes.append(df)
# ===== LOAD HEADER LINES =====
saved_header_lines = session_data.get("header_lines", [])
app_instance.header_lines = []
for saved_hl in saved_header_lines:
# Handle both old (direct DataFrame) and new (serialized dict) formats
if isinstance(saved_hl, pd.DataFrame):
# Legacy format: direct pickle of DataFrame
hl_df = saved_hl
elif isinstance(saved_hl, dict) and "columns" in saved_hl:
# New format: use DataFrameSerializer
hl_df = DataFrameSerializer.deserialize(saved_hl)
else:
continue
app_instance.header_lines.append(hl_df)
app_instance.fit_results = session_data.get("fit_results", {})
app_instance.number_plots = session_data.get("number_plots", 0)
try :
app_instance.logger_path = session_data.get("logger_path", None)
# Recreate the logger
setup_logging(app_instance.logger_path)
except :
log_filename = session_data.get("log_filename")
# To ensure compatibility with different OS
default_dir = os.path.dirname(file_path)
reconstructed_path = os.path.join(default_dir, log_filename)
app_instance.logger_path = reconstructed_path
# Recreate the logger
setup_logging(app_instance.logger_path)
app_instance.logger = logging.getLogger(__name__)
is_new_format = session_data.get("__hyloa__", False)
file_version = session_data.get("file_format_version", 0)
if is_new_format:
app_instance.logger.info(
f"Loaded HYLOA session (format v{file_version})"
)
else:
app_instance.logger.info(
"Loaded legacy HYLOA session (no signature)"
)
app_instance.logger.info("Logger restored from session file.")
app_instance.open_default_panels()
# Recreate all plot's control panels
plot_control_pos = session_data.get("plot_control_pos")
plot_widgets_data = session_data.get("plot_widgets", {})
plot_names_data = session_data.get("plot_names", {})
# Loop only over widget because widgets and names share the same keys
for idx_str, plot_info in plot_widgets_data.items():
idx = int(idx_str)
plot_name = plot_names_data.get(idx, f"Graph {idx}")
widget = PlotControlWidget(app_instance, idx, plot_name)
for i in reversed(range(widget.pair_layout.count())):
widget.pair_layout.itemAt(i).widget().setParent(None)
widget.selected_pairs.clear()
app_instance.plot_widgets[idx] = widget
app_instance.plot_names[idx] = plot_name
# Retrieve selected pairs ...
for file_str, x_str, y_str in plot_info.get("selected_pairs", []):
widget.add_pair(file_text=file_str, x_col=x_str, y_col=y_str)
# ... and customization
widget.plot_customizations = plot_info.get("plot_customizations", {})
# Create sub window for panel
app_instance.create_plot_tabs()
app_instance.plot_tabs.addTab(widget, plot_name)
app_instance.plot_widgets[idx] = widget
app_instance.plot_names[idx] = plot_name
if plot_control_pos and app_instance.plot_control_subwindow is not None:
QTimer.singleShot(0, lambda: app_instance.plot_control_subwindow.move(plot_control_pos[0], plot_control_pos[1]))
widget.plot() # Force the plot
# ... and for plot windows
fig_geom = session_data.get("plot_windows_geometry", {}).get(idx)
fig_sub = app_instance.figure_subwindows.get(idx)
if fig_geom and fig_sub:
fig_sub.setGeometry(fig_geom["x"], fig_geom["y"], fig_geom["width"], fig_geom["height"])
if fig_geom.get("minimized"):
fig_sub.showMinimized()
# --- restore worksheets ---
worksheet_data = session_data.get("worksheets", {})
for idx_key, ws_info in worksheet_data.items():
try:
idx = int(idx_key)
except:
idx = idx_key
ws_name = ws_info.get("name", f"Worksheet {idx}")
# Create worksheet instance with name
ws = WorksheetWindow(app_instance.mdi_area, name=ws_name,
logger=app_instance.logger, app_instance=app_instance)
# Add to mdi area BEFORE restoring content (important!)
app_instance.mdi_area.addSubWindow(ws)
# Save references in app_instance
app_instance.worksheet_windows[idx] = ws
app_instance.worksheet_names[idx] = ws_name
app_instance.worksheet_subwindows[idx] = ws
if isinstance(idx, int) and idx > app_instance.number_worksheets:
app_instance.number_worksheets = idx + 1
# Restore content & plots; from_session_data will set geometry and schedule show
ws.from_session_data(ws_info.get("content", {}))
# Ensure it's visible (from_session_data schedules showNormal/showMinimized)
QTimer.singleShot(0, lambda w=ws: w.show())
# Rename main window to reflect the loaded session
base_name = os.path.basename(file_path)
name, _ = os.path.splitext(base_name)
app_instance.setWindowTitle(f"HYLOA - {name}")
QMessageBox.information(parent_widget, "Session Loaded",
f"Session loaded from file:\n{file_path}")
#except Exception as e:
# QMessageBox.critical(parent_widget, "Error",
# f"Error while loading session:\n{e}")
except Exception:
import traceback
err = traceback.format_exc()
print(err)
QMessageBox.critical(
parent_widget,
"Error",
f"Error while loading session:\n\n{err}"
)