This was very difficult for a quantitative analyst to examine because the separation between variables and constants was not distinct. It has already been observed in another answer that your naming convention is odd. Attaching constants to the class
is a bad choice and makes it more difficult to understand what is updated on the instance and what is not.
Specifically indicating what are constants.
The first thing I did was rename all of your constants so that I try to observe any optimisations in functions:
import numpy as np from matplotlib import pyplot as plt # Physical constants C_mu0 = 4 * np.pi * 1e-7 C_gamma = 2.2127e5 C_kb = 1.38e-23 C_e = 1.6e-19 C_me = 9.1e-31 C_hbar = 1.054e-34 C_alpha = 0.05 C_gamma_eff = C_gamma / (1 + C_alpha ** 2) # MTJ dimensions C_D = 50e-9 C_tf = 1.1e-9 C_tox = 1.4e-9 C_AMTJ = np.pi * (C_D ** 2) / 4 C_vf = C_AMTJ * C_tf # Demag tensors C_Nx = (np.pi * C_tf) / (4 * C_D) C_Ny = C_Nx C_Nz = 1 - (2 * C_Nx) # Mag field constants C_HEX = -(5 / 1e4) / C_mu0 C_Hex = np.array([0, C_HEX, 0]) # C_HPMA = None # C_HVCMA = None # C_HDEMAG = None # Magnetic Parameters C_Ms0 = 0.786e6 C_Ki0 = 5.274e-4 C_Beta0 = 97.3e-15 # Electrical C_VMTJ = -0.2 # Temperature dependent parameters C_Msx = 1.73 C_Tc = 875 C_gammai = 2.18 C_gammab = 2.83 C_P0 = 0.58 C_asp = 2e-5 # MTJ resistance C_phi = 0.4 C_RA = 5e3 C_F = 33141 C_Rp = (C_tox / (C_F * C_phi ** 0.5 * C_AMTJ)) * np.exp( (1.025 * C_tox * (2 * C_e * C_me * C_phi) ** 0.5) / C_hbar) C_Vh = 0.5 C_TMR = 1.0 class Mtj: def __init__(self, step_size, t_max): # Mag field constants self.HPMA = None self.HVCMA = None self.HDEMAG = None self.temperature = None # Electrical self.Istt = None self.Jstt = None self.Hstt = None # Temperature dependent parameters self.Ms = None self.Ki = None self.Beta = None self.P = None # simulation initialization self.step_size = step_size self.t_max = t_max self.num_steps = round(self.t_max / self.step_size) self.m_initial = np.array([0, 0, 1]) self.magnetization = np.zeros((self.num_steps, 3)) self.time = np.zeros(self.num_steps) self.m = self.m_initial self.m_r = np.array([0, 0, 1]) def Set_temp_params(self, temperature): self.temperature = temperature self.Ms = C_Ms0 * (1 - (self.temperature / C_Tc) ** C_Msx) self.Ki = C_Ki0 * (self.Ms / C_Ms0) ** C_gammai self.Beta = C_Beta0 * (self.Ms / C_Ms0) ** C_gammab self.P = C_P0 * (1 - C_asp * self.temperature ** 1.5) self.Hstt = (C_hbar * self.P) / (2 * C_e * C_mu0 * self.Ms * C_tf) * C_gamma def Set_mag_fields(self): self.HPMA = (2 * self.Ki) / (C_mu0 * self.Ms * C_tf) self.HVCMA = - (2 * self.Beta * abs(C_VMTJ)) / (C_mu0 * self.Ms * C_tox * C_tf) self.HDEMAG = -self.Ms * np.array([C_Nx, C_Ny, C_Nz]) def calculate_Rmtj(self): self.Rmtj = (C_Rp * (1 + (C_VMTJ / C_Vh) ** 2 + C_TMR)) \ / (1 + (C_VMTJ ** 2 / C_Vh ** 2) + C_TMR * (0.5 * (1 + self.m[2]))) def llg(self): self.calculate_Rmtj() self.Istt = C_VMTJ / self.Rmtj self.Jstt = self.Istt / C_AMTJ sigmath = np.random.randn(3) sigmath = sigmath / np.linalg.norm(sigmath) Hthermal = np.sqrt((2 * C_kb * self.temperature * C_alpha) / ( C_mu0 * self.Ms * C_gamma * C_vf * self.step_size)) * sigmath Hpma = np.array([0, 0, self.HPMA * self.m[2]]) Hvcma = np.array([0, 0, self.HVCMA * self.m[2]]) Hdemag = self.HDEMAG * self.m Heff = Hpma + Hdemag + C_Hex + Hvcma + Hthermal dmdt = -C_gamma_eff * np.cross(self.m, Heff) - C_alpha * C_gamma_eff * np.cross(self.m, np.cross(self.m, Heff)) - \ self.Hstt * self.Jstt * np.cross(self.m, np.cross(self.m, self.m_r)) return dmdt def run(self): for i in range(0, self.num_steps): dmdt = self.llg() self.m = self.m + self.step_size * dmdt self.m /= np.linalg.norm(self.m) self.magnetization[i] = self.m self.time[i] = i * self.step_size def plot(self): fig, ax = plt.subplots() ax.plot(self.time, self.magnetization[:, 0], label='Mx') ax.plot(self.time, self.magnetization[:, 1], label='My') ax.plot(self.time, self.magnetization[:, 2], label='Mz') ax.set_xlabel('Time (s)') ax.set_ylabel('Magnetization') ax.legend() plt.show()
Mixing initialisation and execution
Secondly you have a separate process for initialisating step sizes, which you do on the instance and a separate method for setting temperature parameters. You should set the temperature parameters at initialisation of the class.
Then I relabelled temperature names as constants internal to the class so that it was further obvious what was updated during a run.
class Mtj: def __init__(self, step_size, t_max, temperature): # Mag field constants self.HPMA = None self.HVCMA = None self.HDEMAG = None # Temperature self.temperature = temperature self.C_Ms = C_Ms0 * (1 - (self.temperature / C_Tc) ** C_Msx) self.C_Ki = C_Ki0 * (self.C_Ms / C_Ms0) ** C_gammai self.C_Beta = C_Beta0 * (self.C_Ms / C_Ms0) ** C_gammab self.C_P = C_P0 * (1 - C_asp * self.temperature ** 1.5) self.C_Hstt = (C_hbar * self.C_P) / (2 * C_e * C_mu0 * self.C_Ms * C_tf) * C_gamma # Electrical self.Istt = None self.Jstt = None # simulation initialization self.step_size = step_size self.t_max = t_max self.num_steps = round(self.t_max / self.step_size) self.m_initial = np.array([0, 0, 1]) self.magnetization = np.zeros((self.num_steps, 3)) self.time = np.zeros(self.num_steps) self.m = self.m_initial self.m_r = np.array([0, 0, 1]) def Set_mag_fields(self): self.HPMA = (2 * self.C_Ki) / (C_mu0 * self.C_Ms * C_tf) self.HVCMA = - (2 * self.C_Beta * abs(C_VMTJ)) / (C_mu0 * self.C_Ms * C_tox * C_tf) self.HDEMAG = -self.C_Ms * np.array([C_Nx, C_Ny, C_Nz]) def calculate_Rmtj(self): self.Rmtj = (C_Rp * (1 + (C_VMTJ / C_Vh) ** 2 + C_TMR)) \ / (1 + (C_VMTJ ** 2 / C_Vh ** 2) + C_TMR * (0.5 * (1 + self.m[2]))) def llg(self): self.calculate_Rmtj() self.Istt = C_VMTJ / self.Rmtj self.Jstt = self.Istt / C_AMTJ sigmath = np.random.randn(3) sigmath = sigmath / np.linalg.norm(sigmath) Hthermal = np.sqrt((2 * C_kb * self.temperature * C_alpha) / ( C_mu0 * self.C_Ms * C_gamma * C_vf * self.step_size)) * sigmath Hpma = np.array([0, 0, self.HPMA * self.m[2]]) Hvcma = np.array([0, 0, self.HVCMA * self.m[2]]) Hdemag = self.HDEMAG * self.m Heff = Hpma + Hdemag + C_Hex + Hvcma + Hthermal dmdt = -C_gamma_eff * np.cross(self.m, Heff) - C_alpha * C_gamma_eff * np.cross(self.m, np.cross(self.m, Heff)) - \ self.C_Hstt * self.Jstt * np.cross(self.m, np.cross(self.m, self.m_r)) return dmdt
After doing this I could observe that set_mag_fields
was just setting constants as well. So this was restructured into the initialisation.
class Mtj: def __init__(self, step_size, t_max, temperature): # Temperature self.temperature = temperature self.C_Ms = C_Ms0 * (1 - (self.temperature / C_Tc) ** C_Msx) self.C_Ki = C_Ki0 * (self.C_Ms / C_Ms0) ** C_gammai self.C_Beta = C_Beta0 * (self.C_Ms / C_Ms0) ** C_gammab self.C_P = C_P0 * (1 - C_asp * self.temperature ** 1.5) self.C_Hstt = (C_hbar * self.C_P) / (2 * C_e * C_mu0 * self.C_Ms * C_tf) * C_gamma # Mag field constants self.C_HPMA = (2 * self.C_Ki) / (C_mu0 * self.C_Ms * C_tf) self.C_HVCMA = - (2 * self.C_Beta * abs(C_VMTJ)) / (C_mu0 * self.C_Ms * C_tox * C_tf) self.C_HDEMAG = -self.C_Ms * np.array([C_Nx, C_Ny, C_Nz])
Standardising routines
I removed calculate_Rmtj
. A better name for it would have been set_Rmtj
but it was a one line function out of context with the rest of your code style.
Unfortunately I have to get back to work now.. But with the increased visibility it might be easier to look for further optimisations and any possible vectorisations. I am sure this can be improved further for performance gains.
Full Updated code
import numpy as np from matplotlib import pyplot as plt # Physical constants C_mu0 = 4 * np.pi * 1e-7 C_gamma = 2.2127e5 C_kb = 1.38e-23 C_e = 1.6e-19 C_me = 9.1e-31 C_hbar = 1.054e-34 C_alpha = 0.05 C_gamma_eff = C_gamma / (1 + C_alpha ** 2) # MTJ dimensions C_D = 50e-9 C_tf = 1.1e-9 C_tox = 1.4e-9 C_AMTJ = np.pi * (C_D ** 2) / 4 C_vf = C_AMTJ * C_tf # Demag tensors C_Nx = (np.pi * C_tf) / (4 * C_D) C_Ny = C_Nx C_Nz = 1 - (2 * C_Nx) # Mag field constants C_HEX = -(5 / 1e4) / C_mu0 C_Hex = np.array([0, C_HEX, 0]) # C_HPMA = None # C_HVCMA = None # C_HDEMAG = None # Magnetic Parameters C_Ms0 = 0.786e6 C_Ki0 = 5.274e-4 C_Beta0 = 97.3e-15 # Electrical C_VMTJ = -0.2 # Temperature dependent parameters C_Msx = 1.73 C_Tc = 875 C_gammai = 2.18 C_gammab = 2.83 C_P0 = 0.58 C_asp = 2e-5 # MTJ resistance C_phi = 0.4 C_RA = 5e3 C_F = 33141 C_Rp = (C_tox / (C_F * C_phi ** 0.5 * C_AMTJ)) * np.exp( (1.025 * C_tox * (2 * C_e * C_me * C_phi) ** 0.5) / C_hbar) C_Vh = 0.5 C_TMR = 1.0 class Mtj: def __init__(self, step_size, t_max, temperature): # Temperature self.temperature = temperature self.C_Ms = C_Ms0 * (1 - (self.temperature / C_Tc) ** C_Msx) self.C_Ki = C_Ki0 * (self.C_Ms / C_Ms0) ** C_gammai self.C_Beta = C_Beta0 * (self.C_Ms / C_Ms0) ** C_gammab self.C_P = C_P0 * (1 - C_asp * self.temperature ** 1.5) self.C_Hstt = (C_hbar * self.C_P) / (2 * C_e * C_mu0 * self.C_Ms * C_tf) * C_gamma # Mag field constants self.C_HPMA = (2 * self.C_Ki) / (C_mu0 * self.C_Ms * C_tf) self.C_HVCMA = - (2 * self.C_Beta * abs(C_VMTJ)) / (C_mu0 * self.C_Ms * C_tox * C_tf) self.C_HDEMAG = -self.C_Ms * np.array([C_Nx, C_Ny, C_Nz]) # Electrical self.Istt = None self.Jstt = None # simulation initialization self.step_size = step_size self.t_max = t_max self.num_steps = round(self.t_max / self.step_size) self.m_initial = np.array([0, 0, 1]) self.magnetization = np.zeros((self.num_steps, 3)) self.time = np.zeros(self.num_steps) self.m = self.m_initial self.m_r = np.array([0, 0, 1]) def llg(self): self.Rmtj = (C_Rp * (1 + (C_VMTJ / C_Vh) ** 2 + C_TMR)) \ / (1 + (C_VMTJ ** 2 / C_Vh ** 2) + C_TMR * (0.5 * (1 + self.m[2]))) self.Istt = C_VMTJ / self.Rmtj self.Jstt = self.Istt / C_AMTJ sigmath = np.random.randn(3) sigmath = sigmath / np.linalg.norm(sigmath) Hthermal = np.sqrt((2 * C_kb * self.temperature * C_alpha) / ( C_mu0 * self.C_Ms * C_gamma * C_vf * self.step_size)) * sigmath Hpma = np.array([0, 0, self.C_HPMA * self.m[2]]) Hvcma = np.array([0, 0, self.C_HVCMA * self.m[2]]) Hdemag = self.C_HDEMAG * self.m Heff = Hpma + Hdemag + C_Hex + Hvcma + Hthermal dmdt = -C_gamma_eff * np.cross(self.m, Heff) - C_alpha * C_gamma_eff * np.cross(self.m, np.cross(self.m, Heff)) - \ self.C_Hstt * self.Jstt * np.cross(self.m, np.cross(self.m, self.m_r)) return dmdt def run(self): for i in range(0, self.num_steps): dmdt = self.llg() self.m = self.m + self.step_size * dmdt self.m /= np.linalg.norm(self.m) self.magnetization[i] = self.m self.time[i] = i * self.step_size def plot(self): fig, ax = plt.subplots() ax.plot(self.time, self.magnetization[:, 0], label='Mx') ax.plot(self.time, self.magnetization[:, 1], label='My') ax.plot(self.time, self.magnetization[:, 2], label='Mz') ax.set_xlabel('Time (s)') ax.set_ylabel('Magnetization') ax.legend() plt.show() mtj = Mtj(1e-12, 10e-9, 450) mtj.run() mtj.plot()
Actually you can now observe that these lines:
sigmath = np.random.randn(3) sigmath = sigmath / np.linalg.norm(sigmath) Hthermal = np.sqrt( (2 * C_kb * self.temperature * C_alpha) / (C_mu0 * self.C_Ms * C_gamma * C_vf * self.step_size) ) * sigmath
contain purely constants and random variables. You can extract this outside the class and create one large vectorised array rather than calculating it repeatedly in each loop.
Profiling
np.cross
accounts for 60% of the profiling speed.

