# SPDX-License-Identifier: MPL-2.0
#
# ODAT-SE -- an open framework for data analysis
# Copyright (C) 2020- The University of Tokyo
#
# This Source Code Form is subject to the terms of the Mozilla Public License, v. 2.0.
# If a copy of the MPL was not distributed with this file, You can obtain one at http://mozilla.org/MPL/2.0/.
import os
import argparse
import numpy as np
import matplotlib.pyplot as plt
# Try to import tqdm for progress bar, set to None if not available
try:
from tqdm import tqdm
except ImportError:
tqdm = None
[docs]
def parse_options():
"""
Parse command line arguments and configuration files.
Returns
-------
dict
Dictionary containing all configuration options.
"""
# Default configuration options
config = {
"columns": None, # Column names to draw histograms
"weight_column": -1, # Column index for weights (default: last column)
"bins": 60, # Number of bins for histograms
"range": None, # Range of variables [xmin, xmax]
"data_dir": ".", # Directory containing data files
"output_dir": ".", # Directory for output files
"format": ["png"], # Output image format(s)
"progress": False, # Whether to show progress bar
"xlabel": None, # Label for x-axis
"field_list": [], # List of field labels
"input_files": [], # List of input files
}
# Set up command line argument parser
parser = argparse.ArgumentParser(description='Generate histograms for files in a directory.')
parser.add_argument("--config", help="Configuration file.")
parser.add_argument("--params", help="Parameter file used for calculation.")
parser.add_argument("-c", "--columns", help="Column names to draw histograms.")
parser.add_argument("-w", "--weight_column", type=int, help="Column id for weights. default=-1 (last column).")
parser.add_argument("-b", "--bins", type=int, help="Number of bins.")
parser.add_argument("-r", "--range", type=str, help="Range of variables, \"xmin,xmax\".")
parser.add_argument("-d", "--data_dir", type=str, help="Path to data directory")
parser.add_argument("-o", "--output_dir", type=str, help="Path to output directory.")
parser.add_argument("-f", "--format", type=str, help="File type of output images. default=png")
parser.add_argument("--xlabel", type=str, help="Label for x-axis.")
parser.add_argument("--field_list", type=str, help="Field labels.")
parser.add_argument("--progress", action="store_true", help="Show progress bar.")
parser.add_argument("input_files", nargs="*", type=str, help="Path to data file(s).")
args = parser.parse_args()
# Read input parameter file used for calculation
if args.params:
params = read_toml(args.params)
# Extract relevant parameters from the parameter file
if "algorithm" in params:
if "param" in params["algorithm"]:
# Get min and max ranges if available
if "min_list" in params["algorithm"]["param"]:
min_list = params["algorithm"]["param"]["min_list"]
max_list = params["algorithm"]["param"]["max_list"]
# Create range list for each variable
config["range"] = [[float(xmin), float(xmax)] for xmin, xmax in zip(min_list, max_list)]
# Get field labels if available
if "label_list" in params["algorithm"]:
config["field_list"] = ["beta", "fx"] + params["algorithm"]["label_list"] + ["weight"]
# Read config file in TOML format if specified
if args.config:
config.update(read_toml(args.config))
# Convert format string to list if needed
if isinstance(config["format"], str):
config["format"] = config["format"].split(",")
# Override config with command-line arguments if specified
if args.columns:
config["columns"] = [s.strip() for s in args.columns.split(",")]
if args.weight_column is not None:
config["weight_column"] = args.weight_column
if args.bins:
config["bins"] = args.bins
if args.range is not None:
if args.range == "":
config["range"] = None
else:
config["range"] = [float(s) for s in args.range.split(",")]
if args.data_dir:
config["data_dir"] = args.data_dir
if args.output_dir:
config["output_dir"] = args.output_dir
if args.format:
config["format"] = args.format.split(",")
if args.progress:
config["progress"] = args.progress
if args.input_files:
config["input_files"] = args.input_files
if args.field_list:
config["field_list"] = [s.strip() for s in args.field_list.split(",")]
if args.xlabel:
config["xlabel"] = args.xlabel
return config
[docs]
def show_options(opt):
"""
Display the current configuration options.
Parameters
----------
opt : dict
Dictionary containing configuration options.
"""
for k, v in opt.items():
print(f"{k} = {v}")
[docs]
def find_files(data_dir):
"""
Find all result files in the specified directory.
Parameters
----------
data_dir : str
Directory to search for files.
Returns
-------
list
Sorted list of file paths matching the pattern.
"""
return sorted([os.path.join(data_dir, f) for f in os.listdir(data_dir)
if f.startswith("result_T") and f.endswith("_summarized.txt")])
[docs]
def read_toml(input_filename):
"""
Reads the TOML configuration file and extracts required parameters.
Parameters
----------
input_filename : str
Path to the TOML configuration file.
Returns
-------
dict
Contents of configuration file.
"""
try:
import tomllib
except ImportError:
import tomli as tomllib
if tomllib.__version__ < "1.2.0":
raise ImportError("tomli 1.2.0 or later required")
with open(input_filename, "rb") as fp:
dict_toml = tomllib.load(fp)
return dict_toml
[docs]
def main():
"""
Main function to generate 1D histograms from data files.
"""
# Parse command line options and configuration files
opt = parse_options()
show_options(opt)
# Extract key configuration parameters
columns = opt["columns"]
weight_column = opt["weight_column"]
bins = opt["bins"]
configured_field_list = opt["field_list"] # may be empty; default is per-file
# Determine histogram range and axis sharing behavior
hist_range = opt["range"]
if hist_range is None:
auto_axis = True # Automatically determine axis range
share_axis = False # Don't share axes between subplots
else:
auto_axis = False # Use specified range
if isinstance(hist_range, list):
if isinstance(hist_range[0], list):
share_axis = False # Different range for each variable
else:
share_axis = True # Same range for all variables
else:
raise ValueError("unknown data type for range parameter: {}".format(type(hist_range)))
# Create output directory if it doesn't exist
os.makedirs(opt["output_dir"], exist_ok=True)
# Get list of files to process
file_list = opt["input_files"] if opt["input_files"] else find_files(opt["data_dir"])
# Add progress bar if requested and tqdm is available
if tqdm and opt["progress"]:
file_list = tqdm(file_list)
# Process each file
err = 0
for file_path in file_list:
try:
# Load the data file
is_beta = None
data = []
with open(file_path, "r") as f:
for line in f:
if line.startswith("#"):
# Detect whether column 1 is beta or T from the header
if is_beta is None:
if " beta" in line:
is_beta = True
elif " T" in line:
is_beta = False
continue
items = line.split()
if not items:
continue # skip blank lines
data.append([float(s) for s in items])
data = np.array(data)
if data.ndim != 2 or data.shape[0] == 0:
print(f"Skipping {file_path}: no data rows")
err += 1
continue
ndata, ncols = data.shape
# Extract beta value in float (0.00101) or scientific notation (e.g., 1.01e-3)
beta_value = f"{data[0,0]:#.6g}"
# Get weights from the specified column
weights = data[:, weight_column]
# Normalize weights so they sum to 1
normalized_weights = weights / np.sum(weights)
# Column labels / indices, recomputed per file so that files with
# differing widths are not mis-indexed by a value cached from the
# first file. Standard format: beta, fx, x1, .., xn, weight.
field_list = configured_field_list if configured_field_list else \
(["beta", "fx"] + [f"x{i-1}" for i in range(2, ncols-1)] + ["weight"])
field_id = {s: idx for idx, s in enumerate(field_list)}
if columns:
z_columns = [field_id[col] for col in columns]
else:
# Use all data columns (excluding beta, fx, and weight)
z_columns = [i for i in range(2, ncols-1)]
num_variables = len(z_columns)
# Map each per-variable range to its data column. When config["range"]
# is a per-parameter list it is ordered as the parameters x1..xn,
# which occupy columns 2..ncols-2; columns outside that (beta, fx,
# weight) get no fixed range and fall back to auto.
range_by_col = {}
if not auto_axis and not share_axis:
for k, rng in enumerate(hist_range):
range_by_col[2 + k] = rng
# Create figure with subplots for each variable
fig, axes = plt.subplots(num_variables, 1, figsize=(10, 4 * num_variables), sharex=share_axis)
if num_variables == 1:
axes = [axes] # Ensure axes is a list even if there is only one variable
# Plot histogram for each variable
for i, col_index in enumerate(z_columns):
z = data[:, col_index] # Get data for this variable
# Determine range for histogram
if auto_axis:
hrange = None # Let matplotlib determine range automatically
elif share_axis:
hrange = hist_range # Same range for all variables
else:
# Per-variable range, keyed by column (None for non-parameter columns)
hrange = range_by_col.get(col_index)
# Create histogram with normalized weights
axes[i].hist(z, bins=bins, range=hrange, weights=normalized_weights)
axes[i].set_title(f'{field_list[col_index]} 1D Histogram')
axes[i].set_ylabel(f'{field_list[col_index]}')
axes[i].grid(True, linestyle='--', linewidth=0.5, alpha=0.7) # Add grid lines
if hrange is not None:
axes[i].set_xlim(hrange) # Set x-axis limits
# Set x-label for bottom subplot if provided
if opt["xlabel"]:
axes[-1].set_xlabel(opt["xlabel"])
plt.tight_layout()
# Create output filename based on input filename
file_name = os.path.basename(file_path)
file_base, file_ext = os.path.splitext(file_name)
# Replace "_summarized" with beta value in filename
if "_summarized" in file_base:
beta_tag = f"_beta_{beta_value}" if is_beta else f"_T_{beta_value}"
file_base = file_base.replace("_summarized", beta_tag)
# Save plot in each requested format
for suf in opt["format"]:
plot_filename = f"1Dhistogram_{file_base}.{suf}"
plt.savefig(os.path.join(opt["output_dir"], plot_filename))
if not opt["progress"]:
print(f"Histogram created: {plot_filename}")
# Clean up plot objects
plt.clf()
plt.close()
except Exception as e:
print(f"Error occurred while processing {file_path}: {type(e).__name__}: {e}")
err += 1
# Print summary message
if err == 0:
print(f"All histograms have been saved in {opt['output_dir']}")
else:
print("ERROR: Some histograms have not been created because of errors.")
if __name__ == "__main__":
main()