diff --git a/DCNumPro_1.0.py b/DCNumPro_1.0.py new file mode 100644 index 0000000000000000000000000000000000000000..9c9917ea33d44dab7e46df67c46ccb0cea659596 --- /dev/null +++ b/DCNumPro_1.0.py @@ -0,0 +1,4132 @@ +""" Configuration start """ +# Perform on cluster? False = no, True = yes +cluster = False +# Graphically display particles in 3D? False = no, True = yes +display_particles = False +# Final test for overlaps? False = no, True = yes +final_test = True +# Display initial distribution vs. distribution of placed particles? False = no, True = yes +display_distribution = True +# Display detailed progress: status of particle at each step? False = no, True = yes +# If False, only overall percentage progress will be displayed +display_detailed_progress = False +# Display solver warnings? False = no, True = yes +display_solver_warnings = False +# Output Excel file +name_excel_file = 'Test_300' +# Number of particles +n_particles_sim = 500 +# Number of classes +n_classes = 10 +# Median +mu = 500 +# Standard deviation +sigma = 130 +# Estimated hold-up +holdup_est = 0.6 +# Random seed +seed = 5 +# Number of cores for multiprocessing. 0 = automated determination of available cores +n_cores = 6 +# Display particle within its cell? False = no, True = yes +# Is used for debugging or illustriation +display_particle_cell = False +# Specify holdup steps +holdup_delta = 0.025 +# Stepsize for random loose packing simulation +stepsize = 1 +""" Configuration end """ +""" Import packages """ +import os +import time +import sys +import math +import random +import xlwt +import warnings +import multiprocessing +import numpy as np +import pandas as pd +import sympy as sy +import scipy.stats as stats +import matplotlib.pyplot as plt +from scipy.integrate import quad +from scipy.stats import norm +from scipy.stats import lognorm +from cycler import cycler +from functools import reduce +from scipy.optimize import fsolve +from scipy.optimize import minimize +from tess import Container +from functools import partial +from scipy.spatial import Delaunay +from scipy.spatial import ConvexHull +from mpl_toolkits.mplot3d.art3d import Poly3DCollection +from tqdm import tqdm +from collections import defaultdict +from collections import Counter +""" Simulation starts from here """ +# Solver warning messages +if display_solver_warnings == False: + warnings.filterwarnings("ignore") +# If calculation on cluster overwrite parameters above +# Specify parameter in the .job file +if cluster == True: + import argparse + parser = argparse.ArgumentParser(description='Train Mask R-CNN to detect particles.') + parser.add_argument('--name_excel_file', required=True) + parser.add_argument('--n_particles_sim', required=True, type=int) + parser.add_argument('--n_classes', required=True, type=int) + parser.add_argument('--holdup_est', required=True, type=float) + parser.add_argument('--seed', required=True, type=int) + parser.add_argument('--n_cores', required=True, type=int) + parser.add_argument('--holdup_delta', required=True, type=float) + parser.add_argument('--stepsize', required=True, type=float) + parser.add_argument('--mu', required=True, type=float) + parser.add_argument('--sigma', required=True, type=float) + args = parser.parse_args() + NAME_EXCEL_FILE = (args.name_excel_file + '.xls') + N_PARTICLES_SIM = args.n_particles_sim + N_CLASSES = args.n_classes + HOLDUP_EST = args.holdup_est + SEED = args.seed + N_CORES = args.n_cores + HOLDUP_DELTA = args.holdup_delta + STEPSIZE = args.stepsize + mu = args.mu + sigma = args.sigma + ROOT_DIR = os.path.join("/rwthfs/rz/cluster", os.path.abspath("../output")) +else: + NAME_EXCEL_FILE = (name_excel_file + '.xls') + N_PARTICLES_SIM = int(n_particles_sim) + N_CLASSES = int(n_classes) + HOLDUP_EST = float(holdup_est) + SEED = int(seed) + N_CORES = int(n_cores) + HOLDUP_DELTA = float(holdup_delta) + STEPSIZE = stepsize + ROOT_DIR = os.path.abspath("../output") + +""" Functions """ +def calc_distribution(N_PARTICLES_SIM, N_CLASSES): + """ + Generate nuber distribution of particles based on the defined distribution parameters mu and sigma calculated from D_32. + Args: + D_32 (list of floats): Sauter mean diameter. + N_PARTICLES_SIM (float): Number of particles. + N_CLASSES (float): Number of classes. + Returns: + d_particles (list of floats): Particle diameters. + """ + # CDF parameter for log-normal DSD + # Mean of log values + mu_log = (np.log((mu**2)/(((sigma**2)+(mu**2))**0.5))) + # Standard deviation of log values + sigma_log = ((np.log(((sigma**2)/(mu**2))+1))**0.5) + # DSD boundaries which are d_05 and d_95 corresponding to the 5th and 95th percentile of the DSD + d_05, d_95 = calc_DSD_boundaries(mu_log, sigma_log) + # Class interval + delta_class = (d_95-d_05)/N_CLASSES + # Class diameters + d_classes = np.linspace((d_05+delta_class/2), (d_95-delta_class/2), N_CLASSES) + # Determine minimum and maximum class diameter + d_min = np.min(d_classes) + d_max = np.max(d_classes) + # Class boundaries + boundaries = [d_05 + i * delta_class for i in range(N_CLASSES+1)] + # Class sizes + class_sizes, q0_initial, q3_initial, volume_total = calc_class_sizes(mu_log, sigma_log, boundaries, N_PARTICLES_SIM, d_classes) + # Sauter mean diameter + D_32 = np.sum(q0_initial * d_classes**3) / np.sum(q0_initial * d_classes**2) + # Actual number of particles + n_particles_init = int(np.sum(class_sizes)) + # Create array q0 [class_sizes, d_classes] + q0 = np.concatenate((class_sizes.reshape(-1,1), d_classes.reshape(-1,1)), axis=1) + + return q0, n_particles_init, q0_initial, q3_initial, d_classes, d_min, d_max, volume_total, d_05, d_95, delta_class, D_32 + +def calc_DSD_boundaries(mu, sigma): + """ + Calculate DSD boundaries d_05 and d_95 corresponding to the 5th and 95th percentile of the DSD. + Args: + mu (float): Mean of the logarithm of the values. + sigma (float): Standard deviation of the logarithm of the values. + Returns: + d_min (float): Minimum diameter. + d_max (float): Maximum diameter. + """ + # Find the z-value corresponding to the 5th and 95th percentile of the standard normal distribution + d_05_percentile = norm.ppf(0.05) + d_95_percentile = norm.ppf(0.95) + # Calculate the 5th and 95th percentile of the log-normal distribution + d_05 = np.exp(mu + sigma * d_05_percentile) + d_95 = np.exp(mu + sigma * d_95_percentile) + + return d_05, d_95 + +def calc_class_sizes(mu, sigma, boundaries, N_PARTICLES_SIM, d_classes): + """ + Calculate class sizes for a log-normal distribution with parameters mu and sigma. + Args: + mu (float): Mean of the logarithm of the values. + sigma (float): Standard deviation of the logarithm of the values. + boundaries (list of floats): Boundaries of the classes. + Returns: + class_sizes (list of floats): Quantity for each class defined by the boundaries. + """ + # Ensure boundaries are sorted + boundaries = sorted(boundaries) + # Create a log-normal distribution object + # Scale parameter for lognorm is sigma + # Scale parameter exp(mu) for shifting the distribution + dist = lognorm(s=sigma, scale=np.exp(mu)) + # Calculate probabilities for each interval + q3_initial = [] + for i in range(len(boundaries) - 1): + lower_bound = boundaries[i] + upper_bound = boundaries[i + 1] + lb = dist.cdf(lower_bound) + ub = dist.cdf(upper_bound) + # Probability that a value falls within the interval [lower_bound, upper_bound] + prob = dist.cdf(upper_bound) - dist.cdf(lower_bound) + # Calculate class sizes + q3_initial.append(prob) + q3_initial = np.array(q3_initial) + # Convert class volume probabilities into number probabilities + q0_initial = q3_initial / d_classes**3 + # Normalize number probabilities. + # It ignores that the class volume probabilities include only 90vol% of the distribution. + # However, since we use number probabilities to distribute a certain number of particles + # within these considered 90vol%, this calculation is valid + q0_initial /= np.sum(q0_initial) + # Distribute the nubmer of particles to the classes considering number probabilities and round + class_sizes = N_PARTICLES_SIM * np.array(q0_initial) + np.round(class_sizes, decimals=0, out=class_sizes) + # Total particle volume + volume_total = np.sum(class_sizes * (math.pi * d_classes**3 / 6)) + + return class_sizes, q0_initial, q3_initial, volume_total + +def calc_particles(q0, n_particles_init): + """ + Fill an array with particles from number distribution q0 and determine their drop coordinates randomly. + Args: + q0 (array): Number distribution. + n_particles_init (float): Actualnumber of particles. + Returns: + M_particles_initial (array): Array with particles and their drop coordinates + """ + # Array columns: + # 0: x-coordinate, 1: y-coordinate, 2: z-coordinate, 3: index, 4: diameter, 5: class number (0 to n) + M_particles_initial = np.zeros(shape=(int(n_particles_init), 6)) + # Assign particle diameters and class numbers + j = 0 + for k in range(len(q0)): + n = int(q0[k, 0]) + for i in range(n): + M_particles_initial[j, 4] = q0[k, 1] + M_particles_initial[j, 5] = k + j = j + 1 + # Assign indices and randomly generated coordinates + for i in range(len(M_particles_initial)): + M_particles_initial[i, 0] = random.uniform(-A_0 + (M_particles_initial[i, 4]) / 2, A_0 - (M_particles_initial[i, 4]) / 2) + M_particles_initial[i, 1] = random.uniform(-A_0 + (M_particles_initial[i, 4]) / 2, A_0 - (M_particles_initial[i, 4]) / 2) + M_particles_initial[i, 2] = None + M_particles_initial[i, 3] = i + np.random.shuffle(M_particles_initial) + + return M_particles_initial + +def particle_info(M_particles, index): + # x-coordinate + x = M_particles[index, 0] + # y-coordinate + y = M_particles[index, 1] + # z-coordinate + z = M_particles[index, 2] + # Index + p_index = M_particles[index, 3] + # Diameter + p_diameter = M_particles[index, 4] + # Class + p_class = int(M_particles[index, 5]) + + return x, y, z, p_index, p_diameter, p_class + +def find_z_drop(y, x, M_particle_steady, test_range, d_max_steady): + """ Determine drop position of particles """ + # Define testing area + y_min = y - test_range + y_max = y + test_range + x_min = x - test_range + x_max = x + test_range + # Check particles in steady state for interraction with testing area: + # in y direction + particle_to_y = np.argwhere(np.logical_and(M_particle_steady[:, 2] > y_min, M_particle_steady[:, 2] < y_max)) + # in x direction + particle_to_x = np.argwhere(np.logical_and(M_particle_steady[:, 3] > x_min, M_particle_steady[:, 3] < x_max)) + # Combine the results of both tests (arrays with indices of particles which interract with testing area) + particle_to_check = reduce(np.intersect1d, (particle_to_y, particle_to_x)) + # Convert array to list + particle_to_check = np.array(particle_to_check).tolist() + # Create list with z-coordinates of interracting particles + z_list = [] + for i in particle_to_check: + value = M_particle_steady[i, 1] + z_list.append(value) + # Check list for entries and pick the max value + if not z_list: + max_z_value = 0 + else: + max_z_value = max(z_list) + # Calculate dropping height + z = (max_z_value + d_max_steady)*1.2 + + return z + +def calc_overlap(d_i, z_i, y_i, x_i, M_particle_steady, test_range): + """ + Function that checks overlaps of an unstable particle. + It also double check the collision criteria of the calc_sedimentation function. + If calc_sedimentation function works properly, only one overlapping should be identified. + """ + # Variable to identifiycollisions with walls + walls = 0 + # Counting variable for number of collisions with steady state particles + n_overlaps = 0 + # Array with indices of particles in steady state which overlap with the considered particle + overlaps = [] + """ Define region of interrest """ + z_min = z_i - test_range + if z_min < 0: + z_min = 0 + z_max = z_i + test_range + y_min = y_i - test_range + y_max = y_i + test_range + x_min = x_i - test_range + x_max = x_i + test_range + """ Checking particles within the testing range for possible overlappings """ + # Arrays in x, y and z direction with indices of particles, which could overlap with the considered particle + x_array = np.argwhere(np.logical_and(M_particle_steady[:, 3] > x_min, M_particle_steady[:, 3] < x_max)) + y_array = np.argwhere(np.logical_and(M_particle_steady[:, 2] > y_min, M_particle_steady[:, 2] < y_max)) + z_array = np.argwhere(np.logical_and(M_particle_steady[:, 1] > z_min, M_particle_steady[:, 1] < z_max)) + # Combine the arrays + particle_to_check = reduce(np.intersect1d, (x_array, y_array, z_array)) + # Convert array to list + particle_to_check = np.array(particle_to_check).tolist() + """ Check each particle that may overlap for an actual overlapping """ + for i in particle_to_check: + # Retrieve information about particles that may overlap with the particle under consideration from the steady state array + # Coordinates + z_j = float(M_particle_steady[i, 1]) + y_j = float(M_particle_steady[i, 2]) + x_j = float(M_particle_steady[i, 3]) + # Diameter + d_j = float(M_particle_steady[i, 5]) + # distance between the particle's centers when they collide + dist_required = (d_i + d_j) / 2 + # Actual distance between the particle's centers + distance_particles = math.sqrt((z_i-z_j)**2 + (y_i-y_j)**2 + (x_i-x_j)**2) + # Check whether particles overlap and store the overlapping particle in array + if dist_required - distance_particles > 1e-9: + overlaps.append(i) + # Remove duplicates from a list + overlaps = list(set(overlaps)) + # Determine number of collisions + n_overlaps = len(overlaps) + """ Now checking for overlappings with walls """ + # Wall in positive x direction + if (x_i + d_i / 2) >= A_0: + walls = 1 + # Wall in negative x direction + elif (x_i - d_i / 2) <= -A_0: + walls = -1 + # Wall in positive y direction + elif (y_i + d_i / 2) >= A_0: + walls = 2 + # Wall in negative y direction + elif (y_i - d_i / 2) <= -A_0: + walls = -2 + # Wall in positive x direction and positive y direction + elif (x_i + d_i / 2) >= A_0 and (y_i + d_i / 2) >= A_0: + walls = 3 + # Wall in negative x direction and positive y direction + elif (x_i - d_i / 2) <= -A_0 and (y_i + d_i / 2) >= A_0: + walls = -3 + # Wall in positive x direction and negative y direction + elif (x_i + d_i / 2) >= A_0 and (y_i - d_i / 2) <= -A_0: + walls = 4 + # Wall in negative x direction and negaive y direction + elif (x_i - d_i / 2) <= -A_0 and (y_i - d_i / 2) <= -A_0: + walls = -4 + + return n_overlaps, overlaps, walls + +def position_update1(y_i, x_i, d_i, M_particle_steady, overlaps): + """ Function that updates the position of the considered particle overlapping with one steady state particle. """ + # Coordinates of the steady state particle + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Diameter of the steady state particle + d1 = float(M_particle_steady[overlaps[0], 5]) + # Required distance between the centers of the considered particle and the steady state particle + dist_1i = (d1 + d_i) / 2 + # Actual distance between the centers of the considered particle and the steady state particle in x and y coordinate + delta_x = x_i - x1 + delta_y = y_i - y1 + dist_1i_current = np.sqrt(delta_x**2 + delta_y**2) + # Calculate the scaling factor + scaling_factor = dist_1i / dist_1i_current + # Calculate the new position + x = x1 + delta_x * scaling_factor + y = y1 + delta_y * scaling_factor + + return y, x + +def position_update2(z_i, y_i, x_i, d_i, M_particle_steady, overlaps): + """ + Function to update the position of the considered particle if it overlaps with two particles. + There are two mathematical solutions of the new position, but only one logical. + A solver is used to determine these solutions. + First, an intersection circle plane is calculated corresponding to the intersection area of two auxiliary spheres. + The intersection plane is used to identify the guesses of the both solutions of the considered particle's new position. + Moreover, the plane is used to check wheter the solver solutions are correct. + The centers of the auxiliary spheres corresponds to the center coordinates of the both steady state particles. + The radii of the auxiliary spheres corresponds to the sums of the steady state particles' radii and the considered particle's radius. + To identify the logical solution of the both solutions, the distances between the original position and the new positions are calculated. + The new position with the lowest distance to the original position is the logical solution. + """ + # Used in case an error occurs during position update. + error_position_update2 = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Diameter of the first particle in steady state + d1 = float(M_particle_steady[overlaps[0], 5]) + # Center coordinates of the first auxiliary sphere + c1 = np.array([z1, y1, x1]) + # Radius of the first auxiliary sphere + r1 = (d_i + d1) / 2 + # Coordinates of the second particle in steady state + z2 = float(M_particle_steady[overlaps[1], 1]) + y2 = float(M_particle_steady[overlaps[1], 2]) + x2 = float(M_particle_steady[overlaps[1], 3]) + # Diameter of the second particle in steady state + d2 = float(M_particle_steady[overlaps[1], 5]) + # Center coordinates of the second auxiliary sphere + c2 = np.array([z2, y2, x2]) + # Radius of the second auxiliary sphere + r2 = (d_i + d2) / 2 + """ Calculation of the intersection circle plane """ + # Vector between the centers + V_centers_12 = c2 - c1 + # Distance between the centers + dist_centers_12 = np.linalg.norm(V_centers_12) + # Normal vector of the intersection circle plane + V_unit_centers_12 = V_centers_12 / dist_centers_12 + # Distance from the first sphere's center to the intersection circle plane + dist_c1_plane = (r1**2 - r2**2 + dist_centers_12**2) / (2 * dist_centers_12) + # Center of the intersection circle (point on the intersection plane closest to sphere1) + c_ic = c1 + dist_c1_plane * V_unit_centers_12 + """ + Calculation of the reflected position of the considered particle. + First, the vector between the centers of the considered particle and the intersection circle is determined. + Then, this vector is reflected such a way that the projection of this vector on the yx plane is rotated by 180 degrees on the yx plane. + The coordiantes of the reflected position are used as second guess for the solver to determine the second solver solution. + """ + # Vector between the centers of the considered particle and the intersection circle + V_centers_ici = np.array([z_i, y_i, x_i]) - np.array(c_ic) + # Reflected vector + V_reflected = np.array([V_centers_ici[0], -V_centers_ici[1], -V_centers_ici[2]]) + # Reflected position + P_reflected = np.array(c_ic) + V_reflected + # Coordinates for the initial guess + x_01 = x_i + y_01 = y_i + x_02 = P_reflected[2] + y_02 = P_reflected[1] + # Initial guess values + initial_guess1 = [x_01, y_01] + initial_guess2 = [x_02, y_02] + """ + Solver to find possible new positions of the considered particle + The first guess is the logical one. The second is just to determine the second solver solution. + The second solver solution is just to check wheter the first one is the right one + """ + x_s1, y_s1 = fsolve(solver_position2, initial_guess1, args=(c1, c2, r1, r2, z_i)) + x_s2, y_s2 = fsolve(solver_position2, initial_guess2, args=(c1, c2, r1, r2, z_i)) + """ Check wheter the solver found two different solutions or the same twice """ + # Error value is 1 if the solver found the same position twice + if x_s1 == x_s2 and y_s1 == y_s2: + error_position_update2 = 1 + # Exclude values behind the simulation walls + if x_s1 < -A_0 or x_s1 > A_0: + x_s1 = None + if x_s2 < -A_0 or x_s2 > A_0: + x_s2 = None + if y_s1 < -A_0 or y_s1 > A_0: + y_s1 = None + if y_s2 < -A_0 or y_s2 > A_0: + y_s2 = None + """ + Determine the actual new position of the considered particle from the both solver solutions. + Must be the closest position to the original position of the considered particle + """ + # Choose the new position based on the validity of values + if y_s1 == None or x_s1 == None: + if y_s2 != None and x_s2 != None: + x = x_s2 + y = y_s2 + else: + error_position_update2 = 3 + return y_i, x_i, error_position_update2 + elif y_s2 == None or x_s2 == None: + if y_s1 != None or x_s1 != None: + x = x_s1 + y = y_s1 + else: + error_position_update2 = 3 + return y_i, x_i, error_position_update2 + else: + # Vectors between the centers of the considered particle's original position and the new positions + V_centers_new1 = np.array([z_i, y_i, x_i]) - np.array([z_i, y_s1, x_s1]) + V_centers_new2 = np.array([z_i, y_i, x_i]) - np.array([z_i, y_s2, x_s2]) + # Distances between these centers + dist_centers_new1 = np.linalg.norm(V_centers_new1) + dist_centers_new2 = np.linalg.norm(V_centers_new2) + # Choose new position based on the shortest distance + if dist_centers_new1 <= dist_centers_new2: + x = x_s1 + y = y_s1 + else: + x = x_s2 + y = y_s2 + """ + Check wheter the new positions' coordinates are located on the intersection plane + Just double checking wheter the solver found correct solutions + The equation of the intersection plane is Az+By+Cx+D=0, where A, B, C and D are the equation coefficients. + """ + # Determine the plane equation coefficients + # The normal vector to the plane is the same as the V_centers_12 + coeff_A, coeff_B, coeff_C = V_unit_centers_12 + # Calculate D using the point on the plane + coeff_D = -(coeff_A * c_ic[0] + coeff_B * c_ic[1] + coeff_C * c_ic[2]) + # Check whether the new positions of considered particle is located on the intersection plane + on_plane = check_point_on_plane(z_i, y, x, coeff_A, coeff_B, coeff_C, coeff_D) + # Error value is 2 if the position of the first solver solution is not located on the intersection plane + if on_plane == False: + error_position_update2 = 2 + + return y, x, error_position_update2 + +def solver_position2(params, c1, c2, r1, r2, z_i): + """ Solver to calculate the new position of the considered particle if it overlaps with two particles """ + # Solver variables + x, y = params + # Equation of a circle. The circles coresponds here to the auxiliary spheres + eq1 = (x - c1[2])**2 + (y - c1[1])**2 + (z_i - c1[0])**2 - r1**2 + eq2 = (x - c2[2])**2 + (y - c2[1])**2 + (z_i - c2[0])**2 - r2**2 + return [eq1, eq2] + +def check_point_on_plane(z_i, y, x, coeff_A, coeff_B, coeff_C, coeff_D): + """ Function that checks whether a point lies on a plane or not (plane equation in coordinate form) """ + # Calculate the left-hand side of the plane equation + check_zero = z_i*coeff_A + y*coeff_B + x*coeff_C + coeff_D + # Check if the left-hand side is approximately zero (account for floating-point precision) + on_plane = abs(check_zero) < 1e-9 + return on_plane + +def position_update3(z_i, y_i, x_i, d_i, M_particle_steady, overlaps): + """ + Function to update the position of the considered particle if it overlaps with three particles. + There are two mathematical solutions of the new position, but only one logical. + A solver is used to determine these solutions. + First, a triangle plane is calculated which crosses the centers of the three steady state particles. + Moreover, the centroid of the triangle is determined. + The triangle plane is used to identify the guesses of the both solutions of the considered particle's new position. + It is tested whether the both solutions differ and if they are correct. + To identify the logical solution of the both solutions, the z-coordinates of the both soultions and the centroid are compared. + The new position with the z-coordinate above the centroid is the logical solution. + """ + # Used in case an error occurs during position update. + error_position_update3 = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Diameter of the first particle in steady state + d1 = float(M_particle_steady[overlaps[0], 5]) + # Center coordinates of the first auxiliary sphere + c1 = np.array([z1, y1, x1]) + # Radius of the first auxiliary sphere + r1 = (d_i + d1) / 2 + # Coordinates of the second particle in steady state + z2 = float(M_particle_steady[overlaps[1], 1]) + y2 = float(M_particle_steady[overlaps[1], 2]) + x2 = float(M_particle_steady[overlaps[1], 3]) + # Diameter of the second particle in steady state + d2 = float(M_particle_steady[overlaps[1], 5]) + # Center coordinates of the second auxiliary sphere + c2 = np.array([z2, y2, x2]) + # Radius of the second auxiliary sphere + r2 = (d_i + d2) / 2 + # Coordinates of the third particle in steady state + z3 = float(M_particle_steady[overlaps[2], 1]) + y3 = float(M_particle_steady[overlaps[2], 2]) + x3 = float(M_particle_steady[overlaps[2], 3]) + # Diameter of the third particle in steady state + d3 = float(M_particle_steady[overlaps[2], 5]) + # Center coordinates of the third auxiliary sphere + c3 = np.array([z3, y3, x3]) + # Radius of the third auxiliary sphere + r3 = (d_i + d3) / 2 + """ Calculation of the triangle's plane """ + # Calculate vectors AB and AC + AB = np.array([z2 - z1, y2 - y1, x2 - x1]) + AC = np.array([z3 - z1, y3 - y1, x3 - x1]) + # Compute the normal vector to the plane (cross product of AB and AC) + V_triangle_norm = np.cross(AB, AC) + A, B, C = V_triangle_norm + # Calculate D using the plane equation Ax + By + Cz + D = 0 + # Using point A (x1, y1, z1) to find D + D = -(A * z1 + B * y1 + C * x1) + """ + Calculation of the reflected position of the considered particle. + First, the distance between the center of the considered particle and the triangle's plane is determined. + Then, the position of the considered particle is reflected about the triangle's plane + The coordiantes of the reflected position are used as second guess for the solver to determine the second solver solution. + """ + # Calculate the distance from the considered particle's center to the plane + distance_pi = (A * z_i + B * y_i + C * x_i + D) / np.linalg.norm(V_triangle_norm) + # Calculate the reflection point on the plane + P_reflected = np.array([z_i, y_i, x_i]) - 2 * distance_pi * V_triangle_norm / np.linalg.norm(V_triangle_norm) + """ Solver to find possible new position of the considered particle """ + # Coordinates for the initial guess + x_01 = x_i + y_01 = y_i + z_01 = z_i + x_02 = P_reflected[2] + y_02 = P_reflected[1] + z_02 = P_reflected[0] + # Initial guess values + initial_guess1 = [x_01, y_01, z_01] + initial_guess2 = [x_02, y_02, z_02] + # Solver + x_s1, y_s1, z_s1 = fsolve(solver_position3, initial_guess1, args=(c1, c2, c3, r1, r2, r3)) + x_s2, y_s2, z_s2 = fsolve(solver_position3, initial_guess2, args=(c1, c2, c3, r1, r2, r3)) + """ Check wheter the solver found two different solutions or the same twice """ + # Error value is 1 if the solver found the same position twice + if x_s1 == x_s2 and y_s1 == y_s2 and z_s1 == z_s2: + error_position_update3 = 1 + # Exclude values behind the simulation walls + if x_s1 < -A_0 or x_s1 > A_0: + x_s1 = None + if x_s2 < -A_0 or x_s2 > A_0: + x_s2 = None + if y_s1 < -A_0 or y_s1 > A_0: + y_s1 = None + if y_s2 < -A_0 or y_s2 > A_0: + y_s2 = None + """ + Determine the actual new position of the considered particle from the both solver solutions. + Must be the closest position to the original position of the considered particle + """ + # Z-coordinate of polygon's centroid + z_cen = (z1 + z2 + z3) / 3 + # If the center of the considered particle is located above the centroid, + # the coordinates of the first solver solution are assigned to the center of the considered particle + if x_s1 != None and y_s1 != None and z_s1 > z_cen: + z = z_s1 + y = y_s1 + x = x_s1 + # If the center of the considered particle is located above the centroid, + # the coordinates of the second solver solution are assigned to the center of the considered particle + elif x_s2 != None and y_s2 != None and z_s2 > z_cen: + z = z_s2 + y = y_s2 + x = x_s2 + else: + error_position_update3 = 2 + return z_i, y_i, x_i, error_position_update3 + """ + Check wheter the distances between the centers of the three steady state particles + and the new center position of the considered particle are in the samerange as the sums of their radii. + """ + same_range = check_distances3(r1, r2, r3, c1, c2, c3, z, y, x) + # Error value is 2 if the distance between the steady state particles and the position of the solver solution + # is not in the same range as the sums of their radii + if same_range == False: + error_position_update3 = 3 + return z_i, y_i, x_i, error_position_update3 + + return z, y, x, error_position_update3 + +def solver_position3(params, c1, c2, c3, r1, r2, r3): + """ Solver to calculate the new position of the considered particle if it overlaps with three particles """ + # Solver variables + x, y, z = params + # Equation of a circle. The circles coresponds here to the auxiliary spheres + eq1 = (x - c1[2])**2 + (y - c1[1])**2 + (z - c1[0])**2 - r1**2 + eq2 = (x - c2[2])**2 + (y - c2[1])**2 + (z - c2[0])**2 - r2**2 + eq3 = (x - c3[2])**2 + (y - c3[1])**2 + (z - c3[0])**2 - r3**2 + return [eq1, eq2, eq3] + +def check_distances3(r1, r2, r3, c1, c2, c3, z_s, y_s, x_s): + """ Function that checks wheter the distances between the centers of the three steady state particles + and the new center position of the considered particle are in the same range as the sums of their radii. """ + # True if distances are in the same range as the sums of radii + same_range = True + # Vectors between the centers of the steady state particles and the new center positions of the considered particle + V_centers_1i = np.array([z_s, y_s, x_s]) - c1 + V_centers_2i = np.array([z_s, y_s, x_s]) - c2 + V_centers_3i = np.array([z_s, y_s, x_s]) - c3 + # Distance between the centers of the steady state particles and the new center position of the considered particle + dist_centers_1i = np.linalg.norm(V_centers_1i) + dist_centers_2i = np.linalg.norm(V_centers_2i) + dist_centers_3i = np.linalg.norm(V_centers_3i) + # Check if the left-hand side is approximately zero (account for floating-point precision) + if abs(dist_centers_1i-r1) > 1e-9 or abs(dist_centers_2i-r2) > 1e-9 or abs(dist_centers_3i-r3) > 1e-9: + same_range = False + + return same_range + +def check_steadystate3(z_i, y_i, x_i, M_particle_steady, overlaps): + """ + Function to check the steady state. + As soon as considered particle collides with three steady state particles, a steady state check is performed. + Considered particle is in steady state if its center lies within the y-x-plane of a triangle located between the + steady state particle centers. + Barycentric coordinate method is used to determine if the center of the considered particle lies within the polygon. + Moreover, barycentric coordinates are used to determine the subdomain of the triangle where the center lies. + Based on the subdomain, the particle rolling direction is identified. + """ + # Used in case an error occurs during steadystate check. + error_check_steadystate3 = 0 + # Variable for steady state check condition + steady_check = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Coordinates of the second particle in steady state + z2 = float(M_particle_steady[overlaps[1], 1]) + y2 = float(M_particle_steady[overlaps[1], 2]) + x2 = float(M_particle_steady[overlaps[1], 3]) + # Coordinates of the third particle in steady state + z3 = float(M_particle_steady[overlaps[2], 1]) + y3 = float(M_particle_steady[overlaps[2], 2]) + x3 = float(M_particle_steady[overlaps[2], 3]) + # Z-coordinate of triangle's centroid + z_cen = (z1 + z2 + z3) / 3 + # Check whether the center of the considered particle is located above the centroid + # Double checking at this point, since already handled in the position_update3 function. + # Error value is 1 when the center is located below the centroid + if z_i < z_cen: + error_check_steadystate3 = 1 + return steady_check, overlaps, error_check_steadystate3 + """ Barycentric coordinate technique """ + # Denominator + denominator = ((y2 - y3) * (x1 - x3) + (x3 - x2) * (y1 - y3)) + # Check whether the triangle points are collinear + # Error value is 2 when the triangle points are collinear + if denominator == 0: + error_check_steadystate3 = 2 + return steady_check, overlaps, error_check_steadystate3 + # Barycentric coordinates + a = ((y2 - y3) * (x_i - x3) + (x3 - x2) * (y_i - y3)) / denominator + b = ((y3 - y1) * (x_i - x3) + (x1 - x3) * (y_i - y3)) / denominator + c = 1 - a - b + # Determine the subdomain of the triangle and thus wheter it is in steady state or in which direction it is rolling + if (0 <= a <= 1) and (0 <= b <= 1) and (0 <= c <= 1): + steady_check = 3 + elif a < 0 and b >= 0 and c >= 0: + # Rolling along steady state particle Nr.2 and Nr.3 + overlaps = np.delete(overlaps, 0) + steady_check = 2 + elif b < 0 and a >= 0 and c >= 0: + # Rolling along steady state particle Nr.1 and Nr.3 + overlaps = np.delete(overlaps, 1) + steady_check = 2 + elif c < 0 and a >= 0 and b >= 0: + # Rolling along steady state particle Nr.1 and Nr.2 + overlaps = np.delete(overlaps, 2) + steady_check = 2 + elif a < 0 and b < 0 and c > 1: + # Rolling along steady state particle Nr.3 + overlaps = np.delete(overlaps, [0, 1]) + steady_check = 1 + elif a < 0 and c < 0 and b > 1: + # Rolling along steady state particle Nr.2 + overlaps = np.delete(overlaps, [0, 2]) + steady_check = 1 + elif b < 0 and c < 0 and a > 1: + # Rolling along steady state particle Nr.1 + overlaps = np.delete(overlaps, [1, 2]) + steady_check = 1 + else: + # Error value is 3 when the subdomain of the triangle could not be determined correctly + error_check_steadystate3 = 3 + + return steady_check, overlaps, error_check_steadystate3 + +def position_update1w(z_i, y_i, x_i, d_i, M_particle_steady, overlaps, walls): + """ + Function to update the position of the considered particle if it overlaps with one particle and one wall. + There are two mathematical solutions of the new position, but only one logical. + A solver is used to determine these solutions. + First, an intersection circle plane is calculated corresponding to the intersection area of one auxiliary sphere and the wall. + The intersection plane is used to identify the guesses of the both solutions of the considered particle's new position. + Moreover, the plane is used to check wheter the solver solutions are correct. + The center of the auxiliary sphere corresponds to the center coordinates of the steady state particle. + The radius of the auxiliary sphere corresponds to the sums of the steady state particle's radius and the considered particle's radius. + To identify the logical solution of the both solutions, the distances between the original position and the new positions are calculated. + The new position with the lowest distance to the original position is the logical solution. + """ + # Used in case an error occurs during position update. + error_position_update1w = 0 + # Coordinates of the particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Diameter of the particle in steady state + d1 = float(M_particle_steady[overlaps[0], 5]) + # Center coordinates of the auxiliary sphere + c1 = np.array([z1, y1, x1]) + # Radius of the auxiliary sphere + r1 = (d_i + d1) / 2 + # Coordinates of the wall + z_w1 = z1 + if walls == 1: + x_w1 = A_0 + y_w1 = y1 + elif walls == -1: + x_w1 = -A_0 + y_w1 = y1 + elif walls == 2: + x_w1 = x1 + y_w1 = A_0 + elif walls == -2: + x_w1 = x1 + y_w1 = -A_0 + # Center coordinates of the wall + c_w1 = np.array([z_w1, y_w1, x_w1]) + # Distance between wall and center of considered particle if they are in contact + dist_wi = d_i / 2 + """ Calculation of the intersection circle plane """ + # Vector between the centers + V_centers_1w = c_w1 - c1 + # Distance between the centers + dist_centers_1w = np.linalg.norm(V_centers_1w) + # Normal vector of the intersection circle plane + V_unit_centers_1w = V_centers_1w / dist_centers_1w + # Distance from the first sphere's center to the intersection circle plane + dist_c1_plane = dist_centers_1w - dist_wi + # Center of the intersection circle (point on the intersection plane closest to sphere1) + c_ic = c1 + dist_c1_plane * V_unit_centers_1w + """ + Calculation of the reflected position of the considered particle. + First, the vector between the centers of the considered particle and the intersection circle is determined. + Then, this vector is reflected such a way that the projection of this vector on the yx plane is rotated by 180 degrees on the yx plane. + The coordiantes of the reflected position are used as second guess for the solver to determine the second solver solution. + """ + # Vector between the centers of the considered particle and the intersection circle + V_centers_ici = np.array([z_i, y_i, x_i]) - np.array(c_ic) + # Reflected vector + V_reflected = np.array([V_centers_ici[0], -V_centers_ici[1], -V_centers_ici[2]]) + # Reflected position + P_reflected = np.array(c_ic) + V_reflected + # Coordinates for the initial guess + x_01 = x_i + y_01 = y_i + x_02 = P_reflected[2] + y_02 = P_reflected[1] + # If contact is with X-wall + if walls == 1 or walls == -1: + # Initial guess values + initial_guess1 = y_01 + initial_guess2 = y_02 + # X-coordinate of the new position + if walls == 1: + x_s = A_0 - d_i / 2 + elif walls == -1: + x_s = -A_0 + d_i / 2 + """ + Solver to find possible new positions of the considered particle + The first guess is the logical one. The second is just to determine the second solver solution. + The second solver solution is just to check wheter the first one is the right one + """ + y_s1 = fsolve(solver_position1w, initial_guess1, args=(c1, r1, z_i, x_s, walls)) + y_s2 = fsolve(solver_position1w, initial_guess2, args=(c1, r1, z_i, x_s, walls)) + y_s1 = y_s1[0] + y_s2 = y_s2[0] + """ Check wheter the solver found two different solutions or the same twice """ + # Error value is 1 if the solver found the same position twice + if y_s1 == y_s2: + error_position_update1w = 1 + # Exclude values behind the simulation walls + if y_s1 < -A_0 or y_s1 > A_0: + y_s1 = None + if y_s2 < -A_0 or y_s2 > A_0: + y_s2 = None + """ + Determine the actual new position of the considered particle from the both solver solutions. + Must be the closest position to the original position of the considered particle + """ + # Coose the new position based on the validity of values + if y_s1 == None and y_s2 != None: + y = y_s2 + elif y_s2 == None and y_s1 != None: + y = y_s1 + elif y_s1 == None and y_s2 == None: + error_position_update1w = 3 + return y_i, x_i, error_position_update1w + else: + # Vectors between the centers of the considered particle's original position and the new positions + V_centers_new1 = np.array([z_i, y_i, x_i]) - np.array([z_i, y_s1, x_s]) + V_centers_new2 = np.array([z_i, y_i, x_i]) - np.array([z_i, y_s2, x_s]) + # Distances between these centers + dist_centers_new1 = np.linalg.norm(V_centers_new1) + dist_centers_new2 = np.linalg.norm(V_centers_new2) + # Choose new position based on the shortest distance + if dist_centers_new1 <= dist_centers_new2: + y = y_s1 + else: + y = y_s2 + x = x_s + # If contact is with Y-wall + elif walls == 2 or walls == -2: + # Initial guess values + initial_guess1 = x_01 + initial_guess2 = x_02 + # Y-coordinate of the new position + if walls == 2: + y_s = A_0 - d_i / 2 + elif walls == -2: + y_s = -A_0 + d_i / 2 + """ + Solver to find possible new positions of the considered particle + The first guess is the logical one. The second is just to determine the second solver solution. + The second solver solution is just to check wheter the first one is the right one + """ + x_s1 = fsolve(solver_position1w, initial_guess1, args=(c1, r1, z_i, y_s, walls)) + x_s2 = fsolve(solver_position1w, initial_guess2, args=(c1, r1, z_i, y_s, walls)) + x_s1 = x_s1[0] + x_s2 = x_s2[0] + """ Check wheter the solver found two different solutions or the same twice """ + # Error value is 1 if the solver found the same position twice + if x_s1 == x_s2: + error_position_update1w = 1 + # Exclude values behind the simulation walls + if x_s1 < -A_0 or x_s1 > A_0: + x_s1 = None + if x_s2 < -A_0 or x_s2 > A_0: + x_s2 = None + """ + Determine the actual new position of the considered particle from the both solver solutions. + Must be the closest position to the original position of the considered particle + """ + # Coose the new position based on the validity of values + if x_s1 == None and x_s2 != None: + x = x_s2 + elif x_s2 == None and x_s1 != None: + x = x_s1 + elif x_s1 == None and x_s2 == None: + error_position_update1w = 3 + return y_i, x_i, error_position_update1w + else: + # Vectors between the centers of the considered particle's original position and the new positions + V_centers_new1 = np.array([z_i, y_i, x_i]) - np.array([z_i, y_s, x_s1]) + V_centers_new2 = np.array([z_i, y_i, x_i]) - np.array([z_i, y_s, x_s2]) + # Distances between these centers + dist_centers_new1 = np.linalg.norm(V_centers_new1) + dist_centers_new2 = np.linalg.norm(V_centers_new2) + # Choose new position based on the shortest distance + if dist_centers_new1 <= dist_centers_new2: + x = x_s1 + else: + x = x_s2 + # Assign x-coordinate + y = y_s + """ + Check wheter the new positions' coordinates are located on the intersection plane + Just double checking wheter the solver found correct solutions + The equation of the intersection plane is Az+By+Cx+D=0, where A, B, C and D are the equation coefficients. + """ + # Determine the plane equation coefficients + # The normal vector to the plane is the same as the V_centers_12 + coeff_A, coeff_B, coeff_C = V_unit_centers_1w + # Calculate D using the point on the plane + coeff_D = -(coeff_A * c_ic[0] + coeff_B * c_ic[1] + coeff_C * c_ic[2]) + # Check whether the new positions of considered particle is located on the intersection plane + on_plane = check_point_on_plane(z_i, y, x, coeff_A, coeff_B, coeff_C, coeff_D) + # Error value is 2 if the position of the first solver solution is not located on the intersection plane + if on_plane == False: + error_position_update1w = 2 + return y_i, x_i, error_position_update1w + + return y, x, error_position_update1w + +def solver_position1w(params, c1, r1, z_i, par, walls): + """ Solver to calculate the new position of the considered particle if it overlaps with one particle and one wall """ + if walls == 1 or walls == -1: + # Solver variables + y = params + # Equation of a circle. The circles coresponds here to the auxiliary spheres + eq1 = (par - c1[2])**2 + (y - c1[1])**2 + (z_i - c1[0])**2 - r1**2 + return eq1 + elif walls == 2 or walls == -2: + # Solver variables + x = params + # Equation of a circle. The circles coresponds here to the auxiliary spheres + eq1 = (x - c1[2])**2 + (par - c1[1])**2 + (z_i - c1[0])**2 - r1**2 + return eq1 + +def position_update2w(z_i, y_i, x_i, d_i, M_particle_steady, overlaps, walls): + """ + Function to update the position of the considered particle if it overlaps with two particles and one wall. + There are two mathematical solutions of the new position, but only one logical. + A solver is used to determine these solutions. + First, a triangle plane is calculated which crosses the centers of the two steady state particles and the wall. + Moreover, the centroid of the triangle is determined. + The triangle plane is used to identify the guesses of the both solutions of the considered particle's new position. + It is tested whether the both solutions differ and if they are correct. + To identify the logical solution of the both solutions, the z-coordinates of the both soultions and the centroid are compared. + The new position with the z-coordinate above the centroid is the logical solution. + """ + # Used in case an error occurs during position update. + error_position_update2w = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Diameter of the first particle in steady state + d1 = float(M_particle_steady[overlaps[0], 5]) + # Center coordinates of the first auxiliary sphere + c1 = np.array([z1, y1, x1]) + # Radius of the first auxiliary sphere + r1 = (d_i + d1) / 2 + # Coordinates of the second particle in steady state + z2 = float(M_particle_steady[overlaps[1], 1]) + y2 = float(M_particle_steady[overlaps[1], 2]) + x2 = float(M_particle_steady[overlaps[1], 3]) + # Diameter of the second particle in steady state + d2 = float(M_particle_steady[overlaps[1], 5]) + # Center coordinates of the second auxiliary sphere + c2 = np.array([z2, y2, x2]) + # Radius of the second auxiliary sphere + r2 = (d_i + d2) / 2 + # Coordinates of the wall + z_w1 = (z1+z2)/2 + if walls == 1: + x_w1 = A_0 + y_w1 = (y1+y2)/2 + elif walls == -1: + x_w1 = -A_0 + y_w1 = (y1+y2)/2 + elif walls == 2: + x_w1 = (x1+x2)/2 + y_w1 = A_0 + elif walls == -2: + x_w1 = (x1+x2)/2 + y_w1 = -A_0 + """ Calculation of the triangle's plane """ + # Calculate vectors AB and AC + AB = np.array([z2 - z1, y2 - y1, x2 - x1]) + AC = np.array([z_w1 - z1, y_w1 - y1, x_w1 - x1]) + # Compute the normal vector to the plane (cross product of AB and AC) + V_triangle_norm = np.cross(AB, AC) + A, B, C = V_triangle_norm + # Calculate D using the plane equation Ax + By + Cz + D = 0 + # Using point A (x1, y1, z1) to find D + D = -(A * z1 + B * y1 + C * x1) + """ + Calculation of the reflected position of the considered particle. + First, the distance between the center of the considered particle and the triangle's plane is determined. + Then, the position of the considered particle is reflected about the triangle's plane + The coordiantes of the reflected position are used as second guess for the solver to determine the second solver solution. + """ + # Calculate the distance from the considered particle's center to the plane + distance_pi = (A * z_i + B * y_i + C * x_i + D) / np.linalg.norm(V_triangle_norm) + # Calculate the reflection point on the plane + P_reflected = np.array([z_i, y_i, x_i]) - 2 * distance_pi * V_triangle_norm / np.linalg.norm(V_triangle_norm) + """ Solver to find possible new position of the considered particle """ + # Coordinates for the initial guess + x_01 = x_i + y_01 = y_i + z_01 = z_i + x_02 = P_reflected[2] + y_02 = P_reflected[1] + z_02 = P_reflected[0] + # If contact is with X-wall + if walls == 1 or walls == -1: + # Initial guess values + initial_guess1 = [y_01, z_01] + initial_guess2 = [y_02, z_02] + # X-coordinate of the new position + if walls == 1: + x_s1 = A_0 - d_i / 2 + x_s2 = A_0 - d_i / 2 + elif walls == -1: + x_s1 = -A_0 + d_i / 2 + x_s2 = -A_0 + d_i / 2 + # Solver + y_s1, z_s1 = fsolve(solver_position2w, initial_guess1, args=(c1, c2, r1, r2, x_s1, walls)) + y_s2, z_s2 = fsolve(solver_position2w, initial_guess2, args=(c1, c2, r1, r2, x_s2, walls)) + """ Check wheter the solver found two different solutions or the same twice """ + # Error value is 1 if the solver found the same position twice + if y_s1 == y_s2 and z_s1 == z_s2: + error_position_update2w = 1 + # Exclude values behind the simulation walls + if y_s1 < -A_0 or y_s1 > A_0: + y_s1 = None + if y_s2 < -A_0 or y_s2 > A_0: + y_s2 = None + # If contact is with Y-wall + if walls == 2 or walls == -2: + # Initial guess values + initial_guess1 = [x_01, z_01] + initial_guess2 = [x_02, z_02] + # X-coordinate of the new position + if walls == 2: + y_s1 = A_0 - d_i / 2 + y_s2 = A_0 - d_i / 2 + elif walls == -2: + y_s1 = -A_0 + d_i / 2 + y_s2 = -A_0 + d_i / 2 + # Solver + x_s1, z_s1 = fsolve(solver_position2w, initial_guess1, args=(c1, c2, r1, r2, y_s1, walls)) + x_s2, z_s2 = fsolve(solver_position2w, initial_guess2, args=(c1, c2, r1, r2, y_s2, walls)) + """ Check wheter the solver found two different solutions or the same twice """ + # Error value is 1 if the solver found the same position twice + if x_s1 == x_s2 and z_s1 == z_s2: + error_position_update2w = 1 + # Exclude values behind the simulation walls + if x_s1 < -A_0 or x_s1 > A_0: + y_s1 = None + if x_s2 < -A_0 or x_s2 > A_0: + x_s2 = None + """ + Determine the actual new position of the considered particle from the both solver solutions. + Must be the closest position to the original position of the considered particle + """ + # Z-coordinate of triangles's centroid + z_cen = (z1 + z2 + z_w1) / 3 + # If the center of the considered particle is located above the centroid, + # the coordinates of the first solver solution are assigned to the center of the considered particle + if x_s1 != None and y_s1 != None and z_s1 > z_cen: + z = z_s1 + y = y_s1 + x = x_s1 + # If the center of the considered particle is located above the centroid, + # the coordinates of the second solver solution are assigned to the center of the considered particle + elif x_s2 != None and y_s2 != None and z_s2 > z_cen: + z = z_s2 + y = y_s2 + x = x_s2 + else: + error_position_update2w = 2 + return z_i, y_i, x_i, error_position_update2w + """ + Check wheter the distances between the centers of the three steady state particles + and the new center position of the considered particle are in the samerange as the sums of their radii. + """ + same_range = check_distances2w(r1, r2, c1, c2, d_i, z, y, x, walls) + # Error value is 2 if the distance between the steady state particles and the position of the solver solution + # is not in the same range as the sums of their radii + if same_range == False: + error_position_update2w = 3 + return z_i, y_i, x_i, error_position_update2w + + return z, y, x, error_position_update2w + +def solver_position2w(params, c1, c2, r1, r2, par, walls): + """ Solver to calculate the new position of the considered particle if it overlaps with two particles and one wall """ + if walls == 1 or walls == -1: + # Solver variables + y, z = params + # Equation of a circle. The circles coresponds here to the auxiliary spheres + eq1 = (par - c1[2])**2 + (y - c1[1])**2 + (z - c1[0])**2 - r1**2 + eq2 = (par - c2[2])**2 + (y - c2[1])**2 + (z - c2[0])**2 - r2**2 + return [eq1, eq2] + elif walls == 2 or walls == -2: + # Solver variables + x, z = params + # Equation of a circle. The circles coresponds here to the auxiliary spheres + eq1 = (x - c1[2])**2 + (par - c1[1])**2 + (z - c1[0])**2 - r1**2 + eq2 = (x - c2[2])**2 + (par - c2[1])**2 + (z - c2[0])**2 - r2**2 + return [eq1, eq2] + +def check_distances2w(r1, r2, c1, c2, d_i, z_s, y_s, x_s, walls): + """ Function that checks wheter the distances between the centers of the two steady state particles, the wall, + and the new center position of the considered particle are in the same range as the sums of their radii. """ + # True if distances are in the same range as the sums of radii + same_range = True + # Vectors between the centers of the steady state particles and the new center positions of the considered particle + V_centers_1i = np.array([z_s, y_s, x_s]) - c1 + V_centers_2i = np.array([z_s, y_s, x_s]) - c2 + # Distance between the centers of the steady state particles and the new center position of the considered particle + dist_centers_1i = np.linalg.norm(V_centers_1i) + dist_centers_2i = np.linalg.norm(V_centers_2i) + # Distance between the center of the considered particle and the wall + if walls == 1 or walls == -1: + dist_wi = A_0 - abs(x_s) + elif walls == 2 or walls == -2: + dist_wi = A_0 - abs(y_s) + # Check if the left-hand side is approximately zero (account for floating-point precision) + if abs(dist_centers_1i-r1) > 1e-9 or abs(dist_centers_2i-r2) > 1e-9 or abs(dist_wi-d_i/2) > 1e-9: + same_range = False + + return same_range + +def check_steadystate2w(z_i, y_i, x_i, particle_i, M_particle_steady, overlaps, walls): + """ + Function to check the steady state. + As soon as considered particle collides with two steady state particles and one wall, a steady state check is performed. + Considered particle is in steady state if its center lies within the y-x-plane of a triangle located between the + steady state particle centers and the wall. + Barycentric coordinate method is used to determine if the center of the considered particle lies within the polygon. + Moreover, barycentric coordinates are used to determine the subdomain of the triangle where the center lies. + Based on the subdomain, the particle rolling direction is identified. + """ + # Used in case an error occurs during steadystate check. + error_check_steadystate3 = 0 + # Variable for steady state check condition + steady_check = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Coordinates of the second particle in steady state + z2 = float(M_particle_steady[overlaps[1], 1]) + y2 = float(M_particle_steady[overlaps[1], 2]) + x2 = float(M_particle_steady[overlaps[1], 3]) + # Coordinates of the wall + z_w1 = z_i + if walls == 1: + x_w1 = A_0 + y_w1 = y_i + elif walls == -1: + x_w1 = -A_0 + y_w1 = y_i + elif walls == 2: + x_w1 = x_i + y_w1 = A_0 + elif walls == -2: + x_w1 = x_i + y_w1 = -A_0 + # Z-coordinate of triangle's centroid + z_cen = (z1 + z2 + z_w1) / 3 + # Check whether the center of the considered particle is located above the centroid + # Double checking at this point, since already handled in the position_update3 function. + # Error value is 1 when the center is located below the centroid + if z_i < z_cen: + error_check_steadystate3 = 1 + return steady_check, overlaps, error_check_steadystate3 + """ Barycentric coordinate technique """ + # Denominator + denominator = ((y2 - y_w1) * (x1 - x_w1) + (x_w1 - x2) * (y1 - y_w1)) + # Check whether the triangle points are collinear + # Error value is 2 when the triangle points are collinear + if denominator == 0: + error_check_steadystate3 = 2 + return steady_check, overlaps, error_check_steadystate3 + # Barycentric coordinates + a = ((y2 - y_w1) * (x_i - x_w1) + (x_w1 - x2) * (y_i - y_w1)) / denominator + b = ((y_w1 - y1) * (x_i - x_w1) + (x1 - x_w1) * (y_i - y_w1)) / denominator + c = 1 - a - b + # Determine the subdomain of the triangle and thus wheter it is in steady state or in which direction it is rolling + if (0 <= a <= 1) and (0 <= b <= 1) and (0 <= c <= 1): + steady_check = 3 + elif a < 0 and b >= 0 and c >= 0: + # Rolling along steady state particle Nr.2 and wall + overlaps = np.delete(overlaps, 0) + steady_check = 4 + elif b < 0 and a >= 0 and c >= 0: + # Rolling along steady state particle Nr.1 and wall + overlaps = np.delete(overlaps, 1) + steady_check = 4 + elif c < 0 and a >= 0 and b >= 0: + # Rolling along steady state particle Nr.1 and Nr.2 + steady_check = 2 + elif a < 0 and c < 0 and b > 1: + # Rolling along steady state particle Nr.2 + overlaps = np.delete(overlaps, 0) + steady_check = 1 + elif b < 0 and c < 0 and a > 1: + # Rolling along steady state particle Nr.1 + overlaps = np.delete(overlaps, 1) + steady_check = 1 + else: + # Error value is 3 when the subdomain of the triangle could not be determined correctly + error_check_steadystate3 = 3 + + return steady_check, overlaps, error_check_steadystate3 + +def position_update1w2(z_i, y_i, x_i, d_i, M_particle_steady, overlaps, walls): + """ + Function to update the position of the considered particle if it overlaps with one particle and two walls. + There are two mathematical solutions of the new position, but only one logical. + A solver is used to determine these solutions. + First, a triangle plane is calculated which crosses the center of the steady state particle and the walls. + Moreover, the centroid of the triangle is determined. + The triangle plane is used to identify the guesses of the both solutions of the considered particle's new position. + It is tested whether the both solutions differ and if they are correct. + To identify the logical solution of the both solutions, the z-coordinates of the both soultions and the centroid are compared. + The new position with the z-coordinate above the centroid is the logical solution. + """ + # Used in case an error occurs during position update. + error_position_update1w2 = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Diameter of the first particle in steady state + d1 = float(M_particle_steady[overlaps[0], 5]) + # Center coordinates of the first auxiliary sphere + c1 = np.array([z1, y1, x1]) + # Radius of the first auxiliary sphere + r1 = (d_i + d1) / 2 + # Coordinates of the wall + z_w1 = z1 + z_w2 = z1 + if walls == 3: + x_w1 = A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = A_0 + elif walls == -3: + x_w1 = -A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = A_0 + elif walls == 4: + x_w1 = A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = -A_0 + elif walls == -4: + x_w1 = -A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = -A_0 + """ Calculation of the triangle's plane """ + # Calculate vectors AB and AC + AB = np.array([z_w2 - z1, y_w2 - y1, x_w2 - x1]) + AC = np.array([z_w1 - z1, y_w1 - y1, x_w1 - x1]) + # Compute the normal vector to the plane (cross product of AB and AC) + V_triangle_norm = np.cross(AB, AC) + A, B, C = V_triangle_norm + # Calculate D using the plane equation Ax + By + Cz + D = 0 + # Using point A (x1, y1, z1) to find D + D = -(A * z1 + B * y1 + C * x1) + """ + Calculation of the reflected position of the considered particle. + First, the distance between the center of the considered particle and the triangle's plane is determined. + Then, the position of the considered particle is reflected about the triangle's plane + The coordiantes of the reflected position are used as second guess for the solver to determine the second solver solution. + """ + # Calculate the distance from the considered particle's center to the plane + distance_pi = (A * z_i + B * y_i + C * x_i + D) / np.linalg.norm(V_triangle_norm) + # Calculate the reflection point on the plane + P_reflected = np.array([z_i, y_i, x_i]) - 2 * distance_pi * V_triangle_norm / np.linalg.norm(V_triangle_norm) + """ Solver to find possible new position of the considered particle """ + # Coordinates for the initial guess + z_01 = z_i + z_02 = P_reflected[0] + # Initial guess values + initial_guess1 = z_01 + initial_guess2 = z_02 + # X and y-coordinates of the new position + if walls == 3: + x = A_0 - d_i / 2 + y = A_0 - d_i / 2 + elif walls == -3: + x = -A_0 + d_i / 2 + y = A_0 - d_i / 2 + elif walls == 4: + x = A_0 - d_i / 2 + y = -A_0 + d_i / 2 + elif walls == -4: + x = -A_0 + d_i / 2 + y = -A_0 + d_i / 2 + # Solver + z_s1 = fsolve(solver_position1w2, initial_guess1, args=(c1, r1, x, y)) + z_s2 = fsolve(solver_position1w2, initial_guess2, args=(c1, r1, x, y)) + z_s1 = z_s1[0] + z_s2 = z_s2[0] + """ Check wheter the solver found two different solutions or the same twice """ + # Error value is 1 if the solver found the same position twice + if z_s1 == z_s2: + error_position_update1w2 = 1 + """ + Determine the actual new position of the considered particle from the both solver solutions. + Must be the closest position to the original position of the considered particle + """ + # Z-coordinate of polygon's centroid + z_cen = z1 + # If the center of the considered particle is located above the centroid, + # the coordinates of the first solver solution are assigned to the center of the considered particle + if z_s1 > z_cen: + z = z_s1 + # If the center of the considered particle is located above the centroid, + # the coordinates of the second solver solution are assigned to the center of the considered particle + elif z_s2 > z_cen: + z = z_s2 + else: + error_position_update1w2 = 2 + return z_i, y_i, x_i, error_position_update1w2 + """ + Check wheter the distances between the centers of the three steady state particles + and the new center position of the considered particle are in the samerange as the sums of their radii. + """ + same_range = check_distances1w2(r1, c1, d_i, z, y, x) + # Error value is 2 if the distance between the steady state particles and the position of the solver solution + # is not in the same range as the sums of their radii + if same_range == False: + error_position_update1w2 = 3 + return z_i, y_i, x_i, error_position_update1w2 + + return z, y, x, error_position_update1w2 + +def solver_position1w2(params, c1, r1, x_s, y_s): + """ Solver to calculate the new position of the considered particle if it overlaps one particle and two walls """ + # Solver variables + z = params + # Equation of a circle. The circles coresponds here to the auxiliary spheres + eq1 = (x_s - c1[2])**2 + (y_s - c1[1])**2 + (z - c1[0])**2 - r1**2 + return eq1 + +def check_distances1w2(r1, c1, d_i, z_s, y_s, x_s): + """ Function that checks wheter the distances between the center of one steady state particles, the walls, + and the new center position of the considered particle are in the same range as the sums of their radii. """ + # True if distances are in the same range as the sums of radii + same_range = True + # Vectors between the centers of the steady state particles and the new center positions of the considered particle + V_centers_1i = np.array([z_s, y_s, x_s]) - c1 + # Distance between the centers of the steady state particles and the new center position of the considered particle + dist_centers_1i = np.linalg.norm(V_centers_1i) + # Distances between the center of the considered particle and the walls + dist_w1i = A_0 - abs(x_s) + dist_w2i = A_0 - abs(y_s) + # Check if the left-hand side is approximately zero (account for floating-point precision) + if abs(dist_centers_1i-r1) > 1e-9 or abs(dist_w2i-d_i/2) > 1e-9 or abs(dist_w1i-d_i/2) > 1e-9: + same_range = False + + return same_range + +def check_steadystate1w2(z_i, y_i, x_i, M_particle_steady, overlaps, walls): + """ + Function to check the steady state. + As soon as considered particle collides with one steady state particle and two walls, a steady state check is performed. + Considered particle is in steady state if its center lies within the y-x-plane of a triangle located between the + steady state particle center and the walls. + Barycentric coordinate method is used to determine if the center of the considered particle lies within the polygon. + Moreover, barycentric coordinates are used to determine the subdomain of the triangle where the center lies. + Based on the subdomain, the particle rolling direction is identified. + """ + # Used in case an error occurs during steadystate check. + error_check_steadystate1w2 = 0 + # Variable for steady state check condition + steady_check = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Coordinates of the wall + if walls == 3: + x_w1 = A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = A_0 + elif walls == -3: + x_w1 = -A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = A_0 + elif walls == 4: + x_w1 = A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = -A_0 + elif walls == -4: + x_w1 = -A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = -A_0 + # Z-coordinate of triangle's centroid + z_cen = z1 + # Check whether the center of the considered particle is located above the centroid + # Double checking at this point, since already handled in the position_update3 function. + # Error value is 1 when the center is located below the centroid + if z_i < z_cen: + error_check_steadystate1w2 = 1 + return steady_check, overlaps, error_check_steadystate1w2 + """ Barycentric coordinate technique """ + # Denominator + denominator = ((y_w2 - y_w1) * (x1 - x_w1) + (x_w1 - x_w2) * (y1 - y_w1)) + # Check whether the triangle points are collinear + # Error value is 2 when the triangle points are collinear + if denominator == 0: + error_check_steadystate1w2 = 2 + return steady_check, overlaps, error_check_steadystate1w2 + # Barycentric coordinates + a = ((y_w2 - y_w1) * (x_i - x_w1) + (x_w1 - x_w2) * (y_i - y_w1)) / denominator + b = ((y_w1 - y1) * (x_i - x_w1) + (x1 - x_w1) * (y_i - y_w1)) / denominator + c = 1 - a - b + # Determine the subdomain of the triangle and thus wheter it is in steady state or in which direction it is rolling + if (0 <= a <= 1) and (0 <= b <= 1) and (0 <= c <= 1): + steady_check = 3 + elif b < 0 and a >= 0 and c >= 0: + # Rolling along steady state particle Nr.1 and wall Nr.2 + if walls == 3 or walls == -3: + walls = 2 + elif walls == 4 or walls == -4: + walls = -2 + steady_check = 4 + elif c < 0 and a >= 0 and b >= 0: + # Rolling along steady state particle Nr.1 and wall Nr.1 + if walls == 3 or walls == 4: + walls = 1 + elif walls == -3 or walls == -4: + walls = -1 + steady_check = 4 + elif b < 0 and c < 0 and a > 1: + # Rolling along steady state particle Nr.1 + steady_check = 1 + else: + # Error value is 3 when the subdomain of the triangle could not be determined correctly + error_check_steadystate1w2 = 3 + + return steady_check, overlaps, walls, error_check_steadystate1w2 + +def position_update2w2(z_i, y_i, x_i, d_i, M_particle_steady, overlaps, walls): + """ + Function to update the position of the considered particle if it overlaps with two particles and two walls. + There are two mathematical solutions of the new position, but only one logical. + A solver is used to determine these solutions. + First, a polygon plane is calculated which crosses the centers of the two steady state particles and the walls. + Moreover, the centroid of the polygon is determined. + The polygon plane is used to identify the guesses of the both solutions of the considered particle's new position. + It is tested whether the both solutions differ and if they are correct. + To identify the logical solution of the both solutions, the z-coordinates of the both soultions and the centroid are compared. + The new position with the z-coordinate above the centroid is the logical solution. + """ + # Used in case an error occurs during position update. + error_position_update2w2 = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Diameter of the first particle in steady state + d1 = float(M_particle_steady[overlaps[0], 5]) + # Radius of the first auxiliary sphere + r1 = (d_i + d1) / 2 + # Center coordinates of the first auxiliary sphere + c1 = np.array([z1, y1, x1]) + # Coordinates of the second particle in steady state + z2 = float(M_particle_steady[overlaps[1], 1]) + y2 = float(M_particle_steady[overlaps[1], 2]) + x2 = float(M_particle_steady[overlaps[1], 3]) + # Diameter of the second particle in steady state + d2 = float(M_particle_steady[overlaps[1], 5]) + # Radius of the first auxiliary sphere + r2 = (d_i + d2) / 2 + # Center coordinates of the second auxiliary sphere + c2 = np.array([z2, y2, x2]) + # Coordinates of the wall + z_w1 = (z1+z2)/2 + z_w2 = (z1+z2)/2 + """ Solver to find possible new position of the considered particle """ + # Array with the center coordinates and radii of the steady state particles + particles = [ + {'center': c1, 'diameter': d1}, + {'center': c2, 'diameter': d2}] + # Coordinates for the initial guess + x_01 = x_i + y_01 = y_i + z_01 = z_i + # Initial guess values + initial_guess = [x_01, y_01, z_01] + # Bounds + bnds = [(None, None), (-A_0, A_0), (-A_0, A_0)] + # Perform the optimization + result = minimize(solver_position2w2, initial_guess, args=(particles, d_i), method='L-BFGS-B', bounds=bnds, tol=1e-10) + # Extract the coordinates + z_s, y_s, x_s = result.x[:3] + """ + Check wheter the distances between the centers of the four steady state particles + and the new center position of the considered particle are in the samerange as the sums of their radii. + """ + # Check if the left-hand side is approximately zero (account for floating-point precision) + same_range = check_distances2w2(r1, r2, c1, c2, d_i, z_s, y_s, x_s) + # Error value is 1 if the distance between the steay state particles and the position of the first solver solution + # is not in the same range as the sums of their radii + if same_range == False: + error_position_update2w2 = 1 + return z_i, y_i, x_i, error_position_update2w2 + """ + Check whether the new position of the considered particles is located above the centroid of the steady state particles + """ + # Z-coordinate of polygon's centroid + z_cen = (z1 + z2 + z_w1 + z_w2) / 4 + # If the center of the considered particle is located above the centroid, + # the new coordinates are assigned to the center of the considered particle + if z_s > z_cen: + z = z_s + y = y_s + x = x_s + # If the center of the considered particle is located below the centroid, + # the new coordinates are not correct + else: + error_position_update2w2 = 2 + return z_i, y_i, x_i, error_position_update2w2 + + return z, y, x, error_position_update2w2 + +def solver_position2w2(params, particles, d_i): + """ Solver to calculate the new position of the considered particle if it overlaps with two particles and two walls """ + # Center coordinates of the considered particle are the optimizing parameters + c_i = np.array(params[:3]) + # Mini + total_error = 0 + # Calculate the total error between the actual and required distances of the particle centers + for particle in particles: + # Center of the steady state particle + c_j = particle['center'] + # Diameter of the steady state particle + d_j = particle['diameter'] + # Actual distance between the centers of the considered particle and the steady state particle + distance = np.linalg.norm(c_i - c_j) + # Required distance between the centers of the considered particle and the steady state particle + required_distance = (d_i + d_j)/2 + # Minimization function + total_error += 100*(distance - required_distance)/required_distance + # Actual distance between the center of the considered particle and the wall + distance_w1 = A_0 - abs(c_i[2]) + distance_w2 = A_0 - abs(c_i[1]) + # Required distance between the center of the considered particle and the steady state particle + required_distance_w = d_i/2 + # Update minimization function + total_error += 100*abs(distance_w1 - required_distance_w)/required_distance_w + total_error += 100*abs(distance_w2 - required_distance_w)/required_distance_w + + return total_error + +def check_distances2w2(r1, r2, c1, c2, d_i, z_s, y_s, x_s): + """ Function that checks wheter the distances between the centers of the two steady state particles, the walls, + and the new center position of the considered particle are in the same range as the sums of their radii. """ + # True if distances are in the same range as the sums of radii + same_range = True + # Vectors between the centers of the steady state particles and the new center positions of the considered particle + V_centers_1i = np.array([z_s, y_s, x_s]) - c1 + V_centers_2i = np.array([z_s, y_s, x_s]) - c2 + # Distance between the centers of the steady state particles and the new center position of the considered particle + dist_centers_1i = np.linalg.norm(V_centers_1i) + dist_centers_2i = np.linalg.norm(V_centers_2i) + # Distance between the center of the considered particle and the wall + dist_w1i = A_0 - abs(x_s) + dist_w2i = A_0 - abs(y_s) + # Check if the left-hand side is approximately zero (account for floating-point precision) + if abs(dist_centers_1i-r1) > 1e-9 or abs(dist_centers_2i-r2) > 1e-9 or abs(dist_w1i-d_i/2) > 1e-9 or abs(dist_w2i-d_i/2) > 1e-9: + same_range = False + + return same_range + +def check_steadystate2w2(z_i, y_i, x_i, M_particle_steady, overlaps, walls): + """ + Function to check the steady state. + As soon as considered particle collides with two steady state particles and two walls, a steady state check is performed. + Considered particle is in steady state if its center lies within the y-x-plane of a polygon located between the + steady state particle centers and the walls. + Barycentric coordinate method is used to determine if the center of the condsidered particle lies within the polygon. + For this purpose, the polygon is devided into two triangles. + Moreover, barycentric coordinates are used to determine the subdomain of the polygon where the point lies. + Based on the subdomain, the particle rolling direction is identified. + """ + # Used in case an error occurs during steadystate check. + error_check_steadystate2w2 = 0 + # Variable for steady state check condition + steady_check = 0 + # Coordinates of the first particle in steady state + z1 = float(M_particle_steady[overlaps[0], 1]) + y1 = float(M_particle_steady[overlaps[0], 2]) + x1 = float(M_particle_steady[overlaps[0], 3]) + # Coordinates of the second particle in steady state + z2 = float(M_particle_steady[overlaps[1], 1]) + y2 = float(M_particle_steady[overlaps[1], 2]) + x2 = float(M_particle_steady[overlaps[1], 3]) + # Coordinates of the wall + z_w1 = (z1+z2)/2 + z_w2 = (z1+z2)/2 + if walls == 3: + x_w1 = A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = A_0 + elif walls == -3: + x_w1 = -A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = A_0 + elif walls == 4: + x_w1 = A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = -A_0 + elif walls == -4: + x_w1 = -A_0 + y_w1 = y_i + x_w2 = x_i + y_w2 = -A_0 + # Z-coordinate of polygon's centroid + z_cen = (z1 + z2 + z_w1 + z_w2) / 4 + # Check whether the center of the considered particle is located above the centroid + # Double checking at this point, since already handled in the position_update4 function. + # Error value is 1 when the center is located below the centroid + if z_i < z_cen: + error_check_steadystate2w2 = 1 + return steady_check, overlaps, error_check_steadystate2w2 + """ + The polygon is devided into two triangles. + The first triangle is defined by the centers of the steady state particles 1, 2 and 4. 1=A, 2=B, 4=D + The second triangle is defined by the centers of the steady state particles 2, 3 and 4. 2=B, 3=C, 4=D + Barycentric coordinate technique is applied on each triangle + """ + # Denominator of the first triangle + denominator1 = ((y2 - y_w1) * (x1 - x_w1) + (x_w1 - x2) * (y1 - y_w1)) + # Denominator of the second triangle + denominator2 = ((y_w2 - y_w1) * (x2 - x_w1) + (x_w1 - x_w2) * (y2 - y_w1)) + # Check whether the triangle points are collinear + # Error value is 2 when the triangle points are collinear + if denominator1 == 0 or denominator2 == 0: + error_check_steadystate2w2 = 2 + return steady_check, overlaps, error_check_steadystate2w2 + # Barycentric coordinates of the first triangle + a = ((y2 - y_w1) * (x_i - x_w1) + (x_w1 - x2) * (y_i - y_w1)) / denominator1 + b1 = ((y_w1 - y1) * (x_i - x_w1) + (x1 - x_w1) * (y_i - y_w1)) / denominator1 + d1 = 1 - a - b1 + # Barycentric coordinates of the second triangle + b2 = ((y_w2 - y_w1) * (x_i - x_w1) + (x_w1 - x_w2) * (y_i - y_w1)) / denominator2 + c = ((y_w1 - y2) * (x_i - x_w1) + (x2 - x_w1) * (y_i - y_w1)) / denominator2 + d2 = 1 - b2 - c + # Determine the subdomain of the polygon and thus wheter it is in steady state or in which direction it is rolling + if (0 <= a <= 1) and (0 <= b1 <= 1) and (0 <= d1 <= 1): + steady_check = 3 + elif (0 <= b2 <= 1) and (0 <= c <= 1) and (0 <= d2 <= 1): + steady_check = 3 + elif b1 < 0 and d1 < 0: + # Rolling along steady state particle Nr.1 + overlaps = np.delete(overlaps, 1) + steady_check = 1 + elif d1 < 0 and d2 < 0: + # Rolling along steady state particle Nr.2 + overlaps = np.delete(overlaps, 0) + steady_check = 1 + elif d1 < 0 and b1 >= 0 and d2 >= 0: + # Rolling along steady state particle Nr.1 and Nr.2 + steady_check = 2 + elif d2 < 0 and b2 >= 0 and d1 >= 0: + # Rolling along steady state particle Nr.2 and wall Nr.1 + if walls == 3 or walls == 4: + walls = 1 + elif walls == -3 or walls == -4: + walls = -1 + overlaps = np.delete(overlaps, 0) + steady_check = 4 + elif b1 < 0 and d1 >= 0 and b2 >= 0: + # Rolling along steady state particle Nr.1 and wall Nr.2 + if walls == 3 or walls == -3: + walls = 2 + elif walls == 4 or walls == -4: + walls = -2 + overlaps = np.delete(overlaps, 1) + steady_check = 4 + else: + # Error value is 3 when the subdomain of the polygon could not be determined correctly + error_check_steadystate2w2 = 3 + + return steady_check, overlaps, walls, error_check_steadystate2w2 + +def plot_particles(M_particle_steady): + """ Function to graphically display spheres in 3D """ + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + # Center coordinates of the steady state particles + centers = M_particle_steady[:,[1,2,3]] + # Radii + radii = M_particle_steady[:,5]/2 + # Iterate through all spheres + for center, radius in zip(centers, radii): + # Create a grid of points + u = np.linspace(0, 2 * np.pi, 100) + v = np.linspace(0, np.pi, 100) + x = radius * np.outer(np.cos(u), np.sin(v)) + center[2] # x-axis in the (z, y, x) order + y = radius * np.outer(np.sin(u), np.sin(v)) + center[1] # y-axis in the (z, y, x) order + z = radius * np.outer(np.ones(np.size(u)), np.cos(v)) + center[0] # z-axis in the (z, y, x) order + # Plot the surface + ax.plot_surface(x, y, z, color='b', alpha=0.3) + # Set the same limits for x, y, and z axes + ax.set_xlim(-1000, 1000) + ax.set_ylim(-1000, 1000) + ax.set_zlim(0, 500) + # Set the aspect ratio to be equal + ax.set_aspect('equal', adjustable='box') + # Set labels + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_zlabel('Z') + # Show the plot + plt.show() + +def voro_tess(M_particle_tess, tess_start, tess_end, diameters_0): + """ Function to perform Tesselation """ + cntr = Container(M_particle_tess, limits=(tess_start, tess_end), radii=(diameters_0/2), periodic=False) + # Cell volumes + volumes_cells = [round(v.volume(), 3) for v in cntr] + # Cell neighbors + cell_neighbors = [v.neighbors() for v in cntr] + # Nested tuple structure with x,y,z coordinates of the cell face normals in form: + # [[(x,y,z), (x,y,z), ...], [(x,y,z), (x,y,z), ...], ...] + cell_face_normals = [v.normals() for v in cntr] + # Nested tuple structure with x,y,z coordinates of the cell verticies in form: + # [[(x,y,z), (x,y,z), ...], [(x,y,z), (x,y,z), ...], ...] + cell_vertices = [v.vertices() for v in cntr] + # A list of the indices of the vertices of each face. + vertices_indices = [v.face_vertices() for v in cntr] + + return volumes_cells, cell_neighbors, cell_face_normals, cell_vertices, vertices_indices + +def calc_grow(particle_i, volumes_particles, diameters, cell_face_fixed, distances_faces_max, cell_face_normals, vertices_indices, cell_vertices, display_particle_cell, particle_status, cell_status): + # Particle volume + volume_particle_i = volumes_particles[particle_i] + # Particle radius + radius_particle_i = diameters[particle_i] / 2 + # List of fixed faces of the considered particle + cell_face_fixed_i = cell_face_fixed[particle_i] + # Verticies of the cell corresponding to the particle + cell_vertices_i = cell_vertices[particle_i] + # List of maximal distances between particle center and cell faces for the considered particle + distances_faces_max_i = distances_faces_max[particle_i] + # Normal vectors of cell faces corresponding to the particle + cell_face_normals_i = cell_face_normals[particle_i] + # Indices of the vertices of each cell face corresponding to the particle. + vertices_indices_i = vertices_indices[particle_i] + # Cell status + cell_status_i = cell_status[particle_i] + # Initial guess for solver + initial_guess = radius_particle_i + # Bounds + bnds = [(0, np.max(distances_faces_max_i))] + # Solver method + solver_method = 'SLSQP' + if cell_status_i == False: + # Perform the optimization + result = minimize(solver_distances, initial_guess, args=(cell_face_fixed_i, distances_faces_max_i, volume_particle_i, cell_face_normals_i, vertices_indices_i, cell_vertices_i, display_particle_cell, radius_particle_i), method=solver_method, bounds=bnds, tol=1e-3) + # Obtain the value of the minimized function + minimized_value = result.fun + # If value to high, mark particle as invalid + if minimized_value > 1.0: + particle_status[particle_i] = False + # Extract the distance + distance_face_var = result.x[0] + # Iterate through each face of the cell to update the list of fixed cell faces + for face in range(len(cell_face_normals_i)): + if cell_face_fixed[particle_i][face] == True: + continue + else: + # If the distance is less than or equal to the radius of the sphere, the face intersects with the sphere. + # Thus it is set to fixed + if (np.abs(distance_face_var) - distances_faces_max[particle_i][face]) > 1e-9: + cell_face_fixed[particle_i][face] = True + else: + # Iterate through each face of the cell to update the list of fixed cell faces + for face in range(len(cell_face_normals_i)): + if cell_face_fixed[particle_i][face] == True: + continue + else: + cell_face_fixed[particle_i][face] = True + + return cell_face_fixed, particle_status + +def solver_distances(params, cell_face_fixed_i, distances_faces_max_i, volume_particle_i, cell_face_normals_i, vertices_indices_i, cell_vertices_i, display_particle_cell, radius_particle_i): + """ Solver to calculate the distance between the particle center and the variable cell faces """ + # Variable distance from particle center to the cell face + distance_face_var = params + # Minimization value + total_error = 0 + # Calculate new verticies + cell_vertices_i_new = calc_verticies(cell_face_normals_i, vertices_indices_i, distances_faces_max_i, distance_face_var, cell_face_fixed_i, cell_vertices_i, display_particle_cell, radius_particle_i) + # Check arrayfor NaN values + if np.isnan(cell_vertices_i_new).any(): + total_error = 10000 + else: + # Calculate polyhedron volume + volume_polyhedron = calc_volume_polyhedron(cell_vertices_i_new) + # Minimization function + total_error = 100*abs(volume_particle_i - volume_polyhedron) / volume_particle_i + + return total_error + +def calc_verticies(cell_face_normals_i, vertices_indices_i, distances_faces_max_i, distance_face_var, cell_face_fixed_i, cell_vertices_i, display_particle_cell, radius_particle_i): + """ Function to calculate new verticies coordinates """ + # Predefine list with aim distances between particle center and cell faces + distances_faces = [] + # Chech each face of polyhedron + for face_index in range(len(cell_face_normals_i)): + # If face is from beginning fixed set distance to particle radius + if cell_face_fixed_i[face_index] == True: + distance_face = distances_faces_max_i[face_index] + # If the distance of variable face is bigger than the distance between particle center and the cell face (max. distance) + # set the distance equal to the distance between particle center and the cell face + elif distance_face_var >= distances_faces_max_i[face_index]: + distance_face = distances_faces_max_i[face_index] + # Else set distance to variable distance value + else: + distance_face = distance_face_var + # Update list + distances_faces.append(distance_face) + # Calculate the difference between the aim distances and maximal distances + delta_distances = [abs(a - b) for a, b in zip(distances_faces_max_i, distances_faces)] + # Number of vertices + n_verticies = len(cell_vertices_i) + # Predefine list for new vertices + cell_vertices_i_new = [(None, None, None) for _ in range(n_verticies)] + # Consider each vertex of polyhedron + for vertex_index in range(len(cell_vertices_i)): + # Find every face that is connected with the considered vertex + face_index = [i for i, subarray in enumerate(vertices_indices_i) if vertex_index in subarray] + # Define move distances of vetex + distances_move = [delta_distances[i] for i in face_index] + # Define move vectors of vetex + vectors_move = np.array([cell_face_normals_i[i] for i in face_index]) + # Compute the displacement vector + displacement = sum(distance * normal for distance, normal in zip(distances_move, vectors_move)) + # Calculate the new position coordinates of vertex + verticies_coordinates = cell_vertices_i[vertex_index] - displacement + # Update list + cell_vertices_i_new[vertex_index] = verticies_coordinates + # Convert the list of arrays to a 2D NumPy array + cell_vertices_i_new = np.vstack(cell_vertices_i_new) + + """ Graphically display particles in 3D """ + if display_particle_cell == True: + plot_particle_cell(cell_vertices_i_new, vertices_indices_i, cell_face_fixed_i, radius_particle_i) + + return cell_vertices_i_new + +def plot_particle_cell(cell_vertices_i_new, vertices_indices_i, cell_face_fixed_i, radius_particle_i): + """ Function to graphically display particle and cell """ + # Boolean list indicating which faces to mark in red + mark_faces = cell_face_fixed_i + # Calculate the convex hull + #hull = ConvexHull(cell_vertices_i_new) + # Extract faces + #faces = hull.simplices + # Create a 3D plot + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + # Function to get coordinates of vertices for each face + def get_face_vertices(face_indices): + return [cell_vertices_i_new[idx] for idx in face_indices] + # Create Poly3DCollection from faces + poly3d = Poly3DCollection([get_face_vertices(face) for face in vertices_indices_i], alpha=0.25, edgecolors='r') + # Set face colors based on mark_faces list + face_colors = ['red' if mark else 'blue' for mark in mark_faces] + poly3d.set_facecolor(face_colors) + # Add collection to plot + ax.add_collection3d(poly3d) + # Plot vertices + ax.scatter(cell_vertices_i_new[:, 0], cell_vertices_i_new[:, 1], cell_vertices_i_new[:, 2]) + # Create a grid of points of the particle + u = np.linspace(0, 2 * np.pi, 100) + v = np.linspace(0, np.pi, 100) + x = radius_particle_i * np.outer(np.cos(u), np.sin(v)) + center_particle_i[0] # x-axis in the (z, y, x) order + y = radius_particle_i * np.outer(np.sin(u), np.sin(v)) + center_particle_i[1] # y-axis in the (z, y, x) order + z = radius_particle_i * np.outer(np.ones(np.size(u)), np.cos(v)) + center_particle_i[2] # z-axis in the (z, y, x) order + # Plot the surface + ax.plot_surface(x, y, z, color='b', alpha=0.3) + # Label vertices + for i, v in enumerate(cell_vertices_i_new): + ax.text(v[0], v[1], v[2], str(i), color='black', fontsize=10) + # Set the aspect ratio to be equal + ax.set_aspect('equal', adjustable='box') + # Set labels + ax.set_xlabel('X') + ax.set_ylabel('Y') + ax.set_zlabel('Z') + # Show plot + plt.show() + +def calc_volume_polyhedron(cell_vertices_i_new): + """ Function to calculates the polyhedron volume based on the Tetrahedral Decomposition method""" + # Polyhedron volume + volume_polyhedron = 0 + # Perform Delaunay triangulation to get tetrahedra + delaunay = Delaunay(cell_vertices_i_new) + # Consider each tetrahedron + for simplex in delaunay.simplices: + # Get tetrahedron verticies + verticies_tetrahedron = cell_vertices_i_new[simplex] + # Calculate the tetrahedron volumes + volume_tetrahedron = calc_tetrahedron_volume(verticies_tetrahedron) + # Calculate the volume of the polyhedron by summing tetrahedron volumes + volume_polyhedron += volume_tetrahedron + + return volume_polyhedron + +def calc_tetrahedron_volume(tetra): + """ Function to calculates the volume of a tetrahedron based on its vertices. """ + # Tetrahedron vertices + a, b, c, d = tetra + # Calculate tetrahedron volume + volume_tetrahedron = abs(np.dot(a-d, np.cross(b-d, c-d))) / 6 + + return volume_tetrahedron + +def solver_grow_factor(params, holdup_end, volume_cube, diameters_0, volumes_cells): + """ Solver to calculate the grow factor """ + # Variable grow factor + grow_factor = params + # Minimization value + total_error = 0 + # New diameters + diameters_new = diameters_0 * grow_factor + # New volumes + volumes_particles_new = (np.pi / 6) * diameters_new **(3) + # Correct volumes + for i in range(len(volumes_particles_new)): + if volumes_particles_new[i] - volumes_cells[i] > 1e-9: + volumes_particles_new[i] = volumes_cells[i] + # New holdup + holdup_new = np.sum(volumes_particles_new) / volume_cube + # + total_error = 100*abs(holdup_end - holdup_new) / holdup_end + + return total_error + +def calc_contact_probabilities(average_contacts, contact_probabilities, cell_face_fixed, diameters_neighbors, diameters_0, grow_step, particle_status): + """ + This function calculates the average contact number of each class and the contact probability between classes. + Particles which are in contact with walls at their initial state are excluded. + If solver issues occur at growing simulation of particles those particles are excluded. + Wall contacts are excluded. + Only contacts between fixed faces are included in the calculation. + """ + """ Count contacts """ + # Initialize a dictionary to hold the contact counts + contact_counts = defaultdict(lambda: defaultdict(int)) + # Iterate over each particle and its neighbors to count the contacts + for particle_i in range(len(diameters_0)): + particle = diameters_0[particle_i] + for neighbor_j in range(len(diameters_neighbors[particle_i])): + neighbor = diameters_neighbors[particle_i][neighbor_j] + # Exclude particle with status "False", wall contacts (None values) and faces which are not fixed + if particle_status[particle_i] != False and neighbor != None and cell_face_fixed[particle_i][neighbor_j] == True: + # Increase number of contacts + contact_counts[particle][neighbor] += 1 + """ Calculate total contacts """ + # Initialize a dictionary to hold the total contacts for each particle class + total_contacts = defaultdict(int) + # Sum the total contacts for each particle class + for class_particle in contact_counts: + total_contacts[class_particle] += sum(contact_counts[class_particle].values()) + # Sort value_counts by keys (values) in ascending order and convert to a list of tuples + total_contacts_sorted = sorted(total_contacts.items()) + # Extract only the counts (second column) + total_contacts_values = [count for value, count in total_contacts_sorted] + """ Calculate class sizes """ + # Count occurrences of each diameter + class_size = Counter() + # Iterate through data_list and include values based on include_list + for class_diameter, status in zip(diameters_0, particle_status): + if status == True: + class_size[class_diameter] += 1 + # Sort value_counts by keys (values) in ascending order and extract only the counts + class_size_values = [class_size[value] for value in sorted(class_size)] + """ Calculate average contact nubmers based on class sizes and total contacts """ + # Perform element-wise division + for i in range(len(class_size_values)): + average_contacts[grow_step][i] = total_contacts_values[i] / class_size_values[i] + """ Calculate contact probability based on the contact counts and total contacts """ + # Initialize a dictionary to hold the probabilities + contact_probabilities_dict = defaultdict(lambda: defaultdict(float)) + # Iterate over each particle and its neighbors to calculate the probabilities + for class_particle, classes_neighbors in contact_counts.items(): + for class_neighbor, count_contacts in classes_neighbors.items(): + # Avoid division by zero + if total_contacts[class_particle] > 0: + # Calculate the contact probabilities + contact_probabilities_dict[class_particle][class_neighbor] = 100 * count_contacts / total_contacts[class_particle] + # Convert the contact_probabilities dictionary into a sorted list (sorted in ascending order) + contact_counts_sorted = sorted(set(contact_counts)) + # Iterate over each particle and its neighbors to update the list in ascending order + for class_particle_i in range(len(contact_counts_sorted)): + class_particle = contact_counts_sorted[class_particle_i] + row = [] + for class_neighbor in contact_counts_sorted: + row.append(contact_probabilities_dict[class_particle].get(class_neighbor, 0)) + contact_probabilities[grow_step][class_particle_i] = row + + return average_contacts, contact_probabilities + +def calc_q(diameters, n_particle, d_classes): + ' Function to calculate the number and volume distributions' + # Predefine array for class sizes after random loose packing simulation + class_sizes = np.zeros(len(d_classes), dtype=int) + # Categorize cell diameters into classes + for particle in diameters: + for i in range(len(d_classes)): + lb_class = d_classes[i] - delta_class/2 + ub_class = d_classes[i] + delta_class/2 + if lb_class <= particle < ub_class: + class_sizes[i] += 1 + break + # Final number distribution after growing simulation + q0 = class_sizes / float(n_particle) + # Final volume distribution after growing simulation + q3 = 0.9*(q0 * (np.pi/6) * d_classes**3) / np.sum(q0 * (np.pi/6) * d_classes**3) + + return q0, q3 + +""" MAIN """ +if __name__ == "__main__": + # Time stamp + start_time_rlp = time.time() + # Set certain variables to integer + SEED = int(SEED) + N_PARTICLES_SIM = int(N_PARTICLES_SIM) + print('Starting simulation with seed', SEED) + # Set random seed + random.seed(SEED) + np.random.seed(SEED) + sys.stdout.write("Generating particle size distribution...") + # Number distribution of particles + q0, n_particles_init, q0_initial, q3_initial, d_classes, d_min, d_max, volume_total, d_05, d_95, delta_class, D_32 = calc_distribution(N_PARTICLES_SIM, N_CLASSES) + # Volume of the simulation environment + volume_sim = volume_total / HOLDUP_EST + # Cuboid's edge lenght (µm) + L_0 = volume_sim ** (1/3) + # Half of cuboid's edge lenght (µm) + A_0 = L_0 / 2 + # Mix particles in array and determine their coordinates randomly --> initial array for particles + M_particles_initial = calc_particles(q0, n_particles_init) + # Array for particles in steady state. + # Each row corresponds to one particle. + # Columns: 0: index of this array, 1: z-coordinate, 2: y-coordinate, 3: x-coordinate, 4: steady state status, + # 5: particle diameter, 6: index of initial array, 7: particle class + # further columns (len(q0)) corresponds to number of classes of the distribution. + # Thus, contact particle classes can be assigned to each particle in array. + M_particle_steady = np.zeros((1, (8 + len(q0)))) + sys.stdout.write("completed\n") + # Use tqdm to track progress + with tqdm(total=n_particles_init, desc='Random loose packing simulation progress', unit='particle') as pbar: + # Drop and roll loop: + for particle_i in range(len(M_particles_initial)): + if display_detailed_progress == True: + print('Particle Nr.:', particle_i) + """ Take information of considered particles from initial particle array to define its initial state """ + x, y, z, index_i, d, class_i = particle_info(M_particles_initial, particle_i) + """ Define test range: the distance between the centers of the considered particle and the bigest particlein steady state""" + # List of all particles in steady state + d_steady = M_particle_steady[:, 5] + # Diameter of biggest particle in steady state + d_max_steady = np.max(d_steady) + # Test range + test_range = 1.2*((d + d_max_steady) / 2) + # The first particle is simply set because there is nothing there before. + if particle_i == 0: + # Calculate new z-coordinate + z = 0 + d / 2 + """Fill steady state array""" + # Particle index + M_particle_steady[0, 0] = particle_i + # z-coordinate + M_particle_steady[0, 1] = z + # y-coordinate + M_particle_steady[0, 2] = y + # x-coordinate + M_particle_steady[0, 3] = x + # Contact with ground, 1=yes + M_particle_steady[0, 4] = 1 + # Particle diameter + M_particle_steady[0, 5] = d + # Initial index + M_particle_steady[0, 6] = index_i + # Particle class + M_particle_steady[0, 7] = class_i + continue + # Calculate dropping height based on the location of the heighest particle + z = find_z_drop(y, x, M_particle_steady, test_range, d_max_steady) + # Initial dropping position + z_drop = z + y_drop = y + x_drop = x + """ + While loop for particle sedimentation. + In this loop it is checked whether a particle sediments or collides with other particles. + If the considered particle does not overlap with other particles, it is moved downwards. + If the considered particle overlaps with other particles, + depending on the number of overlappings, its position is updated by the corresponding function. + The position is updated so that the particles touch each other but do not overlap. + After the position update the considered particleis tested for overlapping again because it could + overlap with other particles due to the update. + Depending on the number of overlaps, the position is updated over and over until no overlapping after + the position update occurs. Only when no overlappings are present it is moved downwards. + When three or four overlappings are present, the particle is checked for steady state. + If it is not in steady state, it is calculated at which particle it would roll along in the next loop step + and the corresponding position update function is applied. This is neccessary to prevent the particle to get stucked. + This loop is repeated agian and again until the considered particle reches its steady state position at the ground, + at three or at four particles. + """ + # Counter variables and lists + # Number of overplaps + n_overlaps = 0 + # List of overlaps + overlaps = [] + # Variable for overlapping walls + walls = 0 + # Error Variables + error_position_update2 = 0 + error_position_update3 = 0 + error_position_update4 = 0 + error_position_update1w = 0 + error_position_update2w = 0 + error_position_update3w = 0 + error_position_update1w2 = 0 + error_position_update2w2 = 0 + error_check_steadystate3 = 0 + error_check_steadystate4 = 0 + error_check_steadystate2w = 0 + error_check_steadystate3w = 0 + error_check_steadystate1w2 = 0 + error_check_steadystate2w2 = 0 + # Argument for the while loop + steady = 0 + # Aborting variable for the while loop + counter_sedi = 0 + # Borders of the aborting variable for the while loop + counter_sedi_max1 = 10000 + counter_sedi_max2 = 2 * counter_sedi_max1 + counter_sedi_max3 = 3 * counter_sedi_max1 + # Error counter + n_error = 0 + # If fatal error is True, the while loop is leaved + fatal_error = False + # While loop fpr particle sedimentation + while steady == 0: + # Increase aborting variable for the while loop + counter_sedi = counter_sedi + 1 + # Try again if it takes too long or an error occurs + if counter_sedi == counter_sedi_max1 or n_error == 1: + z = z_drop + y = y_drop + x = y_drop + if n_error == 1: + n_error = n_error + 1 + continue + # Assign new coordinates if it takes again too long or an error occurs again + if counter_sedi == counter_sedi_max2 or n_error == 3: + x = random.uniform(-A_0 + d / 2, A_0 - d / 2) + y = random.uniform(-A_0 + d / 2, A_0 - d / 2) + z = find_z_drop(y, x, M_particle_steady, test_range, d_max_steady) + if n_error == 3: + n_error = n_error + 1 + continue + # Abort while loop by abort variable if trying again and assigning new coordinates does not work + # or an error occurs three times + if counter_sedi > counter_sedi_max3 or n_error == 5: + n_error = 0 + if display_detailed_progress == True: + print('Timeout in the sedimentation loop or too many errors. Check sedimentation loop.') + fatal_error = True + break + # Calculate overlapping/collision + n_overlaps, overlaps, walls = calc_overlap(d, z, y, x, M_particle_steady, test_range) + # If the considered particle does not overlap with other particles, it is moved downwards. + if n_overlaps == 0: + # If z == d/2, the considered particle reached the ground + if z == d/2: + # If steady = 1, steady state position is reched on the ground + steady = 1 + else: + z = z - STEPSIZE + # Overlap with one steady state particle in total + elif n_overlaps == 1: + # index of the first steady state particle, the considered particle collides with + index1 = overlaps[0] + # No overlappings with walls + if walls == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with one steady state particle.') + # If the considered particle overlaps with one steady state particle, + # its position is updated by position_update1 function. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # Overlap with one wall + elif walls == 1 or walls == -1 or walls == 2 or walls == -2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with one steady state particle and wall.') + # If the considered particle overlaps with one steady state particle and one wall, + # its position is updated by position_update1w function. + y_s, x_s, error_position_update1w = position_update1w(z, y, x, d, M_particle_steady, overlaps, walls) + # If no range error occurs, the new coordinates are assignet + if error_position_update1w == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update1w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update1w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update1w == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # Overlap with two walls + elif walls == 3 or walls == -3 or walls == 4 or walls == -4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with one steady state particle and two walls.') + # If the considered particle overlaps with one steady state particle and two walls, + # its position is updated by position_update1w function. + z_s, y_s, x_s, error_position_update1w2 = position_update1w2(z, y, x, d, M_particle_steady, overlaps, walls) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update1w2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update1w2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the position of the considered particle is located below the centroid of the steady state particles + elif error_position_update1w2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update1w2 function. Position of the considered particle is located below the centroid of the steady state particles.') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the position of the first solver solution (the logical one) is not located on the intersection plane + elif error_position_update1w2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w2 function. The logical new position determnined by the solver is not located on the intersection plane. ') + z = z + STEPSIZE / 2 + continue + # If particleis in contact with one steady state particle and two walls, it could be in steady state + # Checking for overlaps after position update + n_overlaps, overlaps1w2, walls = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate1w2 function + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is tested for steady state at one particle and two walls.') + steady_check, overlaps, walls, error_check_steadystate1w2 = check_steadystate1w2(z_s, y_s, x_s, M_particle_steady, overlaps, walls) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate1w2 == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate1w2 == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate1w2 == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched on one particle and two walls + steady = 6 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along one steady state particle and wall + # Thus its position is updated by position_update2 function + elif steady_check == 4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle and wall') + # Function for position update at two contacts. + y_s, x_s, error_position_update1w = position_update1w(z, y, x, d, M_particle_steady, overlaps, walls) + # If no range error occurs, the new coordinates are assignet + if error_position_update1w == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update1w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update1w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update1w == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + continue + # If overlaps occur, the considered particle is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and two walls.') + z = z + STEPSIZE / 2 + continue + # Checking for overlaps after position update + n_overlaps, overlaps, walls = calc_overlap(d, z, y, x, M_particle_steady, test_range) + # Overlap with two steady state particles in total + if n_overlaps == 1: + # index of the second steady state particle, the considered particle collides with + index2 = overlaps[0] + # Update overlaps array + overlaps = np.array([index1, index2]) + # No overlappings with walls + if walls == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with two steady state particles.') + # If the considered particle overlaps with one steady state particle after position update at one particle, + # it is in contact with two particles. Thus, its position is updated by position_update2 function. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # Overlap with one wall + elif walls == 1 or walls == -1 or walls == 2 or walls == -2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with two steady state particles and one wall.') + # If the considered particle overlaps with one steady state particle and one wall after position update at one particle, + # it is in contact with two particles and one wall. Thus, its position is updated by position_update2w function. + z_s, y_s, x_s, error_position_update2w = position_update2w(z, y, x, d, M_particle_steady, overlaps, walls) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update2w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2w function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # If particle is in contact with two steady state particle and one wall, it could be in steady state + # Checking for overlaps after position update + n_overlaps, overlaps2w, walls = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If the particle overlaps with a second wall, its position is updated in the next loop + if walls == 3 or walls == -3 or walls == 4 or walls == -4 or walls == 0: + continue + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate1w2 function + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is tested for steady state at two particles and one wall.') + steady_check, overlaps, error_check_steadystate2w = check_steadystate2w(z_s, y_s, x_s, particle_i, M_particle_steady, overlaps, walls) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate2w == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate2w == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate2w == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched at two particles and one wall + steady = 4 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If steady_check == 4, the particle is not in steady state and would roll along one steady state particle and wall + # Thus its position is updated by position_update1w function + elif steady_check == 4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle and wall') + # Function for position update at two contacts. + y_s, x_s, error_position_update1w = position_update1w(z, y, x, d, M_particle_steady, overlaps, walls) + # If no range error occurs, the new coordinates are assignet + if error_position_update1w == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update1w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update1w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update1w == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + continue + # If overlaps occur, the considered particle is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and two walls.') + z = z + STEPSIZE / 2 + continue + # Overlap with two walls + elif walls == 3 or walls == -3 or walls == 4 or walls == -4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with one steady state particle and two walls.') + # If the considered particle overlaps with one steady state particle and two walls after position update at one particle, + # it is in contact with two particles and two walls. Thus, its position is updated by position_update2w function. + z_s, y_s, x_s, error_position_update2w2 = position_update2w2(z, y, x, d, M_particle_steady, overlaps, walls) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update2w2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2w2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2w2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2w2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # If particle is in contact with two steady state particle and two walls, it could be in steady state + # Checking for overlaps after position update + n_overlaps, overlaps2w2, walls = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If the particle overlaps with onlyone wall, its position is updated in the next loop + if walls == 1 or walls == -1 or walls == 2 or walls == -2 or walls == 0: + continue + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate1w2 function + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is tested for steady state at two particles and two walls.') + steady_check, overlaps, walls, error_check_steadystate2w2 = check_steadystate2w2(z_s, y_s, x_s, M_particle_steady, overlaps, walls) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate2w2 == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate2w2 == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate2w2 == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched at two particles and two walls + steady = 7 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If steady_check == 4, the particle is not in steady state and would roll along one steady state particle and wall + # Thus its position is updated by position_update1w function + elif steady_check == 4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle and wall') + # Function for position update at two contacts. + y_s, x_s, error_position_update1w = position_update1w(z, y, x, d, M_particle_steady, overlaps, walls) + # If no range error occurs, the new coordinates are assignet + if error_position_update1w == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update1w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update1w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update1w == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + continue + # If overlaps occur, the considered particle is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and two walls.') + z = z + STEPSIZE / 2 + continue + # Checking for overlaps after position update + n_overlaps, overlaps, walls = calc_overlap(d, z, y, x, M_particle_steady, test_range) + # Three overlappings in total + if n_overlaps == 1: + # index of the third steady state particle, the considered particle collides with + index3 = overlaps[0] + # Update overlaps array + overlaps = np.array([index1, index2, index3]) + # No overlappings with walls + if walls == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles.') + # If the considered particle overlaps with one steady state particle after position update at two particles, + # it is in contact with three particles. Thus, its position is updated by position_update3 function. + z_s, y_s, x_s, error_position_update3 = position_update3(z, y, x, d, M_particle_steady, overlaps) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update3 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the position of the considered particle is located below the centroid of the steady state particles + elif error_position_update3 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Position of the considered particle is located below the centroid of the steady state particles.') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the position of the first solver solution (the logical one) is not located on the intersection plane + elif error_position_update3 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. The logical new position determnined by the solver is not located on the intersection plane. ') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than one wall after position update at three particles, + # it is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and walls.') + z = z + STEPSIZE / 2 + continue + # Checking for overlaps after position update + n_overlaps, overlaps3, error_overlap = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If value is True, an error has occurred in the calc_overlapping function. + # The considered particle is stored in the steady state array with NaN values. + if error_overlap == True: + if display_detailed_progress == True: + print('Error in the calc_overlapping function. Check calc_overlapping function.') + fatal_error = True + break + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is checked for steady state at three steady state particles.') + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate3 function + steady_check, overlaps, error_check_steadystate3 = check_steadystate3(z_s, y_s, x_s, M_particle_steady, overlaps) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate3 == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate3 == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate3 == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched on three particles + steady = 2 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If the considered particle overlaps with more than one steady state particle after position update at three contacts, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than three particles') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than two steady state particle after position update at two contacts, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than three particles') + z = z + STEPSIZE / 2 + continue + # Overlap with three steady state particles in total + elif n_overlaps == 2: + # Indices of the second and third steady state particles, the considered particle collides with + index2 = overlaps[0] + index3 = overlaps[1] + # Update overlaps array + overlaps = np.array([index1, index2, index3]) + # No overlappings with walls + if walls == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles.') + # If the considered particle overlaps with two steady state particle after position update at one contact, + # it is in contact with three particles. Thus, its position is updated by position_update3 function. + z_s, y_s, x_s, error_position_update3 = position_update3(z, y, x, d, M_particle_steady, overlaps) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update3 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the position of the considered particle is located below the centroid of the steady state particles + elif error_position_update3 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Position of the considered particle is located below the centroid of the steady state particles.') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the position of the first solver solution (the logical one) is not located on the intersection plane + elif error_position_update3 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. The logical new position determnined by the solver is not located on the intersection plane. ') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than one wall after position update at three particles, + # it is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and walls.') + z = z + STEPSIZE / 2 + continue + # Checking for overlaps after position update + n_overlaps, overlaps3, error_overlap = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If value is True, an error has occurred in the calc_overlapping function. + # The considered particle is stored in the steady state array with NaN values. + if error_overlap == True: + if display_detailed_progress == True: + print('Error in the calc_overlapping function. Check calc_overlapping function.') + fatal_error = True + break + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is checked for steady state at three steady state particles.') + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate3 function + steady_check, overlaps, error_check_steadystate3 = check_steadystate3(z_s, y_s, x_s, M_particle_steady, overlaps) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate3 == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate3 == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate3 == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched on three particles + steady = 2 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If the considered particle overlaps with more than one steady state particle after position update at three contacts, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than three particles') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than three steady state particle after position update at one contact, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than three particles') + z = z + STEPSIZE / 2 + continue + # Two overlaps with steady state particles in total + elif n_overlaps == 2: + # Indices of the second and third steady state particles, the considered particle collides with + index1 = overlaps[0] + index2 = overlaps[1] + # No overlappings with walls + if walls == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with two steady state particles.') + # If the considered particle overlaps with two steady state particles, + # its position is updated by position_update2 function. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # Overlap with one wall + elif walls == 1 or walls == -1 or walls == 2 or walls == -2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with two steady state particles and one wall.') + # If the considered particle overlaps with one steady state particle and one wall after position update at one particle, + # it is in contact with two particles and one wall. Thus, its position is updated by position_update2w function. + z_s, y_s, x_s, error_position_update2w = position_update2w(z, y, x, d, M_particle_steady, overlaps, walls) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update2w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2w function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # If particle is in contact with two steady state particle and one wall, it could be in steady state + # Checking for overlaps after position update + n_overlaps, overlaps2w, walls = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If the particle overlaps with a second wall, its position is updated in the next loop + if walls == 3 or walls == -3 or walls == 4 or walls == -4 or walls == 0: + continue + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate1w2 function + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is tested for steady state at two particles and one wall.') + steady_check, overlaps, error_check_steadystate2w = check_steadystate2w(z_s, y_s, x_s, particle_i, M_particle_steady, overlaps, walls) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate2w == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate2w == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate2w == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched at two particles and one wall + steady = 4 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If steady_check == 4, the particle is not in steady state and would roll along one steady state particle and wall + # Thus its position is updated by position_update1w function + elif steady_check == 4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle and wall') + # Function for position update at two contacts. + y_s, x_s, error_position_update1w = position_update1w(z, y, x, d, M_particle_steady, overlaps, walls) + # If no range error occurs, the new coordinates are assignet + if error_position_update1w == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update1w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update1w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update1w == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + continue + # If overlaps occur, the considered particle is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and two walls.') + z = z + STEPSIZE / 2 + continue + # Overlap with two walls + elif walls == 3 or walls == -3 or walls == 4 or walls == -4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with one steady state particle and two walls.') + # If the considered particle overlaps with one steady state particle and two walls after position update at one particle, + # it is in contact with two particles and two walls. Thus, its position is updated by position_update2w function. + z_s, y_s, x_s, error_position_update2w2 = position_update2w2(z, y, x, d, M_particle_steady, overlaps, walls) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update2w2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2w2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2w2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2w2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # If particle is in contact with two steady state particle and two walls, it could be in steady state + # Checking for overlaps after position update + n_overlaps, overlaps2w2, walls = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If the particle overlaps with onlyone wall, its position is updated in the next loop + if walls == 1 or walls == -1 or walls == 2 or walls == -2 or walls == 0: + continue + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate1w2 function + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is tested for steady state at two particles and two walls.') + steady_check, overlaps, walls, error_check_steadystate2w2 = check_steadystate2w2(z_s, y_s, x_s, M_particle_steady, overlaps, walls) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate2w2 == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate2w2 == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate2w2 == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched at two particles and two walls + steady = 7 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If steady_check == 4, the particle is not in steady state and would roll along one steady state particle and wall + # Thus its position is updated by position_update1w function + elif steady_check == 4: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle and wall') + # Function for position update at two contacts. + y_s, x_s, error_position_update1w = position_update1w(z, y, x, d, M_particle_steady, overlaps, walls) + # If no range error occurs, the new coordinates are assignet + if error_position_update1w == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update1w == 1: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update1w == 2: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update1w == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + continue + # If overlaps occur, the considered particle is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and two walls.') + z = z + STEPSIZE / 2 + continue + # Checking for overlaps after position update + n_overlaps, overlaps, walls = calc_overlap(d, z, y, x, M_particle_steady, test_range) + # Three overlaps with steady state particles in total + if n_overlaps == 1: + # Index of the second and third steady state particles, the considered particle collides with + index3 = overlaps[0] + # Update overlaps array + overlaps = np.array([index1, index2, index3]) + # No overlappings with walls + if walls == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles.') + # If the considered particle overlaps with one steady state particle after position update at two contacts, + # it is in contact with three particles. Thus, its position is updated by position_update3 function. + z_s, y_s, x_s, error_position_update3 = position_update3(z, y, x, d, M_particle_steady, overlaps) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update3 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the position of the considered particle is located below the centroid of the steady state particles + elif error_position_update3 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Position of the considered particle is located below the centroid of the steady state particles.') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the position of the first solver solution (the logical one) is not located on the intersection plane + elif error_position_update3 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. The logical new position determnined by the solver is not located on the intersection plane. ') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than one wall after position update at three particles, + # it is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and walls.') + z = z + STEPSIZE / 2 + continue + # Checking for overlaps after position update + n_overlaps, overlaps3, error_overlap = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If value is True, an error has occurred in the calc_overlapping function. + # The considered particle is stored in the steady state array with NaN values. + if error_overlap == True: + if display_detailed_progress == True: + print('Error in the calc_overlapping function. Check calc_overlapping function.') + fatal_error = True + break + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is checked for steady state at three steady state particles.') + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate3 function + steady_check, overlaps, error_check_steadystate3 = check_steadystate3(z_s, y_s, x_s, M_particle_steady, overlaps) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate3 == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate3 == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate3 == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched on three particles + steady = 2 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If the considered particle overlaps with more than one steady state particle after position update at three contacts, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than three particles') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than two steady state particle after position update at two contacts, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than four particles') + z = z + STEPSIZE / 2 + continue + # Three overlaps with steady state particles in total + elif n_overlaps == 3: + # Indices of the first, second and third steady state particles, the considered particle collides with + index1 = overlaps[0] + index2 = overlaps[1] + index3 = overlaps[2] + # No overlappings with walls + if walls == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles.') + # If the considered particle overlaps with three steady state particles, + # its position is updated by position_update3 function. + z_s, y_s, x_s, error_position_update3 = position_update3(z, y, x, d, M_particle_steady, overlaps) + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + if error_position_update3 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Solver found the same new position twice. Calculation proceeds with the found value.') + # Error value is 2 if the position of the considered particle is located below the centroid of the steady state particles + elif error_position_update3 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. Position of the considered particle is located below the centroid of the steady state particles.') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the position of the first solver solution (the logical one) is not located on the intersection plane + elif error_position_update3 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update3 function. The logical new position determnined by the solver is not located on the intersection plane. ') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than one wall after position update at three particles, + # it is moved 1/10 of the step upwards an handled again in the next loop. + else: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'collides with three steady state particles and walls.') + z = z + STEPSIZE / 2 + continue + # Checking for overlaps after position update + n_overlaps, overlaps3, error_overlap = calc_overlap(d, z_s, y_s, x_s, M_particle_steady, test_range) + # If no overlaps occure, the particle is tested for steady state + if n_overlaps == 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i ,'is checked for steady state at three steady state particles.') + # With three contact partners, the considered particle can be in a steady state, + # which is tested by the check_steadystate3 function + steady_check, overlaps, error_check_steadystate3 = check_steadystate3(z_s, y_s, x_s, M_particle_steady, overlaps) + # Error value is 1 if particles center is located below the triangle's centroid of the steady state particles + if error_check_steadystate3 == 1: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The center of the considered particle is located below the triangles centroid of the steady state particles.') + n_error = n_error + 1 + continue + # Error value is 2 if the triangle points of the steady state particles are collinear + elif error_check_steadystate3 == 2: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The triangle points of the steady state particles are collinear.') + n_error = n_error + 1 + continue + # Error value is 3 if the subdomain of the triangle could not be determined correctly + elif error_check_steadystate3 == 3: + if display_detailed_progress == True: + print('Error in the check_steadystate3 function. The subdomain of the triangle could not be determined correctly.') + n_error = n_error + 1 + continue + # If steady_check == 0, the particle is in steady state + if steady_check == 3: + # Assigning the coordinates of the considered partcile to its steady state coordinates at three particles + z = z_s + y = y_s + x = x_s + # If steady = 2, steady state position is reched on three particles + steady = 2 + # If steady_check == 1, the particle is not in steady state and would roll along one steady state particle + # Thus its position is updated by position_update1 function + elif steady_check == 1: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along one particle') + # Function for position update at one contact. + y, x = position_update1(y, x, d, M_particle_steady, overlaps) + # If steady_check == 2, the particle is not in steady state and would roll along two steady state particles + # Thus its position is updated by position_update2 function + elif steady_check == 2: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'exits the steady state check and is now rolling along two particles') + # Function for position update at two contacts. + y_s, x_s, error_position_update2 = position_update2(z, y, x, d, M_particle_steady, overlaps) + # If no range error occurs, the new coordinates are assignet + if error_position_update2 == 0: + y = y_s + x = x_s + # Error value is 1 if the solver found the same new position of the considered particle twice. + # The error is only displayed but the calculation is continued, as the position could be correct + elif error_position_update2 == 1: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. Solver found the same new position twice. Calculation proceeds with the found value.') + y = y_s + x = x_s + # Error value is 2 if the distance between the centers of the two steady state particles and the position + # of the first solver solution (the logical one) are not in the same range as the sums of their radii + elif error_position_update2 == 2: + if display_detailed_progress == True: + print('Error in the error_position_update2 function. The logical solution of the new position determined by the solver lies not in the correct range. ') + z = z + STEPSIZE / 2 + continue + # Error value is 3 if the both solver solutions lies behind the simulation borders + elif error_position_update2 == 3: + if display_detailed_progress == True: + print('Error in the error_position_update1w function. The both solver solutions lies behind the simulation borders. ') + n_error = n_error + 1 + continue + # If the considered particle overlaps with more than one steady state particle after position update at three contacts, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 0: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than four particles') + z = z + STEPSIZE / 2 + continue + # If the considered particle overlaps with more than four steady state particle, + # it is in contact with more than four particles. + # In this case, the particle is moved 1/10 of the step upwards an handled again in the next loop. + elif n_overlaps > 3: + if display_detailed_progress == True: + print('Particle Nr.', particle_i, 'collides with more than four particles') + z = z + STEPSIZE / 2 + continue + # If particle reaches the ground, it is stored in steady state array. + if z <= d / 2: + z = d / 2 + """ Display final particle status and store the steady state particle in the steady state array. """ + # Display if the considered particle is in steady state + if display_detailed_progress == True: + if steady == 1: + print('Particle Nr.', particle_i, 'reaches the ground.') + elif steady == 2: + print('Particle Nr.', particle_i, 'has reached steady state on three particles.') + elif steady == 3: + print('Particle Nr.', particle_i, 'has reached steady state on three particles.') + elif steady == 4: + print('Particle Nr.', particle_i, 'has reached steady state on two particles and one wall.') + elif steady == 5: + print('Particle Nr.', particle_i, 'has reached steady state on three particles and one wall.') + elif steady == 6: + print('Particle Nr.', particle_i, 'has reached steady state on one particles and two walls.') + elif steady == 7: + print('Particle Nr.', particle_i, 'has reached steady state on two particles and two walls.') + # If error=True, timeout in the calc_sedimentation function occured. + # The considered particle is stored in the steady state array with NaN values. + if fatal_error == True: + M_particle_steady = np.vstack([M_particle_steady, np.zeros((1, 8 + len(q0)))]) + M_particle_steady[len(M_particle_steady) - 1, 0] = particle_i + M_particle_steady[len(M_particle_steady) - 1, 1] = np.nan + M_particle_steady[len(M_particle_steady) - 1, 2] = np.nan + M_particle_steady[len(M_particle_steady) - 1, 3] = np.nan + M_particle_steady[len(M_particle_steady) - 1, 4] = np.nan + M_particle_steady[len(M_particle_steady) - 1, 5] = d + M_particle_steady[len(M_particle_steady) - 1, 6] = index_i + M_particle_steady[len(M_particle_steady) - 1, 7] = class_i + # The considered particle is stored in the steady state array. + else: + M_particle_steady = np.vstack([M_particle_steady, np.zeros((1, 8 + len(q0)))]) + M_particle_steady[len(M_particle_steady) - 1, 0] = particle_i + M_particle_steady[len(M_particle_steady) - 1, 1] = z + M_particle_steady[len(M_particle_steady) - 1, 2] = y + M_particle_steady[len(M_particle_steady) - 1, 3] = x + M_particle_steady[len(M_particle_steady) - 1, 4] = steady + M_particle_steady[len(M_particle_steady) - 1, 5] = d + M_particle_steady[len(M_particle_steady) - 1, 6] = index_i + M_particle_steady[len(M_particle_steady) - 1, 7] = class_i + # Store contact partners in the steady state array. + # Determine number of collisions + n_overlaps_final = len(overlaps) + if n_overlaps_final == 1: + # Particle classes of the particles in contact + class1 = int(M_particle_steady[overlaps[0], 7]) + # The classes of the particles in contact are assigned to the considered particle + M_particle_steady[len(M_particle_steady) - 1, 8 + class1] = M_particle_steady[len(M_particle_steady) - 1, 8 + class1] + 1 + # The class of the considered particle is assigned to the particles in contact + M_particle_steady[overlaps[0], 8 + class_i] = M_particle_steady[overlaps[0], 8 + class_i] + 1 + elif n_overlaps_final == 2: + # Particle classes of the particles in contact + class1 = int(M_particle_steady[overlaps[0], 7]) + class2 = int(M_particle_steady[overlaps[1], 7]) + # The classes of the particles in contact are assigned to the considered particle + M_particle_steady[len(M_particle_steady) - 1, 8 + class1] = M_particle_steady[len(M_particle_steady) - 1, 8 + class1] + 1 + M_particle_steady[len(M_particle_steady) - 1, 8 + class2] = M_particle_steady[len(M_particle_steady) - 1, 8 + class2] + 1 + # The class of the considered particle is assigned to the particles in contact + M_particle_steady[overlaps[0], 8 + class_i] = M_particle_steady[overlaps[0], 8 + class_i] + 1 + M_particle_steady[overlaps[1], 8 + class_i] = M_particle_steady[overlaps[1], 8 + class_i] + 1 + elif n_overlaps_final == 3: + # Particle classes of the particles in contact + class1 = int(M_particle_steady[overlaps[0], 7]) + class2 = int(M_particle_steady[overlaps[1], 7]) + class3 = int(M_particle_steady[overlaps[2], 7]) + # The classes of the particles in contact are assigned to the considered particle + M_particle_steady[len(M_particle_steady) - 1, 8 + class1] = M_particle_steady[len(M_particle_steady) - 1, 8 + class1] + 1 + M_particle_steady[len(M_particle_steady) - 1, 8 + class2] = M_particle_steady[len(M_particle_steady) - 1, 8 + class2] + 1 + M_particle_steady[len(M_particle_steady) - 1, 8 + class3] = M_particle_steady[len(M_particle_steady) - 1, 8 + class3] + 1 + # The class of the considered particle is assigned to the particles in contact + M_particle_steady[overlaps[0], 8 + class_i] = M_particle_steady[overlaps[0], 8 + class_i] + 1 + M_particle_steady[overlaps[1], 8 + class_i] = M_particle_steady[overlaps[1], 8 + class_i] + 1 + M_particle_steady[overlaps[2], 8 + class_i] = M_particle_steady[overlaps[2], 8 + class_i] + 1 + elif n_overlaps_final == 4: + # Particle classes of the particles in contact + class1 = int(M_particle_steady[overlaps[0], 7]) + class2 = int(M_particle_steady[overlaps[1], 7]) + class3 = int(M_particle_steady[overlaps[2], 7]) + class4 = int(M_particle_steady[overlaps[3], 7]) + # The classes of the particles in contact are assigned to the considered particle + M_particle_steady[len(M_particle_steady) - 1, 8 + class1] = M_particle_steady[len(M_particle_steady) - 1, 8 + class1] + 1 + M_particle_steady[len(M_particle_steady) - 1, 8 + class2] = M_particle_steady[len(M_particle_steady) - 1, 8 + class2] + 1 + M_particle_steady[len(M_particle_steady) - 1, 8 + class3] = M_particle_steady[len(M_particle_steady) - 1, 8 + class3] + 1 + M_particle_steady[len(M_particle_steady) - 1, 8 + class4] = M_particle_steady[len(M_particle_steady) - 1, 8 + class4] + 1 + # The class of the considered particle is assigned to the particles in contact + M_particle_steady[overlaps[0], 8 + class_i] = M_particle_steady[overlaps[0], 8 + class_i] + 1 + M_particle_steady[overlaps[1], 8 + class_i] = M_particle_steady[overlaps[1], 8 + class_i] + 1 + M_particle_steady[overlaps[2], 8 + class_i] = M_particle_steady[overlaps[2], 8 + class_i] + 1 + M_particle_steady[overlaps[3], 8 + class_i] = M_particle_steady[overlaps[3], 8 + class_i] + 1 + # Update the progress bar + pbar.update(1) + # Find NaN values in the steady state array + del_list = np.argwhere(np.isnan(M_particle_steady[:, 1])) + print('Number of particles that could not be placed:', len(del_list)) + print('Diameter of particles that could not be placed:', M_particle_steady[del_list, 5]) + # Delete NaN values from the steady state array + M_particle_steady = np.delete(M_particle_steady, del_list, 0) + # Number of set particles + n_particles_placed = len(M_particle_steady) + """ Check the whole particle packing for particles outside the simulation room boarders. """ + # Mask the values between the bounds in x direction + mask_x = (M_particle_steady[:, 3] <= A_0) & (M_particle_steady[:, 3] >= -A_0) + # Mask the values between the bounds in y direction + mask_y = (M_particle_steady[:, 2] <= A_0) & (M_particle_steady[:, 2] >= -A_0) + # Combine the results of both tests + mask_xy = [False if not mask_x[i] else mask_y[i] for i in range(len(mask_x))] + # Extract particles within the compartiment + M_particle_filtered = M_particle_steady[mask_xy] + # Number of particles after test + n_particles_filtered = len(M_particle_filtered) + # Difference between before and after test + n_particles_delta = n_particles_placed - n_particles_filtered + print('Number of particles that were placed outside simulation boarders (deleted):', n_particles_delta) + """ Check the whole particle packing for overlappings. """ + if final_test == True: + # Counting variable for overlaps + n_overlaps_total = 0 + # Consider each particles in the packing + for i in range(len(M_particle_filtered)): + # Skip NaN values + if np.isnan(M_particle_filtered[i, 1]): + continue + # Retrieve information from the steady state array + # Coordinates + z1 = float(M_particle_filtered[i, 1]) + y1 = float(M_particle_filtered[i, 2]) + x1 = float(M_particle_filtered[i, 3]) + # Diameter + d1 = float(M_particle_filtered[i, 5]) + # Define particle under consideration + particle1 = z1, y1, x1, d1 + """ Define region of interrest """ + z_min = z1 - test_range + if z_min < 0: + z_min = 0 + z_max = z1 + test_range + y_min = y1 - test_range + y_max = y1 + test_range + x_min = x1 - test_range + x_max = x1 + test_range + """ Checking particles within the testing range for possible overlappings """ + # Arrays in x, y and z direction with indices of particles, which could overlap with the considered particle + x_array = np.argwhere(np.logical_and(M_particle_filtered[:, 3] > x_min, M_particle_filtered[:, 3] < x_max)) + y_array = np.argwhere(np.logical_and(M_particle_filtered[:, 2] > y_min, M_particle_filtered[:, 2] < y_max)) + z_array = np.argwhere(np.logical_and(M_particle_filtered[:, 1] > z_min, M_particle_filtered[:, 1] < z_max)) + # Combine the arrays --> Particles to check + particle_to_check = reduce(np.intersect1d, (x_array, y_array, z_array)) + # Convert array to list + particle_to_check = np.array(particle_to_check).tolist() + """ Check each particle that may overlap for an actual overlapping """ + for j in range(len(particle_to_check)): + if i != j: + # Retrieve information from the steady state array + # Coordinates + z2 = float(M_particle_filtered[j, 1]) + y2 = float(M_particle_filtered[j, 2]) + x2 = float(M_particle_filtered[j, 3]) + # Diameter + d2 = float(M_particle_filtered[j, 5]) + # Define particle + particle2 = z2, y2, x2, d2 + # Required min distance between particles + dist_required = (d1 + d2) / 2 + # Actual distance between particles + dist_actual = ((x2 - x1) ** 2 + (y2 - y1) ** 2 + (z2 - z1) ** 2) ** 0.5 + # If actual distanze is below the min distance, they overlap + if (dist_required - dist_actual) > 1e-9: + # Update counting variable + n_overlaps_total = n_overlaps_total + 1 + # Since every particle is checked with every particle, the overlaps are counted double + n_overlaps_total = int(n_overlaps_total / 2) + print('Total overlaps after final check:', n_overlaps_total) + print('Random loose packing simulation time:') + print("--- %s seconds ---" % (time.time() - start_time_rlp)) + # Time stamp + start_time_growing = time.time() + """ Delete upper part of particle packing with lower hold-up than average """ + # z-coordinate of the top particle + z_particle_max = np.max(M_particle_filtered[:, 1]) + # Number of testing compartiments + n_compartiments = int(round(z_particle_max / (D_32/2), 0)) + # Testing compartment height + delta_z = z_particle_max / n_compartiments + # Volume of testing compartiment + volume_compartiment = delta_z * L_0**2 + # Predefine a list to calculate compartiment holdups + holdup_compartiments = np.zeros(n_compartiments) + # Iterate through each comartiment and calculate its holdup + for i in range(n_compartiments): + # Mask the values between the defined compartiment bounds + mask_holdup = (M_particle_filtered[:, 1] <= ((n_compartiments-i) * delta_z)) & (M_particle_filtered[:, 1] >= ((n_compartiments-i-1) * delta_z)) + # Extract particles within the compartiment + M_particle_compartiment = M_particle_filtered[mask_holdup] + # Volume of particles within the compartiment + volume_particles_holdup = np.sum((np.pi / 6) * M_particle_compartiment[:, 5]**3) + # Compartiment holdup + holdup_compartiment_i = volume_particles_holdup / volume_compartiment + # Update list of compartiment holdups + holdup_compartiments[i] = holdup_compartiment_i + # Upper boandary of average holdup calculation + bound_up_holdup = int(round(n_compartiments * 0.3, 0)) + # Average holdup in packing + holdup_average = np.mean(holdup_compartiments[bound_up_holdup:n_compartiments-1]) + for holdup_boandary in range(n_compartiments): + if abs(holdup_average - holdup_compartiments[holdup_boandary]) < 0.05: + break + # Max. compartiment height + z_compartiment_max = (n_compartiments - holdup_boandary) * delta_z + # Extract final particles + M_particle_final = M_particle_filtered[M_particle_filtered[:, 1] <= z_compartiment_max] + # Final height of packing + z_packing = max(M_particle_final[:,1] + M_particle_final[:,5] / 2) + # Final number of particles in packing + n_particle_final_rlp = len(M_particle_final) + print('Final number of particles after deleting the top part of the packing with low holdup:', n_particle_final_rlp) + """ Graphically display particles in 3D """ + if display_particles == True: + plot_particles(M_particle_final) + """ + Tesselation. + From here the coordinates order is x,y,z since the tesselation package uses this order. + Perform Tesselation and get information from Tesselation. + The cells are polyhedrons. + The lists are defined as follows: + volumes_cells: cell volumes + cell_neighbors: cell neighbors + cell_face_normals: normal vectors of cell faces + cell_vertices: cell verticies + vertices_indices: indices of the vertices of each cell face + """ + sys.stdout.write("Tesselation...") + # Starting coordinates for Tesselation, x, y, z + tess_start = (-A_0, -A_0, 0) + # Final coordinates for Tesselation, x, y, z + tess_end = (A_0, A_0, z_packing) + # Create an array with coordinates for tesselation + M_particle_tess = M_particle_final[:, [3, 2, 1]] + # Initial particle diameters + diameters_0 = M_particle_final[:,5] + # Calculate tesselation + volumes_cells, cell_neighbors, cell_face_normals, cell_vertices, vertices_indices = voro_tess(M_particle_tess, tess_start, tess_end, diameters_0) + sys.stdout.write("completed\n") + # Convert list to array + volumes_cells = np.array(volumes_cells) + """ Calculate initial/max distances between particle center and polyhedron faces + and determine cell faces which are from beginning in contact with other cells due to initial particle contact """ + sys.stdout.write("Determining initial particle conditions...") + # Predefine list for initial/max distances between particle center and polyhedron faces + distances_faces_max = [ + [ + [None for _ in sub_inner_tuple] + for sub_inner_tuple in inner_tuple + ] + for inner_tuple in cell_face_normals + ] + # Predefine list for cell faces which are from beginning in contact with other cells due to initial particle contact + cell_face_fixed = [ + [ + [None for _ in sub_inner_tuple] + for sub_inner_tuple in inner_tuple + ] + for inner_tuple in cell_face_normals + ] + # Check each particle from particles array + for particle_i in range(len(M_particle_tess)): + # List of fixed cell faces + cell_face_fixed_i = [] + # List of distances between particle center and faces at each iteration step for volume grow + distances_faces_max_i = [] + # Particle center coordinates + center_particle_i = M_particle_tess[particle_i] + # Initial particle radius + radius_particle_i_0 = diameters_0[particle_i] / 2 + # Verticies of the cell corresponding to the particle + cell_vertices_i = cell_vertices[particle_i] + # Indices of the vertices of each cell face corresponding to the particle. + vertices_indices_i = vertices_indices[particle_i] + # Normal vectors of cell faces corresponding to the particle + cell_face_normals_i = cell_face_normals[particle_i] + # Iterate through each face of the cell + for face in range(len(cell_face_normals_i)): + # Indices of the vertices of the considered cell face + indices_cell_face = vertices_indices_i[face] + # Vector between the particle center and one point on the face area (vertex) + vector_center_vertex = center_particle_i - cell_vertices_i[indices_cell_face[0]] + # Distance between particle/cell center and the cell face + distance_face = np.abs(np.dot(vector_center_vertex, cell_face_normals_i[face]) / np.linalg.norm(cell_face_normals_i[face])) + # If the distance is less than or equal to the radius of the sphere, the face intersects with the sphere. + # Thus it is set to fixed + if np.abs(np.abs(distance_face) - radius_particle_i_0) < 1e-9: + face_fixed = True + else: + face_fixed = False + # Update lists + distances_faces_max_i.append(distance_face) + cell_face_fixed_i.append(face_fixed) + # Update lists + distances_faces_max[particle_i] = distances_faces_max_i + cell_face_fixed[particle_i] = cell_face_fixed_i + """ Calculate the contact probability of the initial state """ + # Create a new list with cell neighbor diameters of each particle + # None values are walls + diameters_neighbors = [ + [diameters_0[i] if i >= 0 else None for i in sublist] + for sublist in cell_neighbors + ] + # List to label particles. Label is False if particle is in contact with walls at its initial state + # or if the solver found no solution during the growing simulation + particle_status_0 = list([True] * len(diameters_0)) + # Check conditions and update particle_status list accordingly + # If None values (walls) of the diameters_neighbors list are at the same position as True values (fixed faces) of the + # cell_face_fixed list, the particle are in contact with walls at their initial state. Thus their label is set to False + for i in range(len(particle_status_0)): + for j in range(len(diameters_neighbors[i])): + if diameters_neighbors[i][j] is None and cell_face_fixed[i][j]: + particle_status_0[i] = False + # Number of particles not in contact with walls + n_particle_nowall = int(np.sum(particle_status_0)) + # Volume of cells not in contact with walls + volumes_cells_nowall = volumes_cells[particle_status_0] + # Initial diameters of particles not in contact with walls + diameters_nowall_0 = diameters_0[particle_status_0] + # Initial volumes of particles not in contact with walls + volumes_particles_nowall_0 = (np.pi / 6) * diameters_nowall_0 **(3) + # Grow factor. This factor is defined by the lowest cell volume to particle volume ratio + # since the volume of each particle can be increased max. to this value due to the required constant size distribution + # Only particles/cells not in contact with walls are considered + # Calculate the grow factor only where the particle_status_0 is True + grow_factor_limit = round(np.min((volumes_cells_nowall / volumes_particles_nowall_0) ** (1/3)), 2) + grow_factor_max = round(np.max((volumes_cells_nowall / volumes_particles_nowall_0) ** (1/3)), 2) + if grow_factor_limit <= 1.0: + print('Fatal error: Tesselation failed. Try another seed') + sys.exit() + # Volume of cube surrounding the packing + volume_cube = z_packing * L_0**2 + # Final particle diameters at constant growing + diameters_final = diameters_0 * grow_factor_limit + # Final particle volumes at constant growing + volumes_particles_final = (np.pi / 6) * diameters_final **(3) + # Correct volumes + for i in range(len(volumes_particles_final)): + if volumes_particles_final[i] - volumes_cells[i] > 1e-9: + volumes_particles_final[i] = volumes_cells[i] + # Packing final holdup at constant growing + holdup_final = np.sum(volumes_particles_final) / volume_cube + # Starting holdup for grow simulation + holdup_start = 0.525 + # Grow steps at constant dsd size increase + grow_steps_const = int(math.floor((holdup_final - holdup_start) / HOLDUP_DELTA)) + # Grow steps at non-constant dsd size increase + grow_steps_nonconst = int(math.floor((1.0 - holdup_final) / 0.2)) + # Total grow steps + grow_steps = grow_steps_const + grow_steps_nonconst + # Predefine list with grow factors + grow_factor_list=[] + # Grow factor the simulations starts from + for i in range(grow_steps+1): + if i <= grow_steps_const: + holdup_end = holdup_start + HOLDUP_DELTA * i + else: + holdup_end = 1.0 - (grow_steps + 1 - i) * 0.2 + # Initial guess for solver + initial_guess = grow_factor_limit + # Bounds + bnds = [(1, grow_factor_max)] + # Solver method + solver_method = 'SLSQP' + # Perform the optimization + result = minimize(solver_grow_factor, initial_guess, args=(holdup_end, volume_cube, diameters_0, volumes_cells), method=solver_method, bounds=bnds, tol=1e-5) + # Obtain the value of the minimized function + minimized_value = result.fun + # Obtain grow factor value + grow_factor = result.x[0] + grow_factor_list.append(grow_factor) + # Predefine list of contact probabilities + contact_probabilities = [[[[] for _ in range(N_CLASSES)] for _ in range(N_CLASSES)] for _ in range(grow_steps+4)] + # Predefine list of average contacts + average_contacts = [[None] * N_CLASSES for _ in range(grow_steps+4)] + # Determine average contacts and contact probabilities of initial state and update the list + average_contacts, contact_probabilities = calc_contact_probabilities(average_contacts, contact_probabilities, cell_face_fixed, diameters_neighbors, diameters_0, 0, particle_status_0) + # Set all faces to fixed which is the case in final state + cell_face_fixed_final = [[True for _ in row] for row in cell_face_fixed] + # Determine average contacts and contact probabilities of final state and update the list + average_contacts, contact_probabilities = calc_contact_probabilities(average_contacts, contact_probabilities, cell_face_fixed_final, diameters_neighbors, diameters_0, grow_steps+3, particle_status_0) + # Final distribution after random loose packing simulation + q0_rlp, q3_rlp = calc_q(diameters_nowall_0, n_particle_nowall, d_classes) + # Equivalent particle diameters from cell volumes + diameters_cells = ((6/np.pi) * volumes_cells)**(1/3) + # Diameters of droplets not in contact with walls + diameters_cells_nowall = diameters_cells[particle_status_0] + # Max. diameter + d_cells_max = np.max(diameters_cells_nowall) + # Update distribution classes + if d_cells_max > d_95: + # Final number of classes + n_classes_final = math.ceil((d_cells_max - d_05) / delta_class) + # Final class diameters + d_classes_final = np.array([d_05+delta_class/2 + i * delta_class for i in range(n_classes_final)]) + else: + d_classes_final = d_classes + # Final distribution after growing simulation + q0_final, q3_final = calc_q(diameters_cells_nowall, n_particle_nowall, d_classes_final) + # Particle initial volumes + volumes_particles = (np.pi / 6) * diameters_0 **(3) + # Total particle volume + volume_particle_total = np.sum(volumes_particles) + # Predefine list of packing holdups + holdup_packing = np.zeros(grow_steps+4) + # Packing initial holdup + holdup_packing[0] = volume_particle_total / volume_cube + # Packing final holdup + holdup_packing[grow_steps+3] = 1.0 + # + sys.stdout.write("completed\n") + # List to label particles. Label is False if particle's volume is smaller than its cell volume. + cell_status = list([False] * len(diameters_0)) + # For loop to simulate growing + for i_grow in range(grow_steps + 2): + print('Particle grow simulation. Step:', i_grow+1, 'of', (grow_steps + 2)) + # Increase diameters + if i_grow <= grow_steps_const: + diameters = diameters_0 * grow_factor_list[i_grow] + elif i_grow == (grow_steps_const + 1): + diameters = diameters_0 * grow_factor_limit + else: + diameters = diameters_0 * grow_factor_list[i_grow-1] + # Update volumes + volumes_particles = (np.pi / 6) * diameters **(3) + # Check and update cell status + for i in range(len(cell_status)): + if volumes_particles[i] - volumes_cells[i] > 1e-9: + volumes_particles[i] = volumes_cells[i] + cell_status[i] = True + # Final total particle volume + volume_particle_total = np.sum(volumes_particles) + # Update packing holdup + holdup_packing[i_grow+1] = volume_particle_total / volume_cube + # List of numbers to be squared + particle_indices = list(range(0, len(M_particle_tess))) + sys.stdout.write("Initializing multiprocessing...") + # Initialize manager for shared data + with multiprocessing.Manager() as manager: + # Lists shared with manager + cell_face_fixed = manager.list([manager.list(sublist) for sublist in cell_face_fixed]) + particle_status = manager.list(particle_status_0) + # Number of processes to use + if N_CORES == 0: + num_processes = multiprocessing.cpu_count() + else: + num_processes = N_CORES + # Create a pool of worker processes + with multiprocessing.Pool(processes=num_processes) as pool: + # Create partial function for parallel processing + calc_partial = partial(calc_grow, + volumes_particles = volumes_particles, + diameters = diameters, + cell_face_fixed = cell_face_fixed, + distances_faces_max = distances_faces_max, + cell_face_normals = cell_face_normals, + vertices_indices = vertices_indices, + cell_vertices = cell_vertices, + display_particle_cell = display_particle_cell, + particle_status = particle_status, + cell_status = cell_status) + # List to keep track of AsyncResult objects + async_results = [] + # Distribute the tasks among the worker processes using apply_async + for index in particle_indices: + result = pool.apply_async(calc_partial, (index,)) + async_results.append(result) + sys.stdout.write("completed\n") + # Use tqdm to track progress + with tqdm(total=len(async_results), desc='Particle grow simulation progress', unit='particle') as pbar: + results = [] + for result in async_results: + results.append(result.get()) + # Update progress bar after each task completes + pbar.update(1) + # No need to close the pool and wait, 'with' statement handles it + # Convert shared lists to regular lists for further processing + cell_face_fixed = [list(sublist) for sublist in cell_face_fixed] + particle_status = list(particle_status) + # Determine contact probabilities and update the list + average_contacts, contact_probabilities = calc_contact_probabilities(average_contacts, contact_probabilities, cell_face_fixed, diameters_neighbors, diameters_0, i_grow+1, particle_status) + # Final number of particles considered for the growing simulation + n_particle_final_gs = int(np.sum(particle_status)) + """ Save results as Excel sheet. """ + # Convert the arrays into dataframes + holdup_packing_df = pd.DataFrame(100*holdup_packing) + average_contacts_df = pd.DataFrame(average_contacts) + contact_probabilities_df = pd.DataFrame(contact_probabilities) + q0_initial_df = pd.DataFrame(100*q0_initial) + q0_rlp_df = pd.DataFrame(100*q0_rlp) + q0_final_df = pd.DataFrame(100*q0_final) + q3_initial_df = pd.DataFrame(100*q3_initial) + q3_rlp_df = pd.DataFrame(100*q3_rlp) + q3_final_df = pd.DataFrame(100*q3_final) + # Round class diameters + np.round(d_classes_final, decimals=0, out=d_classes_final) + d_classes_final_df = pd.DataFrame(d_classes_final) + # Define headers + headers_simulation = ['Seed', 'Final number of particles'] + headers_holdup_packing = ['Grow step', 'Hold-up'] + headers_d_classes = ['Class', 'Class diameter'] + header_average_contacts = ['Grow step'] + headers_dsd = ['q0_initial', 'q0_rlp', 'q0_final', 'q3_initial', 'q3_rlp', 'q3_final'] + for i in range(N_CLASSES): + header_average_contacts.append(f'Class {i+1}') + header_contact_probabilities = [] + for i in range(N_CLASSES): + header_contact_probabilities.append(f'Class {i+1}') + # Create a new Excel workbook + workbook = xlwt.Workbook() + # Create a new Excel sheet for the holdup data + sheet_main = workbook.add_sheet('Main') + # Write simulation settings headers to Excel + for row_index, header in enumerate(headers_simulation): + sheet_main.write(row_index, 0, header) + # Write simulation settings data to Excel + sheet_main.write(0, 1, SEED) + sheet_main.write(1, 1, n_particle_final_rlp) + sheet_main.write(1, 2, n_particle_nowall) + sheet_main.write(1, 3, n_particle_final_gs) + # Write holdup headers to Excel + for row_index, header in enumerate(headers_holdup_packing): + sheet_main.write(row_index + 3, 0, header) + # Write holdup data to Excel + for col_index, value in enumerate(holdup_packing_df.values): + sheet_main.write(3, col_index + 1, col_index) + sheet_main.write(4, col_index + 1, value[0]) + # Write classes headers to Excel + for row_index, header in enumerate(headers_d_classes): + sheet_main.write(row_index + 6, 0, header) + # Write classes data to Excel + for col_index in range(len(d_classes_final)): + sheet_main.write(6, col_index + 1, col_index + 1) + for col_index, value in enumerate(d_classes_final_df.values): + sheet_main.write(7, col_index + 1, value[0]) + # Write DSD headers to Excel + for row_index, header in enumerate(headers_dsd): + sheet_main.write(row_index + 9, 0, header) + # Write DSD data to Excel + for col_index, value in enumerate(q0_initial_df.values): + sheet_main.write(9, col_index + 1, value[0]) + for col_index, value in enumerate(q0_rlp_df.values): + sheet_main.write(10, col_index + 1, value[0]) + for col_index, value in enumerate(q0_final_df.values): + sheet_main.write(11, col_index + 1, value[0]) + for col_index, value in enumerate(q3_initial_df.values): + sheet_main.write(12, col_index + 1, value[0]) + for col_index, value in enumerate(q3_rlp_df.values): + sheet_main.write(13, col_index + 1, value[0]) + for col_index, value in enumerate(q3_final_df.values): + sheet_main.write(14, col_index + 1, value[0]) + # Create a new Excel sheet for the contacts data + sheet_contacts = workbook.add_sheet('Contacts') + # Write contacts headers to Excel + for col_index, header in enumerate(header_average_contacts): + sheet_contacts.write(0, col_index, header) + # Write contacts data to Excel + for row_index, values in enumerate(average_contacts_df.values): + sheet_contacts.write(row_index + 1, 0, row_index) + for col_index, value in enumerate(values): + sheet_contacts.write(row_index + 1, col_index + 1, value) + # Iterate through the nested dataframe of contact probabilities + for sheet_index, sheet_data in enumerate(contact_probabilities_df.values): + # Create a new Excel sheet for each inner list + sheet_probabilities = workbook.add_sheet(f'Probabilities Step {sheet_index}') + # Write probabilities headers to Excel + for col_index, header in enumerate(header_contact_probabilities): + sheet_probabilities.write(0, col_index + 1, header) + sheet_probabilities.write(col_index + 1, 0, header) + # Iterate through each sublist + for row_index, sublist in enumerate(sheet_data): + # Write contact probabilities to Excel + for col_index, value in enumerate(sublist): + sheet_probabilities.write(row_index + 1, col_index + 1, value) + # Save the workbook to a file + file_name = os.path.join(ROOT_DIR, NAME_EXCEL_FILE) + workbook.save(file_name) + """ Graphically display particles in 3D """ + if display_particles == True: + plot_particles(M_particle_final) + print('Particle growing simulation time:') + print("--- %s seconds ---" % (time.time() - start_time_growing)) + print('Total simulation time:') + print("--- %s seconds ---" % (time.time() - start_time_rlp)) + sss = 1 \ No newline at end of file