It runs 50,000 times. You can observe that one of the equations does not need to be calculated twice: np.cross(self.m, Heff)
can be precalculated and substitued in, which will reduce to 40,000 times.
You may also find that your triple vector cross products are more rapidly solved by the alternative formulation. But for this you would have to experiment.

Edited with suggestions
I took the constants and random variables to initialisation and also reduced the cross product computations to 40,000. Speed improved:

I also used the alternative formulation for the triple cross product as follows:
m_dot_m = np.dot(self.m, self.m) m_dot_Heff = np.dot(self.m, Heff) m_dot_r = np.dot(self.m, self.m_r) m_x_m_x_Heff = self.m * m_dot_Heff - Heff * m_dot_m m_x_m_x_r = self.m * m_dot_r - self.m_r * m_dot_m cross_m_Heff = np.cross(self.m, Heff) dmdt = -C_gamma_eff * cross_m_Heff - C_alpha * C_gamma_eff * \ m_x_m_x_Heff - \ self.C_Hstt * self.Jstt * m_x_m_x_r
This was very effective, notice the time for llg

The real gains
You mentioned that you need to do this 10,000 times. You cannot vectorize or parallelize the above because the operations need to be performed in sequence.
However you can, and should, vectorize the run of 10,000 of these simulations. For example the array magnetization
has dimension (num_steps, 3), but you aim to create a 3D tensor which is of dimension (num_steps, 3, 10000).
That is why I reformulated the equation using dot products, so that later you can vectorize all of these formulae to obtain the speed from numpy's C implementation.
Thus far
import numpy as np from matplotlib import pyplot as plt # Physical constants C_mu0 = 4 * np.pi * 1e-7 C_gamma = 2.2127e5 C_kb = 1.38e-23 C_e = 1.6e-19 C_me = 9.1e-31 C_hbar = 1.054e-34 C_alpha = 0.05 C_gamma_eff = C_gamma / (1 + C_alpha ** 2) # MTJ dimensions C_D = 50e-9 C_tf = 1.1e-9 C_tox = 1.4e-9 C_AMTJ = np.pi * (C_D ** 2) / 4 C_vf = C_AMTJ * C_tf # Demag tensors C_Nx = (np.pi * C_tf) / (4 * C_D) C_Ny = C_Nx C_Nz = 1 - (2 * C_Nx) # Mag field constants C_HEX = -(5 / 1e4) / C_mu0 C_Hex = np.array([0, C_HEX, 0]) # C_HPMA = None # C_HVCMA = None # C_HDEMAG = None # Magnetic Parameters C_Ms0 = 0.786e6 C_Ki0 = 5.274e-4 C_Beta0 = 97.3e-15 # Electrical C_VMTJ = -0.2 # Temperature dependent parameters C_Msx = 1.73 C_Tc = 875 C_gammai = 2.18 C_gammab = 2.83 C_P0 = 0.58 C_asp = 2e-5 # MTJ resistance C_phi = 0.4 C_RA = 5e3 C_F = 33141 C_Rp = (C_tox / (C_F * C_phi ** 0.5 * C_AMTJ)) * np.exp( (1.025 * C_tox * (2 * C_e * C_me * C_phi) ** 0.5) / C_hbar) C_Vh = 0.5 C_TMR = 1.0 class Mtj: def __init__(self, step_size, t_max, temperature): # Temperature self.temperature = temperature self.C_Ms = C_Ms0 * (1 - (self.temperature / C_Tc) ** C_Msx) self.C_Ki = C_Ki0 * (self.C_Ms / C_Ms0) ** C_gammai self.C_Beta = C_Beta0 * (self.C_Ms / C_Ms0) ** C_gammab self.C_P = C_P0 * (1 - C_asp * self.temperature ** 1.5) self.C_Hstt = (C_hbar * self.C_P) / (2 * C_e * C_mu0 * self.C_Ms * C_tf) * C_gamma # Mag field constants self.C_HPMA = (2 * self.C_Ki) / (C_mu0 * self.C_Ms * C_tf) self.C_HVCMA = - (2 * self.C_Beta * abs(C_VMTJ)) / (C_mu0 * self.C_Ms * C_tox * C_tf) self.C_HDEMAG = -self.C_Ms * np.array([C_Nx, C_Ny, C_Nz]) # Electrical self.Istt = None self.Jstt = None # simulation initialization self.step_size = step_size self.t_max = t_max self.num_steps = round(self.t_max / self.step_size) self.m_initial = np.array([0, 0, 1]) self.magnetization = np.zeros((self.num_steps, 3)) self.time = np.zeros(self.num_steps) self.m = self.m_initial self.m_r = np.array([0, 0, 1]) sigmath = np.random.randn(3, self.num_steps) sigmath = sigmath / np.linalg.norm(sigmath, axis=0) self.C_Hthermal = np.sqrt((2 * C_kb * self.temperature * C_alpha) / ( C_mu0 * self.C_Ms * C_gamma * C_vf * self.step_size)) * sigmath def llg(self, i): self.Rmtj = (C_Rp * (1 + (C_VMTJ / C_Vh) ** 2 + C_TMR)) \ / (1 + (C_VMTJ ** 2 / C_Vh ** 2) + C_TMR * (0.5 * (1 + self.m[2]))) self.Istt = C_VMTJ / self.Rmtj self.Jstt = self.Istt / C_AMTJ Hpma = np.array([0, 0, self.C_HPMA * self.m[2]]) Hvcma = np.array([0, 0, self.C_HVCMA * self.m[2]]) Hdemag = self.C_HDEMAG * self.m Heff = Hpma + Hdemag + C_Hex + Hvcma + self.C_Hthermal[:, i] m_dot_m = np.dot(self.m, self.m) m_dot_Heff = np.dot(self.m, Heff) m_dot_r = np.dot(self.m, self.m_r) m_x_m_x_Heff = self.m * m_dot_Heff - Heff * m_dot_m m_x_m_x_r = self.m * m_dot_r - self.m_r * m_dot_m cross_m_Heff = np.cross(self.m, Heff) dmdt = -C_gamma_eff * cross_m_Heff - C_alpha * C_gamma_eff * \ m_x_m_x_Heff - \ self.C_Hstt * self.Jstt * m_x_m_x_r return dmdt def run(self): for i in range(0, self.num_steps): dmdt = self.llg(i) self.m = self.m + self.step_size * dmdt self.m /= np.linalg.norm(self.m) self.magnetization[i] = self.m self.time[i] = i * self.step_size def plot(self): fig, ax = plt.subplots() ax.plot(self.time, self.magnetization[:, 0], label='Mx') ax.plot(self.time, self.magnetization[:, 1], label='My') ax.plot(self.time, self.magnetization[:, 2], label='Mz') ax.set_xlabel('Time (s)') ax.set_ylabel('Magnetization') ax.legend() plt.show() mtj = Mtj(1e-12, 10e-9, 450) mtj.run() mtj.plot()
np.random.randn(3)
call. 8^)\$\endgroup\$@numba.jit
is a good start, but if you really want performance, write it in C or C++. That's the depressing truth about python performance: there isn't any. The good news is your code is mainly numeric calculations so translating it should be fairly straightforward.\$\endgroup\$