Tensor Network for Anomaly Detection in Latent Space of Proton-Proton Collision Events at the LHC#
Implementation of the anomaly detection pipeline from the paper: 10.1088/2632-2153/ae0243.
Install tn4ml directly from GitHub:
git clone https://github.com/bsc-quantic/tn4ml.git
Install an additional package for data handling:
h5py
(optional) Download dataset#
The dimensionality of the dataset is reduced by passing it through autoencoder. If you are interested more in the autoencoder’s architecture, please refer to [*].
Reduced dataset can be downloaded from Zenodo:
Description of filenames:
latentrep_QCD_sig.h5: train dataset (QCD - background)latentrep_QCD_sig_testclustering.h5: test dataset (QCD - background)latentrep_RSGraviton_WW_NA_35.h5: test dataset (Signal $mathrm{NA G rightarrow WW}$)
Or it can be downloaded directly in the pipeline.py.
Run training and evaluation pipeline#
Data Parameters
save_dir(str): Path to directory for saving results (default = "results/")load_dir(str): Path to directory for loading the datafeature_range(list): Feature range for scaling (default = [0, 1])seed(int): Seed for random number generatorstandardization(str): Standardization of data ("yes"or"no",default = "yes")minmax(str): Minmax scaling of data ("yes"or"no",default = "yes")embedding(str): Embedding type for input data (e.g.,"legendre_4","fourier_2","hermite_3")test_size(int): Number of samples for testingtrain_size(int): Number of samples for trainingsignal_name(str): Name of signal dataset (default = "RSGraviton_WW_NA_35")latent (int): Latent space dimension
MPS Parameters
bond_dim(int): Bond dimension of MPS (default = 5)initializer(str): Type of MPS initializationshape_method(str): Method for distributing bond dimensions (default = "even")
Training Parameters
lr(float): Learning rate (default = 1e-3)min_delta(float): Minimum improvement required for early stopping (default = 0)patience (int): Number of epochs with no improvement before early stopping (default = 20)epochs (int): Maximum number of training epochs (default = 100)batch_size(int): Number of samples per training batch (default = 32)run(int): Number of training repetitions with different seeds
python pipeline.py -save_dir results/ \
-load_dir QML_paper_data \
-feature_range 0 1 \
-minmax yes \
-embedding laguerre_2 \
-test_size 5000 \
-train_size 10000 \
-bond_dim 8 \
-initializer unitary \
-lr 0.001 \
-patience 25 \
-epochs 100 \
-batch_size 128 \
-run 1 \
-latent 4
Run evaluation only#
Data Parameters
save_dir(str): Path to directory for saving results (default = "results/")load_dir(str): Path to directory for loading the datafeature_range(list): Feature range for scaling (default = [0, 1])seed(int): Seed for random number generatorstandardization(str): Standardization of data ("yes"or"no",default = "yes")minmax(str): Minmax scaling of data ("yes"or"no",default = "yes")embedding(str): Embedding type for input data (e.g.,"legendre_4","fourier_2","hermite_3")test_size(int): Number of samples for testingsignal_name(str): Name of signal dataset (default = "RSGraviton_WW_NA_35",options: "RSGraviton_WW_BR_15", "AtoHZ_to_ZZZ_35")latent (int): Latent space dimension
MPS Parameters
bond_dim(int): Bond dimension of MPS (default = 5)initializer(str): Type of MPS initialization
Training Parameters
batch_size(int): Number of samples per training batch (default = 32)run(int): Number of training repetitions with different seeds
python evaluation.py -save_dir results/ \
-load_dir QML_paper_data \
-feature_range 0 1 \
-minmax yes \
-embedding laguerre_2 \
-test_size 5000 \
-bond_dim 8 \
-initializer unitary \
-batch_size 128 \
-run 1 \
-latent 4
Additional Example Scripts#
Below are the Python scripts used in this example. You can view their contents directly on this page, or access them on GitHub.
evaluation.py:
evaluation.pyView on GitHubevaluation.py#1import argparse 2import os 3 4import h5py 5import jax 6import jax.numpy as jnp 7import joblib 8import numpy as np 9from utils import ( 10 Colors, 11 _ensure_data_exists, 12 calc_fidelity_batch, 13 load_test_data, 14) 15 16import tn4ml 17from tn4ml.embeddings import ( 18 Embedding, 19 FourierEmbedding, 20 HermiteEmbedding, 21 LaguerreEmbedding, 22 LegendreEmbedding, 23) 24from tn4ml.initializers import gramschmidt, rand_unitary 25from tn4ml.models.model import load_model 26 27if __name__ == "__main__": 28 parser = argparse.ArgumentParser( 29 description="read arguments for training of TN model" 30 ) 31 parser.add_argument( 32 "-save_dir", 33 dest="save_dir", 34 type=str, 35 help="path to directory for saving results", 36 default="results/", 37 ) 38 parser.add_argument( 39 "-load_dir", 40 dest="load_dir", 41 type=str, 42 help="path to directory for loading the data", 43 ) 44 45 # data params 46 parser.add_argument( 47 "-feature_range", 48 dest="feature_range", 49 type=float, 50 nargs=2, 51 default=[0, 1], 52 help="Feature range for scaling", 53 ) 54 parser.add_argument( 55 "-seed", dest="seed", type=int, help="Seed for random number generator" 56 ) 57 parser.add_argument( 58 "-standardization", 59 dest="standardization", 60 type=str, 61 default="yes", 62 choices=["yes", "no"], 63 help="Standardization of data", 64 ) 65 parser.add_argument( 66 "-minmax", 67 dest="minmax", 68 type=str, 69 default="yes", 70 choices=["yes", "no"], 71 help="Minmax scaling of data", 72 ) 73 parser.add_argument( 74 "-embedding", dest="embedding", type=str, help="Embedding type for input data" 75 ) 76 parser.add_argument("-test_size", dest="test_size", type=int, help="Test size") 77 parser.add_argument( 78 "-signal_name", 79 dest="signal_name", 80 type=str, 81 default="RSGraviton_WW_NA_35", 82 help="Name of signal", 83 ) 84 85 # MPS params 86 parser.add_argument( 87 "-bond_dim", dest="bond_dim", type=int, default=5, help="Bond dimension" 88 ) 89 parser.add_argument( 90 "-initializer", dest="initializer", type=str, help="Type of MPS initialization" 91 ) 92 93 # testing params 94 parser.add_argument("-batch_size", dest="batch_size", type=int, default=32) 95 parser.add_argument( 96 "-run", dest="run", type=int, help="Number of training repetitions" 97 ) 98 99 parser.add_argument( 100 "-latent", dest="latent", type=int, help="Latent space dimension" 101 ) 102 103 args = parser.parse_args() 104 params = vars(args) 105 106 # Get paths to all data files, downloading them if necessary 107 print( 108 Colors.YELLOW.value + "Checking data folder..." + Colors.RESET.value + "\n", 109 end="", 110 ) 111 112 _ensure_data_exists(args.load_dir, args.latent) 113 114 print(Colors.BLUE.value + "Importing data... " + Colors.RESET.value + "\n", end="") 115 116 if args.standardization == "yes": 117 save_dir = ( 118 args.save_dir 119 + "/" 120 + args.initializer 121 + "/10k_standard/" 122 + str(args.embedding) 123 + "/lat" 124 + str(args.latent) 125 + "/bond" 126 + str(args.bond_dim) 127 + "/run_" 128 + str(args.run) 129 ) 130 elif args.minmax == "yes": 131 if tuple(args.feature_range) == (-1, 1): 132 save_dir = ( 133 args.save_dir 134 + "/" 135 + args.initializer 136 + "/10k_minmax-11/" 137 + str(args.embedding) 138 + "/lat" 139 + str(args.latent) 140 + "/bond" 141 + str(args.bond_dim) 142 + "/run_" 143 + str(args.run) 144 ) 145 else: 146 save_dir = ( 147 args.save_dir 148 + "/" 149 + args.initializer 150 + "/10k_minmax01/" 151 + str(args.embedding) 152 + "/lat" 153 + str(args.latent) 154 + "/bond" 155 + str(args.bond_dim) 156 + "/run_" 157 + str(args.run) 158 ) 159 else: 160 save_dir = ( 161 args.save_dir 162 + "/" 163 + args.initializer 164 + "/10k/" 165 + str(args.embedding) 166 + "/lat" 167 + str(args.latent) 168 + "/bond" 169 + str(args.bond_dim) 170 + "/run_" 171 + str(args.run) 172 ) 173 174 # set standardization and minmax to bool 175 standardization = args.standardization == "yes" 176 177 minmax = args.minmax == "yes" 178 179 # check result dir 180 if not os.path.exists(save_dir): # noqa: PTH110 181 # Create a new directory because it does not exist 182 os.makedirs(save_dir) # noqa: PTH103 183 184 if args.seed is not None: 185 # Use specified seed for reproducibility 186 seed = args.seed 187 print( 188 Colors.YELLOW.value 189 + f"Using specified seed: {seed}" 190 + Colors.RESET.value 191 + "\n", 192 end="", 193 ) 194 else: 195 # Generate random seed for exploration 196 seed = int.from_bytes(os.urandom(4), "big") 197 print( 198 Colors.YELLOW.value 199 + f"Using random seed: {seed}" 200 + Colors.RESET.value 201 + "\n", 202 end="", 203 ) 204 205 # Set random seed 206 np.random.seed(seed) # noqa: NPY002 207 key = jax.random.PRNGKey(seed) 208 209 # Set JAX to use 64-bit precision 210 # This is important for numerical stability in some cases 211 jax.config.update("jax_enable_x64", True) 212 213 # Parse embedding string to get type and degree 214 embedding_string = args.embedding 215 try: 216 embedding_type, degree_str = embedding_string.split("_", 1) 217 degree = int(degree_str) 218 except ValueError: 219 raise ValueError( 220 Colors.RED.value 221 + f"Invalid embedding format: {embedding_string}. Expected format: 'name_degree' (e.g., 'fourier_2')" 222 + Colors.RESET.value 223 ) from None 224 225 # Initialize embedding based on type and degree 226 if embedding_type == "fourier": 227 phys_dim = ( 228 degree * 2 229 ) # Each frequency component adds 2 dimensions (sin and cos) 230 embedding: Embedding = FourierEmbedding(p=degree) 231 elif embedding_type == "legendre": 232 phys_dim = degree + 1 # Legendre polynomials from degree 0 to degree 233 embedding = LegendreEmbedding(degree=degree) 234 elif embedding_type == "laguerre": 235 phys_dim = degree + 1 # Laguerre polynomials from degree 0 to degree 236 embedding = LaguerreEmbedding(degree=degree) 237 elif embedding_type == "hermite": 238 phys_dim = degree + 1 # Hermite polynomials from degree 0 to degree 239 embedding = HermiteEmbedding(degree=degree) 240 else: 241 raise ValueError( 242 Colors.RED.value 243 + f"Invalid embedding type: {embedding_type}. Supported types: fourier, legendre, laguerre, hermite" 244 + Colors.RESET.value 245 ) 246 247 # Set the standard deviation for the initializer 248 # This is a heuristic value based on the bond dimension and physical dimension from the paper https://arxiv.org/abs/2310.20498 249 std = np.power(float(phys_dim * args.bond_dim), -1) 250 251 # Define the possible initializers 252 initializers = { 253 "gramschmidt_n_std": gramschmidt("normal", std, dtype=jnp.float64), 254 "randn_std": tn4ml.initializers.randn(std), 255 "randn_1e-2": tn4ml.initializers.randn(1e-2), 256 "unitary": rand_unitary(), 257 } 258 259 # Check if the initializer is valid 260 if args.initializer not in initializers: 261 raise ValueError( 262 Colors.RED.value 263 + f"Invalid initializer: {args.initializer}. Supported initializers: {', '.join(initializers.keys())}" 264 + Colors.RESET.value 265 ) 266 267 print( 268 Colors.BLUE.value + "Loading the model..." + Colors.RESET.value + "\n", end="" 269 ) 270 # Save model 271 model_name = "model" 272 model = load_model(model_name, save_dir) 273 274 # EVALUATION 275 print(Colors.BLUE.value + "Evaluating model..." + Colors.RESET.value + "\n", end="") 276 277 # Load scalers 278 prefix = "train_qcd" 279 if args.standardization == "yes": 280 scaler_path = os.path.join(save_dir, f"scaler_standard_{prefix}.pkl") # noqa: PTH118 281 if os.path.exists(scaler_path): # noqa: PTH110 282 with open(scaler_path, "rb") as f: # noqa: PTH123 283 scaler = joblib.load(f) 284 else: 285 raise FileNotFoundError( 286 Colors.RED.value 287 + f"Scaler file not found: {scaler_path}" 288 + Colors.RESET.value 289 ) 290 else: 291 scaler = None 292 293 if args.minmax == "yes": 294 scaler_path = os.path.join(save_dir, f"scaler_minmax_{prefix}.pkl") # noqa: PTH118 295 if os.path.exists(scaler_path): # noqa: PTH110 296 with open(scaler_path, "rb") as f: # noqa: PTH123 297 min_max_scaler = joblib.load(f) 298 else: 299 raise FileNotFoundError( 300 Colors.RED.value 301 + f"Scaler file not found: {scaler_path}" 302 + Colors.RESET.value 303 ) 304 else: 305 min_max_scaler = None 306 307 # Load test data 308 read_data_dir = f"{args.load_dir}/latent{args.latent}" 309 qcd_test_scaled = load_test_data( 310 f"{read_data_dir}", 311 dataset_type="qcd", 312 scaler=scaler, 313 min_max_scaler=min_max_scaler, 314 test_size=args.test_size, 315 shuffle_seed=seed, 316 ) 317 318 sig_test_scaled = load_test_data( 319 f"{read_data_dir}/latentrep_{args.signal_name}.h5", 320 dataset_type="signal", 321 scaler=scaler, 322 min_max_scaler=min_max_scaler, 323 test_size=args.test_size, 324 shuffle_seed=seed, 325 ) 326 327 # Calculate Fidelity - AD score 328 print( 329 Colors.YELLOW.value 330 + "Calculating fidelity scores..." 331 + Colors.RESET.value 332 + "\n", 333 end="", 334 ) 335 336 fid_qcd = calc_fidelity_batch( 337 qcd_test_scaled, model, embedding=embedding, batch_size=args.batch_size 338 ) 339 340 fid_sig = calc_fidelity_batch( 341 sig_test_scaled, model, embedding=embedding, batch_size=args.batch_size 342 ) 343 344 # Save anomaly scores 345 print( 346 Colors.BLUE.value + "Saving fidelity scores..." + Colors.RESET.value + "\n", 347 end="", 348 ) 349 with h5py.File(f"{save_dir}/fidelity_scores_{args.signal_name}.h5", "w") as file: 350 file.create_dataset("loss_qcd", data=fid_qcd) 351 file.create_dataset("loss_sig", data=fid_sig)
pipeline.py: * View in browser:
pipeline.py* View on GitHubpipeline.py#1import argparse 2import json 3import os 4 5import h5py 6import jax 7import jax.numpy as jnp 8import matplotlib.pyplot as plt 9import numpy as np 10import optax 11from utils import ( 12 Colors, 13 _ensure_data_exists, 14 calc_fidelity_batch, 15 load_test_data, 16 load_train_data, 17) 18 19import tn4ml 20from tn4ml.embeddings import ( 21 Embedding, 22 FourierEmbedding, 23 HermiteEmbedding, 24 LaguerreEmbedding, 25 LegendreEmbedding, 26) 27from tn4ml.initializers import gramschmidt, rand_unitary 28from tn4ml.metrics import NegLogLikelihood 29from tn4ml.models.mps import MPS_initialize 30from tn4ml.util import EarlyStopping, TrainingType 31 32if __name__ == "__main__": 33 parser = argparse.ArgumentParser( 34 description="read arguments for training of TN model" 35 ) 36 parser.add_argument( 37 "-save_dir", 38 dest="save_dir", 39 type=str, 40 help="path to directory for saving results", 41 default="results/", 42 ) 43 parser.add_argument( 44 "-load_dir", 45 dest="load_dir", 46 type=str, 47 help="path to directory for loading the data", 48 ) 49 50 # data params 51 parser.add_argument( 52 "-feature_range", 53 dest="feature_range", 54 type=float, 55 nargs=2, 56 default=[0, 1], 57 help="Feature range for scaling", 58 ) 59 parser.add_argument( 60 "-seed", dest="seed", type=int, help="Seed for random number generator" 61 ) 62 parser.add_argument( 63 "-standardization", 64 dest="standardization", 65 type=str, 66 default="yes", 67 choices=["yes", "no"], 68 help="Standardization of data", 69 ) 70 parser.add_argument( 71 "-minmax", 72 dest="minmax", 73 type=str, 74 default="yes", 75 choices=["yes", "no"], 76 help="Minmax scaling of data", 77 ) 78 parser.add_argument( 79 "-embedding", dest="embedding", type=str, help="Embedding type for input data" 80 ) 81 parser.add_argument("-test_size", dest="test_size", type=int, help="Test size") 82 parser.add_argument("-train_size", dest="train_size", type=int, help="Train size") 83 parser.add_argument( 84 "-signal_name", 85 dest="signal_name", 86 type=str, 87 default="RSGraviton_WW_NA_35", 88 help="Name of signal", 89 ) 90 91 # MPS params 92 parser.add_argument( 93 "-bond_dim", dest="bond_dim", type=int, default=5, help="Bond dimension" 94 ) 95 parser.add_argument( 96 "-initializer", dest="initializer", type=str, help="Type of MPS initialization" 97 ) 98 parser.add_argument("-shape_method", dest="shape_method", type=str, default="even") 99 100 # training params 101 parser.add_argument("-lr", dest="lr", type=float, default=1e-3) 102 parser.add_argument("-min_delta", dest="min_delta", type=float, default=0) 103 parser.add_argument("-patience", dest="patience", type=int, default=20) 104 parser.add_argument("-epochs", dest="epochs", type=int, default=100) 105 parser.add_argument("-batch_size", dest="batch_size", type=int, default=32) 106 parser.add_argument( 107 "-run", dest="run", type=int, help="Number of training repetitions" 108 ) 109 110 parser.add_argument( 111 "-latent", dest="latent", type=int, help="Latent space dimension" 112 ) 113 114 args = parser.parse_args() 115 params = vars(args) 116 117 # Get paths to all data files, downloading them if necessary 118 print( 119 Colors.YELLOW.value + "Checking data folder..." + Colors.RESET.value + "\n", 120 end="", 121 ) 122 123 _ensure_data_exists(args.load_dir, args.latent) 124 125 print(Colors.BLUE.value + "Importing data... " + Colors.RESET.value + "\n", end="") 126 127 if args.standardization == "yes": 128 save_dir = ( 129 args.save_dir 130 + "/" 131 + args.initializer 132 + "/10k_standard/" 133 + str(args.embedding) 134 + "/lat" 135 + str(args.latent) 136 + "/bond" 137 + str(args.bond_dim) 138 + "/run_" 139 + str(args.run) 140 ) 141 elif args.minmax == "yes": 142 if tuple(args.feature_range) == (-1, 1): 143 save_dir = ( 144 args.save_dir 145 + "/" 146 + args.initializer 147 + "/10k_minmax-11/" 148 + str(args.embedding) 149 + "/lat" 150 + str(args.latent) 151 + "/bond" 152 + str(args.bond_dim) 153 + "/run_" 154 + str(args.run) 155 ) 156 else: 157 save_dir = ( 158 args.save_dir 159 + "/" 160 + args.initializer 161 + "/10k_minmax01/" 162 + str(args.embedding) 163 + "/lat" 164 + str(args.latent) 165 + "/bond" 166 + str(args.bond_dim) 167 + "/run_" 168 + str(args.run) 169 ) 170 else: 171 save_dir = ( 172 args.save_dir 173 + "/" 174 + args.initializer 175 + "/10k/" 176 + str(args.embedding) 177 + "/lat" 178 + str(args.latent) 179 + "/bond" 180 + str(args.bond_dim) 181 + "/run_" 182 + str(args.run) 183 ) 184 185 # set standardization and minmax to bool 186 standardization = args.standardization == "yes" 187 188 minmax = args.minmax == "yes" 189 190 # check result dir 191 if not os.path.exists(save_dir): # noqa: PTH110 192 # Create a new directory because it does not exist 193 os.makedirs(save_dir) # noqa: PTH103 194 195 if args.seed is not None: 196 # Use specified seed for reproducibility 197 seed = args.seed 198 print( 199 Colors.BLUE.value 200 + f"Using specified seed: {seed}" 201 + Colors.RESET.value 202 + "\n", 203 end="", 204 ) 205 else: 206 # Generate random seed for exploration 207 seed = int.from_bytes(os.urandom(4), "big") 208 print( 209 Colors.BLUE.value 210 + f"Using random seed: {seed}" 211 + Colors.RESET.value 212 + "\n", 213 end="", 214 ) 215 216 # Set random seed 217 np.random.seed(seed) # noqa: NPY002 218 key = jax.random.PRNGKey(seed) 219 220 # Set JAX to use 64-bit precision 221 # This is important for numerical stability in some cases 222 jax.config.update("jax_enable_x64", True) 223 224 # Load_data 225 read_data_file = f"{args.load_dir}/latent{args.latent}/latentrep_QCD_sig.h5" 226 train_data, scalers = load_train_data( 227 read_data_file, 228 args.train_size, 229 minmax, 230 standardization, 231 feature_range=tuple(args.feature_range), 232 shuffle_seed=seed, 233 save_dir=save_dir, 234 ) 235 236 # Create a MPS model 237 L = train_data.shape[1] 238 print( 239 Colors.BLUE.value + "Number of tensors: " + str(L) + Colors.RESET.value + "\n", 240 end="", 241 ) 242 243 # Parse embedding string to get type and degree 244 embedding_string = args.embedding 245 try: 246 embedding_type, degree_str = embedding_string.split("_", 1) 247 degree = int(degree_str) 248 except ValueError: 249 raise ValueError( # noqa: B904 250 f"Invalid embedding format: {embedding_string}. Expected format: 'name_degree' (e.g., 'fourier_2')" 251 ) 252 253 # Initialize embedding based on type and degree 254 if embedding_type == "fourier": 255 phys_dim = ( 256 degree * 2 257 ) # Each frequency component adds 2 dimensions (sin and cos) 258 embedding: Embedding = FourierEmbedding(p=degree) 259 elif embedding_type == "legendre": 260 phys_dim = degree + 1 # Legendre polynomials from degree 0 to degree 261 embedding = LegendreEmbedding(degree=degree) 262 elif embedding_type == "laguerre": 263 phys_dim = degree + 1 # Laguerre polynomials from degree 0 to degree 264 embedding = LaguerreEmbedding(degree=degree) 265 elif embedding_type == "hermite": 266 phys_dim = degree + 1 # Hermite polynomials from degree 0 to degree 267 embedding = HermiteEmbedding(degree=degree) 268 else: 269 raise ValueError( 270 f"Invalid embedding type: {embedding_type}. Supported types: fourier, legendre, laguerre, hermite" 271 ) 272 273 print( 274 Colors.BLUE.value 275 + f"Using {embedding_type} embedding with degree {degree} (physical dimension: {phys_dim})" 276 + Colors.RESET.value 277 + "\n", 278 end="", 279 ) 280 281 # Set the standard deviation for the initializer 282 # This is a heuristic value based on the bond dimension and physical dimension from the paper https://arxiv.org/abs/2310.20498 283 std = np.power(float(phys_dim * args.bond_dim), -1) 284 285 # Define the possible initializers 286 initializers = { 287 "gramschmidt_n_std": gramschmidt("normal", std, dtype=jnp.float64), 288 "randn_std": tn4ml.initializers.randn(std), 289 "randn_1e-2": tn4ml.initializers.randn(1e-2), 290 "unitary": rand_unitary(), 291 } 292 293 # Check if the initializer is valid 294 if args.initializer not in initializers: 295 raise ValueError( 296 f"Invalid initializer: {args.initializer}. Supported initializers: {', '.join(initializers.keys())}" 297 ) 298 299 # Initialize the MPS model 300 shape_method = ( 301 args.shape_method 302 ) # shape method defines how the tensors are arranged in the MPS 303 compress = ( 304 False # compress the MPS tensors - not used in this example, feature in quimb 305 ) 306 add_identity = False # option to add identity tensors to the MPS 307 canonical_center = 0 # canonical center at the first tensor 308 canonize = (True, 0) # flag to canonize the MPS tensors during training 309 310 print( 311 Colors.BLUE.value + "Initializing MPS model..." + Colors.RESET.value + "\n", 312 end="", 313 ) 314 model = MPS_initialize( 315 L=L, 316 initializer=initializers[args.initializer], 317 key=key, 318 shape_method=shape_method, 319 bond_dim=args.bond_dim, 320 phys_dim=phys_dim, 321 cyclic=False, 322 compress=compress, 323 add_identity=add_identity, 324 canonical_center=canonical_center, 325 boundary="obc", 326 ) 327 328 # Define training parameters 329 optimizer = optax.adam 330 strategy = "global" 331 loss = NegLogLikelihood 332 train_type = TrainingType.UNSUPERVISED 333 learning_rate = args.lr 334 335 # Configure the model with the optimizer, strategy, loss function, and training type 336 model.configure( 337 optimizer=optimizer, 338 strategy=strategy, 339 loss=loss, 340 train_type=train_type, 341 learning_rate=learning_rate, 342 ) 343 344 # Initialize the early stopping callback 345 earlystop = EarlyStopping( 346 min_delta=args.min_delta, patience=args.patience, mode="min", monitor="loss" 347 ) 348 349 # Train the model 350 print(Colors.BLUE.value + "Training model..." + Colors.RESET.value + "\n", end="") 351 history = model.train( 352 train_data, 353 epochs=args.epochs, 354 batch_size=args.batch_size, 355 embedding=embedding, 356 normalize=True, 357 dtype=jnp.float64, 358 earlystop=earlystop, 359 canonize=canonize, 360 seed=seed, 361 shuffle=True, 362 ) 363 364 # -------- SAVE RESULTS AND MODEL ------------- 365 366 print(Colors.BLUE.value + "Saving the model..." + Colors.RESET.value + "\n", end="") 367 # Save model 368 model_name = "model" 369 model.save(model_name, save_dir) 370 371 # Plot loss 372 plt.figure() 373 plt.plot(range(len(history["loss"])), history["loss"], label="train") 374 plt.legend() 375 plt.savefig(save_dir + "/loss.pdf") 376 377 # Save loss 378 np.save(save_dir + "/loss.npy", history["loss"]) 379 380 params_save = { 381 # MPS parameters 382 "bond_dim": str(args.bond_dim), 383 "phys_dim": str(phys_dim), 384 "initializer": str(args.initializer), 385 "shape_method": str(shape_method), 386 "compress": str(compress), 387 "add_identity": str(add_identity), 388 "boundary": "obc", 389 "std": str(std), 390 # Data parameters 391 "embedding": str(embedding_string), 392 "train_size": str(args.train_size), 393 "test_size": str(args.test_size), 394 "signal_name": str(args.signal_name), 395 "feature_range": str(args.feature_range), 396 "standardization": str(args.standardization), 397 "minmax": str(args.minmax), 398 # Training parameters 399 "learning_rate": str(args.lr), 400 "batch_size": str(args.batch_size), 401 "epochs": str(args.epochs), 402 "patience": str(args.patience), 403 "min_delta": str(args.min_delta), 404 # Model configuration 405 "strategy": strategy, 406 "optimizer": "adam", 407 "loss": "NegLogLikelihood", 408 "train_type": "unsupervised", 409 # Seed and paths 410 "seed": str(seed), 411 "save_dir": save_dir, 412 "load_dir": args.load_dir, 413 "latent_space_dim": str(args.latent), 414 } 415 416 # Save parameters 417 print( 418 Colors.BLUE.value + "Saving parameters..." + Colors.RESET.value + "\n", end="" 419 ) 420 with open(os.path.join(save_dir, "parameters.json"), "w") as f: # noqa: PTH118, PTH123 421 json.dump(params_save, f, indent=4) 422 423 # EVALUATION 424 print(Colors.BLUE.value + "Evaluating model..." + Colors.RESET.value + "\n", end="") 425 426 scaler = scalers["standard"] if args.standardization == "yes" else None 427 428 min_max_scaler = scalers["minmax"] if args.minmax == "yes" else None 429 430 # Load test data 431 read_data_dir = f"{args.load_dir}/latent{args.latent}" 432 qcd_test_scaled = load_test_data( 433 f"{read_data_dir}", 434 dataset_type="qcd", 435 scaler=scaler, 436 min_max_scaler=min_max_scaler, 437 test_size=args.test_size, 438 shuffle_seed=seed, 439 ) 440 441 sig_test_scaled = load_test_data( 442 f"{read_data_dir}/latentrep_{args.signal_name}.h5", 443 dataset_type="signal", 444 scaler=scaler, 445 min_max_scaler=min_max_scaler, 446 test_size=args.test_size, 447 shuffle_seed=seed, 448 ) 449 450 # Calculate Fidelity - AD score 451 print( 452 Colors.YELLOW.value 453 + "Calculating fidelity scores..." 454 + Colors.RESET.value 455 + "\n", 456 end="", 457 ) 458 459 fid_qcd = calc_fidelity_batch( 460 qcd_test_scaled, model, embedding=embedding, batch_size=args.batch_size 461 ) 462 463 fid_sig = calc_fidelity_batch( 464 sig_test_scaled, model, embedding=embedding, batch_size=args.batch_size 465 ) 466 467 # Save anomaly scores 468 print( 469 Colors.BLUE.value + "Saving fidelity scores..." + Colors.RESET.value + "\n", 470 end="", 471 ) 472 with h5py.File(f"{save_dir}/fidelity_scores_{args.signal_name}.h5", "w") as file: 473 file.create_dataset("loss_qcd", data=fid_qcd) 474 file.create_dataset("loss_sig", data=fid_sig)
plot.py: * View in browser:
plot.py* View on GitHubplot.py#1import os 2from collections.abc import Collection 3 4import h5py 5import matplotlib.patches as mpatches 6import matplotlib.pyplot as plt 7import numpy as np 8from matplotlib.font_manager import FontProperties 9from sklearn.metrics import auc 10from utils import * 11 12from tn4ml.eval import * 13 14 15def load_anomaly_scores( 16 signal_name: str, 17 initializers_strings: Collection[str], 18 latent_spaces: Collection[int], 19 bond_dim: dict, 20 embedding: str, 21 nruns: int, 22 save_dir: str, 23 train_scaling: str, 24): 25 """ 26 Load anomaly scores for different initializers and bond dimensions. 27 28 Parameters 29 ---------- 30 signal_name : str 31 Name of the signal for which anomaly scores are obtained 32 initializers_strings : Collection[str] 33 List of initializer names 34 latent_spaces : Collection[int] 35 List of latent space dimensions to compare 36 bond_dim : dict 37 Dictionary containing bond dimensions for each latent space - e.g. {'4': [2, 4], '8': [2, 4, 8]} 38 embedding : str 39 Embedding name for the model 40 nruns : int 41 Number of runs to average over 42 save_dir : str 43 Directory to save the plot 44 train_scaling : str 45 Training size and scaling to consider 46 47 Returns 48 ------- 49 tpr_per_init : dict 50 Dictionary containing true positive rates for each initializer and bond dimension 51 tpr_per_init_err : dict 52 Dictionary containing statistical errors for true positive rates - from multiple runs 53 fpr_per_init : dict 54 Dictionary containing false positive rates for each initializer and bond dimension 55 fpr_per_init_err : dict 56 Dictionary containing statistical errors for false positive rates - from multiple runs 57 auc_per_init : dict 58 Dictionary containing area under the curve values for each initializer and bond dimension 59 auc_per_init_err : dict 60 Dictionary containing statistical errors for area under the curve values - from multiple runs 61 fpr_per_tpr_8_per_init : dict 62 Dictionary containing false positive rates for TPR = 0.8 for each initializer and bond dimension 63 fpr_per_tpr_8_per_init_err : dict 64 Dictionary containing statistical errors for false positive rates for TPR = 0.8 - from multiple runs 65 fpr_per_tpr_6_per_init : dict 66 Dictionary containing false positive rates for TPR = 0.6 for each initializer and bond dimension 67 fpr_per_tpr_6_per_init_err : dict 68 Dictionary containing statistical errors for false positive rates for TPR = 0.6 - from multiple runs 69 """ 70 # Initialize dictionaries to store results 71 tpr_per_init = {} 72 tpr_per_init_err = {} 73 fpr_per_init = {} 74 fpr_per_init_err = {} 75 auc_per_init = {} 76 auc_per_init_err = {} 77 fpr_per_tpr_8_per_init = {} 78 fpr_per_tpr_8_per_init_err = {} 79 fpr_per_tpr_6_per_init = {} 80 fpr_per_tpr_6_per_init_err = {} 81 82 for initializer in initializers_strings: 83 for _i, lat in enumerate(latent_spaces): 84 for _j, bond in enumerate(bond_dim[str(lat)]): 85 loss_qcd_runs = [] 86 loss_sig_runs = [] 87 tpr_data = [] 88 fpr_data = [] 89 auc_data = [] 90 fpr_per_tpr_8_data = [] 91 fpr_per_tpr_6_data = [] 92 for run in range(1, nruns + 1): 93 path = f"{save_dir}/{initializer}/{train_scaling}/{embedding}/lat{lat}/bond{bond}/run_{run}/fidelity_scores_{signal_name}.h5" 94 if not os.path.exists(path): # noqa: PTH110 95 continue 96 with h5py.File(path, "r") as file: 97 loss_qcd = file["loss_qcd"][:] 98 loss_sig = file["loss_sig"][:] 99 100 loss_qcd = np.power(loss_qcd, 2) 101 loss_sig = np.power(loss_sig, 2) 102 103 loss_qcd_runs.append(loss_qcd) 104 loss_sig_runs.append(loss_sig) 105 106 fpr, tpr = get_roc_curve_data( 107 loss_sig, loss_qcd, anomaly_det=True 108 ) 109 tpr_data.append(tpr) 110 fpr_data.append(fpr) 111 # Get auc 112 auc_value = auc(fpr, tpr) 113 auc_data.append(auc_value) 114 115 # Get fpr per tpr = {0.8, 0.6} 116 fpr_per_tpr_8 = get_FPR_for_fixed_TPR( 117 0.8, np.array(fpr), np.array(tpr), tolerance=0.01 118 ) 119 fpr_per_tpr_6 = get_FPR_for_fixed_TPR( 120 0.6, np.array(fpr), np.array(tpr), tolerance=0.01 121 ) 122 fpr_per_tpr_8_data.append(fpr_per_tpr_8) 123 fpr_per_tpr_6_data.append(fpr_per_tpr_6) 124 125 loss_qcd = get_mean_and_error(np.array(loss_qcd_runs)) 126 loss_sig = get_mean_and_error(np.array(loss_sig_runs)) 127 128 if np.isnan(loss_qcd[0]).sum() > 0: 129 print(f"{Colors.RED.value}{path}: NaNs{Colors.RESET.value}") 130 continue 131 132 # Get mean error for tpr, fpr 133 tpr_mean_error = get_mean_and_error(np.array(tpr_data)) 134 tpr_per_init[ 135 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 136 ] = tpr_mean_error[0] 137 tpr_per_init_err[ 138 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 139 ] = tpr_mean_error[1] 140 141 fpr_mean_error = get_mean_and_error(1.0 / np.array(fpr_data)) 142 fpr_per_init[ 143 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 144 ] = fpr_mean_error[0] 145 fpr_per_init_err[ 146 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 147 ] = fpr_mean_error[1] 148 149 # AUC mean error 150 auc_mean_error = get_mean_and_error(np.array(auc_data)) 151 auc_per_init[ 152 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 153 ] = auc_mean_error[0] 154 auc_per_init_err[ 155 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 156 ] = auc_mean_error[1] 157 158 # fpr per tpr = 0.8 159 fpr_per_tpr_8_mean_error = get_mean_and_error( 160 1.0 / np.array(fpr_per_tpr_8_data) 161 ) 162 fpr_per_tpr_8_per_init[ 163 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 164 ] = fpr_per_tpr_8_mean_error[0] 165 fpr_per_tpr_8_per_init_err[ 166 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 167 ] = fpr_per_tpr_8_mean_error[1] 168 169 # fpr per tpr = 0.6 170 fpr_per_tpr_6_mean_error = get_mean_and_error( 171 1.0 / np.array(fpr_per_tpr_6_data) 172 ) 173 fpr_per_tpr_6_per_init[ 174 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 175 ] = fpr_per_tpr_6_mean_error[0] 176 fpr_per_tpr_6_per_init_err[ 177 f"init={initializer},bond={bond},lat={lat},s={signal_name}" 178 ] = fpr_per_tpr_6_mean_error[1] 179 180 return ( 181 tpr_per_init, 182 tpr_per_init_err, 183 fpr_per_init, 184 fpr_per_init_err, 185 auc_per_init, 186 auc_per_init_err, 187 fpr_per_tpr_8_per_init, 188 fpr_per_tpr_8_per_init_err, 189 fpr_per_tpr_6_per_init, 190 fpr_per_tpr_6_per_init_err, 191 ) 192 193 194def plot_losses_per_initializer( 195 latent: int, 196 bond_dims: Collection[int], 197 initializers_strings: Collection[str], 198 embedding: str, 199 save_dir: str, 200 N_epochs: int = 1000, 201 train_size: str = "10k", 202 minmax: str = "minmax-11", 203 nruns: int = 5, 204): 205 """Create a subplot grid with training loss plots for all initializers for fixed embedding. 206 207 Parameters 208 ---------- 209 latent : int 210 Latent space dimension 211 bond_dims : Collection[int] 212 List of bond dimensions to compare 213 initializers_strings : Collection[str] 214 List of initializer names to compare 215 embedding : str 216 Embedding name for the model 217 save_dir : str 218 Directory to save the plot 219 N_epochs : int, optional 220 Number of epochs for training, default 1000 221 train_size : str, optional 222 Training size to consider, default "10k" 223 minmax : str, optional 224 Min-Max scaling option, default "minmax-11" 225 nruns : int, optional 226 Number of runs to average over, default 5 227 228 Returns 229 ------- 230 None 231 232 Notes 233 ----- 234 - The function creates a grid of subplots, each showing the training loss for a different initializer. 235 - Each subplot contains multiple lines representing different bond dimensions - corresponding to different colors in a color palette. 236 - The function is assuming the data files are structured in a specific way, but you can change it to fit your needs. 237 238 239 Example 240 ------- 241 >>> plot_losses_per_initializer( 242 ... latent = 4, 243 ... bond_dims = [2, 4, 8, 16], 244 ... initializers_strings = ['unitary_canonize', 'randn_std', 'randn_1e-2', 'gramschmidt_n_std'], 245 ... embedding = 'laguerre_2', 246 ... save_dir = './results', 247 ... N = 1000, 248 ... train_size = '10k', 249 ... minmax = 'minmax-11', 250 ... nruns = 10 251 ... ) 252 253 """ 254 # Calculate grid dimensions 255 n_initializers = len(initializers_strings) 256 n_cols = min(2, n_initializers) # Maximum 4 columns 257 n_rows = (n_initializers + n_cols - 1) // n_cols # Ceiling division 258 259 # Color maps for different bond dimensions 260 color_maps = [ 261 "Blues", 262 "Reds", 263 "Greens", 264 "Purples", 265 "Oranges", 266 "YlOrBr", 267 "GnBu", 268 "PuRd", 269 ] 270 271 # Create figure with subplots 272 _fig, axes = plt.subplots(n_rows, n_cols, figsize=(6 * n_cols, 5 * n_rows)) 273 274 # Flatten axes array for easy indexing if multiple rows/columns 275 if n_rows > 1 or n_cols > 1: # noqa: SIM108 276 axes = axes.flatten() 277 else: 278 axes = [axes] # Convert to list for single subplot case 279 280 # Plot for each initializer 281 for i, initializer in enumerate(initializers_strings): 282 ax = axes[i] 283 284 # Plot for each bond dimension 285 for j, bond in enumerate(bond_dims): 286 # Create color spectrum for this bond dimension 287 cmap = plt.get_cmap(color_maps[j % len(color_maps)]) 288 289 # Process each run 290 for run in range(1, nruns + 1): 291 # Load loss data 292 file_path = f"{save_dir}/{initializer}/{train_size}_{minmax}/{embedding}/lat{latent}/bond{bond}/run_{run}/loss.npy" 293 294 try: 295 # Load and pad with NaN 296 loss_data = np.load(file_path) 297 loss = np.full(N_epochs, np.nan) 298 loss[: len(loss_data)] = loss_data 299 300 # Plot with color from the spectrum 301 color_intensity = 0.3 + 0.6 * (run / nruns) 302 color = cmap(color_intensity) 303 304 # Plot data 305 epochs = np.arange(1, N_epochs + 1) 306 ax.plot(epochs, loss, "-", color=color, linewidth=3, alpha=0.7) 307 308 except FileNotFoundError: 309 print( 310 f"{Colors.RED.value}File not found: {file_path}{Colors.RESET.value}" 311 ) 312 continue 313 314 # Add label for this bond dimension (just once per bond) 315 ax.plot([], [], "-", color=cmap(0.5), label=rf"$\chi$ = {bond}") 316 317 # Style this subplot 318 ax.grid(True, linestyle="--", alpha=0.4) 319 ax.set_xlabel("Epochs", fontsize=15) 320 ax.set_ylabel("Training Loss", fontsize=15) 321 322 # Create a label for the initializer 323 if initializer == "gramschmidt_n_std": 324 initializer = "Gram-Schmidt (normal - std=0.1667)" 325 elif initializer == "randn_std": 326 initializer = "Random (normal - std=0.1667)" 327 elif initializer == "randn_1e-2": 328 initializer = "Random (normal - std=0.01)" 329 elif initializer == "unitary": 330 initializer = "Unitary" 331 332 ax.set_title(f"{initializer}", fontsize=17) 333 ax.set_xlim(0, N_epochs) 334 335 # set font size for ticks 336 ax.tick_params(axis="both", which="major", labelsize=12) 337 ax.tick_params(axis="both", which="minor", labelsize=12) 338 339 axes[0].legend(loc="upper right", frameon=True, fontsize=14) 340 # Hide any unused subplots 341 for j in range(i + 1, n_rows * n_cols): 342 if j < len(axes): 343 axes[j].axis("off") 344 345 # Hide y-axis labels for all but the first column 346 for ax in axes: 347 ax.label_outer() 348 349 plt.tight_layout() 350 351 # Save figure 352 os.makedirs( # noqa: PTH103 353 f"{save_dir}/plots/{train_size}_{minmax}/{embedding}/lat{latent}/", 354 exist_ok=True, 355 ) 356 357 plt.savefig( 358 f"{save_dir}/plots/{train_size}_{minmax}/{embedding}/lat{latent}/train_loss.pdf" 359 ) 360 361 362def compare_anomaly_scores_per_embedding( 363 embeddings: Collection[str], 364 initializer: str, 365 latent: int, 366 bond_dims: Collection[int], 367 signal_name: str, 368 save_dir: str, 369 train_size_scaling: Collection[str], 370 nruns: int = 10, 371): 372 """ 373 Create a single plot with subplots for different embeddings, showing QCD and BSM 374 anomaly scores distributions for each bond dimension with consistent coloring. 375 376 Parameters 377 ---------- 378 embeddings : Collection[str] 379 List of embedding names to compare 380 initializer : str 381 Initializer name for the model 382 latent : int 383 Latent space dimension 384 bond_dims : Collection[int] 385 List of bond dimensions to compare 386 signal_name : str 387 Name of the signal to be used in the plot 388 save_dir : str 389 Directory to save the plot 390 train_size_scaling : Collection[str] 391 List of training sizes and scaling to consider for each embedding 392 nruns : int, optional 393 Number of runs to average over, default 10 394 395 Returns 396 ------- 397 fig : matplotlib.figure.Figure 398 The figure object containing the plot 399 axs : List[matplotlib.axes.Axes] 400 List of axes objects for each subplot 401 402 Notes 403 ----- 404 - The function assumes that the data files are structured in a specific way, 405 with paths containing the bond dimension and run number. 406 - The function uses a consistent color scheme for different bond dimensions across all subplots. 407 - The function handles multiple runs by averaging the results and plotting the distributions. 408 409 410 Example 411 ------- 412 >>> compare_anomaly_scores_per_embedding( 413 ... embeddings = ['laguerre_2', 'legendre_2', 'hermite_2'], 414 ... initializer = 'unitary', 415 ... latent = 4, 416 ... bond_dims = [2, 4, 8, 16], 417 ... signal_name = 'AtoHZ_to_ZZZ_35', 418 ... save_dir = './results', 419 ... train_size_scaling = ['10k_minmax01', '10k_minmax-11', '10k_minmax-11'], 420 ... nruns = 10 421 ... ) 422 """ # noqa: D205 423 # Set up color maps for different bond dimensions - using brighter colors 424 color_maps = [ 425 "Blues", 426 "Reds", 427 "Greens", 428 "Purples", 429 "Oranges", 430 "YlOrBr", 431 "GnBu", 432 "PuRd", 433 ] 434 435 # Create figure with subplots (one per embedding) 436 fig, axs = plt.subplots( 437 1, len(embeddings), figsize=(6 * len(embeddings), 5), sharey=True 438 ) 439 440 # Handle case of single embedding 441 if len(embeddings) == 1: 442 axs = [axs] 443 444 # Create a color dictionary with brighter colors (using 0.8 instead of 0.6) 445 bond_colors = { 446 bond: plt.get_cmap(color_maps[i % len(color_maps)])(0.8) 447 for i, bond in enumerate(bond_dims) 448 } 449 450 # Keep track of handles for the bond dimension legend 451 bond_handles = [] 452 453 # Process each embedding 454 for e, embedding in enumerate(embeddings): 455 train_scaling = list(train_size_scaling)[e] 456 ax = axs[e] 457 458 # Process each bond dimension 459 for bond in bond_dims: 460 loss_qcd_runs = [] 461 loss_sig_runs = [] 462 463 # Process multiple runs 464 for run in range(1, nruns + 1): 465 path = f"{save_dir}/{initializer}/{train_scaling}/{embedding}/lat{latent}/bond{bond}/run_{run}/fidelity_scores_{signal_name}.h5" 466 467 if not os.path.exists(path): # noqa: PTH110 468 continue 469 470 # Load and process data 471 with h5py.File(path, "r") as file: 472 loss_qcd = file["loss_qcd"][:] 473 loss_sig = file["loss_sig"][:] 474 475 # Square the losses (as in original code) 476 loss_qcd = np.power(loss_qcd, 2) 477 loss_sig = np.power(loss_sig, 2) 478 479 loss_qcd_runs.append(loss_qcd) 480 loss_sig_runs.append(loss_sig) 481 482 # Skip if no valid runs found 483 if not loss_qcd_runs or not loss_sig_runs: 484 continue 485 486 # Compute mean and error 487 loss_qcd = get_mean_and_error(np.array(loss_qcd_runs)) 488 loss_sig = get_mean_and_error(np.array(loss_sig_runs)) 489 490 # Skip if NaNs detected 491 if np.isnan(loss_qcd[0]).sum() > 0: 492 print( 493 f"{Colors.RED.value}NaNs detected for {embedding}, bond={bond}{Colors.RESET.value}" 494 ) 495 continue 496 497 # Plot QCD distribution 498 ax.hist( 499 loss_qcd[0], 500 bins=100, 501 fill=False, 502 color=bond_colors[bond], 503 histtype="step", 504 linewidth=2, 505 alpha=1.0, 506 density=True, 507 ) 508 509 # Plot BSM distribution 510 ax.hist( 511 loss_sig[0], 512 bins=100, 513 fill=True, 514 color=bond_colors[bond], 515 histtype="step", 516 linewidth=2, 517 alpha=0.4, 518 linestyle="--", 519 density=True, 520 ) 521 522 # Save handles for bond dimension legend (only once per bond dimension) 523 if e == 0: 524 bond_handles.append( 525 plt.Line2D( 526 [0], 527 [0], 528 color=bond_colors[bond], 529 linewidth=2, 530 label=f"χ = {bond}", 531 ) 532 ) 533 534 # Style the subplot 535 if embedding == "legendre_2": 536 embedding_label = r"Legendre" 537 elif embedding == "fourier_2": 538 embedding_label = r"Fourier" 539 elif embedding == "hermite_2": 540 embedding_label = r"Hermite" 541 elif embedding == "laguerre_2_old_2": 542 embedding_label = r"Laguerre" 543 else: 544 embedding_label = embedding 545 546 ax.set_title(f"{embedding_label}", fontsize=25) 547 ax.set_xlabel(r"Anomaly Score", fontsize=20) 548 ax.set_yscale("log") 549 ax.set_xlim(-0.01, 0.6) 550 551 # Only add y-label to first subplot 552 if e == 0: 553 ax.set_ylabel("Probability Density", fontsize=20) 554 555 # Create style legend handles that accurately represent the visualization 556 style_handles = [ 557 mpatches.Patch( 558 edgecolor="black", facecolor="none", linewidth=2, label="QCD (Background)" 559 ), 560 mpatches.Patch(facecolor="gray", alpha=0.4, label="BSM (Signal)"), 561 ] 562 # set tick size 563 for ax in axs: 564 ax.tick_params(axis="both", which="major", labelsize=15) 565 ax.tick_params(axis="both", which="minor", labelsize=15) 566 567 # Add bond dimension legend at the figure level 568 fig.legend( 569 handles=bond_handles, 570 loc="upper left", 571 bbox_to_anchor=(0.05, 0.87), 572 frameon=True, 573 fontsize=10, 574 title="Bond Dimension", 575 ) 576 577 # Add the QCD/BSM legend to the figure 578 fig.legend( 579 handles=style_handles, 580 loc="upper right", 581 bbox_to_anchor=(0.35, 0.87), 582 frameon=True, 583 fontsize=10, 584 title="Distribution Type", 585 ) 586 587 plt.tight_layout( 588 rect=(0, 0.05, 1, 0.95) 589 ) # Adjust layout to make room for the legends 590 591 # Create save directory if it doesn't exist 592 os.makedirs(f"{save_dir}/plots/comparisons/", exist_ok=True) # noqa: PTH103 593 594 # Save figure 595 plt.savefig( 596 f"{save_dir}/plots/comparisons/embedding_comparison_{initializer}_lat{latent}_{signal_name}.pdf", 597 bbox_inches="tight", 598 ) 599 600 return fig, axs 601 602 603def compare_ROCs_per_bond( # noqa: N802 604 latent: int, 605 bond_dim: Collection[int], 606 initializer: str, 607 initializer_string: str, # noqa: ARG001 608 tpr_per_init: dict, 609 tpr_per_init_err: dict, # noqa: ARG001 610 fpr_per_init: dict, 611 fpr_per_init_err: dict, 612 auc_per_init: dict, 613 auc_per_init_err: dict, 614 save_dir: str, 615 embedding: str, 616 train_scaling: str, 617 signal_name: str, 618): 619 """ 620 Create ROC curve plot for a single initializer with consistent coloring for bond dimensions. 621 622 Parameters 623 ---------- 624 latent : int 625 Latent space dimension 626 bond_dim : Collection[int] 627 List of bond dimensions to compare 628 initializer : str 629 Initializer of the model 630 initializer_string : str 631 String representation of the initializer 632 tpr_per_init : dict 633 Dictionary containing true positive rates for each bond dimension 634 tpr_per_init_err : dict 635 Dictionary containing statistical errors for true positive rates - from multiple runs 636 fpr_per_init : dict 637 Dictionary containing false positive rates for each bond dimension 638 fpr_per_init_err : dict 639 Dictionary containing statistical errors for false positive rates - from multiple runs 640 auc_per_init : dict 641 Dictionary containing area under the curve values for each bond dimension 642 auc_per_init_err : dict 643 Dictionary containing statistical errors for area under the curve values - from multiple runs 644 save_dir : str 645 Directory to save the plot 646 embedding : str 647 Embedding name for the model 648 train_scaling : str 649 Training size and scaling to consider 650 signal_name : str 651 Name of the signal to be used in the plot 652 653 Returns 654 ------- 655 fig : matplotlib.figure.Figure 656 The figure object containing the plot 657 ax : matplotlib.axes.Axes 658 The axes object for the plot 659 """ 660 # Set up color maps for different bond dimensions - using brighter colors 661 color_maps = [ 662 "Blues", 663 "Reds", 664 "Greens", 665 "Purples", 666 "Oranges", 667 "YlOrBr", 668 "GnBu", 669 "PuRd", 670 ] 671 672 # Create a color dictionary with brighter colors for consistent coloring 673 bond_colors = { 674 bond: plt.get_cmap(color_maps[i % len(color_maps)])(0.8) 675 for i, bond in enumerate(bond_dim) 676 } 677 678 # Create a single figure 679 fig, ax = plt.subplots(figsize=(7, 7)) 680 681 for _j, bond in enumerate(bond_dim): 682 key = f"init={initializer},bond={bond},lat={latent},s={signal_name}" 683 684 if key not in tpr_per_init: 685 print(f"{Colors.RED.value}{key} not found{Colors.RESET.value}") 686 continue 687 688 tpr = tpr_per_init[key] 689 # tpr_err = tpr_per_init_err[key] # noqa: ERA001 690 fpr = fpr_per_init[key] 691 fpr_err = fpr_per_init_err[key] 692 auc_value = auc_per_init[key] 693 auc_err = auc_per_init_err[key] 694 695 if signal_name == "RSGraviton_WW_NA_35": # uncertainties are bigger for G_NA 696 band_ind = np.where(tpr > 0.6)[0] 697 else: 698 band_ind = np.where(tpr > 0.35)[0] 699 700 # Use consistent color based on bond dimension 701 ax.plot( 702 tpr, 703 fpr, 704 label=rf"$\chi$ = {bond} ({auc_value * 100.0:.2f})$\pm$({auc_err * 100.0:.2f})", 705 linewidth=2, 706 color=bond_colors[bond], 707 ) # Use consistent color 708 709 # Error calculation in log space 710 log_fpr = np.log10(fpr) 711 rel_err = fpr_err / fpr # Relative error 712 log_err = (0.434) * rel_err # Convert to log10 error (0.434 = 1/ln(10)) 713 714 # Calculate bounds in log space, then convert back 715 log_upper = log_fpr - log_err 716 log_lower = log_fpr + log_err 717 fpr_upper = 10**log_upper # convert back to linear scale 718 fpr_lower = 10**log_lower # convert back to linear scale 719 720 # Add error bands with matching color 721 ax.fill_between( 722 tpr[band_ind], 723 fpr_lower[band_ind], 724 fpr_upper[band_ind], 725 alpha=0.2, 726 color=bond_colors[bond], 727 ) # Match fill color 728 729 # Add vertical dotted lines at TPR = 0.6 and TPR = 0.8 730 ax.axvline(x=0.6, color="black", linestyle=":", linewidth=1.5, alpha=0.3) 731 ax.axvline(x=0.8, color="black", linestyle=":", linewidth=1.5, alpha=0.3) 732 733 # Style the plot 734 # ax.set_title(f'Latent = {latent}, {initializer_string}', fontsize=16) # noqa: ERA001 735 ax.legend(loc="lower left", fontsize=12) 736 ax.set_yscale("log") 737 ax.set_xlabel("TPR", fontsize=20) 738 ax.set_ylabel("FPR$^{-1}$", fontsize=20) 739 ax.set_xticks(np.arange(0, 1.1, 0.2)) 740 ax.tick_params(axis="both", which="major", labelsize=14) 741 ax.tick_params(axis="both", which="minor", labelsize=14) 742 ax.grid(True, alpha=0.3) 743 744 plt.tight_layout() 745 746 # Create directory if it doesn't exist 747 os.makedirs( # noqa: PTH103 748 f"{save_dir}/plots/{train_scaling}/{embedding}/lat{latent}", exist_ok=True 749 ) 750 751 # Save figure 752 plt.savefig( 753 f"{save_dir}/plots/{train_scaling}/{embedding}/lat{latent}/roc_curve_{signal_name}.pdf" 754 ) 755 756 return fig, ax 757 758 759def compare_ROC_by_signal( # noqa: N802 760 signal_names: Collection[str], 761 signal_labels: Collection[str], 762 latent: int, 763 bond_dim: Collection[int], 764 initializer: str, 765 initializer_string: str, # noqa: ARG001 766 tpr_per_init: dict, 767 tpr_per_init_err: dict, # noqa: ARG001 768 fpr_per_init: dict, 769 fpr_per_init_err: dict, 770 auc_per_init: dict, 771 auc_per_init_err: dict, 772 save_dir: str, 773 embedding: str, 774 train_scaling: str, 775): 776 r""" 777 Compare ROC curves for different signal types with fixed model parameters. 778 779 Parameters 780 ---------- 781 signal_names : Collection[str] 782 List of signal names to compare 783 signal_labels : Collection[str] 784 List of signal labels for the legend 785 latent : int 786 Latent space dimension 787 bond_dim : Collection[int] 788 List of bond dimensions to compare 789 initializer : str 790 Initializer of the model 791 initializer_string : str 792 String representation of the initializer 793 tpr_per_init : dict 794 Dictionary containing true positive rates for each signal 795 tpr_per_init_err : dict 796 Dictionary containing statistical errors for true positive rates - from multiple runs 797 fpr_per_init : dict 798 Dictionary containing false positive rates for each signal 799 fpr_per_init_err : dict 800 Dictionary containing statistical errors for false positive rates - from multiple runs 801 auc_per_init : dict 802 Dictionary containing area under the curve values for each signal 803 auc_per_init_err : dict 804 Dictionary containing statistical errors for area under the curve values - from multiple runs 805 save_dir : str 806 Directory to save the plot 807 embedding : str 808 Embedding name for the model 809 train_scaling : str 810 Training size and scaling to consider 811 812 Returns 813 ------- 814 fig : matplotlib.figure.Figure 815 The figure object containing the plot 816 ax : matplotlib.axes.Axes 817 The axes object for the plot 818 819 Example 820 ------- 821 >>> compare_ROC_by_signal( 822 ... signal_names = ['RSGraviton_WW_NA_35', 'AtoHZ_to_ZZZ_35', 'RSGraviton_WW_BR_15'], 823 ... signal_labels = [r'Narrow $G \rightarrow WW$', r'$A \rightarrow HZ \rightarrow ZZZ$', r'Broad $G \rightarrow WW$'], 824 ... latent = 4, 825 ... bond_dim = [2, 4, 8, 16], 826 ... initializer = 'unitary', 827 ... initializer_string = 'Unitary', 828 ... tpr_per_init = tpr_per_init, 829 ... tpr_per_init_err = tpr_per_init_err, 830 ... fpr_per_init = fpr_per_init, 831 ... fpr_per_init_err = fpr_per_init_err, 832 ... auc_per_init = auc_per_init, 833 ... auc_per_init_err = auc_per_init_err, 834 ... save_dir = './results', 835 ... embedding = 'laguerre_2', 836 ... train_scaling = '10k_minmax-11' 837 ... ) 838 839 """ 840 palette = ["#4CA64C", "#FF5733", "#8A2BE2"] 841 842 fig, ax = plt.subplots(figsize=(7, 7)) 843 844 # Store handles and data for legend 845 handles = [] 846 auc_info = [] 847 signal_info = [] 848 849 for i, signal_name in enumerate(signal_names): 850 key = f"init={initializer},bond={bond_dim},lat={latent},s={signal_name}" 851 852 if key not in tpr_per_init: 853 print(f"{Colors.RED.value}{key} not found{Colors.RESET.value}") 854 continue 855 856 tpr = tpr_per_init[key] 857 fpr = fpr_per_init[key] 858 fpr_err = fpr_per_init_err[key] 859 auc_value = auc_per_init[key] 860 auc_err = auc_per_init_err[key] 861 862 # Determine where to show error bands 863 band_ind = np.where(tpr > 0.35)[0] 864 if "RSGraviton_WW_NA" in signal_name: 865 band_ind = np.where(tpr > 0.6)[0] 866 867 # Plot the ROC curve 868 (line,) = ax.plot(tpr, fpr, linewidth=2, color=palette[i]) 869 870 # Store data for legend 871 handles.append(line) 872 auc_info.append(f"{auc_value * 100:.2f}±{auc_err * 100:.2f}") 873 signal_info.append(list(signal_labels)[i]) 874 875 # Error calculation and bands 876 log_fpr = np.log10(fpr) 877 rel_err = fpr_err / fpr 878 log_err = 0.434 * rel_err 879 880 log_upper = log_fpr - log_err 881 log_lower = log_fpr + log_err 882 fpr_upper = 10**log_upper 883 fpr_lower = 10**log_lower 884 885 ax.fill_between( 886 tpr[band_ind], 887 fpr_lower[band_ind], 888 fpr_upper[band_ind], 889 alpha=0.2, 890 color=palette[i], 891 ) 892 893 # Create legend with AUC values first 894 legend_labels = [] 895 for auc_label, signal in zip(auc_info, signal_info, strict=False): 896 legend_labels.append(f"{auc_label} {signal}") 897 898 # Add legend with AUC first 899 legend = ax.legend( 900 handles, legend_labels, loc="lower left", frameon=True, fontsize=12 901 ) 902 903 # Add the column headers with AUC first 904 legend.set_title(" AUC BSM Scenario", prop=FontProperties(size=12)) 905 906 # Add vertical dotted lines at TPR = 0.6 and TPR = 0.8 907 ax.axvline(x=0.6, color="black", linestyle=":", linewidth=1.5, alpha=0.3) 908 ax.axvline(x=0.8, color="black", linestyle=":", linewidth=1.5, alpha=0.3) 909 910 # Style the plot 911 ax.set_yscale("log") 912 ax.set_xlabel("TPR", fontsize=20) 913 ax.set_ylabel("FPR$^{-1}$", fontsize=20) 914 ax.set_xticks(np.arange(0, 1.1, 0.2)) 915 ax.tick_params(axis="both", which="major", labelsize=14) 916 ax.tick_params(axis="both", which="minor", labelsize=14) 917 918 # ax.set_yticks(fontsize=14) # noqa: ERA001 919 ax.grid(True, alpha=0.3) 920 921 plt.tight_layout() 922 923 # Create directory and save 924 os.makedirs( # noqa: PTH103 925 f"{save_dir}/plots/{train_scaling}/{embedding}/lat{latent}", exist_ok=True 926 ) 927 plt.savefig( 928 f"{save_dir}/plots/{train_scaling}/{embedding}/lat{latent}/roc_curve_compare_signals.pdf" 929 ) 930 931 return fig, ax 932 933 934def compare_ROC_by_latent( # noqa: N802 935 latent_spaces: Collection[int], 936 bond_dims: dict, 937 initializer: str, 938 initializer_string: str, # noqa: ARG001 939 signal_name: str, 940 signal_label: str, # noqa: ARG001 941 tpr_per_init: dict, 942 tpr_per_init_err: dict, # noqa: ARG001 943 fpr_per_init: dict, 944 fpr_per_init_err: dict, 945 auc_per_init: dict, 946 auc_per_init_err: dict, 947 save_dir: str, 948 embedding: str, 949 train_scaling: str, 950): 951 """ 952 Compare ROC curves for one signal type across different latent spaces, each with a specific bond dimension. 953 954 Parameters 955 ---------- 956 latent_spaces : Collection[int] 957 List of latent space dimensions to compare 958 bond_dims : dict 959 Dictionary mapping latent space dimensions to bond dimensions 960 initializer : str 961 Initializer of the model 962 initializer_string : str 963 String representation of the initializer 964 signal_name : str 965 Name of the signal anomaly scores are calculated for 966 signal_label : str 967 Label for the signal to be used in the plot 968 tpr_per_init : dict 969 Dictionary containing true positive rates for each latent space 970 tpr_per_init_err : dict 971 Dictionary containing statistical errors for true positive rates - from multiple runs 972 fpr_per_init : dict 973 Dictionary containing false positive rates for each latent space 974 fpr_per_init_err : dict 975 Dictionary containing statistical errors for false positive rates - from multiple runs 976 auc_per_init : dict 977 Dictionary containing area under the curve values for each latent space 978 auc_per_init_err : dict 979 Dictionary containing statistical errors for area under the curve values - from multiple runs 980 save_dir : str 981 Directory to save the plot 982 embedding : str 983 Embedding name for the model 984 train_scaling : str 985 Training size and scaling used in training 986 987 """ 988 # Colors for different latent spaces 989 palette = [ 990 "#E69F00", # Muted orange 991 "#CC6677", # Muted red 992 "#88CCEE", # Muted blue 993 "#000000", # Black 994 "#44AA99", # Muted teal 995 "#AA4499", 996 ] # Muted purple 997 998 if len(latent_spaces) > len(palette): 999 # Generate more colors if needed 1000 cmap = plt.get_cmap("tab10") 1001 palette = [cmap(i) for i in np.linspace(0, 1, len(latent_spaces))] 1002 1003 fig, ax = plt.subplots(figsize=(7, 7)) 1004 1005 # Store handles and data for legend 1006 handles = [] 1007 auc_info = [] 1008 config_info = [] 1009 1010 for i, latent in enumerate(latent_spaces): 1011 # Get corresponding bond dimension for this latent space 1012 bond_dim = bond_dims[str(latent)] 1013 1014 key = f"init={initializer},bond={bond_dim},lat={latent},s={signal_name}" 1015 1016 if key not in tpr_per_init: 1017 print(f"{key} not found") 1018 continue 1019 1020 tpr = tpr_per_init[key] 1021 fpr = fpr_per_init[key] 1022 fpr_err = fpr_per_init_err[key] 1023 auc_value = auc_per_init[key] 1024 auc_err = auc_per_init_err[key] 1025 1026 if auc_value < 0.5: 1027 auc_value = 1 - auc_value 1028 1029 # Determine where to show error bands 1030 band_ind = np.where(tpr > 0.35)[0] 1031 if "RSGraviton_WW_NA" in signal_name: 1032 band_ind = np.where(tpr > 0.6)[0] 1033 1034 # Plot the ROC curve 1035 (line,) = ax.plot(tpr, fpr, linewidth=2, color=palette[i % len(palette)]) 1036 1037 # Store data for legend 1038 handles.append(line) 1039 auc_info.append(f"{auc_value * 100:.2f}±{auc_err * 100:.2f}") 1040 config_info.append(f"lat = {latent}, χ = {bond_dim}") 1041 1042 # Error calculation and bands 1043 log_fpr = np.log10(fpr) 1044 rel_err = fpr_err / fpr 1045 log_err = 0.434 * rel_err 1046 1047 log_upper = log_fpr - log_err 1048 log_lower = log_fpr + log_err 1049 fpr_upper = 10**log_upper 1050 fpr_lower = 10**log_lower 1051 1052 ax.fill_between( 1053 tpr[band_ind], 1054 fpr_lower[band_ind], 1055 fpr_upper[band_ind], 1056 alpha=0.2, 1057 color=palette[i % len(palette)], 1058 ) 1059 1060 # Create legend with AUC values first 1061 legend_labels = [] 1062 for auc_label, config in zip(auc_info, config_info, strict=False): 1063 legend_labels.append(f"{auc_label} {config}") 1064 1065 # Add legend with AUC first 1066 legend = ax.legend( 1067 handles, legend_labels, loc="lower left", frameon=True, fontsize=12 1068 ) 1069 1070 # Add the column headers with AUC first 1071 legend.set_title( 1072 " AUC Configuration", prop=FontProperties(size=12) 1073 ) 1074 1075 # Add vertical dotted lines at TPR = 0.6 and TPR = 0.8 1076 ax.axvline(x=0.6, color="black", linestyle=":", linewidth=1.5, alpha=0.3) 1077 ax.axvline(x=0.8, color="black", linestyle=":", linewidth=1.5, alpha=0.3) 1078 1079 # Style the plot 1080 ax.set_yscale("log") 1081 ax.set_xlabel("TPR", fontsize=20) 1082 ax.set_ylabel("FPR$^{-1}$", fontsize=20) 1083 # ax.set_title(f'Signal: {signal_label}, {initializer_string}', fontsize=16) # noqa: ERA001 1084 ax.set_xticks(np.arange(0, 1.1, 0.2)) 1085 ax.tick_params(axis="both", which="major", labelsize=14) 1086 ax.tick_params(axis="both", which="minor", labelsize=14) 1087 ax.grid(True, alpha=0.3) 1088 1089 plt.tight_layout() 1090 1091 # Create directory and save 1092 os.makedirs(f"{save_dir}/plots/{train_scaling}/{embedding}", exist_ok=True) # noqa: PTH103 1093 plt.savefig( 1094 f"{save_dir}/plots/{train_scaling}/{embedding}/roc_curve_latent_comparison_{signal_name}.pdf" 1095 ) 1096 1097 return fig, ax
utils.py: * View in browser:
utils.py* View on GitHubutils.py#1import os 2import tarfile 3from enum import Enum 4from pathlib import Path 5 6import h5py 7import jax 8import joblib 9import numpy as np 10import wget 11from sklearn.preprocessing import MinMaxScaler, StandardScaler 12 13from tn4ml.embeddings import Embedding, TrigonometricEmbedding, embed 14 15 16class Colors(Enum): 17 """ANSI color codes for terminal text styling.""" 18 19 RESET = "\033[0m" 20 GREEN = "\033[32m" 21 BLUE = "\033[34m" 22 ORANGE = "\033[38;2;255;165;0m" 23 PINK = "\033[38;2;255;105;180m" 24 RED = "\033[31m" 25 YELLOW = "\033[33m" 26 MAGENTA = "\033[35m" 27 CYAN = "\033[36m" 28 BOLD = "\033[1m" 29 UNDERLINE = "\033[4m" 30 31 32def _download_data(data_url: str, data_dir: str | Path = "."): 33 """ 34 Downloads the jet data if it does not already exist. 35 36 Parameters 37 ---------- 38 data_url : str 39 URL to the data file 40 data_dir : str, optional 41 Directory to save the data, default '.' 42 43 Returns 44 ------- 45 None 46 """ # noqa: D401 47 data_path = Path(data_dir) 48 if not data_path.is_dir(): 49 os.makedirs(data_path, exist_ok=True) # noqa: PTH103 50 51 data_file_path = wget.download(data_url, out=str(data_path)) 52 53 data_tar = tarfile.open(data_file_path, "r:gz") # noqa: SIM115 54 data_tar.extractall(str(data_path)) 55 data_tar.close() 56 os.remove(data_file_path) # noqa: PTH107 57 58 59def _ensure_data_exists(data_dir: str = "data", latent: int | None = None) -> None: 60 """ 61 Check if the data directory exists and download all data if it doesn't. 62 Only checks directory existence, not individual files. 63 64 Parameters 65 ---------- 66 data_dir : str, optional 67 Base directory where data should be stored, default "data" 68 latent : int, optional 69 Latent space dimension used for subdirectory, default None 70 71 Returns 72 ------- 73 dict 74 Dictionary with paths to all data files 75 """ # noqa: D205 76 # Create data directory path with latent dimension subfolder 77 base_dir = Path(data_dir) 78 data_dir_path = base_dir / f"latent{latent}" if latent is not None else base_dir 79 80 # ONLY check if the directory exists, not individual files 81 if not data_dir_path.is_dir() or not any(data_dir_path.iterdir()): 82 print( 83 f"{Colors.BLUE.value}Data directory {data_dir_path} does not exist. Downloading complete dataset...{Colors.RESET.value}" 84 + "\n" 85 ) 86 os.makedirs(data_dir_path, exist_ok=True) # noqa: PTH103 87 88 archive_url = "https://zenodo.org/records/7673769/files/QML_paper_data.tar.gz" 89 try: 90 _download_data(archive_url, base_dir) # Download to base dir 91 print( 92 f"{Colors.BLUE.value}Archive downloaded and extracted successfully.{Colors.RESET.value}" 93 + "\n" 94 ) 95 except Exception as e: # noqa: BLE001 96 print( 97 f"{Colors.RED.value}Failed to download archive: {e}{Colors.RESET.value}" 98 + "\n" 99 ) 100 else: 101 print( 102 f"{Colors.YELLOW.value}Data directory {data_dir_path} already exists. Assuming all data is present.{Colors.RESET.value}" 103 + "\n" 104 ) 105 106 return 107 108 109def load_train_data( 110 read_file: str, 111 train_size: int = 10000, 112 apply_minmax: bool = False, 113 apply_standardization: bool = False, 114 feature_range: tuple = (0, 1), 115 shuffle_seed: int = 42, 116 save_dir: str = ".", 117 prefix: str = "train_qcd", 118): 119 """ 120 Load and preprocess training data from a given file. 121 122 Parameters 123 ---------- 124 read_file : str 125 Path to the file containing the training data 126 train_size : int, optional 127 Number of training samples to load, default 10000 128 apply_minmax : bool, optional 129 Whether to apply Min-Max scaling, default False 130 apply_standardization : bool, optional 131 Whether to apply standardization, default False 132 feature_range : tuple, optional 133 The desired range for Min-Max scaling, default (0, 1) 134 shuffle_seed : int, optional 135 Seed for random shuffling, default 42 136 save_dir : str, optional 137 Directory to save scalers, default '.' 138 prefix : str, optional 139 Prefix for scaler filenames, default 'train_qcd' 140 141 Returns 142 ------- 143 np.ndarray 144 Preprocessed training data 145 dict 146 Dictionary of fitted scalers 147 """ 148 # Ensure the save directory exists 149 os.makedirs(save_dir, exist_ok=True) # noqa: PTH103 150 151 # Read and prepare data 152 with h5py.File(read_file, "r") as file: 153 data = file["latent_space"] 154 data = np.concatenate([data[:, 0, :], data[:, 1, :]], axis=-1) 155 print( 156 f"{Colors.BLUE.value}Input data shape: {data.shape}{Colors.RESET.value}" 157 + "\n" 158 ) 159 160 # Shuffle data 161 np.random.seed(shuffle_seed) # noqa: NPY002 162 np.random.shuffle(data) # noqa: NPY002 163 164 data_train = data[:train_size] 165 scalers = {} 166 167 # Apply transformations 168 if apply_standardization: 169 scaler = StandardScaler() 170 data_train = scaler.fit_transform(data_train) 171 scalers["standard"] = scaler 172 scaler_path = os.path.join(save_dir, f"scaler_standard_{prefix}.pkl") # noqa: PTH118 173 joblib.dump(scaler, scaler_path) 174 175 if apply_minmax: 176 min_max_scaler = MinMaxScaler(feature_range=feature_range) 177 data_train = min_max_scaler.fit_transform(data_train) 178 scalers["minmax"] = min_max_scaler 179 scaler_path = os.path.join(save_dir, f"scaler_minmax_{prefix}.pkl") # noqa: PTH118 180 joblib.dump(min_max_scaler, scaler_path) 181 182 return data_train, scalers 183 184 185def load_test_data( 186 read_path: str, 187 dataset_type: str = "qcd", 188 scaler: StandardScaler = None, 189 min_max_scaler: MinMaxScaler = None, 190 test_size: int = 10000, 191 shuffle_seed: int = 42, 192): 193 """ 194 Load and preprocess test data from a given file. 195 196 Parameters 197 ---------- 198 read_path : str 199 Path to the file/directory containing test data 200 dataset_type : str, optional 201 Type of dataset ('qcd' or 'signal'), default 'qcd' 202 scaler : StandardScaler, optional 203 Fitted StandardScaler to apply, default None 204 min_max_scaler : MinMaxScaler, optional 205 Fitted MinMaxScaler to apply, default None 206 test_size : int, optional 207 Number of test samples to use, default 10000 208 shuffle_seed : int, optional 209 Seed for random shuffling, default 42 210 211 Returns 212 ------- 213 np.ndarray 214 Preprocessed test data 215 """ 216 # Determine file path based on dataset type 217 if dataset_type == "qcd": 218 file_path = os.path.join(read_path, "latentrep_QCD_sig_testclustering.h5") # noqa: PTH118 219 else: 220 file_path = read_path # For signal, use the path directly 221 222 # Load and prepare data 223 with h5py.File(file_path, "r") as file: 224 data = file["latent_space"] 225 # Concatenate the two latent space components 226 data_test = np.concatenate([data[:, 0, :], data[:, 1, :]], axis=-1) 227 228 # Shuffle data 229 np.random.seed(shuffle_seed) # noqa: NPY002 230 np.random.shuffle(data_test) # noqa: NPY002 231 data_test = data_test[:test_size] 232 233 print( 234 f"{Colors.BLUE.value}Input test {dataset_type} shape: {data_test.shape}{Colors.RESET.value}" 235 + "\n" 236 ) 237 238 # Apply transformations if provided 239 if scaler is not None: 240 data_test = scaler.transform(data_test) 241 if min_max_scaler is not None: 242 data_test = min_max_scaler.transform(data_test) 243 244 return data_test 245 246 247def calc_fidelity_batch( 248 points, 249 model, 250 embedding: Embedding = TrigonometricEmbedding(), # noqa: B008 251 batch_size: int = 1000, 252): 253 """ 254 Calculate fidelity scores for data points in batches using vectorized operations. 255 256 Parameters 257 ---------- 258 points : np.ndarray 259 Input data points 260 model : TensorNetwork 261 Trained tensor network model 262 embedding : Embedding, optional 263 Embedding function, default TrigonometricEmbedding() 264 batch_size : int, optional 265 Batch size for processing, default 1000 266 267 Returns 268 ------- 269 np.ndarray 270 Array of fidelity scores 271 """ 272 273 # Define a function that processes a single point 274 def single_point_fidelity(point): 275 input_mps = embed(point, embedding) 276 p_mps = input_mps.H & model 277 return abs(p_mps ^ all) 278 279 # Vectorize the function for batch processing 280 batch_fidelity = jax.vmap(single_point_fidelity) 281 282 # Process in batches to avoid memory issues 283 n_samples = len(points) 284 n_batches = (n_samples + batch_size - 1) // batch_size 285 results = [] 286 287 for i in range(n_batches): 288 start_idx = i * batch_size 289 end_idx = min(start_idx + batch_size, n_samples) 290 batch = points[start_idx:end_idx] 291 batch_results = batch_fidelity(batch) 292 results.append(batch_results) 293 294 return np.concatenate(results, axis=0)