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.jitis 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\$