import matplotlib.pyplot as plt import numpy as np import sympy as sp from IPython.display import Math, display from matplotlib.axes import Axes from scipy import constants as const from scipy.integrate import quad from scipy.optimize import root_scalar from scipy.signal import argrelmax,argrelmin from tqdm import tqdm import fewfermions.analysis.units as si from fewfermions.simulate.traps.twod.trap import PancakeTrap from fewfermions.style import FIGS_PATH, setup colors, colors_alpha = setup() def plot_solutions(trap,n_levels,left_cutoff,right_cutoff,n_pot_steps=1000,display_plot=-1,state_mult=1e4,plot=True,ret_num=False): """Plot the potential and solutions for a given trap object (diplay_plot=-1 for all, 0,...,n for individual ones and -2 for none) """ x, y, z = trap.x, trap.y, trap.z axial_width = trap.get_tweezer_rayleigh() pot_ax = trap.subs(trap.get_potential()) pot_diff_ax = sp.diff(pot_ax, trap.z) pot_diff2_ax = sp.diff(pot_diff_ax, trap.z) pot_diff3_ax = sp.diff(pot_diff2_ax, trap.z) pot_diff_ax_numpy = sp.lambdify(trap.z, pot_diff_ax.subs({x: 0, y: 0})) pot_diff2_ax_numpy = sp.lambdify(trap.z, pot_diff2_ax.subs({x: 0, y: 0})) pot_diff3_ax_numpy = sp.lambdify(trap.z, pot_diff3_ax.subs({x: 0, y: 0})) barrier = root_scalar( pot_diff_ax_numpy, x0=1.5 * float(trap.subs(axial_width)), fprime=pot_diff2_ax_numpy, xtol=1e-18, fprime2=pot_diff3_ax_numpy, ).root minimum = root_scalar( pot_diff_ax_numpy, x0=0, fprime=pot_diff2_ax_numpy, xtol=1e-28, fprime2=pot_diff3_ax_numpy, ).root # Solve the hamiltonian numerically in axial direction energies, states, potential, coords = trap.nstationary_solution( trap.z, (left_cutoff, right_cutoff), n_pot_steps, k=n_levels ) # States that are below the potential barrier bound_states = energies < potential(barrier) # Density of states is larger on the left than on the right # Likely that the state in question is a true bound state true_bound_states = np.logical_and( bound_states, np.sum(states[:, coords[z] < barrier] ** 2, axis=1) > np.sum(states[:, coords[z] > barrier] ** 2, axis=1), ) if plot: z_np = np.linspace(left_cutoff, right_cutoff, num=n_pot_steps) ax: plt.Axes fig, ax = plt.subplots(figsize=(2.5, 2.5)) # ax.set_title("Axial") abs_min = np.min(potential(z_np)) ax.fill_between( z_np / si.um, potential(z_np) / const.h / si.kHz, abs_min / const.h / si.kHz, fc=colors_alpha["red"], alpha=0.5, ) count = 0 for i, bound in enumerate(true_bound_states): if not bound: continue energy = energies[i] state = states[i] ax.plot( z_np / si.um, np.where( (energy > potential(z_np)) & (z_np < barrier), energy / const.h / si.kHz, np.nan, ), c="k", lw=0.5, marker="None", ) if display_plot == -1 or display_plot == count: ax.plot(z_np/si.um, state**2 *state_mult, marker="None", c="k") count += 1 ax.plot(z_np / si.um, potential(z_np) / const.h / si.kHz, marker="None") ax.vlines(barrier/ si.um,np.min(potential(z_np) / const.h/ si.kHz),np.max(potential(z_np) / const.h/ si.kHz)) ax.vlines(minimum/ si.um,np.min(potential(z_np) / const.h/ si.kHz),np.max(potential(z_np) / const.h/ si.kHz)) ax.set_title(f"{np.sum(true_bound_states)} bound solutions, power: {trap.subs(trap.power_tweezer)/si.mW}mW") ax.set_xlabel(r"z ($\mathrm{\mu m}$)") ax.set_ylabel(r"E / h (kHz)") if ret_num: return np.sum(true_bound_states) def solutions_power(trap,power,n_levels,left_cutoff,right_cutoff): trap[trap.power_tweezer] = power return plot_solutions(trap,n_levels,left_cutoff,right_cutoff,plot=False, ret_num=True) def root_power(trap,num_atoms,bracket,n_levels,left_cutoff,right_cutoff): f = lambda x: solutions_power(trap,x,n_levels,left_cutoff,right_cutoff) - num_atoms return root_scalar(f,bracket=bracket).root def sweep_power(trap, gradient,n_levels,left_cutoff,right_cutoff,t_spill=25*si.ms,n_pot_steps=1000,max_atoms=10,n_plotpoints=100): """ Sweeps over power and gets the power for 0 atom boundary and the precision level """ x, y, z = trap.x, trap.y, trap.z zr = trap.get_tweezer_rayleigh() trap[trap.grad_z] = gradient initial_power = float(4/3/np.sqrt(3)*trap.subs(zr) * np.pi* trap.subs(trap.m * trap.g + trap.mu_b * trap.grad_z) * trap.subs(trap.waist_tweezer**2/trap.a)) final_power = root_power(trap,max_atoms,[initial_power,initial_power*5],n_levels,left_cutoff,right_cutoff) powers = np.linspace(initial_power,final_power,n_plotpoints) atom_number = np.zeros_like(powers,dtype=float) for i, power in enumerate(tqdm(powers)): trap[trap.power_tweezer] = power # Solve the hamiltonian numerically in axial direction energies, states, potential, coords = trap.nstationary_solution( trap.z, (left_cutoff, right_cutoff), n_pot_steps, k=n_levels ) # Determine the potential and its derivatives pot_ax = trap.subs(trap.get_potential()) pot_diff_ax = sp.diff(pot_ax, trap.z) pot_diff2_ax = sp.diff(pot_diff_ax, trap.z) pot_diff3_ax = sp.diff(pot_diff2_ax, trap.z) pot_diff_ax_numpy = sp.lambdify(trap.z, pot_diff_ax.subs({x: 0, y: 0})) pot_diff2_ax_numpy = sp.lambdify(trap.z, pot_diff2_ax.subs({x: 0, y: 0})) pot_diff3_ax_numpy = sp.lambdify(trap.z, pot_diff3_ax.subs({x: 0, y: 0})) barrier = root_scalar( pot_diff_ax_numpy, x0=1.5 * float(trap.subs(zr)), fprime=pot_diff2_ax_numpy, xtol=1e-28, fprime2=pot_diff3_ax_numpy, ).root minimum = root_scalar( pot_diff_ax_numpy, x0=0, fprime=pot_diff2_ax_numpy, xtol=1e-28, fprime2=pot_diff3_ax_numpy, ).root # States that are below the potential barrier bound_states = energies < potential(barrier) # Density of states is larger on the left than on the right # Likely that the state in question is a true bound state true_bound_states = np.logical_and( bound_states, np.sum(states[:, coords[z] < barrier] ** 2, axis=1) > np.sum(states[:, coords[z] > barrier] ** 2, axis=1), ) if t_spill == 0: atom_num = np.sum(true_bound_states) atom_number = np.append(atom_number,atom_num) continue transmission_probability = np.full_like(energies, np.nan, dtype=float) for j, energy in enumerate(energies): if not true_bound_states[j]: continue intersect_end = root_scalar( lambda x: potential(x) - energy, bracket=(barrier, 3 * float(trap.subs(zr))), ).root intersect_start = root_scalar( lambda x: potential(x) - energy, bracket=(minimum, barrier), ).root s = quad( lambda x: np.sqrt( 2 * float(trap.subs(trap.m)) * np.clip(potential(x) - energy, a_min=0, a_max=None) ) / const.hbar, intersect_start, intersect_end, ) transmission_probability[j] = sp.exp(-2 * s[0]) tunneling_rate = ( transmission_probability * np.abs(energies - potential(minimum)) / const.h ) atom_number[i] = np.sum(np.exp(-t_spill * tunneling_rate[true_bound_states])) return powers, atom_number def sweep_power_old(trap, gradient,dp,n_levels,left_cutoff,right_cutoff,t_spill=25*si.ms,max_spill_steps=200,n_pot_steps=1000,max_atoms=10): """ Sweeps over power and gets the power for 0 atom boundary and the precision level """ x, y, z = trap.x, trap.y, trap.z zr = trap.get_tweezer_rayleigh() trap[trap.grad_z] = gradient initial_power = 4/3/np.sqrt(3)*trap.subs(zr) * np.pi* trap.subs(trap.m * trap.g + trap.mu_b * trap.grad_z) * trap.subs(trap.waist_tweezer**2/trap.a) trap[trap.power_tweezer] = initial_power powers = np.array([initial_power],dtype=float) atom_number = np.array([0.0],dtype=float) i = 0 pbar = tqdm(disable=True) while atom_number[i] np.sum(states[:, coords[z] > barrier] ** 2, axis=1), ) if t_spill == 0: atom_num = np.sum(true_bound_states) atom_number = np.append(atom_number,atom_num) continue transmission_probability = np.full_like(energies, np.nan, dtype=float) for j, energy in enumerate(energies): if not true_bound_states[j]: continue intersect_end = root_scalar( lambda x: potential(x) - energy, bracket=(barrier, 3 * float(trap.subs(zr))), ).root intersect_start = root_scalar( lambda x: potential(x) - energy, bracket=(minimum, barrier), ).root s = quad( lambda x: np.sqrt( 2 * float(trap.subs(trap.m)) * np.clip(potential(x) - energy, a_min=0, a_max=None) ) / const.hbar, intersect_start, intersect_end, ) transmission_probability[j] = sp.exp(-2 * s[0]) tunneling_rate = ( transmission_probability * np.abs(energies - potential(minimum)) / const.h ) atom_num = np.sum(np.exp(-t_spill * tunneling_rate[true_bound_states])) atom_number = np.append(atom_number,atom_num) i += 1 pbar.update(1) pbar.close() if i == max_spill_steps: print(f"{max_atoms} atoms not reached") return powers,atom_number raise Exception(f"{max_atoms} atoms not reached") return powers, atom_number def solutions_gradient(trap,gradient,n_levels,left_cutoff,right_cutoff): trap[trap.grad_z] = gradient return plot_solutions(trap,n_levels,left_cutoff,right_cutoff,plot=False, ret_num=True) def root_gradient(trap,num_atoms,bracket,n_levels,left_cutoff,right_cutoff): f = lambda x: solutions_gradient(trap,x,n_levels,left_cutoff,right_cutoff) - num_atoms return root_scalar(f,bracket=bracket).root def sweep_gradient(trap, power,n_levels,left_cutoff,right_cutoff,t_spill=25*si.ms,n_pot_steps=1000,max_atoms=10,n_plotpoints=100): """ Sweeps over gradient and gets the gradient for 0 atom boundary and the precision level """ x, y, z = trap.x, trap.y, trap.z zr = trap.get_tweezer_rayleigh() trap[trap.power_tweezer] = power initial_grad = float(trap.subs(1/trap.mu_b * (3*np.sqrt(3)/4 * power * trap.a/np.pi/trap.waist_tweezer**2/zr - trap.m * trap.g))) final_grad = root_gradient(trap,max_atoms,[-2.89*si.G/si.cm,initial_grad],n_levels,left_cutoff,right_cutoff) gradients = np.linspace(initial_grad,final_grad,n_plotpoints,dtype=float) atom_number = np.zeros_like(gradients,dtype=float) for i, gradient in enumerate(tqdm(gradients)): #print(i) trap[trap.grad_z] = gradient # Solve the hamiltonian numerically in axial direction energies, states, potential, coords = trap.nstationary_solution( trap.z, (left_cutoff, right_cutoff), n_pot_steps, k=n_levels ) # Determine the potential and its derivatives pot_ax = trap.subs(trap.get_potential()) pot_diff_ax = sp.diff(pot_ax, trap.z) pot_diff2_ax = sp.diff(pot_diff_ax, trap.z) pot_diff3_ax = sp.diff(pot_diff2_ax, trap.z) pot_diff_ax_numpy = sp.lambdify(trap.z, pot_diff_ax.subs({x: 0, y: 0})) pot_diff2_ax_numpy = sp.lambdify(trap.z, pot_diff2_ax.subs({x: 0, y: 0})) pot_diff3_ax_numpy = sp.lambdify(trap.z, pot_diff3_ax.subs({x: 0, y: 0})) barrier = root_scalar( pot_diff_ax_numpy, x0=1.5 * float(trap.subs(zr)), fprime=pot_diff2_ax_numpy, xtol=1e-28, fprime2=pot_diff3_ax_numpy, ).root minimum = root_scalar( pot_diff_ax_numpy, x0=0, fprime=pot_diff2_ax_numpy, xtol=1e-28, fprime2=pot_diff3_ax_numpy, ).root # States that are below the potential barrier bound_states = energies < potential(barrier) # Density of states is larger on the left than on the right # Likely that the state in question is a true bound state true_bound_states = np.logical_and( bound_states, np.sum(states[:, coords[z] < barrier] ** 2, axis=1) > np.sum(states[:, coords[z] > barrier] ** 2, axis=1), ) if t_spill == 0: atom_num = np.sum(true_bound_states) atom_number = np.append(atom_number,atom_num) continue transmission_probability = np.full_like(energies, np.nan, dtype=float) for j, energy in enumerate(energies): if not true_bound_states[j]: continue intersect_end = root_scalar( lambda x: potential(x) - energy, bracket=(barrier, 3 * float(trap.subs(zr))), ).root intersect_start = root_scalar( lambda x: potential(x) - energy, bracket=(minimum, barrier), ).root s = quad( lambda x: np.sqrt( 2 * float(trap.subs(trap.m)) * np.clip(potential(x) - energy, a_min=0, a_max=None) ) / const.hbar, intersect_start, intersect_end, ) transmission_probability[j] = sp.exp(-2 * s[0]) tunneling_rate = ( transmission_probability * np.abs(energies - potential(minimum)) / const.h ) atom_number[i] = np.sum(np.exp(-t_spill * tunneling_rate[true_bound_states])) return gradients, atom_number def sweep_gradient_old(trap, power,dgrad,n_levels,left_cutoff,right_cutoff,t_spill=25*si.ms,max_spill_steps=200,n_pot_steps=1000,max_atoms=10): """ Sweeps over gradient and gets the gradient for 0 atom boundary and the precision level """ x, y, z = trap.x, trap.y, trap.z zr = trap.get_tweezer_rayleigh() trap[trap.power_tweezer] = power initial_grad = float(trap.subs(1/trap.mu_b * (3*np.sqrt(3)/4 * power * trap.a/np.pi/trap.waist_tweezer**2/zr - trap.m * trap.g))) trap[trap.grad_z] = initial_grad gradients = np.array([initial_grad],dtype=float) atom_number = np.array([0.0],dtype=float) i = 0 pbar = tqdm(disable=True) while atom_number[i] np.sum(states[:, coords[z] > barrier] ** 2, axis=1), ) if t_spill == 0: atom_num = np.sum(true_bound_states) atom_number = np.append(atom_number,atom_num) continue transmission_probability = np.full_like(energies, np.nan, dtype=float) for j, energy in enumerate(energies): if not true_bound_states[j]: continue intersect_end = root_scalar( lambda x: potential(x) - energy, bracket=(barrier, 3 * float(trap.subs(zr))), ).root intersect_start = root_scalar( lambda x: potential(x) - energy, bracket=(minimum, barrier), ).root s = quad( lambda x: np.sqrt( 2 * float(trap.subs(trap.m)) * np.clip(potential(x) - energy, a_min=0, a_max=None) ) / const.hbar, intersect_start, intersect_end, ) transmission_probability[j] = sp.exp(-2 * s[0]) tunneling_rate = ( transmission_probability * np.abs(energies - potential(minimum)) / const.h ) atom_num = np.sum(np.exp(-t_spill * tunneling_rate[true_bound_states])) atom_number = np.append(atom_number,atom_num) i += 1 pbar.update(1) pbar.close() if i == max_spill_steps: print(f"{max_atoms} atoms not reached") return gradients,atom_number raise Exception(f"{max_atoms} atoms not reached") return gradients, atom_number def calculate_stepsize(powers, atom_number,plot=False,max_atoms=10,cutoff=0.1): """ Given an array of powers (or gradients) and atom_numbers, returns the average length of steps. The step length is the area where atom number is within cutoff percent of an integer. """ bool_array = np.logical_and(np.abs(atom_number - np.round(atom_number,0)) < cutoff, atom_number > cutoff) #find points where atomnumber is close to integer bool_array = np.logical_and(bool_array, atom_number < (max_atoms-0.5)) if plot: plt.plot(powers,atom_number) plt.plot(powers[bool_array],atom_number[bool_array],"b.") diff = np.diff(bool_array.astype(int)) # Convert to int to use np.diff start_indices = np.where(diff == 1)[0] + 1 # Indices where blobs start end_indices = np.where(diff == -1)[0] + 1 # Indices where blobs end # Special case: handle if the array starts or ends with a True if bool_array[0]: start_indices = np.insert(start_indices, 0, 0) # Add the start of the array if bool_array[-1]: end_indices = np.append(end_indices, len(bool_array)) # Add the end of the array #step length step_len = np.abs(np.mean(np.diff(powers))) # Blob lengths blob_lengths = float(np.mean((end_indices - start_indices)*step_len)) # Results return blob_lengths