├── .gitignore ├── simulation_parameters.txt ├── example_circuit.asc ├── MATLAB scripts ├── get_fft.m ├── find_axis_limits.m ├── make_plots.m ├── analyse_and_plot.m └── analysis_tools.py ├── config.py ├── README.md ├── run.py ├── simulation_tools.py └── analysis_tools.py /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.log 3 | *.net 4 | *.raw 5 | _generated.asc* 6 | data 7 | -------------------------------------------------------------------------------- /simulation_parameters.txt: -------------------------------------------------------------------------------- 1 | # This a comment 2 | 3 | # First simulation run 4 | set R 500 5 | run C 1n 2n 3n 6 | 7 | # Second simulation run 8 | # run C 1n 2n 3n 9 | -------------------------------------------------------------------------------- /example_circuit.asc: -------------------------------------------------------------------------------- 1 | Version 4 2 | SHEET 1 880 680 3 | WIRE 96 64 0 64 4 | WIRE 272 64 176 64 5 | WIRE 0 112 0 64 6 | WIRE 272 128 272 64 7 | WIRE 0 256 0 192 8 | WIRE 272 256 272 192 9 | WIRE 272 256 0 256 10 | WIRE 272 272 272 256 11 | FLAG 272 272 0 12 | SYMBOL voltage 0 96 R0 13 | WINDOW 3 -75 213 Left 2 14 | WINDOW 123 0 0 Left 2 15 | WINDOW 39 0 0 Left 2 16 | SYMATTR InstName V1 17 | SYMATTR Value PULSE(0 10 0 1n 1n 1u 5u 10) 18 | SYMBOL res 80 80 R270 19 | WINDOW 0 32 56 VTop 2 20 | WINDOW 3 0 56 VBottom 2 21 | SYMATTR InstName R1 22 | SYMATTR Value {R} 23 | SYMBOL cap 256 128 R0 24 | SYMATTR InstName C1 25 | SYMATTR Value {C} 26 | TEXT 368 88 Left 2 !.param R=500 27 | TEXT 368 120 Left 2 !.param C=1 28 | TEXT 376 160 Left 2 !.tran 50u 29 | -------------------------------------------------------------------------------- /MATLAB scripts/get_fft.m: -------------------------------------------------------------------------------- 1 | function f_peak = get_fft(time, data, save_plots, file_name) 2 | 3 | % Fast Fourier transform 4 | L = length(time); 5 | time_step = (time(end)-time(1))/L; 6 | Fs = 1/time_step; 7 | 8 | NFFT = 2^nextpow2(L); 9 | Y = fft(data,NFFT)/L; 10 | f = Fs/2*linspace(0,1,NFFT/2+1); 11 | abs_y = 2*abs(Y(1:NFFT/2+1)); 12 | abs_y = 20*log(abs_y); 13 | 14 | % Find local peak 15 | db_peak = max(abs_y(f > 1e7 & f < 1e8)); 16 | f_peak = f(abs_y == db_peak); 17 | 18 | % Plots 19 | hFig = figure(4); 20 | semilogx(f,abs_y, 'LineWidth', 1) 21 | grid on 22 | 23 | xlim([0 3.5e8]) 24 | xlabel('Frequency [Hz]') 25 | ylabel('Amplitude [dB]') 26 | 27 | set(hFig, 'Position', [100 100 800 600]) 28 | set(gca,'FontSize',16) 29 | set(findall(gcf,'type','text'),'FontSize',16) 30 | 31 | if save_plots 32 | set(gcf, 'PaperPosition', [-0.1 0 8 6]); 33 | set(gcf, 'PaperSize', [8 6]); 34 | pdfname = [file_name(1:end-4), '_fft']; 35 | saveas(gcf, pdfname, 'pdf') 36 | end 37 | 38 | end -------------------------------------------------------------------------------- /config.py: -------------------------------------------------------------------------------- 1 | ########################################## 2 | ## CONFIGURATION OF LTspice SIMULATIONS ## 3 | ########################################## 4 | 5 | # File path of the LTspice program installed on the system 6 | LTSpice_executable_path = 'C:\Program Files (x86)\LTC\LTspiceIV\scad3.exe' 7 | # The name of the circuit to be used in simulations 8 | LTSpice_asc_filename = 'example_circuit.asc' 9 | 10 | # The measurements to be saved from the simulation. The appropriate numbers can be found in the .raw file generated at simulation. 11 | variable_numbering = {'time': 0, 'V_c': 2, 'I_c': 3} 12 | # The ordering of the measurements to be saved in the output csv files can be changed below, by changing how the numbers are ordered. 13 | # E.g. switch place of 0 and 1 if you want V_c to be placed left of time in the output csv files. 14 | preffered_sorting = [0, 1, 2] 15 | 16 | # Leave blank if output should be writtten to root folder. The folder specified must be created manually if it doesn't exist. 17 | output_data_path = 'data/' 18 | # Naming convention for output files. Can be numerical names, increasing in value as output is created. 19 | # Parameter names gives the files the name of the parameter that it is run with. 20 | # Assumes number by default. Set to 'parameter' or 'number'. 21 | output_data_naming_convention = 'number' 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # LTspice-cli 2 | 3 | ## About 4 | 5 | LTspice-cli is a command line tool, which enables running simulations in LTspice from the command line. It also has the ability to specify a file containing the circuit parameters which the simulations should be run with. LTspice-cli is written in Python 2.7, and requires NumPy to run. The tool is only tested with LTspice IV. 6 | 7 | ## How to use 8 | 9 | After downloading the files, the config.py file must be edited. Once configurated, a simulation can be run using 10 | ``` 11 | python run.py -r 12 | ``` 13 | To specify a file containing parameters, use 14 | ``` 15 | python run.py -f 16 | ``` 17 | Use option -h to see help in the command line. 18 | 19 | ## Parameter file 20 | 21 | A file can be used to set the parameters in a circuit simulation. A parameter must first be set in LTspice, for example a resistor: 22 | 23 | ``` 24 | .param R=500 25 | ``` 26 | To change a parameter at run time, the `set` keyword in the parameter file can be used as follows: 27 | ``` 28 | set R 1k 29 | ``` 30 | The resistor R will now be set to 1k before running the simulation. To set a range of variables to be used in simulation, the `run` keyword is used. To simulate with the values 1, 100 and 1k for a capacitor C, the following line would be used: 31 | ``` 32 | run C 1 100 1k 33 | ``` 34 | Both `set` and `run` can be used in the same parameter file. Using the `set` command alone will not run a simulation, only change the LTspice file. To comment a line, begin the line with `#`. 35 | -------------------------------------------------------------------------------- /MATLAB scripts/find_axis_limits.m: -------------------------------------------------------------------------------- 1 | function [voltage_interval, current_interval] = find_axis_limits(time, time_interval, current, volt) 2 | 3 | % Axis are globally equal, and zero levels align horizontally 4 | time_interval_1 = time_interval(1,:); 5 | time_interval_2 = time_interval(2,:); 6 | t_1 = find(time >= time_interval_1(1) & time <= time_interval_1(2)); 7 | t_2 = find(time >= time_interval_2(1) & time <= time_interval_2(2)); 8 | voltage_interval = sort([volt(t_1(1)), volt(t_2(1))]); 9 | current_interval = sort([current(t_1(1)), current(t_2(1))]); 10 | 11 | scale_up_voltage = max(volt)/voltage_interval(end); 12 | scale_down_voltage = min(volt)/voltage_interval(end); 13 | scale_up_current = max(current)/current_interval(end); 14 | scale_down_current = min(current)/current_interval(end); 15 | 16 | voltage_interval = voltage_interval(end) - voltage_interval(1); 17 | voltage_interval = [voltage_interval*scale_down_voltage, voltage_interval*scale_up_voltage]; 18 | current_interval = current_interval(end) - current_interval(1); 19 | current_interval = [current_interval*scale_down_current, current_interval*scale_up_current]; 20 | voltage_ratio = abs(max(voltage_interval) / min(voltage_interval)); 21 | current_ratio = abs(max(current_interval) / min(current_interval)); 22 | 23 | if abs(min(voltage_interval)) < 5 || voltage_ratio > current_ratio 24 | voltage_interval(1) = -voltage_interval(2) / current_ratio; 25 | else 26 | current_interval(1) = -current_interval(2) / voltage_ratio; 27 | end 28 | 29 | voltage_margin = max(abs(voltage_interval))*0.05; 30 | current_margin = max(abs(current_interval))*0.05; 31 | voltage_interval = [voltage_interval(1)-voltage_margin voltage_interval(end)+voltage_margin]; 32 | current_interval = [current_interval(1)-current_margin current_interval(end)+current_margin]; 33 | 34 | end 35 | -------------------------------------------------------------------------------- /MATLAB scripts/make_plots.m: -------------------------------------------------------------------------------- 1 | function make_plots(time, time_interval_1, time_interval_2, volt, current, save_plots, file_name) 2 | 3 | 4 | %% Start making plots 5 | hold off 6 | [voltage_interval, current_interval] = find_axis_limits(time, [time_interval_2; time_interval_1], current, volt); 7 | 8 | %% Plot turn-off 9 | 10 | % Find plot intervals 11 | time_interval = time_interval_1; 12 | 13 | % Create plots 14 | hFig = figure(1); 15 | set(hFig, 'Position', [100 100 800 600]) 16 | set(gca,'FontSize',16) 17 | set(findall(gcf,'type','text'),'FontSize',16) 18 | grid on 19 | 20 | xlim(time_interval) 21 | xlabel('Time [us]') 22 | 23 | yyaxis left 24 | plot(time, volt, 'LineWidth', 1) 25 | ylim(voltage_interval) 26 | ylabel('Voltage [V]') 27 | 28 | yyaxis right 29 | plot(time, current, 'LineWidth', 1) 30 | ylim(current_interval) 31 | ylabel('Current [A]') 32 | 33 | if save_plots 34 | set(gcf, 'PaperPosition', [-0.1 0 8 6]); 35 | set(gcf, 'PaperSize', [8 6]); 36 | pdfname = [file_name(1:end-4), '_turnoff']; 37 | saveas(gcf, pdfname, 'pdf') 38 | end 39 | 40 | %% Plot turn-on 41 | 42 | % Find plot intervals 43 | time_interval = time_interval_2; 44 | 45 | % Create plots 46 | hFig = figure(2); 47 | set(hFig, 'Position', [400 100 800 600]) 48 | set(gca,'FontSize',16) 49 | set(findall(gcf,'type','text'),'FontSize',16) 50 | grid on 51 | 52 | xlim(time_interval) 53 | xlabel('Time [us]') 54 | 55 | yyaxis left 56 | plot(time, volt, 'LineWidth', 1) 57 | ylim(voltage_interval) 58 | ylabel('Voltage [V]') 59 | 60 | yyaxis right 61 | plot(time, current, 'LineWidth', 1) 62 | ylim(current_interval) 63 | ylabel('Current [A]') 64 | 65 | if save_plots 66 | set(gcf, 'PaperPosition', [-0.1 0 8 6]); 67 | set(gcf, 'PaperSize', [8 6]); 68 | pdfname = [file_name(1:end-4), '_turnon']; 69 | saveas(gcf, pdfname, 'pdf') 70 | end 71 | end -------------------------------------------------------------------------------- /MATLAB scripts/analyse_and_plot.m: -------------------------------------------------------------------------------- 1 | 2 | % Reload python module 3 | testing = 1; 4 | if testing 5 | clear classes; 6 | mod = py.importlib.import_module('analysis_tools'); 7 | py.reload(mod); 8 | end 9 | 10 | % Analysis parameters 11 | save_plots = 0; 12 | file = 'data/file.csv'; 13 | 14 | % Read raw data 15 | fprintf([file, '\n']) 16 | data = importdata(file, ',', 17); 17 | t = data.data(:,1); 18 | v_ds = data.data(:,2); 19 | i_ds = data.data(:,4); 20 | 21 | % Filter raw data 22 | sr = length(t)/max(t); 23 | ff = 100e6; 24 | wn = ff/(sr*0.5); 25 | [B,A] = butter(2, wn); 26 | v_ds = filter(B, A, v_ds); 27 | i_ds = filter(B, A, i_ds); 28 | % Determine intervals 29 | start = find(t>-5e-7); 30 | start = start(1); 31 | stop = find(t>7.5e-6); 32 | stop = stop(1); 33 | t = t(start:stop) - t(start); 34 | v_ds = v_ds(start:stop); 35 | snubber_voltage = snubber_voltage(start:stop); 36 | i_ds = i_ds(start:stop); 37 | % Remove current bias 38 | current_bias = mean(i_ds(i_ds < 4)); 39 | i_ds = i_ds - current_bias; 40 | % Set nominal levels 41 | nominal_voltage = 600; 42 | nominal_current = mean(i_ds(1:100)); 43 | 44 | % Analyse 45 | 46 | fft_frequency_peak = get_fft(t, v_ds, save_plots, file); 47 | fprintf('FFT gives peak at frequency %4.1f MHz\n', fft_frequency_peak * 1e-6) 48 | 49 | analyzed_data = py.analysis_tools.analyze_data_single(t', i_ds', v_ds', nominal_current, nominal_voltage, snubber_voltage', snubber_resistance); 50 | time_indexes = analyzed_data{1}; 51 | time_indexes = cellfun(@int64, cell(time_indexes)); 52 | 53 | fprintf('Voltage rise time:\t\t%4.1f ns\n', analyzed_data{2}); 54 | fprintf('Voltage fall time:\t\t%4.1f ns\n', analyzed_data{3}); 55 | fprintf('Current rise time:\t\t%4.1f ns\n', analyzed_data{4}); 56 | fprintf('Current fall time:\t\t%4.1f ns\n', analyzed_data{5}); 57 | fprintf('Turn-on loss:\t\t\t%5.3f mJ\n', analyzed_data{6}); 58 | fprintf('Snubber turn-on loss:\t%5.3f mJ\n', analyzed_data{12}); 59 | fprintf('Turn-off loss:\t\t\t%5.3f mJ\n', analyzed_data{7}); 60 | fprintf('Snubber turn-off loss:\t%5.3f mJ\n', analyzed_data{13}); 61 | fprintf('Voltage overshoot:\t\t%4.1f V\n', analyzed_data{8}); 62 | fprintf('Max current:\t\t\t%4.1f A\n', analyzed_data{9}); 63 | fprintf('Ringing frequency:\t\t%4.1f MHz\n', analyzed_data{10}); 64 | fprintf('Damping ratio:\t\t\t%4.1f M 1/s\n', analyzed_data{11}); 65 | 66 | % Make plots 67 | 68 | time = t* 10^6; % Change to microseconds [us] 69 | time_interval_1 = [0 3]; 70 | time_interval_2 = [5 8]; 71 | make_plots(time, time_interval_1, time_interval_2, v_ds, i_ds, save_plots, file); 72 | -------------------------------------------------------------------------------- /run.py: -------------------------------------------------------------------------------- 1 | import sys, getopt 2 | import simulation_tools 3 | import config 4 | try: 5 | import analysis_tools 6 | except ImportError: 7 | pass 8 | 9 | 10 | def simulate(filename=None, do_analysis=False): 11 | 12 | asc_file_path = config.LTSpice_asc_filename 13 | 14 | if filename is not None: 15 | # Parse the file containing the parameters 16 | command_list = simulation_tools.parse_parameter_file(filename) 17 | 18 | # Run the list of commands 19 | number_of_finished_simulations = 0 20 | all_filenames = [] 21 | if command_list is None: 22 | print('Syntax error in parameter file.') 23 | return 24 | for command in command_list: 25 | if command[0] == 's': 26 | parameter = command[1] 27 | value = command[2] 28 | # Set parameter as specified 29 | print('Setting parameter: ' + str(parameter) + '=' + str(value)) 30 | simulation_tools.set_parameters(asc_file_path, parameter, value, True) 31 | if command[0] == 'r': 32 | parameter = command[1] 33 | parameter_values = command[2] 34 | # Run tests with the given parameter and corresponding list of parameter values 35 | # The filenames of the output data is returned 36 | filenames = simulation_tools.run_simulations([parameter, parameter_values], number_of_finished_simulations) 37 | all_filenames.extend(filenames) 38 | number_of_finished_simulations += len(parameter_values) 39 | if do_analysis: 40 | analyze(filenames) 41 | 42 | # If analysis made, make a report with all the analysied data 43 | if do_analysis: 44 | analysis_tools.make_report(all_filenames) 45 | else: 46 | # No parameter file is specified, run simulations with defaults values 47 | simulation_tools.run_simulations() 48 | 49 | def analyze(filenames): 50 | # Any analysis to be done on the simulation results can be be coded here. 51 | for filename in filenames: 52 | analysis_tools.analyze_data(filename) 53 | 54 | def help(): 55 | print 'auto.py -r -f -a\nUsing the option -a to analyze, requires -f to be set' 56 | 57 | def main(argv): 58 | 59 | # Get arguments 60 | try: 61 | opts, args = getopt.getopt(argv, 'hrf:a', ['file=', 'run']) 62 | except getopt.GetoptError: 63 | help() 64 | sys.exit(2) 65 | 66 | # Parse arguments 67 | parameter_file = None 68 | do_analysis = False 69 | for opt, arg in opts: 70 | if opt == '-h': 71 | help() 72 | sys.exit() 73 | elif opt in ('-f', '--file'): 74 | parameter_file = arg 75 | elif opt in ('-a'): 76 | do_analysis = True 77 | elif opt in ('-r', '--run'): 78 | simulate() 79 | sys.exit() 80 | 81 | # Run simulations based on arguments 82 | if parameter_file is not None: 83 | simulate(parameter_file, do_analysis) 84 | 85 | if __name__ == '__main__': 86 | main(sys.argv[1:]) 87 | -------------------------------------------------------------------------------- /simulation_tools.py: -------------------------------------------------------------------------------- 1 | from subprocess import call 2 | import os 3 | from tempfile import mkstemp 4 | from shutil import move 5 | import numpy as np 6 | 7 | import config 8 | 9 | 10 | # ----------- Simulation controls ----------- # 11 | 12 | def run_simulations(parameter_set=None, numerical_name_start=0): 13 | 14 | # Set appropriate variables according to the argument of parameter_set 15 | if parameter_set is not None: 16 | parameter = parameter_set[0] 17 | parameter_value_list = parameter_set[1] 18 | use_default_parameters = False 19 | else: 20 | use_default_parameters = True 21 | 22 | # Specify file paths 23 | file_path = config.LTSpice_asc_filename[:-4] # Use .asc file specified in config, but remove file ending 24 | file_path_generated = file_path + '_generated' 25 | spice_exe_path = config.LTSpice_executable_path 26 | 27 | # Create a list of the generated files 28 | output_filenames = [] 29 | 30 | if not use_default_parameters: 31 | # Run a simulation for each parameter value in the parameter set 32 | for i, parameter_value in enumerate(parameter_value_list): 33 | # Set specified parameters 34 | if config.output_data_naming_convention == 'number': 35 | file_num = str(i + numerical_name_start) 36 | output_name = '0'*(3-len(file_num)) + file_num 37 | output_path = config.output_data_path + output_name + '.txt' 38 | else: 39 | output_path = config.output_data_path + parameter + '=' + str(parameter_value) + '.txt' 40 | output_filenames.append(output_path) 41 | set_parameters(file_path + '.asc', parameter, parameter_value) 42 | print('Starting simulation with the specified parameter: ' + parameter + '=' + str(parameter_value)) 43 | # Run simulation 44 | simulate(spice_exe_path, file_path_generated) 45 | # Set header and cleanup the file 46 | output_header = 'SPICE simulation result. Parameters: ' + ', '.join(get_parameters(file_path_generated + '.asc')) + '\n' # Maybe not add the time variables 47 | clean_raw_file(spice_exe_path, file_path_generated, output_path, output_header) 48 | else: 49 | # Run a simulation with the preset values of the file 50 | output_path = config.output_data_path + 'result.txt' 51 | print('Starting simulation.') 52 | simulate(spice_exe_path, file_path) 53 | # Set header and cleanup the file 54 | output_header = 'SPICE simulation result. Parameters: ' + ', '.join(get_parameters(file_path + '.asc')) + '\n' # Maybe not add the time variables 55 | clean_raw_file(spice_exe_path, file_path, output_path, output_header) 56 | 57 | # Return the list with names of the output filenames 58 | return output_filenames 59 | 60 | def simulate(spice_exe_path, file_path): 61 | file_name = str(file_path.split('\\')[-1]) 62 | print('Simulation starting: ' + file_name + '.asc') 63 | call('"' + spice_exe_path + '" -netlist "' + file_path + '.asc"') 64 | call('"' + spice_exe_path + '" -b -ascii "' + file_path + '.net"') 65 | size = os.path.getsize(file_path + '.raw') 66 | print('Simulation finished: ' + file_name + '.raw created (' + str(size/1000) + ' kB)') 67 | 68 | def clean_raw_file(spice_exe_path, file_path, output_path, output_header): 69 | 70 | # Try to open the requested file 71 | file_name = file_path 72 | try: 73 | f = open(file_path + '.raw', 'r') 74 | except IOError: 75 | # If the requested raw file is not found, simulations will be run, 76 | # assuming a that a corresponding LTspice schematic exists 77 | print('File not found: ' + file_name + '.raw') 78 | simulate(spice_exe_path, file_path) 79 | f = open(file_path + '.raw', 'r') 80 | 81 | print('Cleaning up file: ' + file_name + '.raw') 82 | 83 | reading_header = True 84 | data = [] 85 | data_line = [] 86 | 87 | for line_num, line in enumerate(f): 88 | 89 | if reading_header: 90 | if line_num == 4: 91 | number_of_vars = int(line.split(' ')[-1]) 92 | if line_num == 5: 93 | number_of_points = int(line.split(' ')[-1]) 94 | if line[:7] == 'Values:': 95 | reading_header = False 96 | header_length = line_num + 1 97 | continue 98 | else: 99 | data_line_num = (line_num - header_length) % number_of_vars 100 | if data_line_num in config.variable_numbering.values(): 101 | data_line.append(line.split('\t')[-1].split('\n')[0]) 102 | if data_line_num == number_of_vars - 1: 103 | data.append(data_line) 104 | data_line = [] 105 | 106 | f.close() 107 | 108 | # Rearrange data 109 | variables = sorted(config.variable_numbering, key=config.variable_numbering.__getitem__) 110 | variables = np.array(variables)[config.preffered_sorting].tolist() 111 | data = np.array(data)[:, config.preffered_sorting] 112 | 113 | # Write data to file 114 | try: 115 | f = open(output_path, 'w+') 116 | except IOError: 117 | print('\nThe path specified for saving output data, \'' + config.output_data_path + '\', doesn\'t appear to exist.\nPlease check if the filepath set in \'config.py\' is correct.') 118 | exit(0) 119 | f.write(output_header) 120 | f.write('\t'.join(variables) + '\n') 121 | for line in data: 122 | f.write('\t'.join(line) + '\n') 123 | f.close() 124 | 125 | size = os.path.getsize(output_path) 126 | print('CSV file created: ' + output_path + ' (' + str(size/1000) + ' kB)') 127 | 128 | 129 | 130 | # ----------- Parameter controls ----------- # 131 | 132 | def parse_parameter_file(filename): 133 | 134 | cmd_list = [] 135 | param_file = open(filename, 'r') 136 | 137 | for line in param_file: 138 | line = line.split() 139 | if len(line) == 0: 140 | continue 141 | try: 142 | cmd = line[0] 143 | if cmd[0] == '#': 144 | continue 145 | elif cmd.lower() == 'set': 146 | parameter = line[1] 147 | value = line[2] 148 | cmd_list.append(('s', parameter, value)) 149 | elif cmd.lower() == 'run': 150 | parameter = line[1] 151 | values = line[2:] 152 | cmd_list.append(('r', parameter, values)) 153 | else: 154 | return None # Syntax error 155 | except IndexError: 156 | return None # Syntax error 157 | 158 | return cmd_list 159 | 160 | def set_parameters(file_path, param, param_val, overwrite=False): 161 | f, abs_path = mkstemp() 162 | with open(abs_path,'w') as new_file: 163 | with open(file_path) as old_file: 164 | for line in old_file: 165 | line_list = line.split(' ') 166 | if line_list[0] == 'TEXT': 167 | for element_num, element in enumerate(line_list): 168 | if element.split('=')[0] == param: 169 | line_list[element_num] = param + '=' + str(param_val) 170 | if line_list[-1][-1] != '\n': 171 | line_list[-1] = line_list[-1] + '\n' 172 | new_file.write(' '.join(line_list)) 173 | else: 174 | new_file.write(line) 175 | os.close(f) 176 | if overwrite: 177 | os.remove(file_path) 178 | move(abs_path, file_path) 179 | else: 180 | move(abs_path, file_path[:-4] + '_generated.asc') 181 | 182 | def get_parameters(file_path): 183 | output_list = [] 184 | f = open(file_path, 'r') 185 | for line in f: 186 | line_list = line.split() 187 | if line_list[0] == 'TEXT' and '!.param' in line_list: 188 | output_list.extend(line_list[line_list.index('!.param') + 1:]) 189 | f.close() 190 | return output_list 191 | -------------------------------------------------------------------------------- /analysis_tools.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | import config 3 | 4 | # ------- Control functions ------- # 5 | 6 | def make_report(file_paths): 7 | out = open(config.output_data_path + config.output_data_summary_filename, 'w+') 8 | for file_path in file_paths: 9 | file_path = file_path[:-4] + '_analysis.txt' 10 | f = open(file_path) 11 | lastline = None 12 | for l in f: 13 | lastline = l 14 | out.write(file_path + '\t' + lastline) 15 | f.close() 16 | out.close() 17 | 18 | def read_simulation_output(file_path): 19 | # Read data from file 20 | f = open(file_path) 21 | data=[] 22 | header=[] 23 | # Get lines 24 | for line_num, line in enumerate(f): 25 | if line_num > 1: 26 | data.append([float(num) for num in line.split()]) 27 | else: 28 | header.append(line.split()) 29 | f.close() 30 | # Create numpy array 31 | data = np.array(data, dtype=float) 32 | return (header, data) 33 | 34 | def analyze_data(file_path): 35 | 36 | # Read data 37 | header, data = read_simulation_output(file_path) 38 | 39 | # Pick and define relevant data 40 | # Remove inital data, until 40 us 41 | start_data = np.where(data[:,0] > 40*10**(-6))[0][0] 42 | data = data[start_data:,:] 43 | data = data[:,0:7] 44 | data[:,1] = data[:,1] - data[:,4] # Voltage accros upper mosfet 45 | # Header info 46 | header_variables = header[1] 47 | params = header[0] 48 | # Circuit variables 49 | snubber_resistance = [param.split('=')[1] for param in params if ('R_s' in param.split('='))][0] 50 | snubber_resistance = float(snubber_resistance.split(',')[0]) 51 | output_current = [param.split('=')[1] for param in params if ('I_out' in param.split('='))][0] 52 | output_current = float(output_current.split(',')[0]) 53 | input_voltage = 600 54 | time = data[:, 0] 55 | # Final output list 56 | all_output_info = [] 57 | 58 | # Open file to print 59 | f = open(file_path[:-4] + '_analysis.txt', 'w+') 60 | f.write('Variables: ' + ', '.join(header_variables) + '\n') 61 | f.write(' '.join(params[3:10]) + '\n' + ' '.join(params[10:]) + '\n') 62 | 63 | # Perform analysis for upper and lower part of half-bridge 64 | for i in range(len(data[0,1:])/3): 65 | col = 3*i + 1 66 | 67 | # Find voltage switching times 68 | rise_v, fall_v, rise_found, fall_found = calculate_switching_times(data[:, col], input_voltage) 69 | if rise_found: 70 | rise_time_voltage = (time[rise_v[0]], time[rise_v[1]]) 71 | else: 72 | rise_time_voltage = (0,0) 73 | if fall_found: 74 | fall_time_voltage = (time[fall_v[0]], time[fall_v[1]]) 75 | else: 76 | print time[fall_v[0]] 77 | print time[fall_v[1]] 78 | fall_time_voltage = (0,0) 79 | 80 | # Find current switching times 81 | rise_i, fall_i, rise_found, fall_found = calculate_switching_times(data[:, col+1], output_current) 82 | if rise_found: 83 | rise_time_current = (time[rise_i[0]], time[rise_i[1]]) 84 | else: 85 | rise_time_current = (0,0) 86 | if fall_found: 87 | fall_time_current = (time[fall_i[0]], time[fall_i[1]]) 88 | else: 89 | fall_time_current = (0,0) 90 | 91 | # Get overshoots 92 | voltage_undershoot, voltage_overshoot = calculate_overshoots(data[:, col]) 93 | current_undershoot, current_overshoot = calculate_overshoots(data[:, col+1]) 94 | max_current = max([abs(current_overshoot), abs(current_undershoot)]) 95 | 96 | # Calculate ringing and damping ratio 97 | try: 98 | ringing_freq, decay_ratio = calculate_ringing(time, data[:, col], input_voltage) 99 | period = np.where(time < time[0] + 1/ringing_freq)[0][-1] 100 | except IndexError: 101 | ringing_freq = 0 102 | decay_ratio = 0 103 | period = 50 104 | ringing_freq = ringing_freq / 10**6 # [MHz] 105 | decay_ratio = decay_ratio / 10**6 # [MHz] 106 | 107 | # Calculate turn-on power loss 108 | start = min([rise_i[0], fall_v[0]]) 109 | stop = max([rise_i[1], fall_v[1]]) 110 | start -= 2*(stop-start) 111 | delta = 0 112 | while True: 113 | delta += 100 114 | if max(data[stop+delta:stop+delta+3*period, col+1]) < output_current*1.05: 115 | break 116 | stop+=delta 117 | E_turnon = calc_switch_loss(time[start:stop], data[start:stop, col], data[start:stop, col+1]) *10**3 #[mJ] 118 | E_turnon_snubber = np.multiply(snubber_resistance, calc_switch_loss(time[start:stop], data[start:stop, col+2], data[start:stop, col+2])) *10**3 #[mJ] 119 | 120 | # Calculate turn-off power loss 121 | start = min([fall_i[0], rise_v[0]]) 122 | stop = max([fall_i[1], rise_v[1]]) 123 | start -= 2*(stop-start) 124 | delta = 0 125 | while True: 126 | delta += 100 127 | if max(data[stop+delta:stop+delta+3*period, col]) < input_voltage*1.05: 128 | break 129 | stop+=delta 130 | E_turnoff = calc_switch_loss(time[start:stop], data[start:stop, col], data[start:stop, col+1]) *10**3 #[mJ] 131 | E_turnoff_snubber = np.multiply(snubber_resistance, calc_switch_loss(time[start:stop], data[start:stop, col+2], data[start:stop, col+2])) *10**3 #[mJ] 132 | 133 | 134 | # Write outdata 135 | f.write(['\n------------- Upper transistor ------------- \n\n', '\n------------- Lower transistor ------------- \n\n'][i]) 136 | rise_time_voltage = (rise_time_voltage[1] - rise_time_voltage[0]) * 10**9 # [ns] 137 | fall_time_voltage = (fall_time_voltage[1] - fall_time_voltage[0]) * 10**9 # [ns] 138 | rise_time_current = (rise_time_current[1] - rise_time_current[0]) * 10**9 # [ns] 139 | fall_time_current = (fall_time_current[1] - fall_time_current[0]) * 10**9 # [ns] 140 | all_output_info.extend([rise_time_voltage, fall_time_voltage, rise_time_current, fall_time_current]) 141 | all_output_info.extend([E_turnon, E_turnoff, E_turnon_snubber, E_turnoff_snubber]) 142 | all_output_info.extend([voltage_overshoot, max_current]) 143 | all_output_info.extend([ringing_freq, decay_ratio]) 144 | f.write('Voltage Rise time [ns]: ' + str(rise_time_voltage) + '\n') 145 | f.write('Voltage Fall time [ns]: ' + str(fall_time_voltage) + '\n') 146 | f.write('Current Rise time [ns]: ' + str(rise_time_current) + '\n') 147 | f.write('Current Fall time [ns]: ' + str(fall_time_current) + '\n') 148 | f.write('Turn-on loss [mJ]: ' + str(E_turnon) + '\n') 149 | f.write('Turn-off loss [mJ]: ' + str(E_turnoff) + '\n') 150 | f.write('Turn-on loss snubbers [mJ]: ' + str(E_turnon_snubber) + '\n') 151 | f.write('Turn-off loss snubbers [mJ]: ' + str(E_turnoff_snubber) + '\n') 152 | f.write('Voltage overshoot [V]: ' + str(voltage_overshoot) + '\n') 153 | f.write('Largest current [A]: ' + str(max_current) + '\n') 154 | f.write('Ringing frequency [MHz]: ' + str(ringing_freq) + '\n') 155 | f.write('Damping ratio [M 1/s]: ' + str(decay_ratio) + '\n') 156 | 157 | # Write all info on one final line 158 | f.write('\n') 159 | f.write('\t'.join([str(i) for i in all_output_info]) + '\n') 160 | 161 | # Close file 162 | f.close() 163 | 164 | 165 | # ------- Analysis functions ------- # 166 | 167 | def local_extrema(data): 168 | local_max = np.where(np.r_[True, data[1:] > data[:-1]] & np.r_[data[:-1] > data[1:], True])[0] 169 | local_min = np.where(np.r_[True, data[1:] < data[:-1]] & np.r_[data[:-1] < data[1:], True])[0] 170 | return (local_min, local_max) 171 | 172 | def calculate_switching_times(data, nominal_value): 173 | 174 | local_min, local_max = local_extrema(data) 175 | extrema = np.concatenate([local_min, local_max]) 176 | extrema = np.sort(extrema) 177 | 178 | rise_time_found = False 179 | fall_time_found = False 180 | 181 | prev_x = extrema[0] 182 | for index, x in enumerate(extrema[1:]): 183 | if data[prev_x] < 0.1 * nominal_value and data[x] > 0.9 * nominal_value: 184 | start_rise = prev_x + np.where(data[prev_x:x] > 0.1*nominal_value)[0][0] 185 | stop_rise = prev_x + np.where(data[prev_x:x] > 0.9*nominal_value)[0][0] 186 | rise = (start_rise, stop_rise) 187 | rise_time_found = True 188 | 189 | if data[prev_x] > 0.9 * nominal_value and data[x] < 0.1 * nominal_value: 190 | start_fall = prev_x + np.where(data[prev_x:x] < 0.9*nominal_value)[0][0] 191 | stop_fall = prev_x + np.where(data[prev_x:x] < 0.1*nominal_value)[0][0] 192 | fall = (start_fall, stop_fall) 193 | fall_time_found = True 194 | 195 | if rise_time_found and fall_time_found: 196 | break 197 | prev_x = x 198 | 199 | if not rise_time_found: 200 | prev_x = extrema[0] 201 | for index, x in enumerate(extrema[1:]): 202 | if data[prev_x] < 0.4 * nominal_value and data[x] > 0.6 * nominal_value: 203 | start_rise = np.where(data[:x] < 0.1*nominal_value)[0][-1] 204 | stop_rise = prev_x + np.where(data[prev_x:] > 0.9*nominal_value)[0][0] 205 | rise = (start_rise, stop_rise) 206 | prev_x = x 207 | if not fall_time_found: 208 | prev_x = extrema[0] 209 | for index, x in enumerate(extrema[1:]): 210 | if data[prev_x] > 0.6 * nominal_value and data[x] < 0.4 * nominal_value: 211 | start_fall = np.where(data[:x] > 0.9*nominal_value)[0][-1] 212 | stop_fall = prev_x + np.where(data[prev_x:] < 0.1*nominal_value)[0][0] 213 | fall = (start_fall, stop_fall) 214 | prev_x = x 215 | 216 | try: 217 | return (rise, fall, True, True) # Return 218 | except UnboundLocalError: 219 | # Some value not found 220 | print 'Error: unable to find rise or fall time. Zero is returned.' 221 | return ((0,0), (0,0), False, False) 222 | 223 | def calc_switch_loss(time, voltage, current): 224 | E = 0 225 | for i in range(len(voltage)-1): 226 | E += (time[i+1] - time[i]) / 2.0 * (voltage[i]*current[i] + voltage[i+1]*current[i+1]) 227 | return E 228 | 229 | def calculate_overshoots(data): 230 | return (min(data), max(data)) 231 | 232 | def calculate_ringing(time, data, nominal_value): 233 | # Ringing frequency calculation 234 | number_of_peaks = 5 235 | peak = max(data) 236 | first_peak = np.where(data == peak)[0][0] 237 | index_of_peaks = local_extrema(data[first_peak:])[1] 238 | index_of_peaks = index_of_peaks[0:number_of_peaks] # Pick the first peaks, as defined 239 | index_of_peaks = np.add(index_of_peaks, first_peak) # Shift peaks to correct indexes 240 | peak_times = time[index_of_peaks] 241 | time_diff = [peak_times[i+1] - peak_times[i] for i in range(number_of_peaks-1)] 242 | ringing_freq = 1/np.mean(time_diff) # Calculate frequency based on avg time diff between peaks 243 | # Dacay ratio calculation 244 | voltage_peaks = data[index_of_peaks] - nominal_value 245 | decay_ratios = [] 246 | for i in range(number_of_peaks - 1): 247 | alpha = np.log(voltage_peaks[i+1] / voltage_peaks[i]) / time_diff[i] 248 | decay_ratios.append(alpha) 249 | decay_ratio = (-1) * np.mean(decay_ratios) 250 | return (ringing_freq, decay_ratio) 251 | 252 | def calculate_switching_times_alternative(data, nominal_value): 253 | 254 | going_up = False 255 | going_down = False 256 | 257 | fall = [0,0] 258 | rise = [0,0] 259 | found_rise = False 260 | found_fall = False 261 | 262 | data_point_prev = data[0] 263 | for step, data_point in enumerate(data[1:]): 264 | if data_point > 0.1 * nominal_value and data_point_prev < 0.1 * nominal_value: 265 | if not going_up: 266 | rise[0] = step 267 | going_up = True 268 | going_down = False 269 | if data_point > 0.9 * nominal_value and data_point_prev < 0.9 * nominal_value: 270 | if going_up: 271 | rise[1] = step 272 | found_rise = True 273 | if going_down: 274 | rise[0] = 0 275 | going_up = False 276 | going_down = False 277 | if data_point < 0.9 * nominal_value and data_point_prev > 0.9 * nominal_value: 278 | if not going_down: 279 | fall[0] = step 280 | going_up = False 281 | going_down = True 282 | if data_point < 0.1 * nominal_value and data_point_prev > 0.1 * nominal_value: 283 | if going_down: 284 | fall[1] = step 285 | found_fall = True 286 | if going_up: 287 | fall[0] = 0 288 | going_up = False 289 | going_down = False 290 | 291 | if found_fall and found_rise: 292 | break 293 | 294 | data_point_prev = data_point 295 | 296 | return (rise, fall) 297 | -------------------------------------------------------------------------------- /MATLAB scripts/analysis_tools.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | try: 3 | import config 4 | except ImportError: 5 | print 'Error: could not find config file.' 6 | 7 | # ------- Control functions ------- # 8 | 9 | def make_report(file_paths): 10 | out = open(config.output_data_path + config.output_data_summary_filename, 'w+') 11 | for file_path in file_paths: 12 | file_path = file_path[:-4] + '_analysis.txt' 13 | try: 14 | f = open(file_path) 15 | lastline = None 16 | for l in f: 17 | lastline = l 18 | out.write(file_path + '\t' + lastline) 19 | f.close() 20 | except IOError: 21 | print file_path + ': An error occured, file does not exist.' 22 | out.write(file_path + '\tNo data.\n') 23 | 24 | out.close() 25 | 26 | def read_simulation_output(file_path): 27 | # Read data from file 28 | f = open(file_path) 29 | data=[] 30 | header=[] 31 | # Get lines 32 | for line_num, line in enumerate(f): 33 | if line_num > 1: 34 | data.append([float(num) for num in line.split()]) 35 | else: 36 | header.append(line.split()) 37 | f.close() 38 | # Create numpy array 39 | data = np.array(data, dtype=float) 40 | return (header, data) 41 | 42 | def analyze_data(file_path): 43 | 44 | # Read data 45 | header, data = read_simulation_output(file_path) 46 | 47 | # Pick and define relevant data 48 | # Remove inital data, until 40 us 49 | start_data = np.where(data[:,0] > 40*10**(-6))[0][0] 50 | data = data[start_data:,:] 51 | data = data[:,0:7] 52 | data[:,1] = data[:,1] - data[:,4] # Voltage accros upper mosfet 53 | # Header info 54 | header_variables = header[1] 55 | params = header[0] 56 | # Circuit variables 57 | try: 58 | snubber_resistance = [param.split('=')[1] for param in params if ('R_s2' in param.split('='))][0] 59 | snubber_resistance = float(snubber_resistance.split(',')[0]) 60 | except ValueError: 61 | # ValueError is thrown if R_s2 equals {rn*d} in the LTspice file 62 | try: 63 | snubber_resistance_normalized = [param.split('=')[1] for param in params if ('rn' in param.split('='))][0] 64 | snubber_resistance_normalized = float(snubber_resistance_normalized.split(',')[0]) 65 | damping = [param.split('=')[1] for param in params if ('d' in param.split('='))][0] 66 | damping = float(damping.split(',')[0]) 67 | snubber_resistance = snubber_resistance_normalized * damping 68 | except ValueError: 69 | # ValueError is thrown if damping is very low, e.g. 1n, as this cannot be converted to float 70 | # R_snubber will be set to 0 as snubber is not in use 71 | snubber_resistance = 0 72 | output_current = [param.split('=')[1] for param in params if ('I_out' in param.split('='))][0] 73 | output_current = float(output_current.split(',')[0]) 74 | input_voltage = 600 75 | time = data[:, 0] 76 | # Final output list 77 | all_output_info = [] 78 | 79 | # Open file to print 80 | f = open(file_path[:-4] + '_analysis.txt', 'w+') 81 | f.write('Variables: ' + ', '.join(header_variables) + '\n') 82 | f.write(' '.join(params[3:10]) + '\n' + ' '.join(params[10:]) + '\n') 83 | 84 | # Perform analysis for upper and lower part of half-bridge 85 | for i in range(len(data[0,1:])/3): 86 | col = 3*i + 1 87 | 88 | # Find voltage switching times 89 | rise_v, fall_v, rise_found, fall_found = calculate_switching_times(data[:, col], input_voltage) 90 | if rise_found: 91 | rise_time_voltage = (time[rise_v[0]], time[rise_v[1]]) 92 | else: 93 | rise_time_voltage = (0,0) 94 | if fall_found: 95 | fall_time_voltage = (time[fall_v[0]], time[fall_v[1]]) 96 | else: 97 | print time[fall_v[0]] 98 | print time[fall_v[1]] 99 | fall_time_voltage = (0,0) 100 | 101 | # Find current switching times 102 | rise_i, fall_i, rise_found, fall_found = calculate_switching_times(data[:, col+1], output_current) 103 | if rise_found: 104 | rise_time_current = (time[rise_i[0]], time[rise_i[1]]) 105 | else: 106 | rise_time_current = (0,0) 107 | if fall_found: 108 | fall_time_current = (time[fall_i[0]], time[fall_i[1]]) 109 | else: 110 | fall_time_current = (0,0) 111 | 112 | # Get overshoots 113 | voltage_undershoot, voltage_overshoot = calculate_overshoots(data[:, col]) 114 | current_undershoot, current_overshoot = calculate_overshoots(data[:, col+1]) 115 | max_current = max([abs(current_overshoot), abs(current_undershoot)]) 116 | 117 | # Calculate ringing and damping ratio 118 | try: 119 | ringing_freq, decay_ratio = calculate_ringing(time, data[:, col], input_voltage) 120 | period = np.where(time < time[0] + 1/ringing_freq)[0][-1] 121 | except IndexError: 122 | ringing_freq = 0 123 | decay_ratio = 0 124 | period = 50 125 | ringing_freq = ringing_freq / 10**6 # [MHz] 126 | decay_ratio = decay_ratio / 10**6 # [MHz] 127 | 128 | # Calculate turn-on power loss 129 | start = min([rise_i[0], fall_v[0]]) 130 | stop = max([rise_i[1], fall_v[1]]) 131 | start -= 2*(stop-start) 132 | delta = 0 133 | while True: 134 | delta += 100 135 | if max(data[stop+delta:stop+delta+3*period, col+1]) < output_current*1.05: 136 | break 137 | stop+=delta 138 | E_turnon = calc_switch_loss(time[start:stop], data[start:stop, col], data[start:stop, col+1]) *10**3 #[mJ] 139 | E_turnon_snubber = np.multiply(snubber_resistance, calc_switch_loss(time[start:stop], data[start:stop, col+2], data[start:stop, col+2])) *10**3 #[mJ] 140 | 141 | # Calculate turn-off power loss 142 | start = min([fall_i[0], rise_v[0]]) 143 | stop = max([fall_i[1], rise_v[1]]) 144 | start -= 2*(stop-start) 145 | delta = 0 146 | while True: 147 | delta += 100 148 | if max(data[stop+delta:stop+delta+3*period, col]) < input_voltage*1.05: 149 | break 150 | stop+=delta 151 | E_turnoff = calc_switch_loss(time[start:stop], data[start:stop, col], data[start:stop, col+1]) *10**3 #[mJ] 152 | E_turnoff_snubber = np.multiply(snubber_resistance, calc_switch_loss(time[start:stop], data[start:stop, col+2], data[start:stop, col+2])) *10**3 #[mJ] 153 | 154 | 155 | # Write outdata 156 | f.write(['\n------------- Upper transistor ------------- \n\n', '\n------------- Lower transistor ------------- \n\n'][i]) 157 | rise_time_voltage = (rise_time_voltage[1] - rise_time_voltage[0]) * 10**9 # [ns] 158 | fall_time_voltage = (fall_time_voltage[1] - fall_time_voltage[0]) * 10**9 # [ns] 159 | rise_time_current = (rise_time_current[1] - rise_time_current[0]) * 10**9 # [ns] 160 | fall_time_current = (fall_time_current[1] - fall_time_current[0]) * 10**9 # [ns] 161 | all_output_info.extend([rise_time_voltage, fall_time_voltage, rise_time_current, fall_time_current]) 162 | all_output_info.extend([E_turnon, E_turnoff, E_turnon_snubber, E_turnoff_snubber]) 163 | all_output_info.extend([voltage_overshoot, max_current]) 164 | all_output_info.extend([ringing_freq, decay_ratio]) 165 | f.write('Voltage Rise time [ns]: ' + str(rise_time_voltage) + '\n') 166 | f.write('Voltage Fall time [ns]: ' + str(fall_time_voltage) + '\n') 167 | f.write('Current Rise time [ns]: ' + str(rise_time_current) + '\n') 168 | f.write('Current Fall time [ns]: ' + str(fall_time_current) + '\n') 169 | f.write('Turn-on loss [mJ]: ' + str(E_turnon) + '\n') 170 | f.write('Turn-off loss [mJ]: ' + str(E_turnoff) + '\n') 171 | f.write('Turn-on loss snubbers [mJ]: ' + str(E_turnon_snubber) + '\n') 172 | f.write('Turn-off loss snubbers [mJ]: ' + str(E_turnoff_snubber) + '\n') 173 | f.write('Voltage overshoot [V]: ' + str(voltage_overshoot) + '\n') 174 | f.write('Largest current [A]: ' + str(max_current) + '\n') 175 | f.write('Ringing frequency [MHz]: ' + str(ringing_freq) + '\n') 176 | f.write('Damping ratio [M 1/s]: ' + str(decay_ratio) + '\n') 177 | 178 | # Write all info on one final line 179 | f.write('\n') 180 | f.write('\t'.join([str(i) for i in all_output_info]) + '\n') 181 | 182 | # Close file 183 | f.close() 184 | 185 | def analyze_data_single(time, current, voltage, output_current, input_voltage, snubber_voltage, snubber_resistance): 186 | 187 | # Convert data to np objects 188 | time = np.array(time) 189 | current = np.array(current) 190 | voltage = np.array(voltage) 191 | 192 | # Circuit variables 193 | snubber_voltage = np.array(snubber_voltage) 194 | 195 | # Final output list 196 | all_output_info = [] 197 | 198 | 199 | # Perform analysis for upper and lower part of half-bridge 200 | 201 | # Find voltage switching times 202 | rise_v, fall_v, rise_found, fall_found = calculate_switching_times(voltage, input_voltage, 'Voltage') 203 | if rise_found: 204 | rise_time_voltage = (time[rise_v[0]], time[rise_v[1]]) 205 | else: 206 | rise_time_voltage = (0,0) 207 | if fall_found: 208 | fall_time_voltage = (time[fall_v[0]], time[fall_v[1]]) 209 | else: 210 | fall_time_voltage = (0,0) 211 | 212 | # Find current switching times 213 | rise_i, fall_i, rise_found, fall_found = calculate_switching_times(current, output_current, 'Current') 214 | if rise_found: 215 | rise_time_current = (time[rise_i[0]], time[rise_i[1]]) 216 | else: 217 | rise_time_current = (0,0) 218 | if fall_found: 219 | fall_time_current = (time[fall_i[0]], time[fall_i[1]]) 220 | else: 221 | fall_time_current = (0,0) 222 | 223 | # Switching times indexes 224 | switching_times_indexes = list(rise_v) 225 | switching_times_indexes.extend(list(fall_v)) 226 | switching_times_indexes.extend(list(rise_i)) 227 | switching_times_indexes.extend(list(fall_i)) 228 | switching_times_indexes = [int(i) for i in switching_times_indexes] 229 | 230 | # Get overshoots 231 | voltage_undershoot, voltage_overshoot = calculate_overshoots(voltage) 232 | current_undershoot, current_overshoot = calculate_overshoots(current) 233 | max_current = max([abs(current_overshoot), abs(current_undershoot)]) 234 | 235 | # Calculate ringing and damping ratio 236 | try: 237 | ringing_freq, decay_ratio = calculate_ringing(time, voltage, input_voltage) 238 | period = np.where(time < time[0] + 1/ringing_freq)[0][-1] 239 | except IndexError: 240 | ringing_freq = 0 241 | decay_ratio = 0 242 | period = 50 243 | ringing_freq = ringing_freq / 10**6 # [MHz] 244 | decay_ratio = decay_ratio / 10**6 # [MHz] 245 | 246 | # Calculate turn-on power loss 247 | start = min([rise_i[0], fall_v[0]]) 248 | stop = max([rise_i[1], fall_v[1]]) 249 | start -= (stop-start) 250 | start = max(1,start) 251 | delta = 0 252 | while True: 253 | delta += 100 254 | if max(voltage[stop+delta:stop+delta+3*period]) < input_voltage*0.03: 255 | break 256 | stop+=delta 257 | E_turnon = calc_switch_loss(time[start:stop], voltage[start:stop], current[start:stop]) *10**3 #[mJ] 258 | power_loss_intervals = [start, stop] 259 | E_snubber_on = np.multiply(1/snubber_resistance, calc_switch_loss(time[start:stop], snubber_voltage[start:stop], snubber_voltage[start:stop])) *10**3 #[mJ] 260 | 261 | # Calculate turn-off power loss 262 | start = min([fall_i[0], rise_v[0]]) 263 | stop = max([fall_i[1], rise_v[1]]) 264 | start -= (stop-start) 265 | start = max(1,start) 266 | delta = 0 267 | while True: 268 | delta += 100 269 | if max(voltage[stop+delta:stop+delta+3*period]) < input_voltage*1.03: 270 | break 271 | stop+=delta 272 | E_turnoff = calc_switch_loss(time[start:stop], voltage[start:stop], current[start:stop]) *10**3 #[mJ] 273 | power_loss_intervals.extend([start, stop]) 274 | E_snubber_off = np.multiply(1/snubber_resistance, calc_switch_loss(time[start:stop], snubber_voltage[start:stop], snubber_voltage[start:stop])) *10**3 #[mJ] 275 | 276 | # Add power loss intervals indexes to output_current 277 | power_loss_intervals = [int(i) for i in power_loss_intervals] 278 | switching_times_indexes.extend(power_loss_intervals) 279 | 280 | 281 | # Write outdata 282 | rise_time_voltage = (rise_time_voltage[1] - rise_time_voltage[0]) * 10**9 # [ns] 283 | fall_time_voltage = (fall_time_voltage[1] - fall_time_voltage[0]) * 10**9 # [ns] 284 | rise_time_current = (rise_time_current[1] - rise_time_current[0]) * 10**9 # [ns] 285 | fall_time_current = (fall_time_current[1] - fall_time_current[0]) * 10**9 # [ns] 286 | all_output_info.extend([switching_times_indexes]) 287 | all_output_info.extend([rise_time_voltage, fall_time_voltage, rise_time_current, fall_time_current]) 288 | all_output_info.extend([E_turnon, E_turnoff]) 289 | all_output_info.extend([voltage_overshoot, max_current]) 290 | all_output_info.extend([ringing_freq, decay_ratio]) 291 | all_output_info.extend([E_snubber_on, E_snubber_off]) 292 | 293 | return all_output_info 294 | 295 | 296 | # ------- Analysis functions ------- # 297 | 298 | def local_extrema(data): 299 | local_max = np.where(np.r_[True, data[1:] > data[:-1]] & np.r_[data[:-1] > data[1:], True])[0] 300 | local_min = np.where(np.r_[True, data[1:] < data[:-1]] & np.r_[data[:-1] < data[1:], True])[0] 301 | return (local_min, local_max) 302 | 303 | def calculate_switching_times(data, nominal_value, measurement_type): 304 | 305 | 306 | mid_point = len(data) / 2 307 | if data[mid_point] > nominal_value/2: 308 | start_rise = np.where(data[0:mid_point] > 0.1*nominal_value)[0][0] 309 | stop_rise = np.where(data[0:mid_point] > 0.9*nominal_value)[0][0] 310 | rise = (start_rise, stop_rise) 311 | start_fall = mid_point + np.where(data[mid_point:] < 0.9*nominal_value)[0][0] 312 | stop_fall = mid_point + np.where(data[mid_point:] < 0.1*nominal_value)[0][0] 313 | fall = (start_fall, stop_fall) 314 | else: 315 | start_rise = mid_point + np.where(data[mid_point:] > 0.1*nominal_value)[0][0] 316 | stop_rise = mid_point + np.where(data[mid_point:] > 0.9*nominal_value)[0][0] 317 | rise = (start_rise, stop_rise) 318 | start_fall = np.where(data[0:mid_point] < 0.9*nominal_value)[0][0] 319 | stop_fall = np.where(data[0:mid_point] < 0.1*nominal_value)[0][0] 320 | fall = (start_fall, stop_fall) 321 | 322 | try: 323 | return (rise, fall, True, True) # Return 324 | except UnboundLocalError: 325 | # Some value not found 326 | print measurement_type + ' error: unable to find rise or fall time. Zero is returned.' 327 | return ((0,0), (0,0), False, False) 328 | 329 | 330 | def calc_switch_loss(time, voltage, current): 331 | E = 0 332 | for i in range(len(voltage)-1): 333 | E += (time[i+1] - time[i]) / 2.0 * (voltage[i]*current[i] + voltage[i+1]*current[i+1]) 334 | return E 335 | 336 | def calculate_overshoots(data): 337 | return (min(data), max(data)) 338 | 339 | def calculate_ringing(time, data, nominal_value): 340 | # Ringing frequency calculation 341 | number_of_peaks = 8 342 | peak = max(data) 343 | first_peak = np.where(data == peak)[0][0] 344 | index_of_peaks = local_extrema(data[first_peak:])[1] 345 | index_of_peaks = index_of_peaks[0:number_of_peaks] # Pick the first peaks, as defined 346 | index_of_peaks = np.add(index_of_peaks, first_peak) # Shift peaks to correct indexes 347 | index_of_peaks_temp = [] 348 | for peak_index in index_of_peaks: 349 | if data[peak_index] > nominal_value: 350 | index_of_peaks_temp.append(peak_index) 351 | index_of_peaks = index_of_peaks_temp 352 | peak_times = time[index_of_peaks] 353 | number_of_peaks = len(index_of_peaks) 354 | time_diff = [peak_times[i+1] - peak_times[i] for i in range(number_of_peaks-1)] 355 | ringing_freq = 1/np.mean(time_diff) # Calculate frequency based on avg time diff between peaks 356 | # Dacay ratio calculation 357 | voltage_peaks = data[index_of_peaks] - nominal_value 358 | decay_ratios = [] 359 | for i in range(number_of_peaks - 1): 360 | alpha = np.log(voltage_peaks[i+1] / voltage_peaks[i]) / time_diff[i] 361 | decay_ratios.append(alpha) 362 | decay_ratio = (-1) * np.mean(decay_ratios) 363 | return (ringing_freq, decay_ratio) 364 | 365 | def calculate_switching_times_alternative(data, nominal_value, measurement_type): 366 | 367 | going_up = False 368 | going_down = False 369 | 370 | fall = [0,0] 371 | rise = [0,0] 372 | found_rise = False 373 | found_fall = False 374 | 375 | data_point_prev = data[0] 376 | for step, data_point in enumerate(data[1:]): 377 | if data_point > 0.1 * nominal_value and data_point_prev < 0.1 * nominal_value: 378 | if not going_up: 379 | rise[0] = step 380 | going_up = True 381 | going_down = False 382 | if data_point > 0.9 * nominal_value and data_point_prev < 0.9 * nominal_value: 383 | if going_up: 384 | rise[1] = step 385 | found_rise = True 386 | if going_down: 387 | rise[0] = 0 388 | going_up = False 389 | going_down = False 390 | if data_point < 0.9 * nominal_value and data_point_prev > 0.9 * nominal_value: 391 | if not going_down: 392 | fall[0] = step 393 | going_up = False 394 | going_down = True 395 | if data_point < 0.1 * nominal_value and data_point_prev > 0.1 * nominal_value: 396 | if going_down: 397 | fall[1] = step 398 | found_fall = True 399 | if going_up: 400 | fall[0] = 0 401 | going_up = False 402 | going_down = False 403 | 404 | if found_fall and found_rise: 405 | break 406 | 407 | data_point_prev = data_point 408 | 409 | try: 410 | return (rise, fall, True, True) # Return 411 | except UnboundLocalError: 412 | # Some value not found 413 | print measurement_type + ' error: unable to find rise or fall time. Zero is returned.' 414 | return ((0,0), (0,0), False, False) 415 | --------------------------------------------------------------------------------