diff --git a/tests/test_001_pec_cubic_cavity.py b/tests/test_001_pec_cubic_cavity.py index e015de7..aacf49e 100644 --- a/tests/test_001_pec_cubic_cavity.py +++ b/tests/test_001_pec_cubic_cavity.py @@ -99,7 +99,7 @@ def test_simulation(self): skip_cells = 12 # no. cells to skip in WP integration wake = WakeSolver(q=q, sigmaz=sigmaz, beta=beta, xsource=xs, ysource=ys, xtest=xt, ytest=yt, - save=False, logfile=False, Ez_file='tests/001_Ez.h5', + save=False, Ez_file='tests/001_Ez.h5', skip_cells=skip_cells, ) diff --git a/tests/test_007_mpi_lossy_cavity.py b/tests/test_007_mpi_lossy_cavity.py index 0650d68..968cba2 100644 --- a/tests/test_007_mpi_lossy_cavity.py +++ b/tests/test_007_mpi_lossy_cavity.py @@ -82,6 +82,18 @@ class TestMPILossyCavity: -6.04105997e+01 ,-3.06532160e+01 ,-1.17749936e+01 ,-3.12574866e+00, -7.35339521e-01 ,-1.13085658e-01 , 7.18247535e-01 , 8.73829036e-02]) + gridLogs = {'use_mesh_refinement': False, 'Nx': 60, 'Ny': 60, 'Nz': 140, 'dx': 0.008666666348775227, 'dy': 0.008666666348775227, + 'dz': 0.005714285799435207, 'stl_solids': {'cavity': 'tests/stl/007_vacuum_cavity.stl', 'shell': 'tests/stl/007_lossymetal_shell.stl'}, + 'stl_materials': {'cavity': 'vacuum', 'shell': [30, 1.0, 30]}, 'gridInitializationTime': 0} + + solverLogs = {'use_gpu': False, 'use_mpi': False, 'background': 'pec','bc_low': ['pec', 'pec', 'pec'], + 'bc_high': ['pec', 'pec', 'pec'], + 'dt': 6.970326728398968e-12, 'solverInitializationTime': 0} + + wakeSolverLogs = {'ti': 2.8516132094735135e-09, 'q': 1e-09, 'sigmaz': 0.1, 'beta': 1.0, + 'xsource': 0.0, 'ysource': 0.0, 'xtest': 0.0, 'ytest': 0.0, 'chargedist': None, + 'skip_cells': 10, 'results_folder': 'tests/007_results/', 'wakelength': 10.0, 'simulationTime': 0} + img_folder = 'tests/007_img/' def test_mpi_import(self): @@ -314,4 +326,14 @@ def test_long_impedance(self): assert np.allclose(np.real(wake.Z)[::20], np.real(self.Z), rtol=0.1), "Real Impedance samples failed" assert np.allclose(np.imag(wake.Z)[::20], np.imag(self.Z), rtol=0.1), "Imag Impedance samples failed" assert np.cumsum(np.abs(wake.Z))[-1] == pytest.approx(250910.51090497518, 0.1), "Abs Impedance cumsum failed" - \ No newline at end of file + + def test_log_file(self): + global solver + solver.logger.grid["gridInitializationTime"] = 0 #times can vary + solver.logger.solver["solverInitializationTime"] = 0 + solver.logger.wakeSolver["simulationTime"] = 0 + logfile = os.path.join(solver.logger.wakeSolver["results_folder"], "wakis.log") + assert os.path.exists(logfile), "Log file not created" + assert solver.logger.grid == self.gridLogs, "Grid logs do not match expected values" + assert solver.logger.solver == self.solverLogs, "Solver logs do not match expected values" + assert solver.logger.wakeSolver == self.wakeSolverLogs, "WakeSolver logs do not match expected values" diff --git a/wakis/__init__.py b/wakis/__init__.py index 7daec5e..57ba37f 100644 --- a/wakis/__init__.py +++ b/wakis/__init__.py @@ -10,6 +10,7 @@ from . import materials from . import wakeSolver from . import geometry +from . import logger from . import field_monitors from .field_monitors import FieldMonitor @@ -17,5 +18,6 @@ from .gridFIT3D import GridFIT3D from .solverFIT3D import SolverFIT3D from .wakeSolver import WakeSolver +from .logger import Logger from ._version import __version__ \ No newline at end of file diff --git a/wakis/gridFIT3D.py b/wakis/gridFIT3D.py index d6e447b..06ac90f 100644 --- a/wakis/gridFIT3D.py +++ b/wakis/gridFIT3D.py @@ -7,8 +7,10 @@ import pyvista as pv from functools import partial from scipy.optimize import least_squares +import time from .field import Field +from .logger import Logger try: from mpi4py import MPI @@ -63,10 +65,13 @@ def __init__(self, xmin, xmax, ymin, ymax, zmin, zmax, stl_rotate=[0., 0., 0.], stl_translate=[0., 0., 0.], stl_scale=1.0, stl_colors=None, verbose=1, stl_tol=1e-3): + t0 = time.time() + self.logger = Logger() if verbose: print('Generating grid...') self.verbose = verbose self.use_mpi = use_mpi self.use_mesh_refinement = use_mesh_refinement + self.update_logger(['use_mesh_refinement']) # domain limits self.xmin = xmin @@ -81,6 +86,7 @@ def __init__(self, xmin, xmax, ymin, ymax, zmin, zmax, self.dx = (xmax - xmin) / Nx self.dy = (ymax - ymin) / Ny self.dz = (zmax - zmin) / Nz + self.update_logger(['Nx', 'Ny', 'Nz', 'dx', 'dy', 'dz']) # stl info self.stl_solids = stl_solids @@ -89,6 +95,14 @@ def __init__(self, xmin, xmax, ymin, ymax, zmin, zmax, self.stl_translate = stl_translate self.stl_scale = stl_scale self.stl_colors = stl_colors + self.update_logger(['stl_solids', 'stl_materials']) + if stl_rotate != [0., 0., 0.]: + self.update_logger(['stl_rotate']) + if stl_translate != [0., 0., 0.]: + self.update_logger(['stl_translate']) + if stl_scale != 1.0: + self.update_logger(['stl_scale']) + if stl_solids is not None: self._prepare_stl_dicts() @@ -143,6 +157,9 @@ def __init__(self, xmin, xmax, ymin, ymax, zmin, zmax, if stl_colors is None: self.assign_colors() + self.gridInitializationTime = time.time()-t0 + self.update_logger(['gridInitializationTime']) + def compute_grid(self): X, Y, Z = np.meshgrid(self.x, self.y, self.z, indexing='ij') self.grid = pv.StructuredGrid(X.transpose(), Y.transpose(), Z.transpose()) @@ -838,4 +855,11 @@ def clip(widget): if offscreen: pl.export_html('grid_inspect.html') else: - pl.show() \ No newline at end of file + pl.show() + + def update_logger(self, attrs): + """ + Assigns the parameters handed via attrs to the logger + """ + for atr in attrs: + self.logger.grid[atr] = getattr(self, atr) \ No newline at end of file diff --git a/wakis/logger.py b/wakis/logger.py new file mode 100644 index 0000000..c51b5fe --- /dev/null +++ b/wakis/logger.py @@ -0,0 +1,67 @@ +# copyright ################################# # +# This file is part of the wakis Package. # +# Copyright (c) CERN, 2025. # +# ########################################### # + +from tqdm import tqdm + +import numpy as np +import time +import h5py +import os +import json + +from scipy.constants import c as c_light, epsilon_0 as eps_0, mu_0 as mu_0 +from scipy.sparse import csc_matrix as sparse_mat +from scipy.sparse import diags, hstack, vstack + + + +class Logger(): + + def __init__(self): + self.grid = {} + self.solver = {} + self.wakeSolver = {} + + def save_logs(self): + """ + Save all logs (grid, solver, wakeSolver) into log-file inside the results folder. + """ + logfile = os.path.join(self.wakeSolver["results_folder"], "wakis.log") + + # Write sections + if not os.path.exists(self.wakeSolver["results_folder"]): + os.mkdir(self.wakeSolver["results_folder"]) + + with open(logfile, "w", encoding="utf-8") as fh: + fh.write("Simulation Parameters\n") + fh.write("""=====================\n\n""") + + sections = [ + ("WakeSolver Logs", self.wakeSolver), + ("Solver Logs", self.solver), + ("Grid Logs", self.grid), + ] + + for title, data in sections: + fh.write(f"\n## {title} ##\n") + if not data: + fh.write("(empty)\n") + continue + + # convert non-serializable values to strings recursively + def _convert(obj): + if isinstance(obj, dict): + return {k: _convert(v) for k, v in obj.items()} + if isinstance(obj, (list, tuple)): + return [_convert(v) for v in obj] + try: + json.dumps(obj) + return obj + except Exception: + return str(obj) + + clean = _convert(data) + fh.write(json.dumps(clean, indent=2, ensure_ascii=False)) + fh.write("\n") diff --git a/wakis/routines.py b/wakis/routines.py index b525a41..0427bbc 100644 --- a/wakis/routines.py +++ b/wakis/routines.py @@ -5,6 +5,7 @@ import numpy as np import h5py +import time from tqdm import tqdm from scipy.constants import c as c_light from wakis.sources import Beam @@ -297,6 +298,7 @@ def save_to_h5(self, hf, field, x, y, z, comp, n): if plot_from is None: plot_from = int(self.ti/self.dt) print('Running electromagnetic time-domain simulation...') + t0 = time.time() for n in tqdm(range(Nt)): # Initial condition @@ -340,4 +342,9 @@ def save_to_h5(self, hf, field, x, y, z, comp, n): # Compute wakefield magnitudes is done inside WakeSolver self.wake.solve(compute_plane=compute_plane) - + + # Forward parameters to logger + self.logger.wakeSolver=self.wake.logger.wakeSolver + self.logger.wakeSolver["wakelength"]=wakelength + self.logger.wakeSolver["simulationTime"]=time.time()-t0 + self.logger.save_logs() diff --git a/wakis/solverFIT3D.py b/wakis/solverFIT3D.py index c142264..a94fba0 100644 --- a/wakis/solverFIT3D.py +++ b/wakis/solverFIT3D.py @@ -17,6 +17,7 @@ from .materials import material_lib from .plotting import PlotMixin from .routines import RoutinesMixin +from .logger import Logger try: from cupyx.scipy.sparse import csc_matrix as gpu_sparse_mat @@ -93,7 +94,8 @@ def __init__(self, grid, wake=None, cfln=0.5, dt=None, ''' self.verbose = verbose - if verbose: t0 = time.time() + t0 = time.time() + self.logger = Logger() # Flags self.step_0 = True @@ -110,10 +112,11 @@ def __init__(self, grid, wake=None, cfln=0.5, dt=None, self.one_step = self._one_step if use_stl: self.use_conductors = False + self.update_logger(['use_gpu', 'use_mpi']) # Grid self.grid = grid - + self.background = bg self.Nx = self.grid.Nx self.Ny = self.grid.Ny self.Nz = self.grid.Nz @@ -131,6 +134,7 @@ def __init__(self, grid, wake=None, cfln=0.5, dt=None, self.iA = self.grid.iA self.tL = self.grid.tL self.itA = self.grid.itA + self.update_logger(['grid','background']) # Wake computation self.wake = wake @@ -175,6 +179,7 @@ def __init__(self, grid, wake=None, cfln=0.5, dt=None, if verbose: print('Applying boundary conditions...') self.bc_low = bc_low self.bc_high = bc_high + self.update_logger(['bc_low', 'bc_high']) self.apply_bc_to_C() # Materials @@ -203,6 +208,7 @@ def __init__(self, grid, wake=None, cfln=0.5, dt=None, self.pml_hi = 1.e-1 self.pml_func = np.geomspace self.fill_pml_sigmas() + self.update_logger(['n_pml']) # Timestep calculation if verbose: print('Calculating maximal stable timestep...') @@ -212,6 +218,7 @@ def __init__(self, grid, wake=None, cfln=0.5, dt=None, else: self.dt = dt self.dt = dtype(self.dt) + self.update_logger(['dt']) if self.use_conductivity: # relaxation time criterion tau @@ -252,6 +259,9 @@ def __init__(self, grid, wake=None, cfln=0.5, dt=None, if verbose: print(f'Total initialization time: {time.time() - t0} s') + self.solverInitializationTime = time.time() - t0 + self.update_logger(['solverInitializationTime']) + def update_tensors(self, tensor='all'): '''Update tensor matrices after Field ieps, imu or sigma have been modified @@ -1107,4 +1117,14 @@ def reset_fields(self): for d in ['x', 'y', 'z']: self.E[:, :, :, d] = 0.0 self.H[:, :, :, d] = 0.0 - self.J[:, :, :, d] = 0.0 \ No newline at end of file + self.J[:, :, :, d] = 0.0 + + def update_logger(self, attrs): + """ + Assigns the parameters handed via attrs to the logger + """ + for atr in attrs: + if atr == 'grid': + self.logger.grid = self.grid.logger.grid + else: + self.logger.solver[atr] = getattr(self, atr) \ No newline at end of file diff --git a/wakis/wakeSolver.py b/wakis/wakeSolver.py index 0078ac6..62392a2 100644 --- a/wakis/wakeSolver.py +++ b/wakis/wakeSolver.py @@ -12,6 +12,8 @@ from tqdm import tqdm from scipy.constants import c as c_light +from .logger import Logger + class WakeSolver(): ''' Class for wake potential and impedance calculation from 3D time domain E fields @@ -22,7 +24,7 @@ def __init__(self, wakelength=None, q=1e-9, sigmaz=1e-3, beta=1.0, chargedist=None, ti=None, compute_plane='both', skip_cells=0, add_space=None, Ez_file='Ez.h5', save=True, results_folder='results/', - verbose=0, logfile=False, counter_moving=False): + verbose=0, counter_moving=False): ''' Parameters ---------- @@ -61,9 +63,6 @@ def __init__(self, wakelength=None, q=1e-9, sigmaz=1e-3, beta=1.0, - Charge distribution: lambda.txt, spectrum.txt verbose: bool, default 0 Controls the level of verbose in the terminal output - logfile: bool, default False - Creates a `wake.log` file with the summary of the input parameters - and calculations performed counter_moving: bool, default False If the test charge is moving in the same or opposite direction to the source @@ -161,17 +160,15 @@ def __init__(self, wakelength=None, q=1e-9, sigmaz=1e-3, beta=1.0, #user self.verbose = verbose + self.logger = Logger() self.save = save - self.logfile = logfile self.folder = results_folder if self.save: if not os.path.exists(self.folder): os.mkdir(self.folder) - # create log - if self.log: - self.params_to_log() + self.assign_logs() def solve(self, compute_plane=None, **kwargs): ''' @@ -1115,34 +1112,6 @@ def log(self, txt): if self.verbose: print('\x1b[2;37m'+txt+'\x1b[0m') - if not self.logfile: - return - - title = 'wake' - f = open(title + '.log', "a") - f.write(txt + '\r\n') - f.close() - - def params_to_log(self): - self.log(time.asctime()) - self.log('Wake computation') - self.log('='*24) - self.log(f'* Charge q = {self.q} [C]') - self.log(f'* Beam sigmaz = {self.sigmaz} [m]') - self.log(f'* xsource, ysource = {self.xsource}, {self.ysource} [m]') - self.log(f'* xtest, ytest = {self.xtest}, {self.ytest} [m]') - self.log(f'* Beam injection time ti= {self.ti} [s]') - - if self.chargedist is not None: - if type(self.chargedist) is str: - self.log(f'* Charge distribution file: {self.chargedist}') - else: - self.log(f'* Charge distribution data is provided') - else: - self.log(f'* Charge distribution analytic') - - self.log('\n') - def read_cst_3d(self, path=None, folder='3d', filename='Ez.h5', units=1e-3): ''' Read CST 3d exports folder and store the @@ -1302,4 +1271,18 @@ def sorter(item): self.zf = z self.t = np.array(t) - + def assign_logs(self): + """ + Assigns the parameters of the wake to the logger + """ + self.logger.wakeSolver["ti"]=self.ti + self.logger.wakeSolver["q"]=self.q + self.logger.wakeSolver["sigmaz"]=self.sigmaz + self.logger.wakeSolver["beta"]=self.beta + self.logger.wakeSolver["xsource"]=self.xsource + self.logger.wakeSolver["ysource"]=self.ysource + self.logger.wakeSolver["xtest"]=self.xtest + self.logger.wakeSolver["ytest"]=self.ytest + self.logger.wakeSolver["chargedist"]=self.chargedist + self.logger.wakeSolver["skip_cells"]=self.skip_cells + self.logger.wakeSolver["results_folder"]=self.folder