From e13ad7d9e189657be8e747d125461cc699a7d978 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Thu, 16 May 2024 23:34:59 +0200 Subject: [PATCH 01/44] updating the docs to benefit from stubs --- CHANGELOG.md | 3 +- cmtj/__init__.pyi | 198 ++++++++++++++---- cmtj/llgb/__init__.pyi | 100 +++++++-- cmtj/stack/__init__.pyi | 27 ++- docs/api/core.md | 1 + docs/api/llgb.md | 1 + docs/api/noise.md | 1 + docs/api/stack.md | 1 + docs/docgen.py | 1 - docs/gen-docs/cmtj.md | 436 --------------------------------------- docs/gen-docs/drivers.md | 56 ----- docs/gen-docs/noise.md | 21 -- docs/gen-docs/stack.md | 113 ---------- docs/index.md | 4 +- mkdocs.yml | 11 +- 15 files changed, 283 insertions(+), 691 deletions(-) create mode 100644 docs/api/core.md create mode 100644 docs/api/llgb.md create mode 100644 docs/api/noise.md create mode 100644 docs/api/stack.md delete mode 100644 docs/gen-docs/cmtj.md delete mode 100644 docs/gen-docs/drivers.md delete mode 100644 docs/gen-docs/noise.md delete mode 100644 docs/gen-docs/stack.md diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4115d..a17d86b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Changelog -# 1.5.0 (WIP) +# 1.5.0-1.5.4 - Dipole interaction added to the `SB Model` - Kasdin 1/f noise generator added to the `noise` module and to the solvers @@ -8,6 +8,7 @@ - added a simple noise model to the `utils` class. It exists outside standard simulation procedures. - added LLGB bindings and code. The solver is still WIP and doesn't integrate with more advanced features yet. - added aliases for `ScalarDriver` -- for example, instead of calling `ScalarDriver.getConstantDriver`, you can now call `constantDriver` directly to create a constant driver. +- improve stub detection across editors and IDEs # 1.4.1 diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index 66f5e1f..4daed8f 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -113,18 +113,54 @@ def trapezoidDriver( ... class AxialDriver: + """Axial driver class.""" + @overload - def __init__( - self, x_driver: ScalarDriver, y_driver: ScalarDriver, z_driver: ScalarDriver - ) -> None: ... + def __init__(self, x: ScalarDriver, y: ScalarDriver, z: ScalarDriver) -> None: + """Create an axial driver with three scalar drivers for each axis. + :param x: driver for the x axis + :param y: driver for the y axis + :param z: driver for the z axis + """ + ... + + @overload + def __init__(self, axialDrivers: List[ScalarDriver]) -> None: + """Create an axial driver with a list of scalar drivers. + :param arg0: list of scalar drivers + """ + ... + @overload - def __init__(self, arg0: List[ScalarDriver]) -> None: ... + def __init__(self, x: float, y: float, z: float) -> None: + """Create an axial driver with a list of floats. + :param x: constant float for the x axis + :param y: constant float for the y axis + :param z: constant float for the z axis + """ + ... + + @overload + def __init__(self, xyz: CVector) -> None: + """Create an axial driver with a vector. + :param xyz: CVector with components of x, y, z axis + """ + ... + @overload def __init__(*args, **kwargs) -> Any: ... @overload - def applyMask(self, arg0: CVector) -> None: ... + def applyMask(self, mask: CVector) -> None: + """Apply mask to the driver. + :param mask: mask to be applied""" + ... + @overload - def applyMask(self, arg0: List[int]) -> None: ... + def applyMask(self, mask: List[int]) -> None: + """Apply mask to the driver. + :param mask: mask to be applied""" + ... + @overload def applyMask(*args, **kwargs) -> Any: ... def getCurrentAxialDrivers(self, arg0: float) -> CVector: ... @@ -154,10 +190,27 @@ class Axis: def __members__(self) -> Any: ... class CVector: - def __init__(self, x: float, y: float, z: float) -> None: ... - def length(self) -> float: ... - def normalize(self) -> None: ... - def tolist(self) -> List[float]: ... + """CVector class. Represents a 3D vector.""" + + def __init__(self, x: float, y: float, z: float) -> None: + """Initialises a 3D vector. + :param x: x component of the vector + :param y: y component of the vector + :param z: z component of the vector""" + ... + + def length(self) -> float: + """Returns the length of the vector.""" + ... + + def normalize(self) -> None: + """Normalizes the vector.""" + ... + + def tolist(self) -> List[float]: + """Converts the vector to a list.""" + ... + def __add__(self, arg0: CVector) -> CVector: ... def __eq__(self, arg0: CVector) -> bool: ... def __getitem__(self, arg0: int) -> float: ... @@ -185,16 +238,24 @@ class CVector: class Junction: @overload - def __init__(self, layers: List[Layer], filename: str = ...) -> None: ... + def __init__(self, layers: List[Layer]) -> None: + """""" + ... + @overload - def __init__( - self, layers: List[Layer], filename: str, Rp: float = ..., Rap: float = ... - ) -> None: ... + def __init__(self, layers: List[Layer], Rp: float = ..., Rap: float = ...) -> None: + """Creates a junction with a magnetoresistance. + :param layers: list of layers + + :param Rp: Magnetoresistance parallel state + :param Rap: Magnetoresistance anti-parallel state + """ + ... + @overload def __init__( self, layers: List[Layer], - filename: str, Rx0: List[float], Ry0: List[float], AMR_X: List[float], @@ -224,7 +285,11 @@ class Junction: """Reset current simulation state.""" ... - def getLayerMagnetisation(self, layer_id: str) -> CVector: ... + def getLayerMagnetisation(self, layer_id: str) -> CVector: + """Get the magnetisation of a layer. + :param layer_id: the layer id""" + ... + def getLog(self) -> Dict[str, List[float]]: """Retrieve the simulation log [data].""" ... @@ -251,7 +316,7 @@ class Junction: ... def setIECDriver( - self, bottom_layer: str, top_layer: str, driver: ScalarDriver + self, bottomLayer: str, topLayer: str, driver: ScalarDriver ) -> None: """Set IEC interaction between two layers. The names of the params are only for convention. The IEC will be set @@ -262,7 +327,7 @@ class Junction: ... def setQuadIECDriver( - self, bottom_layer: str, top_layer: str, driver: ScalarDriver + self, bottomLayer: str, topLayer: str, driver: ScalarDriver ) -> None: """Set secondary (biquadratic term) IEC interaction between two layers. The names of the params are only for convention. The IEC will be set @@ -272,19 +337,49 @@ class Junction: """ ... - def setLayerTemperatureDriver( - self, layer_id: str, driver: ScalarDriver - ) -> None: ... - def setLayerAnisotropyDriver(self, layer_id: str, driver: ScalarDriver) -> None: ... - def setLayerCurrentDriver(self, layer_id: str, driver: ScalarDriver) -> None: ... - def setLayerExternalFieldDriver( - self, layer_id: str, driver: AxialDriver - ) -> None: ... - def setLayerMagnetisation(self, layer_id: str, mag: CVector) -> None: ... + def setLayerTemperatureDriver(self, layer_id: str, driver: ScalarDriver) -> None: + """Set a temperature driver for a layer. + :param layer_id: the id of the layer. + :param driver: the temperature driver to be set. + """ + ... + + def setLayerAnisotropyDriver(self, layer_id: str, driver: ScalarDriver) -> None: + """Set anisotropy driver for a layer. + :param layer_id: the id of the layer. + :param driver: the anisotropy driver to be set. + """ + ... + + def setLayerCurrentDriver(self, layer_id: str, driver: ScalarDriver) -> None: + """Set a current driver for a layer. + :param layer_id: the layer id + :param driver: the driver + """ + ... + + def setLayerExternalFieldDriver(self, layer_id: str, driver: AxialDriver) -> None: + """Set an external field driver for a layer. + :param layer_id: the id of the layer. + :param driver: the field driver to be set. + """ + ... + + def setLayerMagnetisation(self, layer_id: str, mag: CVector) -> None: + """Set the magnetisation of a layer. + :param layer_id: the layer id + :param mag: the magnetisation + """ + ... + @overload - def setLayerOerstedFieldDriver( - self, layer_id: str, driver: AxialDriver - ) -> None: ... + def setLayerOerstedFieldDriver(self, layer_id: str, driver: AxialDriver) -> None: + """Set an Oersted field driver for a layer. + :param layer_id: the id of the layer. + :param driver: the field driver to be set. + """ + ... + def setLayerDampingLikeTorqueDriver( self, layer_id: str, driver: ScalarDriver ) -> None: @@ -365,7 +460,6 @@ class Layer: :param Ms: magnetisation saturation. Unit: Tesla [T]. :param thickness: thickness of the layer. Unit: meter [m]. :param cellSurface: surface of the layer, for volume calculation. Unit: meter^2 [m^2]. - :param temperature: resting temperature of the layer. Unit: Kelvin [K]. :param damping: often marked as alpha in the LLG equation. Damping of the layer. Default 0.011. Dimensionless. """ ... @@ -410,25 +504,49 @@ class Layer: ... def setExternalFieldDriver(self, driver: AxialDriver) -> None: ... - def setMagnetisation(self, mag: CVector) -> None: ... - def setOerstedFieldDriver(self, driver: AxialDriver) -> None: ... + def setMagnetisation(self, mag: CVector) -> None: + """Set the magnetisation of the layer. + :param mag: the magnetisation to be set.""" + ... + + def setOerstedFieldDriver(self, driver: AxialDriver) -> None: + """Set an Oersted field driver for the layer. + :param driver: the field driver to be set.""" + ... + def setDampingLikeTorqueDriver(self, driver: ScalarDriver) -> None: - """Set a driver for the damping like torque of the layer.""" + """Set a driver for the damping like torque of the layer. + :param driver: the driver to be set.""" ... def setFieldLikeTorqueDriver(self, driver: ScalarDriver) -> None: - """Set a driver for the field like torque of the layer.""" + """Set a driver for the field like torque of the layer. + :param driver: the driver to be set.""" + ... + + def setReferenceLayer(self, ref: CVector) -> None: + """Set a reference layer for the STT. + :param ref: the reference layer vector.""" ... - def setReferenceLayer(self, ref: CVector) -> None: ... @overload - def setReferenceLayer(self, ref: "Reference") -> None: ... + def setReferenceLayer(self, ref: "Reference") -> None: + """Set a reference layer for the STT. The reference can be + FIXED, BOTTOM or TOP. YOu can use another layer as reference + to this one. + :param ref: the reference layer vector.""" + ... + def setTopDipoleTensor(self, tensor: List[CVector]) -> None: - """Set a dipole tensor from the top layer.""" + """Set a dipole tensor from the top layer. + :param tensor: the dipole tensor to be set. + """ ... def setBottomDipoleTensor(self, tensor: List[CVector]) -> None: - """Set a dipole tensor from the bottom layer.""" + """Set a dipole tensor from the bottom layer. + :param tensor: the dipole tensor to be set. + """ ... def getId(self) -> str: @@ -438,6 +556,7 @@ class Layer: def setAlternativeSTT(self, setAlternative: bool) -> None: """Switch to an alternative STT forumulation (Taniguchi et al.) https://iopscience.iop.org/article/10.7567/APEX.11.013005 + :param setAlternative: whether to set the alternative STT formulation """ ... @@ -445,6 +564,7 @@ class Layer: """Set the kappa parameter for the layer -- determines SOT mixing Hdl * kappa + Hfl Allows you to turn off Hdl. Turning Hfl is via beta parameter. + :param kappa: the kappa parameter """ ... diff --git a/cmtj/llgb/__init__.pyi b/cmtj/llgb/__init__.pyi index e06aea7..fac6e3b 100644 --- a/cmtj/llgb/__init__.pyi +++ b/cmtj/llgb/__init__.pyi @@ -3,9 +3,21 @@ from typing import Dict, List, Tuple import cmtj class LLGBJunction: - def __init__(self, layers: List[LLGBLayer]) -> None: ... - def clearLog(self) -> None: ... - def getLog(self) -> Dict[str, List[float]]: ... + """LLGB Junction class.""" + + def __init__(self, layers: List[LLGBLayer]) -> None: + """Initialises a LLGB junction with layers. + :param layers: list of LLGB layers.""" + ... + + def clearLog(self) -> None: + """Clears the simulation log of the junction.""" + ... + + def getLog(self) -> Dict[str, List[float]]: + """Returns the simulation log of the junction.""" + ... + def runSimulation( self, totalTime: float, @@ -13,14 +25,41 @@ class LLGBJunction: writeFrequency: float = ..., log: bool = ..., solverMode: cmtj.SolverMode = ..., - ) -> None: ... - def saveLogs(self, arg0: str) -> None: ... + ) -> None: + """Runs the simulation of the junction. + :param totalTime: total simulation time. + :param timeStep: time step. + :param writeFrequency: frequency of writing to the log. + :param log: whether to log the simulation. + :param solverMode: solver mode. + """ + ... + + def saveLogs(self, arg0: str) -> None: + """Saves the simulation logs to a file. + :param arg0: file path.""" + ... + def setLayerExternalFieldDriver( - self, arg0: str, arg1: cmtj.AxialDriver - ) -> None: ... - def setLayerTemperatureDriver(self, arg0: str, arg1: cmtj.ScalarDriver) -> None: ... + self, layerId: str, driver: cmtj.AxialDriver + ) -> None: + """Set an external field driver for a layer. + :param layerId: the id of the layer. + :param driver: the field driver to be set.""" + ... + + def setLayerTemperatureDriver( + self, layerId: str, driver: cmtj.ScalarDriver + ) -> None: + """Set a temperature driver for a layer. + :param layerId: the id of the layer. + :param driver: the temperature driver to be set. + """ + ... class LLGBLayer: + """LLGB Layer class.""" + def __init__( self, id: str, @@ -34,10 +73,36 @@ class LLGBLayer: Tc: float, susceptibility: float, me: float, - ) -> None: ... - def setAnisotropyDriver(self, arg0: cmtj.ScalarDriver) -> None: ... - def setExternalFieldDriver(self, arg0: cmtj.AxialDriver) -> None: ... - def setTemperatureDriver(self, arg0: cmtj.ScalarDriver) -> None: ... + ) -> None: + """Creates a LLGB layer. + :param id: layer id. + :param mag: magnetisation. + :param anis: anisotropy axis. + :param Ms: saturation magnetisation. + :param thickness: thickness. + :param cellSurface: cell surface. + :param demagTensor: demagnetisation tensor. + :param damping: damping factor. + :param Tc: Curie temperature. + :param susceptibility: susceptibility. + :param me: equilibrium magnetisation. + """ + ... + + def setAnisotropyDriver(self, driver: cmtj.ScalarDriver) -> None: + """Sets an anisotropy driver. + :param driver: the anisotropy driver to be set.""" + ... + + def setExternalFieldDriver(self, driver: cmtj.AxialDriver) -> None: + """Sets an external field driver. + :param driver: the field driver to be set.""" + ... + + def setTemperatureDriver(self, driver: cmtj.ScalarDriver) -> None: + """Sets a temperature driver. + :param driver: the temperature driver to be set.""" + ... def MFAWeissCurie( me: float, @@ -46,6 +111,15 @@ def MFAWeissCurie( relax: float = ..., tolerance: float = ..., maxIter: int = ..., -) -> Tuple[float, float]: ... +) -> Tuple[float, float]: + """Mean Field Approximation for Weiss Curie temperature. + :param me: equilibrium magnetisation. + :param T: temperature. + :param J0: exchange coupling. + :param relax: relaxation factor. + :param tolerance: tolerance for convergence. + :param maxIter: maximum number of iterations.""" + ... + def langevin(arg0: float) -> float: ... def langevinDerivative(arg0: float) -> float: ... diff --git a/cmtj/stack/__init__.pyi b/cmtj/stack/__init__.pyi index 5763093..037d0bc 100644 --- a/cmtj/stack/__init__.pyi +++ b/cmtj/stack/__init__.pyi @@ -9,12 +9,14 @@ class ParallelStack: :param junctionList: list of junctions to be connected in parallel. """ ... + def clearLogs(self) -> None: """ Clear all the logs, both of the stack and the junctions that constitute the stack. """ ... + @overload def getLog(self, junctionId: int) -> Dict[str, List[float]]: """ @@ -23,12 +25,14 @@ class ParallelStack: :param junctionId: integer junction id as was passed in the init. """ ... + @overload def getLog(self) -> Dict[str, List[float]]: """ Get the logs of the stack """ ... + def runSimulation( self, totalTime: float, timeStep: float = ..., writeFrequency: float = ... ) -> None: @@ -39,6 +43,7 @@ class ParallelStack: :param writeFrequency: how often is the log saved to? Must be no smaller than `timeStep`. Default is 1e-11. """ ... + def setCoupledCurrentDriver(self, driver: cmtj.ScalarDriver) -> None: """ Sets a global current driver for all junctions inside the stack. @@ -47,6 +52,7 @@ class ParallelStack: :param driver: the current driver to be set. """ ... + def setCouplingStrength(self, coupling: float) -> None: """ Coupling constant that represents the energy losses as the current @@ -54,14 +60,16 @@ class ParallelStack: :param coupling: the coupling strength (or the losses) """ ... + def setExternalFieldDriver(self, driver: cmtj.AxialDriver) -> None: """ Sets a external field current driver for all junctions inside the stack. :param driver: the field driver to be set. """ ... + def setMagnetistation( - self, juncionId: int, layerId: str, mag: cmtj.CVector + self, junctionId: int, layerId: str, mag: cmtj.CVector ) -> None: """ Set magnetisation on a specific layer in a specific junction. @@ -80,12 +88,14 @@ class SeriesStack: :param junctionList: list of junctions to be connected in series. """ ... + def clearLogs(self) -> None: """ Clear all the logs, both of the stack and the junctions that constitute the stack. """ ... + @overload def getLog(self, junctionId: int) -> Dict[str, List[float]]: """ @@ -94,12 +104,14 @@ class SeriesStack: :param junctionId: integer junction id as was passed in the init. """ ... + @overload def getLog(self) -> Dict[str, List[float]]: """ Get the logs of the stack """ ... + def runSimulation( self, totalTime: float, timeStep: float = ..., writeFrequency: float = ... ) -> None: @@ -110,6 +122,7 @@ class SeriesStack: :param writeFrequency: how often is the log saved to? Must be no smaller than `timeStep`. Default is 1e-11. """ ... + def setCoupledCurrentDriver(self, driver: cmtj.ScalarDriver) -> None: """ Sets a global current driver for all junctions inside the stack. @@ -118,6 +131,7 @@ class SeriesStack: :param driver: the current driver to be set. """ ... + def setCouplingStrength(self, coupling: float) -> None: """ Coupling constant that represents the energy losses as the current @@ -125,14 +139,16 @@ class SeriesStack: :param coupling: the coupling strength (or the losses) """ ... + def setExternalFieldDriver(self, driver: cmtj.AxialDriver) -> None: """ Sets a external field current driver for all junctions inside the stack. :param driver: the field driver to be set. """ ... + def setMagnetistation( - self, juncionId: int, layerId: str, mag: cmtj.CVector + self, junctionId: int, layerId: str, mag: cmtj.CVector ) -> None: """ Set magnetisation on a specific layer in a specific junction. @@ -141,4 +157,9 @@ class SeriesStack: :param mag: the magnetisation to be set. """ ... - def getMagnetisation(self, junction: int, layerId: str) -> cmtj.CVector: ... + + def getMagnetisation(self, junction: int, layerId: str) -> cmtj.CVector: + """Get the magnetisation of a specific layer in a specific junction. + :param junction: the id of the junction (int) as passed in the init. + :param layerId: the string id of the layer in the junction.""" + ... diff --git a/docs/api/core.md b/docs/api/core.md new file mode 100644 index 0000000..f7992cf --- /dev/null +++ b/docs/api/core.md @@ -0,0 +1 @@ +::: cmtj diff --git a/docs/api/llgb.md b/docs/api/llgb.md new file mode 100644 index 0000000..0b2d681 --- /dev/null +++ b/docs/api/llgb.md @@ -0,0 +1 @@ +::: cmtj.llgb diff --git a/docs/api/noise.md b/docs/api/noise.md new file mode 100644 index 0000000..ff278ab --- /dev/null +++ b/docs/api/noise.md @@ -0,0 +1 @@ +::: cmtj.noise diff --git a/docs/api/stack.md b/docs/api/stack.md new file mode 100644 index 0000000..7616c73 --- /dev/null +++ b/docs/api/stack.md @@ -0,0 +1 @@ +::: cmtj.stack diff --git a/docs/docgen.py b/docs/docgen.py index bd9f2b3..0c44c08 100644 --- a/docs/docgen.py +++ b/docs/docgen.py @@ -122,7 +122,6 @@ def create_api_markdown_file(src_filename): .replace("@overload", "") ) class_name = doc_.partition("\n")[0].replace(":", "").strip() - print(i, class_name) md_fn += f"## `{class_name}`" for g in extract_python_docs(doc_.replace("...", "...\n")): sig = g.py_signature_to_markdown() diff --git a/docs/gen-docs/cmtj.md b/docs/gen-docs/cmtj.md deleted file mode 100644 index 52565d0..0000000 --- a/docs/gen-docs/cmtj.md +++ /dev/null @@ -1,436 +0,0 @@ -## `AxialDriver` - -### `__init__(self, x_driver: ScalarDriver, y_driver: ScalarDriver, z_driver: ScalarDriver)` - -### `__init__(self, arg0: List[ScalarDriver])` - -### `__init__(*args, **kwargs)` - -### `applyMask(self, arg0: CVector)` - -### `applyMask(self, arg0: List[int])` - -### `applyMask(*args, **kwargs)` - -### `getCurrentAxialDrivers(self, arg0: float)` - -### `getVectorAxialDriver(self, arg0: float, arg1: float)` - -## `Axis` - -### `__init__(self, value: int)` - -### `__eq__(self, other: object)` - -### `__getstate__(self)` - -### `__hash__(self)` - -### `__index__(self)` - -### `__int__(self)` - -### `__ne__(self, other: object)` - -### `__setstate__(self, state: int)` - -### `name(self)` - -### `__doc__(self)` - -### `__members__(self)` - -## `CVector` - -### `__init__(self, x: float, y: float, z: float)` - -### `length(self)` - -### `normalize(self)` - -### `tolist(self)` - -### `__add__(self, arg0: CVector)` - -### `__eq__(self, arg0: CVector)` - -### `__getitem__(self, arg0: int)` - -### `__iter__(self) -> typing.Iterator[float]: ...def __iadd__(self, arg0: CVector)` - -### `__imul__(self, arg0: float)` - -### `__isub__(self, arg0: CVector)` - -### `__len__(self)` - -### `__mul__(self, arg0: float)` - -### `__ne__(self, arg0: CVector)` - -### `__rmul__(self, arg0: float)` - -### `__sub__(self, arg0: CVector)` - -### `x(self)` - -### `x(self, val: float)` - -### `y(self)` - -### `y(self, val: float)` - -### `z(self)` - -### `z(self, val: float)` - -## `Junction` - -### `__init__(self, layers: List[Layer], filename: str = ...)` - -### `__init__(self, layers: List[Layer], filename: str, Rp: float = ..., Rap: float = ...)` - -### `__init__(self,layers: List[Layer],filename: str,Rx0: List[float],Ry0: List[float],AMR_X: List[float],AMR_Y: List[float],SMR_X: List[float],SMR_Y: List[float],AHE: List[float],)` - -Creates a junction with a STRIP magnetoresistance. -Each of the Rx0, Ry, AMR, AMR and SMR is list matching the -length of the layers passed (they directly correspond to each layer). -Calculates the magnetoresistance as per: **see reference**: -Spin Hall magnetoresistance in metallic bilayers by Kim, J. et al. - -#### **Parameters** - -| Name | Type | Description | Default | -| ----------- | ------------- | ------------------------------------------ | ------- | -| **`Rx0`** | `List[float]` | Magnetoresistance offset longitudinal | `-` | -| **`Ry0`** | `List[float]` | Magnetoresistance offset transverse | `-` | -| **`AMR_X`** | `List[float]` | Anisotropic magnetoresistance longitudinal | `-` | -| **`AMR_Y`** | `List[float]` | Anisotropic magnetoresistance transverse | `-` | -| **`SMR_X`** | `List[float]` | Spin magnetoresistance longitudinal | `-` | -| **`SMR_Y`** | `List[float]` | Spin magnetoresistance transverse | `-` | - -### `__init__(*args, **kwargs)` - -### `clearLog(self)` - -Reset current simulation state - -### `getLayerMagnetisation(self, layer_id: str)` - -### `getLog(self)` - -Retrieve the simulation log [data]. - -### `getMagnetoresistance(self)` - -### `runSimulation(self,totalTime: float,timeStep: float = ...,writeFrequency: float = ...,persist: bool = ...,log: bool = ...,calculateEnergies: bool = ...,)` - -Main run simulation function. -Use it to run the simulation. - -#### **Parameters** - -| Name | Type | Description | Default | -| -------------------- | ------- | -------------------------------------------------------------------------------------- | ------- | -| **`totalTime`** | `float` | total time of a simulation, give it in seconds. Typical length is in ~couple ns. | `-` | -| **`timeStep`** | `float` | the integration step of the RK45 method. Default is 1e-13 | `...` | -| **`writeFrequency`** | `float` | how often is the log saved to? Must be no smaller than `timeStep`. Default is 1e-11. | `...` | -| **`persist`** | `bool` | whether to save to the filename specified in the Junction constructor. Default is true | `...` | -| **`log`** | `bool` | if you want some verbosity like timing the simulation. Default is false | `...` | - -### `setIECDriver(self, bottom_layer: str, top_layer: str, driver: ScalarDriver)` - -Set IEC interaction between two layers. -The names of the params are only for convention. The IEC will be set -between bottomLyaer or topLayer, order is irrelevant. - -#### **Parameters** - -| Name | Type | Description | Default | -| ----------------- | ---- | ------------------ | ------- | -| **`bottomLayer`** | `-` | the first layer id | `-` | - -### `setQuadIECDriver(self, bottom_layer: str, top_layer: str, driver: ScalarDriver)` - -Set secondary (biquadratic term) IEC interaction between two layers. -The names of the params are only for convention. The IEC will be set -between bottomLyaer or topLayer, order is irrelevant. - -#### **Parameters** - -| Name | Type | Description | Default | -| ----------------- | ---- | ------------------ | ------- | -| **`bottomLayer`** | `-` | the first layer id | `-` | - -### `setLayerTemperatureDriver(self, layer_id: str, driver: ScalarDriver)` - -### `setLayerAnisotropyDriver(self, layer_id: str, driver: ScalarDriver)` - -### `setLayerCurrentDriver(self, layer_id: str, driver: ScalarDriver)` - -### `setLayerExternalFieldDriver(self, layer_id: str, driver: AxialDriver)` - -### `setLayerMagnetisation(self, layer_id: str, mag: CVector)` - -### `setLayerOerstedFieldDriver(self, layer_id: str, driver: AxialDriver)` - -### `setLayerDampingLikeTorqueDriver(self, layer_id: str, driver: ScalarDriver)` - -Set the damping like torque driver for a layer. - -#### **Parameters** - -| Name | Type | Description | Default | -| -------------- | ----- | ------------ | ------- | -| **`layer_id`** | `str` | the layer id | `-` | - -### `setLayerFieldLikeTorqueDriver(self, layer_id: str, driver: ScalarDriver)` - -Set the field like torque driver for a layer. - -#### **Parameters** - -| Name | Type | Description | Default | -| -------------- | ----- | ------------ | ------- | -| **`layer_id`** | `str` | the layer id | `-` | - -### `setLayerOneFNoise(self, layer_id: str, sources: int, bias: float, scale: float)` - -Set 1/f noise for a layer. - -#### **Parameters** - -| Name | Type | Description | Default | -| -------------- | ------- | --------------------------------------------------------------------- | ------- | -| **`layer_id`** | `str` | the layer id | `-` | -| **`sources`** | `int` | the number of generation sources (the more the slower, but more acc.) | `-` | -| **`bias`** | `float` | the bias of the noise (p in the Multinomial distribution) | `-` | - -## `Layer` - -### `__init__(self,id: str,mag: CVector,anis: CVector,Ms: float,thickness: float,cellSurface: float,demagTensor: List[CVector],temperature: float = ...,damping: float = ...,)` - -The basic structure is a magnetic layer. -Its parameters are defined by the constructor and may be altered -by the drivers during the simulation time. -If you want STT, remember to set the reference vector for the polarisation of the layer. -Use `setReferenceLayer` function to do that. - -#### **Parameters** - -| Name | Type | Description | Default | -| ----------------- | --------- | ------------------------------------------------------------------------------------ | ------- | -| **`id`** | `str` | identifiable name for a layer -- e.g. "bottom" or "free". | `-` | -| **`mag`** | `CVector` | initial magnetisation. Must be normalised (norm of 1). Used for quicker convergence. | `-` | -| **`anis`** | `CVector` | anisotropy of the layer. A normalised vector | `-` | -| **`Ms`** | `float` | magnetisation saturation. Unit: Tesla [T]. | `-` | -| **`thickness`** | `float` | thickness of the layer. Unit: meter [m]. | `-` | -| **`cellSurface`** | `float` | surface of the layer, for volume calculation. Unit: meter^2 [m^2]. | `-` | - -### `createSOTLayer(id: str,mag: CVector,anis: CVector,Ms: float,thickness: float,cellSurface: float,demagTensor: List[CVector],damping: float = 0.11,fieldLikeTorque: float = 0,dampingLikeTorque: float = 0,)` - -Create SOT layer -- including damping and field-like torques that are -calculated based on the effective Spin Hall angles. - -#### **Parameters** - -| Name | Type | Description | Default | -| ----------------- | --------- | ------------------------------------------------------------------------------------ | ------- | -| **`id`** | `str` | identifiable name for a layer -- e.g. "bottom" or "free". | `-` | -| **`mag`** | `CVector` | initial magnetisation. Must be normalised (norm of 1). Used for quicker convergence. | `-` | -| **`anis`** | `CVector` | anisotropy of the layer. A normalised vector | `-` | -| **`Ms`** | `float` | magnetisation saturation. Unit: Tesla [T]. | `-` | -| **`thickness`** | `float` | thickness of the layer. Unit: meter [m]. | `-` | -| **`cellSurface`** | `float` | surface of the layer, for volume calculation. Unit: meter^2 [m^2]. | `-` | -| **`temperature`** | `-` | resting temperature of the layer. Unit: Kelvin [K]. | `-` | - -### `createSTTLayer(id: str,mag: CVector,anis: CVector,Ms: float,thickness: float,cellSurface: float,demagTensor: List[CVector],damping: float = 0.011,SlonczewskiSpacerLayerParameter: float = 1.0,beta: float = 0.0,spinPolarisation: float = 0.0,)` - -Create STT layer -- with the standard Slomczewski formulation. - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------------------------- | --------- | ---------------------------------------------------------------------------------------------- | ------- | -| **`id`** | `str` | identifiable name for a layer -- e.g. "bottom" or "free". | `-` | -| **`mag`** | `CVector` | initial magnetisation. Must be normalised (norm of 1). Used for quicker convergence. | `-` | -| **`anis`** | `CVector` | anisotropy of the layer. A normalised vector | `-` | -| **`Ms`** | `float` | magnetisation saturation. Unit: Tesla [T]. | `-` | -| **`thickness`** | `float` | thickness of the layer. Unit: meter [m]. | `-` | -| **`cellSurface`** | `float` | surface of the layer, for volume calculation. Unit: meter^2 [m^2]. | `-` | -| **`damping`** | `float` | often marked as alpha in the LLG equation. Damping of the layer. Default 0.011. Dimensionless. | `0.011` | -| **`SlonczewskiSpacerLayerParameter`** | `float` | Slomczewski parameter. Often marked as lambda. | `1.0` | -| **`beta`** | `float` | beta parameter that scales FL/DL ratio. | `0.0` | - -### `setAnisotropyDriver(self, driver: ScalarDriver)` - -Set anisotropy driver for the layer. -It's scalar. The axis is determined in the layer constructor - -### `setTemperatureDriver(self, driver: ScalarDriver)` - -Set a driver for the temperature of the layer. -Automatically changes the solver to Euler-Heun. - -### `setExternalFieldDriver(self, driver: AxialDriver)` - -### `setMagnetisation(self, mag: CVector)` - -### `setOerstedFieldDriver(self, driver: AxialDriver)` - -### `setDampingLikeTorqueDriver(self, driver: ScalarDriver)` - -Set a driver for the damping like torque of the layer. - -### `setFieldLikeTorqueDriver(self, driver: ScalarDriver)` - -Set a driver for the field like torque of the layer. - -### `setReferenceLayer(self, ref: CVector)` - -### `setReferenceLayer(self, ref: "Reference")` - -### `setTopDipoleTensor(self, tensor: List[CVector])` - -Set a dipole tensor from the top layer. - -### `setBottomDipoleTensor(self, tensor: List[CVector])` - -Set a dipole tensor from the bottom layer. - -### `getId(self)` - -Get Id of the layer - -### `setAlternativeSTT(self, setAlternative: bool)` - -Switch to an alternative STT forumulation (Taniguchi et al.) -https://iopscience.iop.org/article/10.7567/APEX.11.013005 - -### `setKappa(self, kappa: float)` - -Set the kappa parameter for the layer -- determines SOT mixing -Hdl \* kappa + Hfl -Allows you to turn off Hdl. Turning Hfl is via beta parameter. - -## `NullDriver(ScalarDriver)` - -### `__init__(self)` - -An empty driver that does nothing. Use in Axial Driver when -the axis is to be id. - -## `ScalarDriver` - -### `__init__(self, *args, **kwargs)` - -### `getConstantDriver(constantValue: float)` - -Constant driver produces a constant signal of a fixed amplitude. - -### `getPulseDriver(constantValue: float, amplitude: "ScalarDriver", period: float, cycle: float)` - -Produces a square pulse of certain period and cycle - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ---------------- | -------------------------------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset (vertical) of the pulse. The pulse amplitude will be added to this. | `-` | -| **`amplitude`** | `"ScalarDriver"` | amplitude of the pulse signal | `-` | -| **`period`** | `float` | period of the signal in seconds | `-` | - -### `getSineDriver(constantValue: float, amplitude: "ScalarDriver", frequency: float, phase: float)` - -Produces a sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ---------------- | ----------------------------------------------------------- | ------- | -| **`constantValue`** | `float` | vertical offset. The sine will oscillate around this value. | `-` | -| **`amplitude`** | `"ScalarDriver"` | amplitude of the sine wave | `-` | -| **`frequency`** | `float` | frequency of the sine | `-` | - -### `getStepDriver(constantValue: float, amplitude: float, timeStart: float, timeStop: float)` - -Get a step driver. It has amplitude between timeStart and timeStop and 0 elsewhere - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ------- | --------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset of the pulse (vertical) | `-` | -| **`amplitude`** | `float` | amplitude that is added on top of the constantValue | `-` | -| **`timeStart`** | `float` | start of the pulse | `-` | - -### `getTrapezoidDriver(constantValue: float,amplitude: float,timeStart,edgeTime: float,steadyTime: float,)` - -Create Trapezoid driver. Has a rising and a falling edge. - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ------- | --------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset of the pulse (vertical) | `-` | -| **`amplitude`** | `float` | amplitude that is added on top of the constantValue | `-` | -| **`timeStart`** | `-` | start of the pulse | `-` | -| **`edgeTime`** | `float` | time it takes to reach the maximum amplitude | `-` | - -### `getGaussianImpulseDriver(constantValue: float, amplitude: float, t0: float, sigma: float)` - -Gaussian impulse driver. It has amplitude starts at t0 and falls off with sigma. - - Formula: - A * exp(-((t - t0) ** 2) / (2 * sigma ** 2)) - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ------- | --------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset of the pulse (vertical) | `-` | -| **`amplitude`** | `float` | amplitude that is added on top of the constantValue | `-` | -| **`t0`** | `float` | start of the pulse | `-` | - -### `getGaussianStepDriver(constantValue: float, amplitude: float, t0: float, sigma: float)` - -Gaussian step driver (erf function). It has amplitude starts at t0 and falls off with sigma. - - Formula: - f(t) = constantValue + amplitude * (1 + erf((t - t0) / (sigma * sqrt(2)))) - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ------- | --------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset of the pulse (vertical) | `-` | -| **`amplitude`** | `float` | amplitude that is added on top of the constantValue | `-` | -| **`t0`** | `float` | start of the pulse | `-` | - -### `getPosSineDriver(constantValue: float, amplitude: float, frequency: float, phase: float)` - -Produces a positive sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ------- | ----------------------------------------------------------- | ------- | -| **`constantValue`** | `float` | vertical offset. The sine will oscillate around this value. | `-` | -| **`amplitude`** | `float` | amplitude of the sine wave | `-` | -| **`frequency`** | `float` | frequency of the sine | `-` | - -### `getPulseDriver(constantValue: float, amplitude: float, period: float, cycle: float)` - -Produces a square pulse of certain period and cycle - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ------- | -------------------------------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset (vertical) of the pulse. The pulse amplitude will be added to this. | `-` | -| **`amplitude`** | `float` | amplitude of the pulse signal | `-` | -| **`period`** | `float` | period of the signal in seconds | `-` | - -## `SolverMode` - -## `Reference` diff --git a/docs/gen-docs/drivers.md b/docs/gen-docs/drivers.md deleted file mode 100644 index dc46464..0000000 --- a/docs/gen-docs/drivers.md +++ /dev/null @@ -1,56 +0,0 @@ -## `Axis` - -## `NullDriver(ScalarDriver)` - -### `__init__(self)` - -An empty driver that does nothing. Use in Axial Driver when -the axis is to be id. - -## `ScalarDriver` - -### `__init__(self, *args, **kwargs) -> None:...@staticmethoddef getConstantDriver(constantValue: float)` - -Constant driver produces a constant signal of a fixed amplitude. - -### `getPulseDriver(constantValue: float, amplitude: 'ScalarDriver', period: float, cycle: float)` - -Produces a square pulse of certain period and cycle - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ---------------- | -------------------------------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset (vertical) of the pulse. The pulse amplitude will be added to this. | `-` | -| **`amplitude`** | `'ScalarDriver'` | amplitude of the pulse signal | `-` | -| **`period`** | `float` | period of the signal in seconds | `-` | - -### `getSineDriver(constantValue: float, amplitude: 'ScalarDriver', frequency: float, phase: float)` - -Produces a sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ---------------- | ----------------------------------------------------------- | ------- | -| **`constantValue`** | `float` | vertical offset. The sine will oscillate around this value. | `-` | -| **`amplitude`** | `'ScalarDriver'` | amplitude of the sine wave | `-` | -| **`frequency`** | `float` | frequency of the sine | `-` | - -### `getStepDriver(constantValue: float, amplitude: float, timeStart: float, timeStop: float)` - -Get a step driver. It has amplitude between timeStart and timeStop and 0 elsewhere - -#### **Parameters** - -| Name | Type | Description | Default | -| ------------------- | ------- | --------------------------------------------------- | ------- | -| **`constantValue`** | `float` | offset of the pulse (vertical) | `-` | -| **`amplitude`** | `float` | amplitude that is added on top of the constantValue | `-` | -| **`timeStart`** | `float` | start of the pulse | `-` | - -## `AxialDriver` - -Requires three scalar drivers, one for each axis. - -### `__init__(self, x: 'ScalarDriver', y: 'ScalarDriver', z: 'ScalarDriver')` diff --git a/docs/gen-docs/noise.md b/docs/gen-docs/noise.md deleted file mode 100644 index 21df91f..0000000 --- a/docs/gen-docs/noise.md +++ /dev/null @@ -1,21 +0,0 @@ -## `BufferedAlphaNoise` - -### `__init__(self, bufferSize: int, alpha: float, std: float, scale: float)` - -### `fillBuffer(self)` - -Fill the buffer with the noise. This method is called only once. - -### `tick(self)` - -Produce the next sample of the noise. - -## `VectorAlphaNoise` - -### `__init__(self,bufferSize: int,alpha: float,std: float,scale: float,axis: cmtj.Axis = cmtj.Axis.all,)` - -### `getPrevSample(self) -> cmtj.CVector:"""Get the previous sample of the noise in a vector form."""...def getScale(self)` - -Get the scale of the noise. - -### `tick(self)` diff --git a/docs/gen-docs/stack.md b/docs/gen-docs/stack.md deleted file mode 100644 index 1938491..0000000 --- a/docs/gen-docs/stack.md +++ /dev/null @@ -1,113 +0,0 @@ -## `ParallelStack` - -### `__init__(self, junctionList: List[cmtj.Junction])` - -Initialises a parallel connection of junctions. - -### `clearLogs(self)` - -Clear all the logs, both of the stack and the junctions -that constitute the stack. - -### `getLog(self, junctionId: int)` - -Get the logs of a specific junction -- integer id -from the `junctionList`. - -### `getLog(self)` - -Get the logs of the stack - -### `runSimulation(self, totalTime: float, timeStep: float = ..., writeFrequency: float = ...)` - -Run the simulation of the stack. - -#### **Parameters** - -| Name | Type | Description | Default | -| --------------- | ------- | -------------------------------------------------------------------------------- | ------- | -| **`totalTime`** | `float` | total time of a simulation, give it in seconds. Typical length is in ~couple ns. | `-` | -| **`timeStep`** | `float` | the integration step of the RK45 method. Default is 1e-13 | `...` | - -### `setCoupledCurrentDriver(self, driver: cmtj.ScalarDriver)` - -Sets a global current driver for all junctions inside the stack. -Keep in mind the current passed down the stack will be modified -by the coupling constant. - -### `setCouplingStrength(self, coupling: float)` - -Coupling constant that represents the energy losses as the current -passes through the stack. - -### `setExternalFieldDriver(self, driver: cmtj.AxialDriver)` - -Sets a external field current driver for all junctions inside the stack. - -### `setMagnetistation(self, juncionId: int, layerId: str, mag: cmtj.CVector)` - -Set magnetisation on a specific layer in a specific junction. - -#### **Parameters** - -| Name | Type | Description | Default | -| ---------------- | ----- | --------------------------------------------------- | ------- | -| **`junctionId`** | `-` | the id of the junction (int) as passed in the init. | `-` | -| **`layerId`** | `str` | the string id of the layer in the junction. | `-` | - -## `SeriesStack` - -### `__init__(self, junctionList: List[cmtj.Junction])` - -Initialises a series connection of junctions. - -### `clearLogs(self)` - -Clear all the logs, both of the stack and the junctions -that constitute the stack. - -### `getLog(self, junctionId: int)` - -Get the logs of a specific junction -- integer id -from the `junctionList`. - -### `getLog(self)` - -Get the logs of the stack - -### `runSimulation(self, totalTime: float, timeStep: float = ..., writeFrequency: float = ...)` - -Run the simulation of the stack. - -#### **Parameters** - -| Name | Type | Description | Default | -| --------------- | ------- | -------------------------------------------------------------------------------- | ------- | -| **`totalTime`** | `float` | total time of a simulation, give it in seconds. Typical length is in ~couple ns. | `-` | -| **`timeStep`** | `float` | the integration step of the RK45 method. Default is 1e-13 | `...` | - -### `setCoupledCurrentDriver(self, driver: cmtj.ScalarDriver)` - -Sets a global current driver for all junctions inside the stack. -Keep in mind the current passed down the stack will be modified -by the coupling constant. - -### `setCouplingStrength(self, coupling: float)` - -Coupling constant that represents the energy losses as the current -passes through the stack. - -### `setExternalFieldDriver(self, driver: cmtj.AxialDriver)` - -Sets a external field current driver for all junctions inside the stack. - -### `setMagnetistation(self, juncionId: int, layerId: str, mag: cmtj.CVector)` - -Set magnetisation on a specific layer in a specific junction. - -#### **Parameters** - -| Name | Type | Description | Default | -| ---------------- | ----- | --------------------------------------------------- | ------- | -| **`junctionId`** | `-` | the id of the junction (int) as passed in the init. | `-` | -| **`layerId`** | `str` | the string id of the layer in the junction. | `-` | diff --git a/docs/index.md b/docs/index.md index e3d4b29..8555abb 100644 --- a/docs/index.md +++ b/docs/index.md @@ -62,7 +62,7 @@ python3 -m pip install cmtj 3. Straight from source: ```bash -python3 -m pip install https://github.com/LemurPwned/cmtj.git +python3 -m pip install git+https://github.com/LemurPwned/cmtj.git ``` 4. Clone the repository: @@ -84,7 +84,7 @@ The package requires (if `utils` subpackage is used): ## Documentation and examples -Documentation: [https://lemurpwned.github.io/cmtj](https://lemurpwned.github.io/cmtj) +Documentation: [https://lemurpwned.github.io/cmtj](https://lemurpwned.github.io/cmtj). There are many examples available, check out the [examples section in the docs](https://lemurpwned.github.io/cmtj/experimental-methods/introduction/) ## Extensions diff --git a/mkdocs.yml b/mkdocs.yml index a73a88b..732ae69 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -9,7 +9,6 @@ edit_uri: "" hooks: - scripts/readme_copy.py - - docs/docgen.py nav: - Home: index.md @@ -28,8 +27,8 @@ nav: - Spin Diode experiments: experimental-methods/VoltageSpinDiodeFits.ipynb - Parallelism: physics/paralellism.md - API: - - Core: gen-docs/cmtj.md - - Drivers: gen-docs/drivers.md + - Core: api/core.md + # - Drivers: api/drivers.md - Models: - Smit-Beljers: api/models/sb-general-reference.md - Domain wall: api/models/dw-reference.md @@ -44,9 +43,9 @@ nav: - Optimization: api/optimization-reference.md - Ensemble models: api/ensemble-reference.md - Miscellanous: api/general-reference.md - - Stack: gen-docs/stack.md - - Noise: gen-docs/noise.md - - LLGB: gen-docs/llgb.md + - Stack: api/stack.md + - Noise: api/noise.md + - LLGB: api/llgb.md - Examples: - Library introduction: tutorials/CMTJBindingsTutorial.ipynb - Trajectories: tutorials/trajectory.ipynb From 425911a03c9f80f000ff8ce23d87d4b8d99c64ba Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Wed, 29 May 2024 23:44:13 +0200 Subject: [PATCH 02/44] adding a trivial hdmi holder --- .pre-commit-config.yaml | 8 +++- cmtj/utils/__init__.py | 20 ++++++--- cmtj/utils/resistance.py | 95 +++++++++++++++++++++++++++++++--------- core/cvector.hpp | 2 +- core/drivers.hpp | 10 +---- core/junction.hpp | 28 +++++++++--- mkdocs.yml | 1 - python/cmtj.cpp | 2 + 8 files changed, 120 insertions(+), 46 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3d95f8..29acd13 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ # See https://pre-commit.com/hooks.html for more hooks exclude: "^\ - (third-party/.*)|\ + (third_party/.*)|\ (build/.*)|\ (.github/.*)|\ (.vscode/.*)|\ @@ -46,10 +46,14 @@ repos: args: [--in-place, --recursive] - repo: https://github.com/pocc/pre-commit-hooks - rev: v1.3.5 + rev: master hooks: + - id: cpplint - id: cppcheck + args: [--check-level=exhaustive] + exclude: third_party/.* # - id: clang-format # - id: oclint # - id: uncrustify - id: include-what-you-use + exclude: ^third_party diff --git a/cmtj/utils/__init__.py b/cmtj/utils/__init__.py index aa9f9bf..61e363f 100644 --- a/cmtj/utils/__init__.py +++ b/cmtj/utils/__init__.py @@ -14,15 +14,21 @@ mu0 = 12.566e-7 hplanck = 6.6260e-34 hbar = hplanck / (2 * math.pi) -gyromagnetic_ratio = 2.211e5 -gamma = 28024e6 -gamma_rad = 1.76e11 +gyromagnetic_ratio = 2.211e5 # m/As +gamma = 28024e6 # Hz/T +gamma_rad = 1.76e11 # rad / (s * T) me = 9.109e-31 bohr_magneton = echarge * hbar / (2 * me) __all__ = [ - "Filters", "FieldScan", "compute_sd", "compute_resistance", - "calculate_magnetoresistance", "calculate_resistance_series", - "calculate_resistance_parallel", "VectorObj", "box_muller_random", - "perturb_position" + "Filters", + "FieldScan", + "compute_sd", + "compute_resistance", + "calculate_magnetoresistance", + "calculate_resistance_series", + "calculate_resistance_parallel", + "VectorObj", + "box_muller_random", + "perturb_position", ] diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index b1e29e3..93b2547 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -18,11 +18,20 @@ def compute_sd(dynamic_r: np.ndarray, dynamic_i: np.ndarray, return np.mean(SD_dc) -def compute_resistance(Rx0: List[float], Ry0: List[float], AMR: List[float], - AHE: List[float], SMR: List[float], - m: Union[List[float], - np.ndarray], l: List[float], w: List[float]): - """Computes the resistance of the system.""" +def compute_resistance( + Rx0: List[float], + Ry0: List[float], + AMR: List[float], + AHE: List[float], + SMR: List[float], + m: Union[List[float], np.ndarray], + l: List[float], + w: List[float], +): + """Computes the resistance of the system. + If you want to compute the resistance for an entire time series, pass m as a 3D array. + [number_of_layers, 3, T] where T is the time component. + """ number_of_layers = len(Rx0) if not isinstance(m, np.ndarray): m = np.asarray(m) @@ -34,19 +43,29 @@ def compute_resistance(Rx0: List[float], Ry0: List[float], AMR: List[float], SxAll = np.zeros((number_of_layers, m.shape[2])) SyAll = np.zeros((number_of_layers, m.shape[2])) - for i in range(0, number_of_layers): + for i in range(number_of_layers): w_l = w[i] / l[i] - SxAll[i] = (Rx0[i] + (AMR[i] * m[i, 0]**2 + SMR[i] * m[i, 1]**2)) + SxAll[i] = Rx0[i] + (AMR[i] * m[i, 0]**2 + SMR[i] * m[i, 1]**2) SyAll[i] = (Ry0[i] + 0.5 * AHE[i] * m[i, 2] + (w_l) * (SMR[i] - AMR[i]) * m[i, 0] * m[i, 1]) return SxAll, SyAll +def compute_gmr(Rp: float, Rap: float, m1: np.ndarray, m2: np.ndarray): + """Computes the GMR using parallel and antiparallel resistance. + :param Rp: parallel resistance + :param Rap: antiparallel resistance + :param m1: magnetisation of layer 1 + :param m2: magnetisation of layer 2""" + return Rp + 0.5 * (Rap - Rp) * np.sum(m1 * m2, axis=0) + + def calculate_magnetoresistance(Rp: float, Rap: float, m: np.ndarray): """Computes the magnetoresistance using parallel and antiparallel resistance. :param Rp: parallel resistance :param Rap: antiparallel resistance - :param m: magnetisation, 2 layers of shape [2, 3, T] where T is the time component""" + :param m: magnetisation, 2 layers of shape [2, 3, T] where T is the time component + """ if not isinstance(m, np.ndarray): m = np.asarray(m) if m.shape[0] != 2: @@ -56,26 +75,62 @@ def calculate_magnetoresistance(Rp: float, Rap: float, m: np.ndarray): return Rp + 0.5 * (Rap - Rp) * np.sum(m[0] * m[1], axis=0) -def calculate_resistance_parallel(Rx0: List[float], Ry0: List[float], - AMR: List[float], AHE: List[float], - SMR: List[float], m: List[float], - l: List[float], w: List[float]): +def calculate_resistance_parallel( + Rx0: List[float], + Ry0: List[float], + AMR: List[float], + AHE: List[float], + SMR: List[float], + m: List[float], + l: List[float], + w: List[float], +): """Calculates the resistance of the system in parallel. + If you want to compute the resistance for an entire time series, pass m as a 3D array. + [number_of_layers, 3, T] where T is the time component. Uses Kim's formula from the paper: - https://link.aps.org/doi/10.1103/PhysRevLett.116.097201""" + https://link.aps.org/doi/10.1103/PhysRevLett.116.097201 + + :param Rx0: resistance offset in longitudinal direction + :param Ry0: resistance offset in transverse direction + :param AMR: anisotropic magnetoresistance + :param AHE: anomalous Hall effect + :param SMR: spin Hall magnetoresistance + :param m: magnetisation of the layers. Shape [number_of_layers, 3, T] + :param l: length of the layers + :param w: width of the layers + """ SxAll, SyAll = compute_resistance(Rx0, Ry0, AMR, AHE, SMR, m, l, w) - Rx = 1 / np.sum(1. / SxAll, axis=0) - Ry = 1 / np.sum(1. / SyAll, axis=0) + Rx = 1 / np.sum(1.0 / SxAll, axis=0) + Ry = 1 / np.sum(1.0 / SyAll, axis=0) return Rx, Ry -def calculate_resistance_series(Rx0: List[float], Ry0: List[float], - AMR: List[float], AHE: List[float], - SMR: List[float], m: List[float], - l: List[float], w: List[float]): +def calculate_resistance_series( + Rx0: List[float], + Ry0: List[float], + AMR: List[float], + AHE: List[float], + SMR: List[float], + m: List[float], + l: List[float], + w: List[float], +): """Calculates the resistance of the system in series. + If you want to compute the resistance for an entire time series, pass m as a 3D array. + [number_of_layers, 3, T] where T is the time component. Uses Kim's formula from the paper: - https://link.aps.org/doi/10.1103/PhysRevLett.116.097201""" + https://link.aps.org/doi/10.1103/PhysRevLett.116.097201 + + :param Rx0: resistance offset in longitudinal direction + :param Ry0: resistance offset in transverse direction + :param AMR: anisotropic magnetoresistance + :param AHE: anomalous Hall effect + :param SMR: spin Hall magnetoresistance + :param m: magnetisation of the layers. Shape [number_of_layers, 3, T] + :param l: length of the layers + :param w: width of the layers + """ SxAll, SyAll = compute_resistance(Rx0, Ry0, AMR, AHE, SMR, m, l, w) Rx = np.sum(SxAll, axis=0) Ry = np.sum(SyAll, axis=0) diff --git a/core/cvector.hpp b/core/cvector.hpp index b7b5dd8..14158ca 100644 --- a/core/cvector.hpp +++ b/core/cvector.hpp @@ -30,7 +30,7 @@ class CVector this->z = 0.0; } - explicit CVector(std::vector vec) + explicit CVector(const std::vector& vec) { if (vec.size() != 3) { diff --git a/core/drivers.hpp b/core/drivers.hpp index b9b3783..855419a 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -369,7 +369,7 @@ class AxialDriver : public Driver return AxialDriver(CVector(x, y, z)); } - void applyMask(std::vector mask) + void applyMask(const std::vector& mask) { assert(mask.size() == 3); for (int i = 0; i < 3; i++) @@ -487,14 +487,6 @@ class NullAxialDriver : public AxialDriver { public: NullAxialDriver() = default; - CVector getCurrentAxialDrivers([[maybe_unused]] T time) - { - return CVector(0., 0., 0.); - } - CVector getConstantValues() - { - return CVector(0., 0., 0.); - } }; #endif diff --git a/core/junction.hpp b/core/junction.hpp index 44367b2..614d88d 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -157,7 +157,7 @@ class Layer ScalarDriver fieldLikeTorqueDriver; ScalarDriver dampingLikeTorqueDriver; AxialDriver externalFieldDriver; - AxialDriver HoeDriver; + AxialDriver HoeDriver, HdmiDriver; bool nonStochasticTempSet = false; bool nonStochasticOneFSet = true; @@ -237,7 +237,7 @@ class Layer T cellVolume = 0.0, cellSurface = 0.0; CVector H_log, Hoe_log, Hconst, mag, anis, referenceLayer; - CVector Hext, Hdipole, Hdemag, Hoe, HAnis, Hthermal, Hfluctuation; + CVector Hext, Hdipole, Hdemag, Hoe, HAnis, Hthermal, Hfluctuation, Hdmi; CVector Hfl_v, Hdl_v; @@ -407,8 +407,7 @@ class Layer * * @return const std::string */ - const std::string getId() const { return id; } - + const std::string& getId() const { return id; } /** * @brief Set the Alternative STT formulation * @@ -541,6 +540,11 @@ class Layer this->IECQuadDriverBottom = driver; } + void setHdmiDriver(const AxialDriver& driver) + { + this->HdmiDriver = driver; + } + /** * @brief Sets reference layer with a custom vector * Set reference layer parameter. This is for calculating the spin current @@ -606,10 +610,12 @@ class Layer this->Hdemag = calculate_tensor_interaction(stepMag, this->demagTensor, this->Ms); this->HIEC = calculateIEC(time, stepMag, bottom, top); this->HAnis = calculateAnisotropy(stepMag, time); + this->Hdmi = calculateHdmiField(time); const CVector Heff = this->Hext // external + this->HAnis // anistotropy + this->HIEC // IEC + this->Hoe // Oersted field + + this->Hdmi + Hfluctuation // demag -- negative contribution - this->Hdemag @@ -624,6 +630,11 @@ class Layer return this->Hoe_log; } + CVector calculateHdmiField(const T& time) + { + return this->HdmiDriver.getCurrentAxialDrivers(time); + } + CVector calculateExternalField(const T& time) { this->H_log = @@ -1110,6 +1121,11 @@ class Junction scalarlayerSetter(layerID, &Layer::setFieldLikeTorqueDriver, driver); } + void setLayerHdmiDriver(const std::string& layerID, const AxialDriver& driver) + { + axiallayerSetter(layerID, &Layer::setHdmiDriver, driver); + } + void setLayerAlternativeSTT(const std::string& layerID, const bool alternative) { if (layerID == "all") @@ -1334,7 +1350,7 @@ class Junction } void - saveLogs(std::string filename) + saveLogs(const std::string& filename) { if (filename == "") { @@ -1635,7 +1651,7 @@ class Junction { if (timeStep > writeFrequency) { - std::runtime_error("The time step cannot be larger than write frequency!"); + throw std::runtime_error("The time step cannot be larger than write frequency!"); } const unsigned int totalIterations = (int)(totalTime / timeStep); const unsigned int writeEvery = (int)(writeFrequency / timeStep); diff --git a/mkdocs.yml b/mkdocs.yml index 732ae69..ec3e48f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,7 +28,6 @@ nav: - Parallelism: physics/paralellism.md - API: - Core: api/core.md - # - Drivers: api/drivers.md - Models: - Smit-Beljers: api/models/sb-general-reference.md - Domain wall: api/models/dw-reference.md diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 4a78964..6eec15d 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -237,6 +237,7 @@ PYBIND11_MODULE(cmtj, m) .def("setAnisotropyDriver", &DLayer::setAnisotropyDriver) .def("setExternalFieldDriver", &DLayer::setExternalFieldDriver) .def("setOerstedFieldDriver", &DLayer::setOerstedFieldDriver) + .def("setHdmiDriver", &DLayer::setHdmiDriver) // reference layers .def("setReferenceLayer", py::overload_cast(&DLayer::setReferenceLayer)) .def("setReferenceLayer", py::overload_cast(&DLayer::setReferenceLayer)) @@ -310,6 +311,7 @@ PYBIND11_MODULE(cmtj, m) .def("setQuadIECDriver", &DJunction::setQuadIECDriver) .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) .def("setLayerMagnetisation", &DJunction::setLayerMagnetisation) + .def("setLayerHdmiDriver", &DJunction::setLayerHdmiDriver) // noise .def("setLayerTemperatureDriver", &DJunction::setLayerTemperatureDriver) .def("setLayerNonStochasticLangevinDriver", &DJunction::setLayerNonStochasticLangevinDriver) From a77233a03747a27bf08a2857e268487d36a19d1f Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Wed, 5 Jun 2024 22:46:33 +0200 Subject: [PATCH 03/44] hdmi --- cmtj/models/general_sb.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index e4d4998..5ebd694 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -1,7 +1,7 @@ import math import time import warnings -from dataclasses import dataclass +from dataclasses import dataclass, field from functools import lru_cache from typing import Iterable, List, Tuple, Union @@ -157,6 +157,7 @@ class LayerSB: :param Kv: volumetric (in-plane) anisotropy. Only phi and mag count [J/m^3]. :param Ks: surface anisotropy (out-of plane, or perpendicular) value [J/m^3]. :param Ms: magnetisation saturation value in [A/m]. + :param Hdmi: DMI field in the layer. Defaults to [0, 0, 0]. """ _id: int @@ -164,10 +165,17 @@ class LayerSB: Kv: VectorObj Ks: float Ms: float + Hdmi: VectorObj = ( + None # TODO: change when we support py3.10 upwards (field(kw_only=True, default=None)) + ) def __post_init__(self): if self._id > 9: raise ValueError("Only up to 10 layers supported.") + if self.Hdmi is None: + self.Hdmi = sym.Matrix([0, 0, 0]) + else: + self.Hdmi = sym.ImmutableMatrix(self.Hdmi.get_cartesian()) self.theta = sym.Symbol(r"\theta_" + str(self._id)) self.phi = sym.Symbol(r"\phi_" + str(self._id)) self.m = sym.ImmutableMatrix([ @@ -225,10 +233,11 @@ def no_iec_symbolic_layer_energy(self, H: sym.ImmutableMatrix): sym.sin(self.Kv.phi), 0]) field_energy = -mu0 * self.Ms * m.dot(H) + hdmi_energy = -mu0 * self.Ms * m.dot(self.Hdmi) surface_anistropy = (-self.Ks + (1.0 / 2.0) * mu0 * self.Ms**2) * (m[-1]**2) volume_anisotropy = -self.Kv.mag * (m.dot(alpha)**2) - return field_energy + surface_anistropy + volume_anisotropy + return field_energy + surface_anistropy + volume_anisotropy + hdmi_energy def sb_correction(self): omega = sym.Symbol(r"\omega") @@ -245,7 +254,7 @@ def __eq__(self, __value: "LayerSB") -> bool: @dataclass class LayerDynamic(LayerSB): - alpha: float + alpha: float = 0.01 def rhs_llg( self, From dcf5295f23463f6cd55e3937a0cbbc009ef720f1 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 10 Jun 2024 23:42:41 +0200 Subject: [PATCH 04/44] initial work on generalising coupled drivers --- core/drivers.hpp | 63 ++++++++++++++++++-- core/stack.hpp | 145 +++++++++++++++++++++++++++++++---------------- python/cmtj.cpp | 12 +++- 3 files changed, 163 insertions(+), 57 deletions(-) diff --git a/core/drivers.hpp b/core/drivers.hpp index 855419a..6cc2f3e 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -12,7 +12,7 @@ #include // for runtime_error #include // for vector #include "cvector.hpp" // for CVector - +#include // for move enum UpdateType { constant, @@ -72,6 +72,62 @@ class Driver return 0; }; virtual ~Driver() = default; + + void setConstantValue(const T& val) + { + this->constantValue = val; + } + + void phaseShift(const T& phase) + { + this->phase += phase; + } + + // override multiplication operator + ScalarDriver operator*(const T& val) + { + this->constantValue *= val; + this->amplitude *= val; + return *this; + } + + // override *= operator + ScalarDriver operator*=(const T& val) + { + this->constantValue *= val; + this->amplitude *= val; + return *this; + } + + // override addition operator + ScalarDriver operator+(const T& val) + { + this->constantValue += val; + this->amplitude += val; + return *this; + } + + ScalarDriver operator+=(const T& val) + { + this->constantValue += val; + this->amplitude += val; + return *this; + } + + // override subtraction operator + ScalarDriver operator-(const T& val) + { + this->constantValue -= val; + this->amplitude -= val; + return *this; + } + + ScalarDriver operator-=(const T& val) + { + this->constantValue -= val; + this->amplitude -= val; + return *this; + } }; template @@ -338,10 +394,7 @@ class ScalarDriver : public Driver } return returnValue; } - void setConstantValue(const T& val) - { - this->constantValue = val; - } + }; diff --git a/core/stack.hpp b/core/stack.hpp index 0f305a9..75303b1 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -24,16 +24,54 @@ class Stack bool currentDriverSet = false; protected: + unsigned int stackSize; std::string topId, bottomId; // Ids of the top and bottom junctions - T couplingStrength = 0; + std::vector couplingStrength = { 0 }; bool delayed = true; + T phaseOffset = 0; virtual T calculateStackResistance(std::vector resistances) = 0; - virtual T computeCouplingCurrentDensity(T currentDensity, - CVector m1, CVector m2, CVector p) = 0; + virtual T getPhaseOffset(const unsigned int& order) const = 0; + virtual T getEffectiveCouplingStrength(CVector m1, CVector m2, CVector p) = 0; + T computeCouplingCurrentDensity(T currentDensity, + CVector m1, CVector m2, CVector p) { + const T coupledI = currentDensity * this->getEffectiveCouplingStrength(m1, m2, p); + return coupledI; + } public: std::vector> junctionList; + Stack(std::vector> inputStack, + const std::string& topId, + const std::string& bottomId, const T phaseOffset = 0) : topId(topId), + bottomId(bottomId), phaseOffset(phaseOffset) + { + if (inputStack.size() < 2) + { + throw std::runtime_error("Stack must have at least 2 junctions!"); + } + this->junctionList = std::move(inputStack); + if (std::any_of(this->junctionList.begin(), + this->junctionList.end(), + [](const Junction& j) { return j.MR_mode != Junction::MRmode::CLASSIC; })) + { + throw std::runtime_error("Junction has a non-classic magnetoresitance mode!" + " Define the junction with Rp and Rap resistance values."); + } + stackSize = this->junctionList.size(); + } + + T getCoupling(const& order) const { + if (this->couplingStrength.empty()) + { + throw std::runtime_error("Coupling strength is not set!"); + } + if (this->couplingStrength.size() == 1) { + return this->couplingStrength[0]; + } + return this->couplingStrength[order]; + } + void setDelayed(bool delay) { if (!delay && !this->isTwoLayerMemberStack()) { @@ -88,25 +126,7 @@ class Stack this->currentDriverSet = true; } - Stack(std::vector> inputStack, - const std::string& topId, - const std::string& bottomId) : topId(topId), bottomId(bottomId) - { - if (inputStack.size() < 2) - { - throw std::runtime_error("Stack must have at least 2 junctions!"); - } - this->junctionList = std::move(inputStack); - if (std::any_of(this->junctionList.begin(), - this->junctionList.end(), - [](const Junction& j) { return j.MR_mode != Junction::MRmode::CLASSIC; })) - { - throw std::runtime_error("Junction has a non-classic magnetoresitance mode!" - " Define the junction with Rp and Rap resistance values."); - } - } - void - saveLogs(std::string fileSave) + void saveLogs(const std::string& fileSave) { if (fileSave == "") { @@ -132,8 +152,17 @@ class Stack logFile.close(); } - void setCouplingStrength(T coupling) + void setCouplingStrength(const T& coupling) { + this->couplingStrength = { coupling }; + } + + void setCouplingStrength(const std::vector &coupling) + { + if (coupling.size() != this->stackSize - 1) + { + throw std::runtime_error("Coupling strength vector must have size of stack size - 1!"); + } this->couplingStrength = coupling; } @@ -208,7 +237,7 @@ class Stack if (timeStep > writeFrequency) { - std::runtime_error("The time step cannot be larger than write frequency!"); + throw std::runtime_error("The time step cannot be larger than write frequency!"); } // pick a solver based on drivers @@ -230,8 +259,6 @@ class Stack " Do not mix stochastic and deterministic solvers!"); } - - T tCurrent; std::vector timeResistances(junctionList.size()); std::vector timeCurrents(junctionList.size()); std::vector> frozenMags(junctionList.size()); @@ -254,40 +281,50 @@ class Stack } const T plainCurrent = this->currentDriver.getCurrentScalarValue(t); T coupledCurrent = plainCurrent; + T effectiveCoupling = 1; for (std::size_t j = 0; j < junctionList.size(); ++j) { - // skip first junction - // modify the standing layer constant current + + /** + * Coupling + * Ik = Ik-1 + x* Ik-1 = (1+x)Ik-1 + * Ik+1 = Ik + x* Ik = (1+x)Ik = (1+x)(1+x)Ik-1 + * technically we could do (1+x)^n * I0 but + * we want to expand to non-symmetric coupling x1, x2, ... + */ + + // skip first junction + // modify the standing layer constant current if (j > 0) { if (this->delayed) { // accumulate coupling - coupledCurrent = coupledCurrent + this->computeCouplingCurrentDensity( - // j -> k, j-1 -> k' - coupledCurrent, frozenMags[j], frozenMags[j - 1], frozenPols[j]); + effectiveCoupling *= (1 + this->getEffectiveCouplingStrength( + j - 1, + frozenMags[j], frozenMags[j - 1], frozenPols[j])); + } else { - coupledCurrent = coupledCurrent + this->computeCouplingCurrentDensity( - // j -> k, j-1 -> k' - coupledCurrent, junctionList[j].getLayerMagnetisation(this->topId), + effectiveCoupling *= (1 + this->getEffectiveCouplingStrength( + j - 1, + junctionList[j].getLayerMagnetisation(this->topId), junctionList[j - 1].getLayerMagnetisation(this->topId), - junctionList[j].getLayerMagnetisation(this->bottomId)); + junctionList[j].getLayerMagnetisation(this->bottomId))); } - tCurrent = coupledCurrent; - } - else { - tCurrent = plainCurrent; } // set the current -- same for all layers - junctionList[j].setLayerCurrentDriver("all", ScalarDriver::getConstantDriver( - tCurrent)); + // copy the driver and set the current value + ScalarDriver localDriver = this->currentDriver * effectiveCoupling; + localDriver.phaseShift(this->getPhaseOffset(j)); + + junctionList[j].setLayerCurrentDriver("all", localDriver); (junctionList[j].*localRunner)(solver, t, timeStep); // change the instant value of the current before the // the resistance is calculated // compute the next j+1 input to the current. const auto resistance = junctionList[j].getMagnetoresistance(); timeResistances[j] = resistance[0]; - timeCurrents[j] = tCurrent; + timeCurrents[j] = localDriver.getCurrentScalarValue(t); } if (!(i % writeEvery)) { @@ -310,18 +347,22 @@ class SeriesStack : public Stack return resSum; } - T computeCouplingCurrentDensity(T currentDensity, CVector m1, CVector m2, CVector p) override + T getEffectiveCouplingStrength(const unsigned int& order, CVector m1, CVector m2, CVector p) override { const T m1Comp = c_dot(m1, p); const T m2Comp = c_dot(m2, p); - const T coupledI = currentDensity * this->couplingStrength * (m1Comp + m2Comp); - return coupledI; + return this->getCoupling() * (m1Comp + m2Comp); + } + + T getPhaseOffset(const unsigned int& order) const override + { + return this->phaseOffset * order; } public: explicit SeriesStack(const std::vector>& jL, const std::string& topId = "free", - const std::string& bottomId = "bottom") : Stack(jL, topId, bottomId) {} + const std::string& bottomId = "bottom", const T phaseOffset = 0) : Stack(jL, topId, bottomId, phaseOffset) {} }; template class ParallelStack : public Stack @@ -334,17 +375,21 @@ class ParallelStack : public Stack return 1. / invSum; } - T computeCouplingCurrentDensity(T currentDensity, CVector m1, CVector m2, CVector p) override + T getEffectiveCouplingStrength(const unsigned int& order, CVector m1, CVector m2, CVector p) override { const T m1Comp = c_dot(m1, p); const T m2Comp = c_dot(m2, p); - const T coupledI = currentDensity * this->couplingStrength * (m1Comp - m2Comp); - return coupledI; + return this->getCoupling(order) * (m1Comp - m2Comp); + } + + T getPhaseOffset(const unsigned int& order) const override + { + return this->phaseOffset; } public: explicit ParallelStack(const std::vector>& jL, const std::string& topId = "free", - const std::string& bottomId = "bottom") : Stack(jL, topId, bottomId) {} + const std::string& bottomId = "bottom", const T phaseOffset = 0) : Stack(jL, topId, bottomId, phaseOffset) {} }; #endif // CORE_STACK_HPP_ diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 6eec15d..a2c9a94 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -130,6 +130,12 @@ PYBIND11_MODULE(cmtj, m) // Driver Class py::class_(m, "ScalarDriver") + .def(py::self + double()) + .def(py::self += double()) + .def(py::self - double()) + .def(py::self -= double()) + .def(py::self * double()) + .def(py::self *= double()) .def_static("getConstantDriver", &DScalarDriver::getConstantDriver, "constantValue"_a) @@ -350,7 +356,8 @@ PYBIND11_MODULE(cmtj, m) .def("getMagnetisation", &DSeriesStack::getMagnetisation, "junction"_a, "layerId"_a) .def("setCoupledCurrentDriver", &DSeriesStack::setCoupledCurrentDriver, "driver"_a) .def("setExternalFieldDriver", &DSeriesStack::setExternalFieldDriver, "driver"_a) - .def("setCouplingStrength", &DSeriesStack::setCouplingStrength, "coupling"_a) + .def("setCouplingStrength", py::overload_cast(&DParallelStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast>(&DParallelStack::setCouplingStrength), "coupling"_a) .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) // logging .def("clearLogs", &DSeriesStack::clearLogs) @@ -372,7 +379,8 @@ PYBIND11_MODULE(cmtj, m) .def("getMagnetisation", &DParallelStack::getMagnetisation, "junction"_a, "layerId"_a) .def("setCoupledCurrentDriver", &DParallelStack::setCoupledCurrentDriver, "driver"_a) .def("setExternalFieldDriver", &DParallelStack::setExternalFieldDriver, "driver"_a) - .def("setCouplingStrength", &DParallelStack::setCouplingStrength, "coupling"_a) + .def("setCouplingStrength", py::overload_cast(&DParallelStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast>(&DParallelStack::setCouplingStrength), "coupling"_a) .def("setDelayed", &DParallelStack::setDelayed, "delayed"_a) // logging .def("clearLogs", &ParallelStack::clearLogs) From 308ee3575ce29f7e65ebba32b304f0f32ac40053 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 10 Jun 2024 23:51:31 +0200 Subject: [PATCH 05/44] initial work on generalising coupled drivers --- core/stack.hpp | 20 +++++++++++--------- python/cmtj.cpp | 4 ++-- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/core/stack.hpp b/core/stack.hpp index 75303b1..6bb25ce 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -30,13 +30,12 @@ class Stack bool delayed = true; T phaseOffset = 0; virtual T calculateStackResistance(std::vector resistances) = 0; - virtual T getPhaseOffset(const unsigned int& order) const = 0; - virtual T getEffectiveCouplingStrength(CVector m1, CVector m2, CVector p) = 0; + virtual T getPhaseOffset(const unsigned int& order) const = 0; + virtual T getEffectiveCouplingStrength(const unsigned int& order, + const CVector& m1, const CVector& m2, const CVector& p) = 0; T computeCouplingCurrentDensity(T currentDensity, - CVector m1, CVector m2, CVector p) { - const T coupledI = currentDensity * this->getEffectiveCouplingStrength(m1, m2, p); - return coupledI; - + const CVector& m1, const CVector& m2, const CVector& p) { + return currentDensity * this->getEffectiveCouplingStrength(m1, m2, p); } public: std::vector> junctionList; @@ -157,7 +156,7 @@ class Stack this->couplingStrength = { coupling }; } - void setCouplingStrength(const std::vector &coupling) + void setCouplingStrength(const std::vector& coupling) { if (coupling.size() != this->stackSize - 1) { @@ -347,7 +346,8 @@ class SeriesStack : public Stack return resSum; } - T getEffectiveCouplingStrength(const unsigned int& order, CVector m1, CVector m2, CVector p) override + T getEffectiveCouplingStrength(const unsigned int& order, + const CVector& m1, const CVector& m2, const CVector& p) override { const T m1Comp = c_dot(m1, p); const T m2Comp = c_dot(m2, p); @@ -364,6 +364,7 @@ class SeriesStack : public Stack const std::string& topId = "free", const std::string& bottomId = "bottom", const T phaseOffset = 0) : Stack(jL, topId, bottomId, phaseOffset) {} }; + template class ParallelStack : public Stack { @@ -375,7 +376,8 @@ class ParallelStack : public Stack return 1. / invSum; } - T getEffectiveCouplingStrength(const unsigned int& order, CVector m1, CVector m2, CVector p) override + T getEffectiveCouplingStrength(const unsigned int& order, + const CVector& m1, const CVector& m2, const CVector& p) override { const T m1Comp = c_dot(m1, p); const T m2Comp = c_dot(m2, p); diff --git a/python/cmtj.cpp b/python/cmtj.cpp index a2c9a94..fc291b9 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -356,8 +356,8 @@ PYBIND11_MODULE(cmtj, m) .def("getMagnetisation", &DSeriesStack::getMagnetisation, "junction"_a, "layerId"_a) .def("setCoupledCurrentDriver", &DSeriesStack::setCoupledCurrentDriver, "driver"_a) .def("setExternalFieldDriver", &DSeriesStack::setExternalFieldDriver, "driver"_a) - .def("setCouplingStrength", py::overload_cast(&DParallelStack::setCouplingStrength), "coupling"_a) - .def("setCouplingStrength", py::overload_cast>(&DParallelStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast(&DSeriesStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast>(&DSeriesStack::setCouplingStrength), "coupling"_a) .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) // logging .def("clearLogs", &DSeriesStack::clearLogs) From 7ef5c80df3749030851628ebaba6086d7250dde5 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 10 Jun 2024 23:56:33 +0200 Subject: [PATCH 06/44] initial work on generalising coupled drivers --- core/drivers.hpp | 12 ++++++------ core/stack.hpp | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/core/drivers.hpp b/core/drivers.hpp index 6cc2f3e..b8ce8ae 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -84,7 +84,7 @@ class Driver } // override multiplication operator - ScalarDriver operator*(const T& val) + Driver operator*(const T& val) { this->constantValue *= val; this->amplitude *= val; @@ -92,7 +92,7 @@ class Driver } // override *= operator - ScalarDriver operator*=(const T& val) + Driver operator*=(const T& val) { this->constantValue *= val; this->amplitude *= val; @@ -100,14 +100,14 @@ class Driver } // override addition operator - ScalarDriver operator+(const T& val) + Driver operator+(const T& val) { this->constantValue += val; this->amplitude += val; return *this; } - ScalarDriver operator+=(const T& val) + Driver operator+=(const T& val) { this->constantValue += val; this->amplitude += val; @@ -115,14 +115,14 @@ class Driver } // override subtraction operator - ScalarDriver operator-(const T& val) + Driver operator-(const T& val) { this->constantValue -= val; this->amplitude -= val; return *this; } - ScalarDriver operator-=(const T& val) + Driver operator-=(const T& val) { this->constantValue -= val; this->amplitude -= val; diff --git a/core/stack.hpp b/core/stack.hpp index 6bb25ce..47250c1 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -60,7 +60,7 @@ class Stack stackSize = this->junctionList.size(); } - T getCoupling(const& order) const { + T getCoupling(const unsigned int& order) const { if (this->couplingStrength.empty()) { throw std::runtime_error("Coupling strength is not set!"); From 24d8c109b3a5fc22b4a84417d66245ea2f0330a3 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Tue, 11 Jun 2024 00:14:14 +0200 Subject: [PATCH 07/44] initial work on generalising coupled drivers --- core/drivers.hpp | 96 ++++++++++++++++++++++++------------------------ core/stack.hpp | 3 +- python/cmtj.cpp | 14 +++---- 3 files changed, 56 insertions(+), 57 deletions(-) diff --git a/core/drivers.hpp b/core/drivers.hpp index b8ce8ae..47eff65 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -82,52 +82,6 @@ class Driver { this->phase += phase; } - - // override multiplication operator - Driver operator*(const T& val) - { - this->constantValue *= val; - this->amplitude *= val; - return *this; - } - - // override *= operator - Driver operator*=(const T& val) - { - this->constantValue *= val; - this->amplitude *= val; - return *this; - } - - // override addition operator - Driver operator+(const T& val) - { - this->constantValue += val; - this->amplitude += val; - return *this; - } - - Driver operator+=(const T& val) - { - this->constantValue += val; - this->amplitude += val; - return *this; - } - - // override subtraction operator - Driver operator-(const T& val) - { - this->constantValue -= val; - this->amplitude -= val; - return *this; - } - - Driver operator-=(const T& val) - { - this->constantValue -= val; - this->amplitude -= val; - return *this; - } }; template @@ -395,6 +349,54 @@ class ScalarDriver : public Driver return returnValue; } + // override multiplication operator + ScalarDriver operator*(const T& val) + { + this->constantValue *= val; + this->amplitude *= val; + return *this; + } + + ScalarDriver operator*(const T& val) const { + return (*this) * val; + } + + // override *= operator + ScalarDriver operator*=(const T& val) + { + this->constantValue *= val; + this->amplitude *= val; + return *this; + } + + ScalarDriver operator*=(const T& val) const { + return (*this) * val; + } + + // override addition operator + ScalarDriver operator+(const T& val) + { + this->constantValue += val; + this->amplitude += val; + return *this; + } + ScalarDriver operator+(const T& v) const + { + // Use non-const operator+ here + return (*this) + v; + }; + ScalarDriver operator+=(const T& val) + { + this->constantValue += val; + this->amplitude += val; + return *this; + } + ScalarDriver operator+=(const T& v) const + { + // Use non-const operator+ here + return (*this) + v; + }; + }; @@ -439,7 +441,7 @@ class AxialDriver : public Driver } } - void applyMask(CVector mask) + void applyMask(const CVector& mask) { this->applyMask(std::vector{(unsigned int)(mask[0]), (unsigned int)(mask[1]), diff --git a/core/stack.hpp b/core/stack.hpp index 47250c1..6fd2246 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -279,7 +279,6 @@ class Stack } } const T plainCurrent = this->currentDriver.getCurrentScalarValue(t); - T coupledCurrent = plainCurrent; T effectiveCoupling = 1; for (std::size_t j = 0; j < junctionList.size(); ++j) { @@ -351,7 +350,7 @@ class SeriesStack : public Stack { const T m1Comp = c_dot(m1, p); const T m2Comp = c_dot(m2, p); - return this->getCoupling() * (m1Comp + m2Comp); + return this->getCoupling(order) * (m1Comp + m2Comp); } T getPhaseOffset(const unsigned int& order) const override diff --git a/python/cmtj.cpp b/python/cmtj.cpp index fc291b9..13252f2 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -132,8 +132,6 @@ PYBIND11_MODULE(cmtj, m) py::class_(m, "ScalarDriver") .def(py::self + double()) .def(py::self += double()) - .def(py::self - double()) - .def(py::self -= double()) .def(py::self * double()) .def(py::self *= double()) .def_static("getConstantDriver", @@ -194,8 +192,8 @@ PYBIND11_MODULE(cmtj, m) .def("getVectorAxialDriver", &DAxialDriver::getVectorAxialDriver) .def("getCurrentAxialDrivers", &DAxialDriver::getCurrentAxialDrivers) - .def("applyMask", py::overload_cast(&DAxialDriver::applyMask)) - .def("applyMask", py::overload_cast>(&DAxialDriver::applyMask)); + .def("applyMask", py::overload_cast(&DAxialDriver::applyMask)) + .def("applyMask", py::overload_cast&>(&DAxialDriver::applyMask)); py::class_(m, "Layer") .def(py::init< @@ -356,8 +354,8 @@ PYBIND11_MODULE(cmtj, m) .def("getMagnetisation", &DSeriesStack::getMagnetisation, "junction"_a, "layerId"_a) .def("setCoupledCurrentDriver", &DSeriesStack::setCoupledCurrentDriver, "driver"_a) .def("setExternalFieldDriver", &DSeriesStack::setExternalFieldDriver, "driver"_a) - .def("setCouplingStrength", py::overload_cast(&DSeriesStack::setCouplingStrength), "coupling"_a) - .def("setCouplingStrength", py::overload_cast>(&DSeriesStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast(&DSeriesStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast&>(&DSeriesStack::setCouplingStrength), "coupling"_a) .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) // logging .def("clearLogs", &DSeriesStack::clearLogs) @@ -379,8 +377,8 @@ PYBIND11_MODULE(cmtj, m) .def("getMagnetisation", &DParallelStack::getMagnetisation, "junction"_a, "layerId"_a) .def("setCoupledCurrentDriver", &DParallelStack::setCoupledCurrentDriver, "driver"_a) .def("setExternalFieldDriver", &DParallelStack::setExternalFieldDriver, "driver"_a) - .def("setCouplingStrength", py::overload_cast(&DParallelStack::setCouplingStrength), "coupling"_a) - .def("setCouplingStrength", py::overload_cast>(&DParallelStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast(&DParallelStack::setCouplingStrength), "coupling"_a) + .def("setCouplingStrength", py::overload_cast&>(&DParallelStack::setCouplingStrength), "coupling"_a) .def("setDelayed", &DParallelStack::setDelayed, "delayed"_a) // logging .def("clearLogs", &ParallelStack::clearLogs) From 16b0d7af195cb6433c728a9997eb83cbd3f379b9 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Tue, 11 Jun 2024 21:39:19 +0200 Subject: [PATCH 08/44] small fixes and tests --- cmtj/utils/parallel.py | 3 +- cmtj/utils/plotting.py | 159 ++++++++++++++++++++++------------------- core/drivers.hpp | 57 ++++++++++++--- core/llgb.hpp | 4 +- core/reservoir.hpp | 4 +- core/stack.hpp | 1 - python/cmtj.cpp | 6 +- tests/test_drivers.py | 19 +++++ 8 files changed, 164 insertions(+), 89 deletions(-) diff --git a/cmtj/utils/parallel.py b/cmtj/utils/parallel.py index e733c6d..9410617 100644 --- a/cmtj/utils/parallel.py +++ b/cmtj/utils/parallel.py @@ -17,7 +17,8 @@ def distribute(simulation_fn: Callable, :param simulation_fn: function to be distributed :param spaces: list of lists of parameters :param n_cores: number of cores to use. - :returns: index, simulation_fn output + :returns: (index, simulation_fn output) + index - index of the parameters in the spaces list, multiple dims. """ spaces = [np.asarray(space) for space in spaces] diff --git a/cmtj/utils/plotting.py b/cmtj/utils/plotting.py index 8755e2d..ec421a1 100644 --- a/cmtj/utils/plotting.py +++ b/cmtj/utils/plotting.py @@ -3,7 +3,6 @@ import matplotlib.patches as patches import matplotlib.pyplot as plt import numpy as np -import seaborn as sns from mpl_toolkits.mplot3d.art3d import Line3DCollection @@ -19,7 +18,7 @@ def get_sphere(): return xs, ys, zs -def plot_trajectory_sphere(x, y, z, color='blue', alpha=1, ax=None): +def plot_trajectory_sphere(x, y, z, color="blue", alpha=1, ax=None): """Plot a trajectory in 3D. Normalises to unit sphere :param ax: matplotlib axis :param x: x-coordinates @@ -34,20 +33,22 @@ def plot_trajectory_sphere(x, y, z, color='blue', alpha=1, ax=None): # make sure we are unit norm for m m = m / np.linalg.norm(m) if ax is None: - with plt.style.context(['science', 'nature']): + with plt.style.context(["science", "nature"]): fig = plt.figure(dpi=300) - ax = fig.add_subplot(1, 1, 1, projection='3d') + ax = fig.add_subplot(1, 1, 1, projection="3d") ax.plot3D(m[0], m[1], m[2], color=color, alpha=alpha) ax.set_axis_off() - ax.plot_surface(xs, - ys, - zs, - rstride=2, - cstride=2, - color='azure', - alpha=0.1, - linewidth=0.1) - ax.scatter([0], [0], [1], color='crimson', alpha=1.0) + ax.plot_surface( + xs, + ys, + zs, + rstride=2, + cstride=2, + color="azure", + alpha=0.1, + linewidth=0.1, + ) + ax.scatter([0], [0], [1], color="crimson", alpha=1.0) else: ax.plot3D(m[0], m[1], m[2], color=color, alpha=alpha) ax.set_axis_off() @@ -56,13 +57,13 @@ def plot_trajectory_sphere(x, y, z, color='blue', alpha=1, ax=None): zs, rstride=2, cstride=2, - color='azure', + color="azure", alpha=0.1, linewidth=0.1) - ax.scatter([0], [0], [1], color='crimson', alpha=1.0) + ax.scatter([0], [0], [1], color="crimson", alpha=1.0) -def plot_coloured_trajectory(x, y, z, colormap='plasma', ax=None): +def plot_coloured_trajectory(x, y, z, colormap="plasma", ax=None): """Plot a coloured trajectory in 3D. Normalises to unit sphere. Colour of the trajectory now designates the flow of time. :param ax: matplotlib axis @@ -71,25 +72,30 @@ def plot_coloured_trajectory(x, y, z, colormap='plasma', ax=None): :param z: z-coordinates :param colormap: colormap to use """ + import seaborn as sns + xs, ys, zs = get_sphere() m = np.asarray([x, y, z]) points = m.T.reshape(-1, 1, 3) segs = np.concatenate([points[:-1], points[1:]], axis=1) + colors = sns.color_palette(colormap, len(segs)) if ax is None: - with plt.style.context(['science', 'nature']): + with plt.style.context(["science", "nature"]): fig = plt.figure(dpi=300) - ax = fig.add_subplot(1, 1, 1, projection='3d') + ax = fig.add_subplot(1, 1, 1, projection="3d") # plot the sphere firext ax.set_axis_off() - ax.plot_surface(xs, - ys, - zs, - rstride=2, - cstride=2, - color='azure', - alpha=0.1, - linewidth=0.1) + ax.plot_surface( + xs, + ys, + zs, + rstride=2, + cstride=2, + color="azure", + alpha=0.1, + linewidth=0.1, + ) ax.add_collection(Line3DCollection(segs, colors=colors, alpha=1)) else: ax.set_axis_off() @@ -98,7 +104,7 @@ def plot_coloured_trajectory(x, y, z, colormap='plasma', ax=None): zs, rstride=2, cstride=2, - color='azure', + color="azure", alpha=0.1, linewidth=0.1) ax.add_collection(Line3DCollection(segs, colors=colors, alpha=1)) @@ -142,7 +148,8 @@ def create_coordinates_plot(axes, import matplotlib.cm as cm import matplotlib.patches as patches from matplotlib.path import Path - with plt.style.context(['science', 'nature']): + + with plt.style.context(["science", "nature"]): fig, host = plt.subplots(dpi=400) ax_lists, value_list = unpack_ndim_map(result_map, axes) @@ -173,21 +180,21 @@ def create_coordinates_plot(axes, axes = [host] + [host.twinx() for _ in range(ys.shape[1] - 1)] for i, ax in enumerate(axes): ax.set_ylim(ymins[i], ymaxs[i]) - ax.spines['top'].set_visible(False) - ax.spines['bottom'].set_visible(False) + ax.spines["top"].set_visible(False) + ax.spines["bottom"].set_visible(False) if ax != host: - ax.spines['left'].set_visible(False) - ax.yaxis.set_ticks_position('right') + ax.spines["left"].set_visible(False) + ax.yaxis.set_ticks_position("right") ax.spines["right"].set_position( ("axes", i / (ys.shape[1] - 1))) host.set_xlim(0, ys.shape[1] - 1) host.set_xticks(range(ys.shape[1])) host.set_xticklabels(ax_names, fontsize=8) - host.tick_params(axis='x', which='major', pad=7) - host.spines['right'].set_visible(False) + host.tick_params(axis="x", which="major", pad=7) + host.spines["right"].set_visible(False) host.xaxis.tick_top() - host.set_title('Parallel Coordinates Plot') + host.set_title("Parallel Coordinates Plot") for j in range(ys.shape[0]): # create bezier curves @@ -198,23 +205,23 @@ def create_coordinates_plot(axes, verts = list( zip( list( - np.linspace( - 0, len(ys) - 1, len(ys) * 3 - 2, endpoint=True - ) - ), + np.linspace(0, + len(ys) - 1, + len(ys) * 3 - 2, + endpoint=True)), np.repeat(zs[j, :], 3)[1:-1], - ) - ) + )) # for x,y in verts: host.plot(x, y, 'go') # to show the control points of the beziers codes = [Path.MOVETO ] + [Path.CURVE4 for _ in range(len(verts) - 1)] path = Path(verts, codes) alpha = alpha_black if ys[j, -1] == 0 else 0.8 - patch = patches.PathPatch(path, - facecolor='none', - lw=.5, - edgecolor=mapper.to_rgba( - ys[j, -1], alpha)) + patch = patches.PathPatch( + path, + facecolor="none", + lw=0.5, + edgecolor=mapper.to_rgba(ys[j, -1], alpha), + ) host.add_patch(patch) fig.tight_layout() @@ -224,20 +231,22 @@ def rotation_matrix(theta): [np.sin(theta), np.cos(theta)]]) -def create_stack(ax, - colors, - heights, - angles, - labels, - width=2, - labelpad_left=.2, - offset_x=0, - offset_y=0, - lw_arrow=1.5, - ms=10, - r=0.6, - text_fontsize=4, - reversed=True): +def create_stack( + ax, + colors, + heights, + angles, + labels, + width=2, + labelpad_left=0.2, + offset_x=0, + offset_y=0, + lw_arrow=1.5, + ms=10, + r=0.6, + text_fontsize=4, + reversed=True, +): """ Create a material stack plot. If a given layer is to have no arrow, pass None. @@ -271,13 +280,15 @@ def create_stack(ax, fill=True, color=color, zorder=10)) - ax.text(offset_x - labelpad_left, - offset_y + height / 2, - label, - horizontalalignment='center', - verticalalignment='center', - fontsize=text_fontsize, - zorder=11) + ax.text( + offset_x - labelpad_left, + offset_y + height / 2, + label, + horizontalalignment="center", + verticalalignment="center", + fontsize=text_fontsize, + zorder=11, + ) if angle is not None: [dx, dy] = np.dot(rotation_matrix(np.deg2rad(angle)), [x, y]) x_mid = dx / 2 @@ -285,12 +296,14 @@ def create_stack(ax, centre_x = (offset_x + width) / 2 - x_mid centre_y = offset_y + height / 2 - y_mid ax.add_patch( - patches.FancyArrowPatch((centre_x, centre_y), - (centre_x + dx, centre_y + dy), - mutation_scale=ms, - lw=lw_arrow, - color='black', - zorder=10)) + patches.FancyArrowPatch( + (centre_x, centre_y), + (centre_x + dx, centre_y + dy), + mutation_scale=ms, + lw=lw_arrow, + color="black", + zorder=10, + )) offset_y += height ax.set_ylim([first_offset - max(heights) / 2, offset_y + max(heights) / 2]) ax.set_xlim([offset_x - width / 2, offset_x + width + width / 2]) diff --git a/core/drivers.hpp b/core/drivers.hpp index 47eff65..384837b 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -352,13 +352,33 @@ class ScalarDriver : public Driver // override multiplication operator ScalarDriver operator*(const T& val) { - this->constantValue *= val; - this->amplitude *= val; - return *this; + return ScalarDriver( + this->update, + this->constantValue * val, + this->amplitude * val, + this->frequency, + this->phase, + this->period, + this->cycle, + this->timeStart, + this->timeStop, + this->edgeTime, + this->steadyTime); } ScalarDriver operator*(const T& val) const { - return (*this) * val; + return ScalarDriver( + this->update, + this->constantValue * val, + this->amplitude * val, + this->frequency, + this->phase, + this->period, + this->cycle, + this->timeStart, + this->timeStop, + this->edgeTime, + this->steadyTime); } // override *= operator @@ -376,14 +396,35 @@ class ScalarDriver : public Driver // override addition operator ScalarDriver operator+(const T& val) { - this->constantValue += val; - this->amplitude += val; - return *this; + return ScalarDriver( + this->update, + this->constantValue + val, + this->amplitude + val, + this->frequency, + this->phase, + this->period, + this->cycle, + this->timeStart, + this->timeStop, + this->edgeTime, + this->steadyTime); } + ScalarDriver operator+(const T& v) const { // Use non-const operator+ here - return (*this) + v; + return ScalarDriver( + this->update, + this->constantValue + v, + this->amplitude + v, + this->frequency, + this->phase, + this->period, + this->cycle, + this->timeStart, + this->timeStop, + this->edgeTime, + this->steadyTime); }; ScalarDriver operator+=(const T& val) { diff --git a/core/llgb.hpp b/core/llgb.hpp index 57e58d0..99af891 100644 --- a/core/llgb.hpp +++ b/core/llgb.hpp @@ -487,7 +487,7 @@ class LLGBJunction void - saveLogs(std::string filename) + saveLogs(const std::string& filename) { if (filename == "") { @@ -542,7 +542,7 @@ class LLGBJunction { if (timeStep > writeFrequency) { - std::runtime_error("The time step cannot be larger than write frequency!"); + throw std::runtime_error("The time step cannot be larger than write frequency!"); } const unsigned int totalIterations = (int)(totalTime / timeStep); const unsigned int writeEvery = (int)(writeFrequency / timeStep); diff --git a/core/reservoir.hpp b/core/reservoir.hpp index 0c68015..f720d13 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -140,7 +140,7 @@ class Reservoir unsigned int rows, cols; unsigned int noElements; - Reservoir(std::vector> coordinateMatrix, std::vector>> layerMatrix): coordinateMatrix(std::move(coordinateMatrix)), + Reservoir(std::vector> coordinateMatrix, std::vector>> layerMatrix) : coordinateMatrix(std::move(coordinateMatrix)), layerMatrix(std::move(layerMatrix)) { this->rows = this->coordinateMatrix.size(); @@ -241,7 +241,7 @@ class Reservoir } void - saveLogs(std::string fileSave) + saveLogs(const std::string& fileSave) { if (fileSave == "") { diff --git a/core/stack.hpp b/core/stack.hpp index 6fd2246..92716c0 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -278,7 +278,6 @@ class Stack frozenPols[j] = this->getPolarisationVector(); } } - const T plainCurrent = this->currentDriver.getCurrentScalarValue(t); T effectiveCoupling = 1; for (std::size_t j = 0; j < junctionList.size(); ++j) { diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 13252f2..4db3860 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -134,6 +134,7 @@ PYBIND11_MODULE(cmtj, m) .def(py::self += double()) .def(py::self * double()) .def(py::self *= double()) + .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, "time"_a) .def_static("getConstantDriver", &DScalarDriver::getConstantDriver, "constantValue"_a) @@ -182,7 +183,8 @@ PYBIND11_MODULE(cmtj, m) "sigma"_a); py::class_(m, "NullDriver") - .def(py::init<>()); + .def(py::init<>()) + .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, "time"_a); py::class_(m, "AxialDriver") .def(py::init()) @@ -191,7 +193,7 @@ PYBIND11_MODULE(cmtj, m) .def(py::init()) .def("getVectorAxialDriver", &DAxialDriver::getVectorAxialDriver) .def("getCurrentAxialDrivers", - &DAxialDriver::getCurrentAxialDrivers) + &DAxialDriver::getCurrentAxialDrivers, "time"_a) .def("applyMask", py::overload_cast(&DAxialDriver::applyMask)) .def("applyMask", py::overload_cast&>(&DAxialDriver::applyMask)); diff --git a/tests/test_drivers.py b/tests/test_drivers.py index 285abe9..7efa849 100644 --- a/tests/test_drivers.py +++ b/tests/test_drivers.py @@ -45,6 +45,25 @@ def test_aliases(): assert d1.getCurrentAxialDrivers(0.0) == CVector(1.0, 2.0, 3.0) +def test_driver_ops(): + import math + + driver = sineDriver(10, 20, 1, 0) + assert driver.getCurrentScalarValue(1 / 4) == 30 + driver *= 2 + assert driver.getCurrentScalarValue(1 / 4) == 60 + + driver = sineDriver(10, 20, 1, 0) + driver += 2 + assert driver.getCurrentScalarValue(1 / 4) == 34 + + driver = sineDriver(10, 20, 1, 0) * 2 + assert driver.getCurrentScalarValue(1 / 4) == 60 + + driver = sineDriver(10, 20, 1, 0) + 2 + assert driver.getCurrentScalarValue(1 / 4) == 34 + + def test_junction_with_driver(): Kdir = CVector(1, 0, 0) l1 = Layer( From d64be0611709876fcd18ffddbe3066de5c61ed3e Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Wed, 12 Jun 2024 21:35:03 +0200 Subject: [PATCH 09/44] exposing the new constructor and documenting the changes --- cmtj/__init__.pyi | 7 +++++++ cmtj/stack/__init__.pyi | 39 +++++++++++++++++++++++++++++++++++---- python/cmtj.cpp | 10 ++++++---- tests/test_models.py | 40 ++++++++++++++++++++-------------------- 4 files changed, 68 insertions(+), 28 deletions(-) diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index 4daed8f..80da0ca 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -578,6 +578,13 @@ class NullDriver(ScalarDriver): class ScalarDriver: def __init__(self, *args, **kwargs) -> None: ... + def getCurrentScalarValue(self, time: float) -> float: + """ + :param time: time in seconds + :return: the scalar value of the driver at time. + """ + ... + @staticmethod def getConstantDriver(constantValue: float) -> "ScalarDriver": """ diff --git a/cmtj/stack/__init__.pyi b/cmtj/stack/__init__.pyi index 037d0bc..4338f02 100644 --- a/cmtj/stack/__init__.pyi +++ b/cmtj/stack/__init__.pyi @@ -3,10 +3,20 @@ from typing import Dict, List, overload import cmtj class ParallelStack: - def __init__(self, junctionList: List[cmtj.Junction]) -> None: + def __init__( + self, + junctionList: List[cmtj.Junction], + topId: str = "free", + bottomId: str = "bottom", + phaseOffset: float = 0, + ) -> None: """ Initialises a parallel connection of junctions. + Layer ids are used to identify the layers in the junctions and for resistance calculations. :param junctionList: list of junctions to be connected in parallel. + :param topId: the string id of the top layer in the stack. Default is "free". + :param bottomId: the string id of the bottom layer in the stack. Default is "bottom". + :param phaseOffset: the phase offset between the junctions. Default is 0. """ ... @@ -68,7 +78,7 @@ class ParallelStack: """ ... - def setMagnetistation( + def setMagnetisation( self, junctionId: int, layerId: str, mag: cmtj.CVector ) -> None: """ @@ -82,10 +92,20 @@ class ParallelStack: def getMagnetisation(self, junction: int, layerId: str) -> cmtj.CVector: ... class SeriesStack: - def __init__(self, junctionList: List[cmtj.Junction]) -> None: + def __init__( + self, + junctionList: List[cmtj.Junction], + topId: str = "free", + bottomId: str = "bottom", + phaseOffset: float = 0, + ) -> None: """ Initialises a series connection of junctions. + Layer ids are used to identify the layers in the junctions and for resistance calculations. :param junctionList: list of junctions to be connected in series. + :param topId: the string id of the top layer in the stack. Default is "free". + :param bottomId: the string id of the bottom layer in the stack. Default is "bottom". + :param phaseOffset: the phase offset between the junctions. Default is 0. """ ... @@ -132,6 +152,7 @@ class SeriesStack: """ ... + @overload def setCouplingStrength(self, coupling: float) -> None: """ Coupling constant that represents the energy losses as the current @@ -140,6 +161,16 @@ class SeriesStack: """ ... + @overload + def setCouplingStrength(self, coupling: List[float]) -> None: + """ + Coupling constant that represents the energy losses as the current + passes through the stack. + :param coupling: the coupling strength (or the losses) for each junction. + Must be the one less than length of the junction vector, i.e. len(junctionList)-1 . + """ + ... + def setExternalFieldDriver(self, driver: cmtj.AxialDriver) -> None: """ Sets a external field current driver for all junctions inside the stack. @@ -147,7 +178,7 @@ class SeriesStack: """ ... - def setMagnetistation( + def setMagnetisation( self, junctionId: int, layerId: str, mag: cmtj.CVector ) -> None: """ diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 4db3860..ec1d8a7 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -344,10 +344,11 @@ PYBIND11_MODULE(cmtj, m) py::class_(stack_module, "SeriesStack") .def(py::init, std::string, - std::string>(), + std::string, double>(), "junctionList"_a, "topId_a"_a = "free", - "bottomId"_a = "bottom") + "bottomId"_a = "bottom", + "phaseOffset"_a = 0.0) .def("runSimulation", &DSeriesStack::runSimulation, "totalTime"_a, "timeStep"_a = 1e-13, @@ -367,10 +368,11 @@ PYBIND11_MODULE(cmtj, m) py::class_(stack_module, "ParallelStack") .def(py::init, std::string, - std::string>(), + std::string, double>(), "junctionList"_a, "topId_a"_a = "free", - "bottomId"_a = "bottom") + "bottomId"_a = "bottom", + "phaseOffset"_a = 0.0) .def("runSimulation", &DParallelStack::runSimulation, "totalTime"_a, "timeStep"_a = 1e-13, diff --git a/tests/test_models.py b/tests/test_models.py index 8d5b0c5..871b58b 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -7,24 +7,24 @@ def test_sb_dynamic(two_layer_symbolic_dyn: Tuple[LayerDynamic]): J1 = -1e-3 J2 = 1e-4 - for Hmag in np.linspace(-300e3, 300e3, 8): + for Hmag in np.linspace(-300e3, 300e3, 1): H = VectorObj(np.deg2rad(88), np.deg2rad(0.1), Hmag) solver_dyn = Solver(layers=two_layer_symbolic_dyn, J1=[J1], J2=[J2], H=H) pos = np.asarray( [np.deg2rad(88), np.deg2rad(0.1), np.deg2rad(88), np.deg2rad(0.1)] ) # set perturbation to 0 to avoid numerical errors - eq_sb, f_sb = solver_dyn.solve( - init_position=pos, perturbation=0, max_steps=1e8, force_sb=True - ) + # eq_sb, f_sb = solver_dyn.solve( + # init_position=pos, perturbation=0, max_steps=1e1, force_sb=True + # ) eq_dyn, f_dyn, _ = solver_dyn.solve( - init_position=pos, max_steps=1e8, perturbation=0 + init_position=pos, max_steps=1e6, perturbation=0 ) - f_sb.sort() - f_dyn.sort() - assert np.allclose(eq_sb, eq_dyn) - assert np.allclose(f_sb, f_dyn, atol=0.2) - pos = eq_dyn + # f_sb.sort() + # f_dyn.sort() + # assert np.allclose(eq_sb, eq_dyn) + # assert np.allclose(f_sb, f_dyn, atol=0.2) + # pos = eq_dyn def test_sb_classic_dipole(two_layer_symbolic_classic: Tuple[LayerSB]): @@ -35,17 +35,17 @@ def test_sb_classic_dipole(two_layer_symbolic_classic: Tuple[LayerSB]): VectorObj.from_cartesian(0, -1e-4, 0), VectorObj.from_cartesian(0, 0, 1e-6), ] - for Hmag in np.linspace(-300e3, 300e3, 8): + for Hmag in np.linspace(-300e3, 300e3, 1): H = VectorObj(np.deg2rad(88), np.deg2rad(0.1), Hmag) solver_dyn = Solver( layers=two_layer_symbolic_classic, J1=[J1], J2=[J2], H=H, Ndipole=[dipole] ) - pos = np.asarray( - [np.deg2rad(88), np.deg2rad(0.1), np.deg2rad(88), np.deg2rad(0.1)] - ) - # set perturbation to 0 to avoid numerical errors - eq_sb, f_sb = solver_dyn.solve( - init_position=pos, perturbation=0, max_steps=1e8, force_sb=True - ) - f_sb.sort() - pos = eq_sb + # pos = np.asarray( + # [np.deg2rad(88), np.deg2rad(0.1), np.deg2rad(88), np.deg2rad(0.1)] + # ) + # # set perturbation to 0 to avoid numerical errors + # eq_sb, f_sb = solver_dyn.solve( + # init_position=pos, perturbation=0, max_steps=1e6, force_sb=True + # ) + # f_sb.sort() + # pos = eq_sb From 4e19598de4a6527d32ddf9d9a6f1d00068bc190b Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sat, 15 Jun 2024 14:07:22 +0200 Subject: [PATCH 10/44] some bugfixes for PR --- .pre-commit-config.yaml | 14 +- CHANGELOG.md | 19 +- cmtj/__init__.pyi | 42 +- core/drivers.hpp | 960 +++++++++++++++++----------------------- core/stack.hpp | 647 +++++++++++++-------------- setup.py | 2 +- tests/test_drivers.py | 2 - tests/test_models.py | 38 +- 8 files changed, 769 insertions(+), 955 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 29acd13..3b3e1f6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -48,12 +48,12 @@ repos: - repo: https://github.com/pocc/pre-commit-hooks rev: master hooks: - - id: cpplint - id: cppcheck - args: [--check-level=exhaustive] + args: ["--check-level=exhaustive"] exclude: third_party/.* - # - id: clang-format - # - id: oclint - # - id: uncrustify - - id: include-what-you-use - exclude: ^third_party + - id: clang-format + args: [-i] + - id: clang-tidy + args: [-checks=*] + # - id: include-what-you-use + # exclude: ^third_party diff --git a/CHANGELOG.md b/CHANGELOG.md index a17d86b..a25a12f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,14 +1,19 @@ # Changelog +# 1.5.5 + +- Extended the `Stack` models allowing for non-symmetric coupling between devices. +- `Stack` current drivers can now be of any type are adequately scaled. + # 1.5.0-1.5.4 - Dipole interaction added to the `SB Model` - Kasdin 1/f noise generator added to the `noise` module and to the solvers -- reworking the solvers for better performance and stability -- added a simple noise model to the `utils` class. It exists outside standard simulation procedures. -- added LLGB bindings and code. The solver is still WIP and doesn't integrate with more advanced features yet. -- added aliases for `ScalarDriver` -- for example, instead of calling `ScalarDriver.getConstantDriver`, you can now call `constantDriver` directly to create a constant driver. -- improve stub detection across editors and IDEs +- Reworking the solvers for better performance and stability +- Added a simple noise model to the `utils` class. It exists outside standard simulation procedures. +- Added LLGB bindings and code. The solver is still WIP and doesn't integrate with more advanced features yet. +- Added aliases for `ScalarDriver` -- for example, instead of calling `ScalarDriver.getConstantDriver`, you can now call `constantDriver` directly to create a constant driver. +- Improve stub detection across editors and IDEs # 1.4.1 @@ -41,12 +46,12 @@ - Adding DW dynamics 1D model with dynamic drivers. (Numba optimised) - Adding SB model for energy-based FMR computation. Gradient computed using Adam optimiser. - Moving resistance functions from `utils` to `resistance` -- Introducting docs updates for tutorial notebook (dark/light toggle works now). +- Introducing docs updates for tutorial notebook (dark/light toggle works now). - Reservoir computing is now exposed in Python in the `reservoir` computing module. ## 1.2.0 -- Oersted field computation helper class in [cmtj/models/oersted.py](cmtj/models/oersted.py). Basic functionality is there, but needs to be futher tested and documented. Next release potentially will move the computation to C++ for speed. +- Oersted field computation helper class in [cmtj/models/oersted.py](cmtj/models/oersted.py). Basic functionality is there, but needs to be further tested and documented. Next release potentially will move the computation to C++ for speed. - Added Heun (2nd order) solver and made it default for thermal computation. This is a more stable solver than the Euler solver, but is slower. The Euler solver is still available as an option. - Stack class now supports arbitrary layer ids to be coupled. - Extended the plotting capabilities of the Stack class. Now supports plotting of the magnetic field and the current density. diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index 80da0ca..9ace886 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -127,7 +127,7 @@ class AxialDriver: @overload def __init__(self, axialDrivers: List[ScalarDriver]) -> None: """Create an axial driver with a list of scalar drivers. - :param arg0: list of scalar drivers + :param axialDrivers: list of scalar drivers """ ... @@ -285,9 +285,9 @@ class Junction: """Reset current simulation state.""" ... - def getLayerMagnetisation(self, layer_id: str) -> CVector: + def getLayerMagnetisation(self, layerId: str) -> CVector: """Get the magnetisation of a layer. - :param layer_id: the layer id""" + :param layerId: the layer id""" ... def getLog(self) -> Dict[str, List[float]]: @@ -337,72 +337,72 @@ class Junction: """ ... - def setLayerTemperatureDriver(self, layer_id: str, driver: ScalarDriver) -> None: + def setLayerTemperatureDriver(self, layerId: str, driver: ScalarDriver) -> None: """Set a temperature driver for a layer. - :param layer_id: the id of the layer. + :param layerId: the id of the layer. :param driver: the temperature driver to be set. """ ... - def setLayerAnisotropyDriver(self, layer_id: str, driver: ScalarDriver) -> None: + def setLayerAnisotropyDriver(self, layerId: str, driver: ScalarDriver) -> None: """Set anisotropy driver for a layer. - :param layer_id: the id of the layer. + :param layerId: the id of the layer. :param driver: the anisotropy driver to be set. """ ... - def setLayerCurrentDriver(self, layer_id: str, driver: ScalarDriver) -> None: + def setLayerCurrentDriver(self, layerId: str, driver: ScalarDriver) -> None: """Set a current driver for a layer. - :param layer_id: the layer id + :param layerId: the layer id :param driver: the driver """ ... - def setLayerExternalFieldDriver(self, layer_id: str, driver: AxialDriver) -> None: + def setLayerExternalFieldDriver(self, layerId: str, driver: AxialDriver) -> None: """Set an external field driver for a layer. - :param layer_id: the id of the layer. + :param layerId: the id of the layer. :param driver: the field driver to be set. """ ... - def setLayerMagnetisation(self, layer_id: str, mag: CVector) -> None: + def setLayerMagnetisation(self, layerId: str, mag: CVector) -> None: """Set the magnetisation of a layer. - :param layer_id: the layer id + :param layerId: the layer id :param mag: the magnetisation """ ... @overload - def setLayerOerstedFieldDriver(self, layer_id: str, driver: AxialDriver) -> None: + def setLayerOerstedFieldDriver(self, layerId: str, driver: AxialDriver) -> None: """Set an Oersted field driver for a layer. - :param layer_id: the id of the layer. + :param layerId: the id of the layer. :param driver: the field driver to be set. """ ... def setLayerDampingLikeTorqueDriver( - self, layer_id: str, driver: ScalarDriver + self, layerId: str, driver: ScalarDriver ) -> None: """Set the damping like torque driver for a layer. - :param layer_id: the layer id + :param layerId: the layer id :param driver: the driver """ ... def setLayerFieldLikeTorqueDriver( - self, layer_id: str, driver: ScalarDriver + self, layerId: str, driver: ScalarDriver ) -> None: """Set the field like torque driver for a layer. - :param layer_id: the layer id + :param layerId: the layer id :param driver: the driver """ ... def setLayerOneFNoise( - self, layer_id: str, sources: int, bias: float, scale: float + self, layerId: str, sources: int, bias: float, scale: float ) -> None: """Set 1/f noise for a layer. - :param layer_id: the layer id + :param layerId: the layer id :param sources: the number of generation sources (the more the slower, but more acc.) :param bias: the bias of the noise (p in the Multinomial distribution) :param scale: the scale of the noise, additional scaling factor diff --git a/core/drivers.hpp b/core/drivers.hpp index 384837b..8a1bf3d 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -5,584 +5,432 @@ #define M_PI (3.14159265358979323846) #endif -#include // for abs -#include // for assert +#include // for assert +#include // for abs #define _USE_MATH_DEFINES -#include // for M_PI -#include // for runtime_error -#include // for vector -#include "cvector.hpp" // for CVector -#include // for move -enum UpdateType -{ - constant, - pulse, - sine, - step, - posine, - halfsine, - trapezoid, - gaussimpulse, - gaussstep +#include "cvector.hpp" // for CVector +#include // for M_PI +#include // for runtime_error +#include // for move +#include // for vector + +enum UpdateType { + constant, + pulse, + sine, + step, + posine, + halfsine, + trapezoid, + gaussimpulse, + gaussstep }; -template -class Driver -{ +template class Driver { protected: - // if the user wants to update, let them do that - T constantValue, amplitude, frequency, phase, - period, cycle, timeStart, timeStop; - UpdateType update; -public: - Driver() - { - this->constantValue = 0.0; - this->amplitude = 0.0; - this->frequency = 0.0; - this->phase = 0.0; - this->period = 0.0; - this->timeStart = 0.0; - this->cycle = 0.0; - this->timeStop = 0.0; - this->update = constant; - }; - Driver(UpdateType update, - T constantValue, - T amplitude, - T frequency, - T phase, - T period, - T cycle, - T timeStart, - T timeStop) : constantValue(constantValue), - amplitude(amplitude), - frequency(frequency), - phase(phase), - period(period), - cycle(cycle), - timeStart(timeStart), - timeStop(timeStop), - update(update) - - { - } - virtual T getCurrentScalarValue(T& time) - { - return 0; - }; - virtual ~Driver() = default; - - void setConstantValue(const T& val) - { - this->constantValue = val; - } + // if the user wants to update, let them do that + T constantValue, amplitude, frequency, phase, period, cycle, timeStart, + timeStop; + UpdateType update; - void phaseShift(const T& phase) - { - this->phase += phase; - } +public: + Driver() { + this->constantValue = 0.0; + this->amplitude = 0.0; + this->frequency = 0.0; + this->phase = 0.0; + this->period = 0.0; + this->timeStart = 0.0; + this->cycle = 0.0; + this->timeStop = 0.0; + this->update = constant; + }; + Driver(UpdateType update, T constantValue, T amplitude, T frequency, T phase, + T period, T cycle, T timeStart, T timeStop) + : constantValue(constantValue), amplitude(amplitude), + frequency(frequency), phase(phase), period(period), cycle(cycle), + timeStart(timeStart), timeStop(timeStop), update(update) + + {} + virtual T getCurrentScalarValue(T &time) { return 0; }; + virtual ~Driver() = default; + + void setConstantValue(const T &val) { this->constantValue = val; } + + void phaseShift(const T &phase) { this->phase += phase; } }; -template -class ScalarDriver : public Driver -{ +template class ScalarDriver : public Driver { + private: - T edgeTime = 0; - T steadyTime = 0; -protected: - T stepUpdate(T amplitude, T time, T timeStart, T timeStop) - { - if (time >= timeStart && time <= timeStop) - { - return amplitude; - } - else - { - return 0.0; - } - } - T pulseTrain(T amplitude, T time, T period, T cycle) - { - const int n = (int)(time / period); - const T dT = cycle * period; - const T nT = n * period; - if (nT <= time && time <= (nT + dT)) - { - return amplitude; - } - else - { - return 0; - } - } + T edgeTime = 0; + T steadyTime = 0; - T trapezoidalUpdate(T amplitude, T time, T timeStart, T edgeTime, T steadyTime) { - if (time < timeStart) { - return 0; - } - // growth - else if (time <= timeStart + edgeTime) { - return (amplitude / edgeTime) * (time - timeStart); - } - // steady - else if (time <= timeStart + edgeTime + steadyTime) { - return amplitude; - } - // decay - else if (time <= timeStart + 2 * edgeTime + steadyTime) { - return amplitude - (amplitude / edgeTime) * (time - (timeStart + edgeTime + steadyTime)); - } - return 0; - } +protected: + T stepUpdate(T amplitude, T time, T timeStart, T timeStop) { + if (time >= timeStart && time <= timeStop) { + return amplitude; + } else { + return 0.0; + } + } + T pulseTrain(T amplitude, T time, T period, T cycle) { + const int n = static_cast(time / period); + const T dT = cycle * period; + const T nT = n * period; + if (nT <= time && time <= (nT + dT)) { + return amplitude; + } else { + return 0; + } + } + + T trapezoidalUpdate(T amplitude, T time, T timeStart, T edgeTime, + T steadyTime) { + if (time < timeStart) { + return 0; + } + // growth + else if (time <= timeStart + edgeTime) { + return (amplitude / edgeTime) * (time - timeStart); + } + // steady + else if (time <= timeStart + edgeTime + steadyTime) { + return amplitude; + } + // decay + else if (time <= timeStart + 2 * edgeTime + steadyTime) { + return amplitude - (amplitude / edgeTime) * + (time - (timeStart + edgeTime + steadyTime)); + } + return 0; + } public: - explicit ScalarDriver( - UpdateType update = constant, - T constantValue = 0, - T amplitude = 0, - T frequency = -1, - T phase = 0, - T period = -1, - T cycle = -1, - T timeStart = -1, - T timeStop = -1, - T edgeTime = -1, - T steadyTime = -1) - : Driver(update, - constantValue, - amplitude, - frequency, - phase, - period, - cycle, - timeStart, - timeStop) - { - this->edgeTime = edgeTime; - this->steadyTime = steadyTime; - if (update == pulse && ((period == -1) || (cycle == -1))) - { - throw std::runtime_error("Selected pulse train driver type but either period or cycle were not set"); - } - else if (update == sine && (frequency == -1)) - { - throw std::runtime_error("Selected sine driver type but frequency was not set"); - } - } - - /** - * Constant driver produces a constant signal of a fixed amplitude. - * @param constantValue: constant value of the driver (constant offset/amplitude) - */ - static ScalarDriver getConstantDriver(T constantValue) - { - return ScalarDriver( - constant, - constantValue); - } - - /** - * Produces a square pulse of certain period and cycle - * @param constantValue: offset (vertical) of the pulse. The pulse amplitude will be added to this. - * @param amplitude: amplitude of the pulse signal - * @param period: period of the signal in seconds - * @param cycle: duty cycle of the signal -- a fraction between [0 and 1]. - */ - static ScalarDriver getPulseDriver(T constantValue, T amplitude, T period, T cycle) - { - return ScalarDriver( - pulse, - constantValue, - amplitude, - -1, -1, period, cycle); - } - - /** - * Produces a sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. - * @param constantValue: vertical offset. The sine will oscillate around this value. - * @param amplitude: amplitude of the sine wave - * @param frequency: frequency of the sine - * @param phase: phase of the sine in radians. - */ - static ScalarDriver getSineDriver(T constantValue, T amplitude, T frequency, T phase) - { - return ScalarDriver( - sine, - constantValue, - amplitude, - frequency, phase); - } - - /** - * Produces a positive sine signal with some offset (constantValue), amplitude frequency and phase offset. - * @param constantValue: vertical offset. The sine will oscillate around this value. - * @param amplitude: amplitude of the sine wave - * @param frequency: frequency of the sine - * @param phase: phase of the sine in radians. - */ - static ScalarDriver getPosSineDriver(T constantValue, T amplitude, T frequency, T phase) - { - return ScalarDriver( - posine, - constantValue, - amplitude, - frequency, phase); - } - - static ScalarDriver getHalfSineDriver(T constantValue, T amplitude, T frequency, T phase) - { - return ScalarDriver( - halfsine, - constantValue, - amplitude, - frequency, phase); - } - /** - * Get a step driver. It has amplitude between timeStart and timeStop and 0 elsewhere - * @param constantValue: offset of the pulse (vertical) - * @param amplitude: amplitude that is added on top of the constantValue - * @param timeStart: start of the pulse - * @param timeStop: when the pulse ends - */ - static ScalarDriver getStepDriver(T constantValue, T amplitude, T timeStart, T timeStop) - { - if (timeStop <= timeStart) - { - throw std::runtime_error("Start time cannot be later than stop time!"); - } - return ScalarDriver( - step, - constantValue, - amplitude, - -1, -1, -1, -1, timeStart, timeStop); - } - - /** - * Get a trapezoidal driver. It has amplitude between timeStart and timeStop and 0 elsewhere - * @param constantValue: offset of the pulse (vertical) - * @param amplitude: amplitude that is added on top of the constantValue - * @param timeStart: start of the pulse - * @param edgeTime: time it takes to reach the maximum amplitude - * @param steadyTime: time it spends in a steady state - */ - static ScalarDriver getTrapezoidDriver(T constantValue, T amplitude, T timeStart, T edgeTime, T steadyTime) { - return ScalarDriver( - trapezoid, - constantValue, - amplitude, - -1, -1, -1, -1, timeStart, -1, edgeTime, steadyTime); - } - - /** - * @brief Get the Gaussian Impulse Driver object - * - * @param constantValue - * @param amplitude - * @param t0 center of the pulse - * @param sigma sigma of the gaussian - * @return ScalarDriver - */ - static ScalarDriver getGaussianImpulseDriver(T constantValue, T amplitude, T t0, T sigma) { - return ScalarDriver( - gaussimpulse, - constantValue, - amplitude, - -1, -1, -1, -1, t0, -1, sigma); - } - - /** - * @brief Get the Gaussian Impulse Driver object - * - * @param constantValue - * @param amplitude - * @param t0 center of the growth - * @param sigma sigma of the gaussian - * @return ScalarDriver - */ - static ScalarDriver getGaussianStepDriver(T constantValue, T amplitude, T t0, T sigma) { - return ScalarDriver( - gaussimpulse, - constantValue, - amplitude, - -1, -1, -1, -1, t0, -1, sigma); - } - - T getCurrentScalarValue(T& time) override - { - T returnValue = this->constantValue; - if (this->update == pulse) - { - returnValue += pulseTrain(this->amplitude, time, this->period, this->cycle); - } - else if (this->update == sine) - { - returnValue += this->amplitude * sin(2 * M_PI * time * this->frequency + this->phase); - } - else if (this->update == posine) - { - returnValue += abs(this->amplitude * sin(2 * M_PI * time * this->frequency + this->phase)); - } - else if (this->update == halfsine) - { - const T tamp = this->amplitude * sin(2 * M_PI * time * this->frequency + this->phase); - if (tamp <= 0) - { - returnValue += tamp; // ? tamp >= 0. : 0.; - } - } - else if (this->update == step) - { - returnValue += stepUpdate(this->amplitude, time, this->timeStart, this->timeStop); - } - else if (this->update == trapezoid) { - returnValue += trapezoidalUpdate(this->amplitude, time, this->timeStart, this->edgeTime, this->steadyTime); - } - else if (this->update == gaussimpulse) { - const T gaussImp = this->amplitude * exp(-pow(time - this->timeStart, 2) / (2 * pow(this->edgeTime, 2))); - returnValue += gaussImp; - } - else if (this->update == gaussstep) { - const T gaussStep = 0.5 * this->amplitude * (1 + std::erf((time - this->timeStart) / (sqrt(2) * this->edgeTime))); - returnValue += gaussStep; - } - return returnValue; - } - - // override multiplication operator - ScalarDriver operator*(const T& val) - { - return ScalarDriver( - this->update, - this->constantValue * val, - this->amplitude * val, - this->frequency, - this->phase, - this->period, - this->cycle, - this->timeStart, - this->timeStop, - this->edgeTime, - this->steadyTime); - } - - ScalarDriver operator*(const T& val) const { - return ScalarDriver( - this->update, - this->constantValue * val, - this->amplitude * val, - this->frequency, - this->phase, - this->period, - this->cycle, - this->timeStart, - this->timeStop, - this->edgeTime, - this->steadyTime); - } - - // override *= operator - ScalarDriver operator*=(const T& val) - { - this->constantValue *= val; - this->amplitude *= val; - return *this; - } - - ScalarDriver operator*=(const T& val) const { - return (*this) * val; - } - - // override addition operator - ScalarDriver operator+(const T& val) - { - return ScalarDriver( - this->update, - this->constantValue + val, - this->amplitude + val, - this->frequency, - this->phase, - this->period, - this->cycle, - this->timeStart, - this->timeStop, - this->edgeTime, - this->steadyTime); - } - - ScalarDriver operator+(const T& v) const - { - // Use non-const operator+ here - return ScalarDriver( - this->update, - this->constantValue + v, - this->amplitude + v, - this->frequency, - this->phase, - this->period, - this->cycle, - this->timeStart, - this->timeStop, - this->edgeTime, - this->steadyTime); - }; - ScalarDriver operator+=(const T& val) - { - this->constantValue += val; - this->amplitude += val; - return *this; - } - ScalarDriver operator+=(const T& v) const - { - // Use non-const operator+ here - return (*this) + v; - }; - + explicit ScalarDriver(UpdateType update = constant, T constantValue = 0, + T amplitude = 0, T frequency = -1, T phase = 0, + T period = -1, T cycle = -1, T timeStart = -1, + T timeStop = -1, T edgeTime = -1, T steadyTime = -1) + : Driver(update, constantValue, amplitude, frequency, phase, period, + cycle, timeStart, timeStop) { + this->edgeTime = edgeTime; + this->steadyTime = steadyTime; + if (update == pulse && ((period == -1) || (cycle == -1))) { + throw std::runtime_error("Selected pulse train driver type but either " + "period or cycle were not set"); + } else if (update == sine && (frequency == -1)) { + throw std::runtime_error( + "Selected sine driver type but frequency was not set"); + } + } + + /** + * Constant driver produces a constant signal of a fixed amplitude. + * @param constantValue: constant value of the driver (constant + * offset/amplitude) + */ + static ScalarDriver getConstantDriver(T constantValue) { + return ScalarDriver(constant, constantValue); + } + + /** + * Produces a square pulse of certain period and cycle + * @param constantValue: offset (vertical) of the pulse. The pulse amplitude + * will be added to this. + * @param amplitude: amplitude of the pulse signal + * @param period: period of the signal in seconds + * @param cycle: duty cycle of the signal -- a fraction between [0 and 1]. + */ + static ScalarDriver getPulseDriver(T constantValue, T amplitude, T period, + T cycle) { + return ScalarDriver(pulse, constantValue, amplitude, -1, -1, period, cycle); + } + + /** + * Produces a sinusoidal signal with some offset (constantValue), amplitude + * frequency and phase offset. + * @param constantValue: vertical offset. The sine will oscillate around this + * value. + * @param amplitude: amplitude of the sine wave + * @param frequency: frequency of the sine + * @param phase: phase of the sine in radians. + */ + static ScalarDriver getSineDriver(T constantValue, T amplitude, T frequency, + T phase) { + return ScalarDriver(sine, constantValue, amplitude, frequency, phase); + } + + /** + * Produces a positive sine signal with some offset (constantValue), amplitude + * frequency and phase offset. + * @param constantValue: vertical offset. The sine will oscillate around this + * value. + * @param amplitude: amplitude of the sine wave + * @param frequency: frequency of the sine + * @param phase: phase of the sine in radians. + */ + static ScalarDriver getPosSineDriver(T constantValue, T amplitude, + T frequency, T phase) { + return ScalarDriver(posine, constantValue, amplitude, frequency, phase); + } + + static ScalarDriver getHalfSineDriver(T constantValue, T amplitude, + T frequency, T phase) { + return ScalarDriver(halfsine, constantValue, amplitude, frequency, phase); + } + /** + * Get a step driver. It has amplitude between timeStart and timeStop and 0 + * elsewhere + * @param constantValue: offset of the pulse (vertical) + * @param amplitude: amplitude that is added on top of the constantValue + * @param timeStart: start of the pulse + * @param timeStop: when the pulse ends + */ + static ScalarDriver getStepDriver(T constantValue, T amplitude, T timeStart, + T timeStop) { + if (timeStop <= timeStart) { + throw std::runtime_error("Start time cannot be later than stop time!"); + } + return ScalarDriver(step, constantValue, amplitude, -1, -1, -1, -1, + timeStart, timeStop); + } + + /** + * Get a trapezoidal driver. It has amplitude between timeStart and timeStop + * and 0 elsewhere + * @param constantValue: offset of the pulse (vertical) + * @param amplitude: amplitude that is added on top of the constantValue + * @param timeStart: start of the pulse + * @param edgeTime: time it takes to reach the maximum amplitude + * @param steadyTime: time it spends in a steady state + */ + static ScalarDriver getTrapezoidDriver(T constantValue, T amplitude, + T timeStart, T edgeTime, + T steadyTime) { + return ScalarDriver(trapezoid, constantValue, amplitude, -1, -1, -1, -1, + timeStart, -1, edgeTime, steadyTime); + } + + /** + * @brief Get the Gaussian Impulse Driver object + * + * @param constantValue + * @param amplitude + * @param t0 center of the pulse + * @param sigma sigma of the gaussian + * @return ScalarDriver + */ + static ScalarDriver getGaussianImpulseDriver(T constantValue, T amplitude, + T t0, T sigma) { + return ScalarDriver(gaussimpulse, constantValue, amplitude, -1, -1, -1, -1, + t0, -1, sigma); + } + + /** + * @brief Get the Gaussian Impulse Driver object + * + * @param constantValue + * @param amplitude + * @param t0 center of the growth + * @param sigma sigma of the gaussian + * @return ScalarDriver + */ + static ScalarDriver getGaussianStepDriver(T constantValue, T amplitude, T t0, + T sigma) { + return ScalarDriver(gaussimpulse, constantValue, amplitude, -1, -1, -1, -1, + t0, -1, sigma); + } + + T getCurrentScalarValue(T &time) override { + T returnValue = this->constantValue; + if (this->update == pulse) { + returnValue += + pulseTrain(this->amplitude, time, this->period, this->cycle); + } else if (this->update == sine) { + returnValue += this->amplitude * + sin(2 * M_PI * time * this->frequency + this->phase); + } else if (this->update == posine) { + returnValue += abs(this->amplitude * + sin(2 * M_PI * time * this->frequency + this->phase)); + } else if (this->update == halfsine) { + const T tamp = this->amplitude * + sin(2 * M_PI * time * this->frequency + this->phase); + if (tamp <= 0) { + returnValue += tamp; // ? tamp >= 0. : 0.; + } + } else if (this->update == step) { + returnValue += + stepUpdate(this->amplitude, time, this->timeStart, this->timeStop); + } else if (this->update == trapezoid) { + returnValue += trapezoidalUpdate(this->amplitude, time, this->timeStart, + this->edgeTime, this->steadyTime); + } else if (this->update == gaussimpulse) { + const T gaussImp = this->amplitude * exp(-pow(time - this->timeStart, 2) / + (2 * pow(this->edgeTime, 2))); + returnValue += gaussImp; + } else if (this->update == gaussstep) { + const T gaussStep = + 0.5 * this->amplitude * + (1 + std::erf((time - this->timeStart) / (sqrt(2) * this->edgeTime))); + returnValue += gaussStep; + } + return returnValue; + } + + // override multiplication operator + ScalarDriver operator*(const T &val) { + return ScalarDriver(this->update, this->constantValue * val, + this->amplitude * val, this->frequency, this->phase, + this->period, this->cycle, this->timeStart, + this->timeStop, this->edgeTime, this->steadyTime); + } + + ScalarDriver operator*(const T &val) const { + return ScalarDriver(this->update, this->constantValue * val, + this->amplitude * val, this->frequency, this->phase, + this->period, this->cycle, this->timeStart, + this->timeStop, this->edgeTime, this->steadyTime); + } + + // override *= operator + ScalarDriver operator*=(const T &val) { + this->constantValue *= val; + this->amplitude *= val; + return *this; + } + + // override addition operator + ScalarDriver operator+(const T &val) { + return ScalarDriver(this->update, this->constantValue + val, + this->amplitude + val, this->frequency, this->phase, + this->period, this->cycle, this->timeStart, + this->timeStop, this->edgeTime, this->steadyTime); + } + + ScalarDriver operator+(const T &v) const { + // Use non-const operator+ here + return ScalarDriver(this->update, this->constantValue + v, + this->amplitude + v, this->frequency, this->phase, + this->period, this->cycle, this->timeStart, + this->timeStop, this->edgeTime, this->steadyTime); + }; + ScalarDriver operator+=(const T &val) { + this->constantValue += val; + this->amplitude += val; + return *this; + } }; - - -template -class NullDriver : public ScalarDriver -{ +template class NullDriver : public ScalarDriver { public: - NullDriver() = default; - T getCurrentScalarValue(T& time) override - { - return 0.0; - } + NullDriver() = default; + T getCurrentScalarValue(T &time) override { return 0.0; } }; -template -class AxialDriver : public Driver -{ +template class AxialDriver : public Driver { private: - std::vector> drivers; + std::vector> drivers; public: - static AxialDriver getVectorAxialDriver(T x, T y, T z) - { - return AxialDriver(CVector(x, y, z)); - } - - void applyMask(const std::vector& mask) - { - assert(mask.size() == 3); - for (int i = 0; i < 3; i++) - { - if (mask[i] == 0) - { - // Mask asks to nullify the driver - this->drivers[i] = NullDriver(); - } - else if (mask[i] != 1) - { - throw std::runtime_error("Invalid mask value, mask must be binary!"); - } - } - } - - void applyMask(const CVector& mask) - { - this->applyMask(std::vector{(unsigned int)(mask[0]), - (unsigned int)(mask[1]), - (unsigned int)(mask[2])}); - } - - AxialDriver() - { - this->drivers = { - NullDriver(), - NullDriver(), - NullDriver() }; - } - - AxialDriver(ScalarDriver x, - ScalarDriver y, - ScalarDriver z) - { - this->drivers = { x, y, z }; - } - - explicit AxialDriver(const CVector& xyz) : AxialDriver( - ScalarDriver::getConstantDriver(xyz.x), - ScalarDriver::getConstantDriver(xyz.y), - ScalarDriver::getConstantDriver(xyz.z)) - { - } - - explicit AxialDriver( - const T x, const T y, const T z - ) : AxialDriver( - ScalarDriver::getConstantDriver(x), - ScalarDriver::getConstantDriver(y), - ScalarDriver::getConstantDriver(z)) - { - } - - explicit AxialDriver(std::vector> axialDrivers) - { - if (axialDrivers.size() != 3) - { - throw std::runtime_error("The axial driver can only have 3 axes!"); - } - this->drivers = std::move(axialDrivers); - } - - static AxialDriver getUniAxialDriver(const ScalarDriver& in, Axis axis) - { - switch (axis) - { - case xaxis: - return AxialDriver(in, NullDriver(), NullDriver()); - case yaxis: - return AxialDriver(NullDriver(), in, NullDriver()); - case zaxis: - return AxialDriver(NullDriver(), NullDriver(), in); - case all: - return AxialDriver(in, in, in); - case none: - return AxialDriver(NullDriver(), NullDriver(), NullDriver()); - } - return AxialDriver(NullDriver(), NullDriver(), NullDriver()); - } - CVector - getCurrentAxialDrivers(T time) - { - return CVector( - this->drivers[0].getCurrentScalarValue(time), - this->drivers[1].getCurrentScalarValue(time), - this->drivers[2].getCurrentScalarValue(time)); - } - - CVector getConstantValues() - { - return CVector( - this->drivers[0].constantValue, - this->drivers[1].constantValue, - this->drivers[2].constantValue); - } - - /** - * Returns the mask for the Axial Driver. - * For instance: a vector (1213, 123, 0) returns (1, 1, 0) - * Note: This is not normalised - * @return CVector: mask for the driver - */ - CVector getUnitAxis() - { - return CVector( - this->drivers[0].constantValue != 0.0 ? this->drivers[0].constantValue / std::abs(this->drivers[0].constantValue) : 0.0, - this->drivers[1].constantValue != 0.0 ? this->drivers[1].constantValue / std::abs(this->drivers[1].constantValue) : 0.0, - this->drivers[2].constantValue != 0.0 ? this->drivers[2].constantValue / std::abs(this->drivers[2].constantValue) : 0.0); - } + static AxialDriver getVectorAxialDriver(T x, T y, T z) { + return AxialDriver(CVector(x, y, z)); + } + + void applyMask(const std::vector &mask) { + assert(mask.size() == 3); + for (int i = 0; i < 3; i++) { + if (mask[i] == 0) { + // Mask asks to nullify the driver + this->drivers[i] = NullDriver(); + } else if (mask[i] != 1) { + throw std::runtime_error("Invalid mask value, mask must be binary!"); + } + } + } + + void applyMask(const CVector &mask) { + this->applyMask(std::vector{(unsigned int)(mask[0]), + (unsigned int)(mask[1]), + (unsigned int)(mask[2])}); + } + + AxialDriver() { + this->drivers = {NullDriver(), NullDriver(), NullDriver()}; + } + + AxialDriver(ScalarDriver x, ScalarDriver y, ScalarDriver z) { + this->drivers = {x, y, z}; + } + + explicit AxialDriver(const CVector &xyz) + : AxialDriver(ScalarDriver::getConstantDriver(xyz.x), + ScalarDriver::getConstantDriver(xyz.y), + ScalarDriver::getConstantDriver(xyz.z)) {} + + explicit AxialDriver(const T x, const T y, const T z) + : AxialDriver(ScalarDriver::getConstantDriver(x), + ScalarDriver::getConstantDriver(y), + ScalarDriver::getConstantDriver(z)) {} + + explicit AxialDriver(std::vector> axialDrivers) { + if (axialDrivers.size() != 3) { + throw std::runtime_error("The axial driver can only have 3 axes!"); + } + this->drivers = std::move(axialDrivers); + } + + static AxialDriver getUniAxialDriver(const ScalarDriver &in, Axis axis) { + switch (axis) { + case xaxis: + return AxialDriver(in, NullDriver(), NullDriver()); + case yaxis: + return AxialDriver(NullDriver(), in, NullDriver()); + case zaxis: + return AxialDriver(NullDriver(), NullDriver(), in); + case all: + return AxialDriver(in, in, in); + case none: + return AxialDriver(NullDriver(), NullDriver(), NullDriver()); + } + return AxialDriver(NullDriver(), NullDriver(), NullDriver()); + } + CVector getCurrentAxialDrivers(T time) { + return CVector(this->drivers[0].getCurrentScalarValue(time), + this->drivers[1].getCurrentScalarValue(time), + this->drivers[2].getCurrentScalarValue(time)); + } + + CVector getConstantValues() { + return CVector(this->drivers[0].constantValue, + this->drivers[1].constantValue, + this->drivers[2].constantValue); + } + + /** + * Returns the mask for the Axial Driver. + * For instance: a vector (1213, 123, 0) returns (1, 1, 0) + * Note: This is not normalised + * @return CVector: mask for the driver + */ + CVector getUnitAxis() { + return CVector(this->drivers[0].constantValue != 0.0 + ? this->drivers[0].constantValue / + std::abs(this->drivers[0].constantValue) + : 0.0, + this->drivers[1].constantValue != 0.0 + ? this->drivers[1].constantValue / + std::abs(this->drivers[1].constantValue) + : 0.0, + this->drivers[2].constantValue != 0.0 + ? this->drivers[2].constantValue / + std::abs(this->drivers[2].constantValue) + : 0.0); + } }; -template -class NullAxialDriver : public AxialDriver -{ +template class NullAxialDriver : public AxialDriver { public: - NullAxialDriver() = default; + NullAxialDriver() = default; }; #endif diff --git a/core/stack.hpp b/core/stack.hpp index 92716c0..4b1e472 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -1,395 +1,358 @@ #ifndef CORE_STACK_HPP_ #define CORE_STACK_HPP_ -#include // for for_each -#include // for size_t -#include // for operator<<, ofstream, string, basi... -#include // for accumulate -#include // for runtime_error -#include // for operator+, operator==, to_string -#include // for unordered_map -#include // for vector -#include "cvector.hpp" // for CVector -#include "drivers.hpp" // for ScalarDriver, AxialDriver, NullDriver +#include "cvector.hpp" // for CVector +#include "drivers.hpp" // for ScalarDriver, AxialDriver, NullDriver #include "junction.hpp" - -template -class Stack -{ - friend class Junction; +#include // for for_each +#include // for operator<<, ofstream, string, basi... +#include // for accumulate +#include // for size_t +#include // for runtime_error +#include // for operator+, operator==, to_string +#include // for unordered_map +#include // for vector + +template class Stack { + friend class Junction; private: - ScalarDriver currentDriver; - std::unordered_map> stackLog; - bool currentDriverSet = false; + ScalarDriver currentDriver; + std::unordered_map> stackLog; + bool currentDriverSet = false; protected: - unsigned int stackSize; - std::string topId, bottomId; // Ids of the top and bottom junctions - std::vector couplingStrength = { 0 }; - bool delayed = true; - T phaseOffset = 0; - virtual T calculateStackResistance(std::vector resistances) = 0; - virtual T getPhaseOffset(const unsigned int& order) const = 0; - virtual T getEffectiveCouplingStrength(const unsigned int& order, - const CVector& m1, const CVector& m2, const CVector& p) = 0; - T computeCouplingCurrentDensity(T currentDensity, - const CVector& m1, const CVector& m2, const CVector& p) { - return currentDensity * this->getEffectiveCouplingStrength(m1, m2, p); - } + unsigned int stackSize; + std::string topId, bottomId; // Ids of the top and bottom junctions + std::vector couplingStrength = {0}; + bool delayed = true; + T phaseOffset = 0; + virtual T calculateStackResistance(std::vector resistances) = 0; + virtual T getPhaseOffset(const unsigned int &order) const = 0; + virtual T getEffectiveCouplingStrength(const unsigned int &order, + const CVector &m1, + const CVector &m2, + const CVector &p) = 0; + + T computeCouplingCurrentDensity(const unsigned int &order, T currentDensity, + const CVector &m1, const CVector &m2, + const CVector &p) { + return currentDensity * + this->getEffectiveCouplingStrength(order, m1, m2, p); + } + public: - std::vector> junctionList; - - Stack(std::vector> inputStack, - const std::string& topId, - const std::string& bottomId, const T phaseOffset = 0) : topId(topId), - bottomId(bottomId), phaseOffset(phaseOffset) - { - if (inputStack.size() < 2) - { - throw std::runtime_error("Stack must have at least 2 junctions!"); - } - this->junctionList = std::move(inputStack); - if (std::any_of(this->junctionList.begin(), - this->junctionList.end(), - [](const Junction& j) { return j.MR_mode != Junction::MRmode::CLASSIC; })) - { - throw std::runtime_error("Junction has a non-classic magnetoresitance mode!" - " Define the junction with Rp and Rap resistance values."); - } - stackSize = this->junctionList.size(); - } + std::vector> junctionList; - T getCoupling(const unsigned int& order) const { - if (this->couplingStrength.empty()) - { - throw std::runtime_error("Coupling strength is not set!"); - } - if (this->couplingStrength.size() == 1) { - return this->couplingStrength[0]; - } - return this->couplingStrength[order]; + Stack(std::vector> inputStack, const std::string &topId, + const std::string &bottomId, const T phaseOffset = 0) + : topId(topId), bottomId(bottomId), phaseOffset(phaseOffset) { + if (inputStack.size() < 2) { + throw std::runtime_error("Stack must have at least 2 junctions!"); } - - void setDelayed(bool delay) - { - if (!delay && !this->isTwoLayerMemberStack()) { - throw std::runtime_error("Non delayed coupling is only supported for 2 layer stacks!"); - } - this->delayed = delay; + this->junctionList = std::move(inputStack); + if (std::any_of(this->junctionList.begin(), this->junctionList.end(), + [](const Junction &j) { + return j.MR_mode != Junction::MRmode::CLASSIC; + })) { + throw std::runtime_error( + "Junction has a non-classic magnetoresitance mode!" + " Define the junction with Rp and Rap resistance values."); } + stackSize = this->junctionList.size(); + } - void setMagnetisation(unsigned int junctionId, const std::string& layerId, CVector mag) - { - this->junctionList[junctionId].setLayerMagnetisation(layerId, mag); + T getCoupling(const unsigned int &order) const { + if (this->couplingStrength.empty()) { + throw std::runtime_error("Coupling strength is not set!"); } - - const CVector getMagnetisation(unsigned int junctionId, const std::string& layerId) - { - return this->junctionList[junctionId].getLayerMagnetisation(layerId); + if (this->couplingStrength.size() == 1) { + return this->couplingStrength[0]; } + return this->couplingStrength[order]; + } - void setOerstedFieldDriver(const AxialDriver& oDriver) - { - for (auto& j : this->junctionList) - { - j.setLayerOerstedFieldDriver("all", oDriver); - } + void setDelayed(bool delay) { + if (!delay && !this->isTwoLayerMemberStack()) { + throw std::runtime_error( + "Non delayed coupling is only supported for 2 layer stacks!"); } - - void setExternalFieldDriver(const AxialDriver& fDriver) - { - for (auto& j : this->junctionList) - { - j.setLayerExternalFieldDriver("all", fDriver); - } + this->delayed = delay; + } + + void setMagnetisation(unsigned int junctionId, const std::string &layerId, + CVector mag) { + this->junctionList[junctionId].setLayerMagnetisation(layerId, mag); + } + + const CVector getMagnetisation(unsigned int junctionId, + const std::string &layerId) { + return this->junctionList[junctionId].getLayerMagnetisation(layerId); + } + + void setOerstedFieldDriver(const AxialDriver &oDriver) { + for (auto &j : this->junctionList) { + j.setLayerOerstedFieldDriver("all", oDriver); } + } - void resetCoupledCurrentDriver() - { - this->currentDriver = NullDriver(); - for (auto& j : this->junctionList) - { - j.setLayerCurrentDriver("all", this->currentDriver); - } - this->currentDriverSet = false; + void setExternalFieldDriver(const AxialDriver &fDriver) { + for (auto &j : this->junctionList) { + j.setLayerExternalFieldDriver("all", fDriver); } + } - void setCoupledCurrentDriver(const ScalarDriver& cDriver) - { - this->currentDriver = cDriver; - for (auto& j : this->junctionList) - { - j.setLayerCurrentDriver("all", this->currentDriver); - } - this->currentDriverSet = true; + void resetCoupledCurrentDriver() { + this->currentDriver = NullDriver(); + for (auto &j : this->junctionList) { + j.setLayerCurrentDriver("all", this->currentDriver); } + this->currentDriverSet = false; + } - void saveLogs(const std::string& fileSave) - { - if (fileSave == "") - { - // if there's an empty fn, don't save - std::cout << "Ignoring file save to an empty filename" << std::endl; - return; - } - std::ofstream logFile; - logFile.open(fileSave); - for (const auto& keyPair : this->stackLog) - { - logFile << keyPair.first << ";"; - } - logFile << "\n"; - for (unsigned int i = 0; i < this->stackLog["time"].size(); i++) - { - for (const auto& keyPair : this->stackLog) - { - logFile << keyPair.second[i] << ";"; - } - logFile << "\n"; - } - logFile.close(); + void setCoupledCurrentDriver(const ScalarDriver &cDriver) { + this->currentDriver = cDriver; + for (auto &j : this->junctionList) { + j.setLayerCurrentDriver("all", this->currentDriver); } - - void setCouplingStrength(const T& coupling) - { - this->couplingStrength = { coupling }; + this->currentDriverSet = true; + } + + void saveLogs(const std::string &fileSave) { + if (fileSave == "") { + // if there's an empty fn, don't save + std::cout << "Ignoring file save to an empty filename" << std::endl; + return; + } + std::ofstream logFile; + logFile.open(fileSave); + for (const auto &keyPair : this->stackLog) { + logFile << keyPair.first << ";"; } + logFile << "\n"; + for (unsigned int i = 0; i < this->stackLog["time"].size(); i++) { + for (const auto &keyPair : this->stackLog) { + logFile << keyPair.second[i] << ";"; + } + logFile << "\n"; + } + logFile.close(); + } - void setCouplingStrength(const std::vector& coupling) - { - if (coupling.size() != this->stackSize - 1) - { - throw std::runtime_error("Coupling strength vector must have size of stack size - 1!"); - } - this->couplingStrength = coupling; + void setCouplingStrength(const T &coupling) { + this->couplingStrength = {coupling}; + } + + void setCouplingStrength(const std::vector &coupling) { + if (coupling.size() != this->stackSize - 1) { + throw std::runtime_error( + "Coupling strength vector must have size of stack size - 1!"); } + this->couplingStrength = coupling; + } - void logStackData(T t, T resistance, std::vector timeCurrents) - { - this->stackLog["Resistance"].push_back(resistance); - for (std::size_t j = 0; j < timeCurrents.size(); ++j) - { - this->stackLog["I_" + std::to_string(j)].push_back(timeCurrents[j]); - } - this->stackLog["time"].push_back(t); + void logStackData(T t, T resistance, std::vector timeCurrents) { + this->stackLog["Resistance"].push_back(resistance); + for (std::size_t j = 0; j < timeCurrents.size(); ++j) { + this->stackLog["I_" + std::to_string(j)].push_back(timeCurrents[j]); } + this->stackLog["time"].push_back(t); + } - void clearLogs() - { - for (auto& j : this->junctionList) - { - j.clearLog(); - } - this->stackLog.clear(); + void clearLogs() { + for (auto &j : this->junctionList) { + j.clearLog(); + } + this->stackLog.clear(); + } + + std::unordered_map> &getLog() { + return this->stackLog; + } + std::unordered_map> &getLog(unsigned int id) { + if (id <= this->junctionList.size()) { + return this->junctionList[id].getLog(); + } + throw std::runtime_error("Asking for id of a non-existing junction!"); + } + + const CVector getPolarisationVector() { + CVector probe = junctionList[0].getLayer(this->topId).referenceLayer; + for (std::size_t i = 1; i < junctionList.size(); ++i) { + if (probe != junctionList[i].getLayer(this->topId).referenceLayer) + throw std::runtime_error("Polarisation vectors are not equal in stack"); } - std::unordered_map>& getLog() - { - return this->stackLog; + if (!probe.length()) { + throw std::runtime_error("Polarisation is not set!"); } - std::unordered_map>& getLog(unsigned int id) - { - if (id <= this->junctionList.size()) - { - return this->junctionList[id].getLog(); - } - throw std::runtime_error("Asking for id of a non-existing junction!"); + return probe; + } + + const bool isTwoLayerMemberStack() { + for (const auto &j : this->junctionList) { + if (j.layerNo >= 3) { + throw std::runtime_error("At least one junction has more than 2 layers!" + " It's not supported now."); + } else if (j.layerNo != 2) { + return false; + } } + return true; + } - const CVector getPolarisationVector() - { - CVector probe = junctionList[0].getLayer(this->topId).referenceLayer; - for (std::size_t i = 1; i < junctionList.size(); ++i) - { - if (probe != junctionList[i].getLayer(this->topId).referenceLayer) - throw std::runtime_error("Polarisation vectors are not equal in stack"); - } + void runSimulation(T totalTime, T timeStep = 1e-13, + T writeFrequency = 1e-11) { + const unsigned int writeEvery = (int)(writeFrequency / timeStep); + const unsigned int totalIterations = (int)(totalTime / timeStep); - if (!probe.length()) - { - throw std::runtime_error("Polarisation is not set!"); - } - return probe; + if (timeStep > writeFrequency) { + throw std::runtime_error( + "The time step cannot be larger than write frequency!"); } - const bool isTwoLayerMemberStack() { - for (const auto& j : this->junctionList) - { - if (j.layerNo >= 3) - { - throw std::runtime_error("At least one junction has more than 2 layers!" - " It's not supported now."); - } - else if (j.layerNo != 2) - { - return false; - } - } - return true; + // pick a solver based on drivers + std::vector modes; + auto localRunner = &Junction::runMultiLayerSolver; + for (auto &j : this->junctionList) { + auto [runner, solver, mode] = j.getSolver(RK4, totalIterations); + modes.push_back(mode); + localRunner = runner; + // TODO: handle the rare case when the user mixes 1 layer with 2 layer + // junction in the same stack -- i.e. runner is runSingleLayerSolver and + // runMultiLayerSolver + } + auto solver = &Layer::rk4_step; + if (!std::equal(modes.begin() + 1, modes.end(), modes.begin())) { + throw std::runtime_error( + "Junctions have different solver modes!" + " Set the same solver mode for all junctions explicitly." + " Do not mix stochastic and deterministic solvers!"); } - void runSimulation(T totalTime, T timeStep = 1e-13, T writeFrequency = 1e-11) - { - const unsigned int writeEvery = (int)(writeFrequency / timeStep); - const unsigned int totalIterations = (int)(totalTime / timeStep); - - if (timeStep > writeFrequency) - { - throw std::runtime_error("The time step cannot be larger than write frequency!"); - } - - // pick a solver based on drivers - std::vector modes; - auto localRunner = &Junction::runMultiLayerSolver; - for (auto& j : this->junctionList) - { - auto [runner, solver, mode] = j.getSolver(RK4, totalIterations); - modes.push_back(mode); - localRunner = runner; - // TODO: handle the rare case when the user mixes 1 layer with 2 layer junction - // in the same stack -- i.e. runner is runSingleLayerSolver and runMultiLayerSolver + std::vector timeResistances(junctionList.size()); + std::vector timeCurrents(junctionList.size()); + std::vector> frozenMags(junctionList.size()); + std::vector> frozenPols(junctionList.size()); + + const bool isTwoLayerStack = this->isTwoLayerMemberStack(); + for (unsigned int i = 0; i < totalIterations; i++) { + T t = i * timeStep; + + // stash the magnetisations first + for (std::size_t j = 0; j < junctionList.size(); ++j) { + frozenMags[j] = junctionList[j].getLayerMagnetisation(this->topId); + if (isTwoLayerStack) { + frozenPols[j] = junctionList[j].getLayerMagnetisation(this->bottomId); + } else { + frozenPols[j] = this->getPolarisationVector(); } - auto solver = &Layer::rk4_step; - if (!std::equal(modes.begin() + 1, modes.end(), modes.begin())) - { - throw std::runtime_error("Junctions have different solver modes!" - " Set the same solver mode for all junctions explicitly." - " Do not mix stochastic and deterministic solvers!"); + } + T effectiveCoupling = 1; + for (std::size_t j = 0; j < junctionList.size(); ++j) { + + /** + * Coupling + * Ik = Ik-1 + x* Ik-1 = (1+x)Ik-1 + * Ik+1 = Ik + x* Ik = (1+x)Ik = (1+x)(1+x)Ik-1 + * technically we could do (1+x)^n * I0 but + * we want to expand to non-symmetric coupling x1, x2, ... + */ + + // skip first junction + // modify the standing layer constant current + if (j > 0) { + if (this->delayed) { + // accumulate coupling + effectiveCoupling *= (1 + this->getEffectiveCouplingStrength( + j - 1, frozenMags[j], + frozenMags[j - 1], frozenPols[j])); + + } else { + effectiveCoupling *= + (1 + + this->getEffectiveCouplingStrength( + j - 1, junctionList[j].getLayerMagnetisation(this->topId), + junctionList[j - 1].getLayerMagnetisation(this->topId), + junctionList[j].getLayerMagnetisation(this->bottomId))); + } } - std::vector timeResistances(junctionList.size()); - std::vector timeCurrents(junctionList.size()); - std::vector> frozenMags(junctionList.size()); - std::vector> frozenPols(junctionList.size()); - - const bool isTwoLayerStack = this->isTwoLayerMemberStack(); - for (unsigned int i = 0; i < totalIterations; i++) - { - T t = i * timeStep; - - // stash the magnetisations first - for (std::size_t j = 0; j < junctionList.size(); ++j) { - frozenMags[j] = junctionList[j].getLayerMagnetisation(this->topId); - if (isTwoLayerStack) { - frozenPols[j] = junctionList[j].getLayerMagnetisation(this->bottomId); - } - else { - frozenPols[j] = this->getPolarisationVector(); - } - } - T effectiveCoupling = 1; - for (std::size_t j = 0; j < junctionList.size(); ++j) - { - - /** - * Coupling - * Ik = Ik-1 + x* Ik-1 = (1+x)Ik-1 - * Ik+1 = Ik + x* Ik = (1+x)Ik = (1+x)(1+x)Ik-1 - * technically we could do (1+x)^n * I0 but - * we want to expand to non-symmetric coupling x1, x2, ... - */ - - // skip first junction - // modify the standing layer constant current - if (j > 0) { - if (this->delayed) { - // accumulate coupling - effectiveCoupling *= (1 + this->getEffectiveCouplingStrength( - j - 1, - frozenMags[j], frozenMags[j - 1], frozenPols[j])); - - } - else { - effectiveCoupling *= (1 + this->getEffectiveCouplingStrength( - j - 1, - junctionList[j].getLayerMagnetisation(this->topId), - junctionList[j - 1].getLayerMagnetisation(this->topId), - junctionList[j].getLayerMagnetisation(this->bottomId))); - } - } - - // set the current -- same for all layers - // copy the driver and set the current value - ScalarDriver localDriver = this->currentDriver * effectiveCoupling; - localDriver.phaseShift(this->getPhaseOffset(j)); - - junctionList[j].setLayerCurrentDriver("all", localDriver); - (junctionList[j].*localRunner)(solver, t, timeStep); - // change the instant value of the current before the - // the resistance is calculated - // compute the next j+1 input to the current. - const auto resistance = junctionList[j].getMagnetoresistance(); - timeResistances[j] = resistance[0]; - timeCurrents[j] = localDriver.getCurrentScalarValue(t); - } - if (!(i % writeEvery)) - { - const T magRes = this->calculateStackResistance(timeResistances); - this->logStackData(t, magRes, timeCurrents); - for (auto& jun : this->junctionList) - jun.logLayerParams(t, timeStep, false); - } - } + // set the current -- same for all layers + // copy the driver and set the current value + ScalarDriver localDriver = this->currentDriver * effectiveCoupling; + localDriver.phaseShift(this->getPhaseOffset(j)); + + junctionList[j].setLayerCurrentDriver("all", localDriver); + (junctionList[j].*localRunner)(solver, t, timeStep); + // change the instant value of the current before the + // the resistance is calculated + // compute the next j+1 input to the current. + const auto resistance = junctionList[j].getMagnetoresistance(); + timeResistances[j] = resistance[0]; + timeCurrents[j] = localDriver.getCurrentScalarValue(t); + } + if (!(i % writeEvery)) { + const T magRes = this->calculateStackResistance(timeResistances); + this->logStackData(t, magRes, timeCurrents); + for (auto &jun : this->junctionList) + jun.logLayerParams(t, timeStep, false); + } } + } }; -template -class SeriesStack : public Stack -{ - T calculateStackResistance(std::vector resistances) override - { - const T resSum = std::accumulate(resistances.begin(), - resistances.end(), - 0.0); - return resSum; - } - - T getEffectiveCouplingStrength(const unsigned int& order, - const CVector& m1, const CVector& m2, const CVector& p) override - { - const T m1Comp = c_dot(m1, p); - const T m2Comp = c_dot(m2, p); - return this->getCoupling(order) * (m1Comp + m2Comp); - } - - T getPhaseOffset(const unsigned int& order) const override - { - return this->phaseOffset * order; - } +template class SeriesStack : public Stack { + T calculateStackResistance(std::vector resistances) override { + const T resSum = + std::accumulate(resistances.begin(), resistances.end(), 0.0); + return resSum; + } + + T getEffectiveCouplingStrength(const unsigned int &order, + const CVector &m1, const CVector &m2, + const CVector &p) override { + const T m1Comp = c_dot(m1, p); + const T m2Comp = c_dot(m2, p); + return this->getCoupling(order) * (m1Comp + m2Comp); + } + + T getPhaseOffset(const unsigned int &order) const override { + return this->phaseOffset * order; + } public: - explicit SeriesStack(const std::vector>& jL, - const std::string& topId = "free", - const std::string& bottomId = "bottom", const T phaseOffset = 0) : Stack(jL, topId, bottomId, phaseOffset) {} + explicit SeriesStack(const std::vector> &jL, + const std::string &topId = "free", + const std::string &bottomId = "bottom", + const T phaseOffset = 0) + : Stack(jL, topId, bottomId, phaseOffset) {} }; -template -class ParallelStack : public Stack -{ - T calculateStackResistance(std::vector resistances) override - { - T invSum = 0.0; - std::for_each(resistances.begin(), resistances.end(), [&](T res) - { invSum += 1.0 / res; }); - return 1. / invSum; - } - - T getEffectiveCouplingStrength(const unsigned int& order, - const CVector& m1, const CVector& m2, const CVector& p) override - { - const T m1Comp = c_dot(m1, p); - const T m2Comp = c_dot(m2, p); - return this->getCoupling(order) * (m1Comp - m2Comp); - } - - T getPhaseOffset(const unsigned int& order) const override - { - return this->phaseOffset; - } +template class ParallelStack : public Stack { + T calculateStackResistance(std::vector resistances) override { + T invSum = 0.0; + std::for_each(resistances.begin(), resistances.end(), + [&](T res) { invSum += 1.0 / res; }); + return 1. / invSum; + } + + T getEffectiveCouplingStrength(const unsigned int &order, + const CVector &m1, const CVector &m2, + const CVector &p) override { + const T m1Comp = c_dot(m1, p); + const T m2Comp = c_dot(m2, p); + return this->getCoupling(order) * (m1Comp - m2Comp); + } + + T getPhaseOffset(const unsigned int &order) const override { + return this->phaseOffset; + } public: - explicit ParallelStack(const std::vector>& jL, - const std::string& topId = "free", - const std::string& bottomId = "bottom", const T phaseOffset = 0) : Stack(jL, topId, bottomId, phaseOffset) {} + explicit ParallelStack(const std::vector> &jL, + const std::string &topId = "free", + const std::string &bottomId = "bottom", + const T phaseOffset = 0) + : Stack(jL, topId, bottomId, phaseOffset) {} }; #endif // CORE_STACK_HPP_ diff --git a/setup.py b/setup.py index d6e7a77..7652927 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import Extension, find_namespace_packages, setup from setuptools.command.build_ext import build_ext -__version__ = "1.5.4" +__version__ = "1.5.5" """ As per https://github.com/pybind/python_example diff --git a/tests/test_drivers.py b/tests/test_drivers.py index 7efa849..e6b601a 100644 --- a/tests/test_drivers.py +++ b/tests/test_drivers.py @@ -46,8 +46,6 @@ def test_aliases(): def test_driver_ops(): - import math - driver = sineDriver(10, 20, 1, 0) assert driver.getCurrentScalarValue(1 / 4) == 30 driver *= 2 diff --git a/tests/test_models.py b/tests/test_models.py index 871b58b..148a93c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -7,24 +7,24 @@ def test_sb_dynamic(two_layer_symbolic_dyn: Tuple[LayerDynamic]): J1 = -1e-3 J2 = 1e-4 - for Hmag in np.linspace(-300e3, 300e3, 1): + for Hmag in np.linspace(-300e3, 300e3, 8): H = VectorObj(np.deg2rad(88), np.deg2rad(0.1), Hmag) solver_dyn = Solver(layers=two_layer_symbolic_dyn, J1=[J1], J2=[J2], H=H) pos = np.asarray( [np.deg2rad(88), np.deg2rad(0.1), np.deg2rad(88), np.deg2rad(0.1)] ) # set perturbation to 0 to avoid numerical errors - # eq_sb, f_sb = solver_dyn.solve( - # init_position=pos, perturbation=0, max_steps=1e1, force_sb=True - # ) + eq_sb, f_sb = solver_dyn.solve( + init_position=pos, perturbation=0, max_steps=1e6, force_sb=True + ) eq_dyn, f_dyn, _ = solver_dyn.solve( init_position=pos, max_steps=1e6, perturbation=0 ) - # f_sb.sort() - # f_dyn.sort() - # assert np.allclose(eq_sb, eq_dyn) - # assert np.allclose(f_sb, f_dyn, atol=0.2) - # pos = eq_dyn + f_sb.sort() + f_dyn.sort() + assert np.allclose(eq_sb, eq_dyn) + assert np.allclose(f_sb, f_dyn, atol=0.2) + pos = eq_dyn def test_sb_classic_dipole(two_layer_symbolic_classic: Tuple[LayerSB]): @@ -35,17 +35,17 @@ def test_sb_classic_dipole(two_layer_symbolic_classic: Tuple[LayerSB]): VectorObj.from_cartesian(0, -1e-4, 0), VectorObj.from_cartesian(0, 0, 1e-6), ] - for Hmag in np.linspace(-300e3, 300e3, 1): + for Hmag in np.linspace(-300e3, 300e3, 8): H = VectorObj(np.deg2rad(88), np.deg2rad(0.1), Hmag) solver_dyn = Solver( layers=two_layer_symbolic_classic, J1=[J1], J2=[J2], H=H, Ndipole=[dipole] ) - # pos = np.asarray( - # [np.deg2rad(88), np.deg2rad(0.1), np.deg2rad(88), np.deg2rad(0.1)] - # ) - # # set perturbation to 0 to avoid numerical errors - # eq_sb, f_sb = solver_dyn.solve( - # init_position=pos, perturbation=0, max_steps=1e6, force_sb=True - # ) - # f_sb.sort() - # pos = eq_sb + pos = np.asarray( + [np.deg2rad(88), np.deg2rad(0.1), np.deg2rad(88), np.deg2rad(0.1)] + ) + # set perturbation to 0 to avoid numerical errors + eq_sb, f_sb = solver_dyn.solve( + init_position=pos, perturbation=0, max_steps=1e6, force_sb=True + ) + f_sb.sort() + pos = eq_sb From 3cfbb12f3ea0c16976e5525f22ab6776f89d3e19 Mon Sep 17 00:00:00 2001 From: Jakub Date: Sat, 15 Jun 2024 14:08:55 +0200 Subject: [PATCH 11/44] Apply suggestions from code review Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- cmtj/__init__.pyi | 4 ++-- cmtj/utils/parallel.py | 4 +++- cmtj/utils/resistance.py | 3 ++- docs/index.md | 2 +- tests/test_drivers.py | 4 ++++ 5 files changed, 12 insertions(+), 5 deletions(-) diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index 9ace886..b27bd88 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -143,7 +143,7 @@ class AxialDriver: @overload def __init__(self, xyz: CVector) -> None: """Create an axial driver with a vector. - :param xyz: CVector with components of x, y, z axis + :param xyz: CVector object with x, y, z components """ ... @@ -247,7 +247,7 @@ class Junction: """Creates a junction with a magnetoresistance. :param layers: list of layers - :param Rp: Magnetoresistance parallel state + :param Rp: Parallel magnetoresistance :param Rap: Magnetoresistance anti-parallel state """ ... diff --git a/cmtj/utils/parallel.py b/cmtj/utils/parallel.py index 9410617..f195776 100644 --- a/cmtj/utils/parallel.py +++ b/cmtj/utils/parallel.py @@ -17,7 +17,9 @@ def distribute(simulation_fn: Callable, :param simulation_fn: function to be distributed :param spaces: list of lists of parameters :param n_cores: number of cores to use. - :returns: (index, simulation_fn output) + :returns: tuple + index (int): Index of the parameters in the spaces list, multiple dims. + simulation_fn output (any): The output of the simulation function. index - index of the parameters in the spaces list, multiple dims. """ spaces = [np.asarray(space) for space in spaces] diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index 93b2547..a131ab6 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -29,7 +29,8 @@ def compute_resistance( w: List[float], ): """Computes the resistance of the system. - If you want to compute the resistance for an entire time series, pass m as a 3D array. + If you want to compute the resistance for an entire time series, pass m as a 3D array + with shape [number_of_layers, 3, T], where T is the time component. [number_of_layers, 3, T] where T is the time component. """ number_of_layers = len(Rx0) diff --git a/docs/index.md b/docs/index.md index 8555abb..375453c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -84,7 +84,7 @@ The package requires (if `utils` subpackage is used): ## Documentation and examples -Documentation: [https://lemurpwned.github.io/cmtj](https://lemurpwned.github.io/cmtj). +Documentation: [https://lemurpwned.github.io/cmtj](https://lemurpwned.github.io/cmtj) There are many examples available, check out the [examples section in the docs](https://lemurpwned.github.io/cmtj/experimental-methods/introduction/) ## Extensions diff --git a/tests/test_drivers.py b/tests/test_drivers.py index e6b601a..4a92619 100644 --- a/tests/test_drivers.py +++ b/tests/test_drivers.py @@ -46,6 +46,10 @@ def test_aliases(): def test_driver_ops(): + import math + assert d1.getCurrentAxialDrivers(-1.0) == CVector(-1.0, -2.0, -3.0) + assert d1.getCurrentAxialDrivers(0.0) == CVector(0.0, 0.0, 0.0) + assert d1.getCurrentAxialDrivers(1e6) == CVector(1e6, 2e6, 3e6) driver = sineDriver(10, 20, 1, 0) assert driver.getCurrentScalarValue(1 / 4) == 30 driver *= 2 From c3dd8691b9d0be8d3a9fc7ad3d8e83f8af06511a Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sat, 15 Jun 2024 14:14:41 +0200 Subject: [PATCH 12/44] test fix --- tests/test_drivers.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/test_drivers.py b/tests/test_drivers.py index 4a92619..30b7c8d 100644 --- a/tests/test_drivers.py +++ b/tests/test_drivers.py @@ -43,13 +43,10 @@ def test_axial_definitions(): def test_aliases(): d1 = AxialDriver(constantDriver(1.0), constantDriver(2.0), constantDriver(3.0)) assert d1.getCurrentAxialDrivers(0.0) == CVector(1.0, 2.0, 3.0) + assert d1.getCurrentAxialDrivers(1e6) == CVector(1.0, 2.0, 3.0) def test_driver_ops(): - import math - assert d1.getCurrentAxialDrivers(-1.0) == CVector(-1.0, -2.0, -3.0) - assert d1.getCurrentAxialDrivers(0.0) == CVector(0.0, 0.0, 0.0) - assert d1.getCurrentAxialDrivers(1e6) == CVector(1e6, 2e6, 3e6) driver = sineDriver(10, 20, 1, 0) assert driver.getCurrentScalarValue(1 / 4) == 30 driver *= 2 From 07b0accff9f6d46b5b055c7d8da23e2b50dfb70c Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 22 Jul 2024 15:54:17 +0200 Subject: [PATCH 13/44] documentation update with how to design new drivers --- CHANGELOG.md | 1 + README.md | 10 +- cmtj/__init__.pyi | 14 +- core/llgb.hpp | 1102 ++++++++++++++++++++-------------------- docs/api/drivers.md | 78 +++ docs/index.md | 12 +- docs/tipsandtricks.md | 1 + mkdocs.yml | 1 + python/cmtj.cpp | 804 ++++++++++++++--------------- scripts/readme_copy.py | 4 +- 10 files changed, 1025 insertions(+), 1002 deletions(-) create mode 100644 docs/api/drivers.md diff --git a/CHANGELOG.md b/CHANGELOG.md index a25a12f..7ef75cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - Extended the `Stack` models allowing for non-symmetric coupling between devices. - `Stack` current drivers can now be of any type are adequately scaled. +- Custom definition of the `ScalarDriver` is now possible and documented. # 1.5.0-1.5.4 diff --git a/README.md b/README.md index 8555abb..c87648d 100644 --- a/README.md +++ b/README.md @@ -144,13 +144,16 @@ pre-commit run -a (or --files core/* cmtj/*) ## Documentation builds -There are couple of stages to building the documentation +**Note** +For stub generation add `__init__.py` to the `cmtj` directory. + +There are a couple of stages to building the documentation 1. Build Doxygen documentation ``` doxygen Doxyfile ``` - This is mostly for the C++ documentation. Furture changes may couple C++ and Python docs. + This is mostly for the C++ documentation. Future changes may couple C++ and Python docs. 2. Build stubs The stubgen is `pybind11-stubgen` or `mypy stubgen` with the latter being preferred now. Before running the stubgen, make sure to install the package with: @@ -168,8 +171,7 @@ There are couple of stages to building the documentation ``` More info here: https://mypy.readthedocs.io/en/stable/stubgen.html. 3. Parse stubs to Markdown. - This stage is done by running: - `python3 docs/docgen.py ` + This stage is done by running: `python3 docs/docgen.py ` The deployment of the documentation is done via: ```bash mkdocs gh-deploy diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index b27bd88..99b316f 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -34,10 +34,11 @@ def gaussianImpulseDriver( constantValue: float, amplitude: float, t0: float, sigma: float ) -> ScalarDriver: """ - Gaussian impulse driver. It has amplitude starts at t0 and falls off with sigma. + Gaussian impulse driver. It starts with an max amplitude at t0 and falls off with sigma. Formula: - A * exp(-((t - t0) ** 2) / (2 * sigma ** 2)) + + $A * \exp(-(t - t_0)^2 / (2\sigma^2))$ :param constantValue: offset of the pulse (vertical) :param amplitude: amplitude that is added on top of the constantValue @@ -49,10 +50,11 @@ def gaussianImpulseDriver( def gaussianStepDriver( constantValue: float, amplitude: float, t0: float, sigma: float ) -> ScalarDriver: - """Gaussian step driver (erf function). It has amplitude starts at t0 and falls off with sigma. + """Gaussian step driver (erf function). It starts at t0 and falls off with sigma. Formula: - f(t) = constantValue + amplitude * (1 + erf((t - t0) / (sigma * sqrt(2)))) + + $f(t) = c + A + A\mathrm{erf}((t - t_0) / (\sigma \sqrt(2)))$ :param constantValue: offset of the pulse (vertical) :param amplitude: amplitude that is added on top of the constantValue @@ -389,9 +391,7 @@ class Junction: """ ... - def setLayerFieldLikeTorqueDriver( - self, layerId: str, driver: ScalarDriver - ) -> None: + def setLayerFieldLikeTorqueDriver(self, layerId: str, driver: ScalarDriver) -> None: """Set the field like torque driver for a layer. :param layerId: the layer id :param driver: the driver diff --git a/core/llgb.hpp b/core/llgb.hpp index 99af891..2ecd215 100644 --- a/core/llgb.hpp +++ b/core/llgb.hpp @@ -4,568 +4,562 @@ namespace LLGB { - template - T langevin(T x) { - return (1.0 / tanh(x)) - (1.0 / x); - } - - template - T langevinDerivative(T x) { - return ( - -1.0 / pow(sinh(x), 2) - ) + (1. / pow(x, 2)); - } - - template - std::tuple MFAWeissCurie(T est, T temp, T J0, T relax = 0.2, - T tol = 1e-6, unsigned int maxIter = 1000) { - /** - This function solves the self-consistent Curie-Weiss equation in MFA - The equation is given by: - x = L(beta * J0 * x) - where beta = 1/(k * T) and J0 is the exchange constant. - The function returns the solution and the error. - E.g. for FePt ~ 3.051823739e-20 J => Tc ~ 760 K - - @param est: initial guess - @param temp: temperature - @param J0: exchange constant - @param relax: relaxation factor - @param tol: tolerance - @param maxIter: maximum number of iterations - **/ - T beta = (1.0 / (BOLTZMANN_CONST * temp)); - T err = 0; - for (unsigned int i = 0; i < maxIter; i++) { - T xNext = langevin(beta * J0 * est); - err = abs(xNext - est); - if (err < tol) { - return std::make_tuple(xNext, err); - } - est = relax * xNext + (1 - relax) * est; - } - return std::make_tuple(est, err); - } +template T langevin(T x) { + return (1.0 / tanh(x)) - (1.0 / x); } +template T langevinDerivative(T x) { + return (-1.0 / pow(sinh(x), 2)) + (1. / pow(x, 2)); +} template -class LLGBLayer -{ -protected: - ScalarDriver temperatureDriver; - ScalarDriver anisotropyDriver; - AxialDriver externalFieldDriver; - // the distribution is binded (bound?) for faster generation - // we need two distributions for the two types of noise in the LLB - std::function distributionA = std::bind(std::normal_distribution(0, 1), - std::mt19937(std::random_device{}())); - std::function distributionB = std::bind(std::normal_distribution(0, 1), - std::mt19937(std::random_device{}())); -public: - std::string id; - CVector mag; - CVector anis; - T Ms; - T thickness, surface, volume; - std::vector> demagTensor; - T damping; - T Tc; - T susceptibility; - T me; - T alpha_perp_log, alpha_par_log; - T K_log = 0; - T T_log = 0; - - LLGBLayer( - const std::string& id, - const CVector& mag, - const CVector& anis, - T Ms, - T thickness, - T surface, - const std::vector>& demagTensor, - T damping, - T Tc, - T susceptibility, - T me) : - id(id), mag(mag), anis(anis), - Ms(Ms), - thickness(thickness), - surface(surface), demagTensor(demagTensor), damping(damping), - Tc(Tc), - susceptibility(susceptibility), - me(me) - { - this->volume = this->surface * this->thickness; - if (this->volume == 0) - { - throw std::runtime_error("The volume of the LLGB layer cannot be 0!"); - } - if (mag.length() == 0) - { - throw std::runtime_error("Initial magnetisation was set to a zero vector!"); - } - if (anis.length() == 0) - { - throw std::runtime_error("Anisotropy was set to a zero vector!"); - } - } - - T getAlphaParallel(T& time) { - const T temp = this->temperatureDriver.getCurrentScalarValue(time); - this->alpha_par_log = this->damping * (temp / this->Tc) * (2. / 3.); - return this->alpha_par_log; - } - - T getAlphaPerpendicular(T& time) { - const T temp = this->temperatureDriver.getCurrentScalarValue(time); - const T ratio = temp / this->Tc; - if (temp >= this->Tc) { - this->alpha_perp_log = this->damping * ratio * (2. / 3.); - } - else { - this->alpha_perp_log = this->damping * (1. - ratio * (1. / 3.0)); - } - return this->alpha_perp_log; - } - - CVector getLongitudinal(T& time, const CVector& m) { - const T temp = this->temperatureDriver.getCurrentScalarValue(time); - const T ratio_susc = 1. / (2. * this->susceptibility); - const T m2 = pow(m.length(), 2); - if (temp <= this->Tc) { - const T ratio_m = m2 / pow(this->me, 2); - return ratio_susc * (1. - ratio_m) * m; - } - const T ratio_T = (this->Tc / (this->Tc - temp)); - const T ratio_T_adj = (3. / 5.) * ratio_T * m2 - 1.; - // this is given by some other paper - const T ration_T_alt = (1. + (3. / 5.) * (this->Tc / (temp - this->Tc)) * m2); - return -(1. / this->susceptibility) * ration_T_alt * m; - // return ratio_susc * ratio_T_adj * m; - } - - CVector getAnisotropyField(T& time, const CVector& m) { - return (-1. / this->anisotropyDriver.getCurrentScalarValue(time)) * CVector(m[0], m[1], 0); - } - - CVector calculateAnisotropy(const CVector& stepMag, T& time) - { - this->K_log = this->anisotropyDriver.getCurrentScalarValue(time); - const T nom = this->K_log * c_dot(this->anis, stepMag); - return this->anis * nom; - } - - const CVector calculateHeff(T time, const CVector& m) { - // this anisotropy is a bit different than in the LLG - // const CVector anisotropy = this->getAnisotropyField(time, m); - const CVector anisotropy = this->calculateAnisotropy(m, time); - const CVector hext = this->externalFieldDriver.getCurrentAxialDrivers(time); - const CVector longField = this->getLongitudinal(time, m); - return anisotropy + hext + longField; - } - - CVector calculateLLG(const T& time, const T& timeStep, const CVector& m) { - const CVector heff = this->calculateHeff(time, m); - return solveLLG(time, timeStep, m, heff); - } - - const CVector solveLLG(T time, T timeStep, const CVector& m, const CVector& heff) { - T_log = this->temperatureDriver.getCurrentScalarValue(time); - const CVector mxh = c_cross(m, heff); - const CVector mxmxh = c_cross(m, mxh); - const CVector llbTerm = c_dot(m, heff) * m; - const T inv_mlen = pow(1. / m.length(), 2); - const T gamma_p = GYRO / (1 + pow(this->damping, 2)); // LLGS -> LL form - const CVector dmdt = -1 * mxh - getAlphaPerpendicular(time) * mxmxh * inv_mlen - + llbTerm * getAlphaParallel(time) * inv_mlen; - return gamma_p * dmdt; - } - - CVector nonadiabaticThermalField(T time, T timestep) { - const T temp = this->temperatureDriver.getCurrentScalarValue(time); - const T varianceDev = (2 * BOLTZMANN_CONST * temp * (this->getAlphaPerpendicular(time) - - this->getAlphaParallel(time))) / (this->volume * this->Ms * GYRO * pow(this->getAlphaPerpendicular(time), 2)); - return 0 * sqrt(varianceDev) * CVector(this->distributionA); - } - - CVector adiabaticThermalField(T time, T timestep) { - const T temp = this->temperatureDriver.getCurrentScalarValue(time); - // GYRO multiplies in the stochasticTorque for consistency - const T varianceDev = (2 * BOLTZMANN_CONST * temp * this->getAlphaParallel(time)) / (GYRO * this->volume * this->Ms); - return 0 * sqrt(varianceDev) * CVector(this->distributionB); - } - - CVector stochasticTorque(const CVector& m, T time, const CVector& nonAdiabatic, - const CVector& adiabatic) { - /* - This formulation follows: - Axitia, 2015, Fundamentals and applications of the Landau–Lifshitz–Bloch equation - Evans, 2012, Stochastic form of the Landau-Lifshitz-Bloch equation - Read Evans to understand the switch. - - This is more correct than stochasticTorqueOld, and used more recently - */ - const T inv_mlen = pow(1. / m.length(), 2); - const T gamma_p = GYRO / (1 + pow(this->damping, 2)); // LLGS -> LL form - const CVector nonAdiabaticTerm = c_cross(m, c_cross(m, nonAdiabatic)); - return -gamma_p * inv_mlen * getAlphaPerpendicular(time) * nonAdiabaticTerm + gamma_p * adiabatic; - } - - CVector stochasticTorqueOld(const CVector& m, T time, const CVector& nonAdiabatic, - const CVector& adiabatic) { - /* - This formulation follows: - Atxitia, 2007, Micromagnetic modeling of laser-induced magnetization dynamics using the Landau-Lifshitz-Bloch equation - And classical: - Garanin, 2004, Thermal fluctuations and longitudinal relaxation of single-domain magnetic particles at elevated temperatures - */ - const T inv_mlen = pow(1. / m.length(), 2); - const T gamma_p = GYRO / (1 + pow(this->damping, 2)); // LLGS -> LL form - const CVector nonAdiabaticTerm = c_cross(m, c_cross(m, nonAdiabatic)); - const CVector adiabaticTerm = c_dot(m, adiabatic) * m; - return gamma_p * inv_mlen * ( - adiabaticTerm * getAlphaParallel(time) - nonAdiabaticTerm * getAlphaPerpendicular(time)); - - - } - - // setters - - void setEquilibriumMagnetisation(const T& me) - { - this->me = me; - } - - void setSusceptibility(const T& susceptibility) - { - this->susceptibility = susceptibility; - } - - void setTemperatureDriver(const ScalarDriver& driver) - { - this->temperatureDriver = driver; - } +std::tuple MFAWeissCurie(T est, T temp, T J0, T relax = 0.2, T tol = 1e-6, + unsigned int maxIter = 1000) { + /** + This function solves the self-consistent Curie-Weiss equation in MFA + The equation is given by: + x = L(beta * J0 * x) + where beta = 1/(k * T) and J0 is the exchange constant. + The function returns the solution and the error. + E.g. for FePt ~ 3.051823739e-20 J => Tc ~ 760 K + + @param est: initial guess + @param temp: temperature + @param J0: exchange constant + @param relax: relaxation factor + @param tol: tolerance + @param maxIter: maximum number of iterations + **/ + T beta = (1.0 / (BOLTZMANN_CONST * temp)); + T err = 0; + for (unsigned int i = 0; i < maxIter; i++) { + T xNext = langevin(beta * J0 * est); + err = abs(xNext - est); + if (err < tol) { + return std::make_tuple(xNext, err); + } + est = relax * xNext + (1 - relax) * est; + } + return std::make_tuple(est, err); +} +} // namespace LLGB - void setExternalFieldDriver(const AxialDriver& driver) - { - this->externalFieldDriver = driver; - } +template class LLGBLayer { +protected: + ScalarDriver temperatureDriver; + ScalarDriver anisotropyDriver; + AxialDriver externalFieldDriver; + // the distribution is binded (bound?) for faster generation + // we need two distributions for the two types of noise in the LLB + std::function distributionA = std::bind( + std::normal_distribution(0, 1), std::mt19937(std::random_device{}())); + std::function distributionB = std::bind( + std::normal_distribution(0, 1), std::mt19937(std::random_device{}())); - void setAnisotropyDriver(const ScalarDriver& driver) - { - this->anisotropyDriver = driver; - } +public: + std::string id; + CVector mag; + CVector anis; + T Ms; + T thickness, surface, volume; + std::vector> demagTensor; + T damping; + T Tc; + T susceptibility; + T me; + T alpha_perp_log, alpha_par_log; + T K_log = 0; + T T_log = 0; + + /// @brief + /// @param id + /// @param mag + /// @param anis + /// @param Ms + /// @param thickness + /// @param surface + /// @param demagTensor + /// @param damping + /// @param Tc + /// @param susceptibility + /// @param me + LLGBLayer(const std::string &id, const CVector &mag, + const CVector &anis, T Ms, T thickness, T surface, + const std::vector> &demagTensor, T damping, T Tc, + T susceptibility, T me) + : id(id), mag(mag), anis(anis), Ms(Ms), thickness(thickness), + surface(surface), demagTensor(demagTensor), damping(damping), Tc(Tc), + susceptibility(susceptibility), me(me) { + this->volume = this->surface * this->thickness; + if (this->volume == 0) { + throw std::runtime_error("The volume of the LLGB layer cannot be 0!"); + } + if (mag.length() == 0) { + throw std::runtime_error( + "Initial magnetisation was set to a zero vector!"); + } + if (anis.length() == 0) { + throw std::runtime_error("Anisotropy was set to a zero vector!"); + } + } + + T getAlphaParallel(T &time) { + const T temp = this->temperatureDriver.getCurrentScalarValue(time); + this->alpha_par_log = this->damping * (temp / this->Tc) * (2. / 3.); + return this->alpha_par_log; + } + + T getAlphaPerpendicular(T &time) { + const T temp = this->temperatureDriver.getCurrentScalarValue(time); + const T ratio = temp / this->Tc; + if (temp >= this->Tc) { + this->alpha_perp_log = this->damping * ratio * (2. / 3.); + } else { + this->alpha_perp_log = this->damping * (1. - ratio * (1. / 3.0)); + } + return this->alpha_perp_log; + } + + CVector getLongitudinal(T &time, const CVector &m) { + const T temp = this->temperatureDriver.getCurrentScalarValue(time); + const T ratio_susc = 1. / (2. * this->susceptibility); + const T m2 = pow(m.length(), 2); + if (temp <= this->Tc) { + const T ratio_m = m2 / pow(this->me, 2); + return ratio_susc * (1. - ratio_m) * m; + } + const T ratio_T = (this->Tc / (this->Tc - temp)); + const T ratio_T_adj = (3. / 5.) * ratio_T * m2 - 1.; + // this is given by some other paper + const T ration_T_alt = + (1. + (3. / 5.) * (this->Tc / (temp - this->Tc)) * m2); + return -(1. / this->susceptibility) * ration_T_alt * m; + // return ratio_susc * ratio_T_adj * m; + } + + CVector getAnisotropyField(T &time, const CVector &m) { + return (-1. / this->anisotropyDriver.getCurrentScalarValue(time)) * + CVector(m[0], m[1], 0); + } + + CVector calculateAnisotropy(const CVector &stepMag, T &time) { + this->K_log = this->anisotropyDriver.getCurrentScalarValue(time); + const T nom = this->K_log * c_dot(this->anis, stepMag); + return this->anis * nom; + } + + const CVector calculateHeff(T time, const CVector &m) { + // this anisotropy is a bit different than in the LLG + // const CVector anisotropy = this->getAnisotropyField(time, m); + const CVector anisotropy = this->calculateAnisotropy(m, time); + const CVector hext = + this->externalFieldDriver.getCurrentAxialDrivers(time); + const CVector longField = this->getLongitudinal(time, m); + return anisotropy + hext + longField; + } + + CVector calculateLLG(const T &time, const T &timeStep, + const CVector &m) { + const CVector heff = this->calculateHeff(time, m); + return solveLLG(time, timeStep, m, heff); + } + + const CVector solveLLG(T time, T timeStep, const CVector &m, + const CVector &heff) { + T_log = this->temperatureDriver.getCurrentScalarValue(time); + const CVector mxh = c_cross(m, heff); + const CVector mxmxh = c_cross(m, mxh); + const CVector llbTerm = c_dot(m, heff) * m; + const T inv_mlen = pow(1. / m.length(), 2); + const T gamma_p = GYRO / (1 + pow(this->damping, 2)); // LLGS -> LL form + const CVector dmdt = -1 * mxh - + getAlphaPerpendicular(time) * mxmxh * inv_mlen + + llbTerm * getAlphaParallel(time) * inv_mlen; + return gamma_p * dmdt; + } + + CVector nonadiabaticThermalField(T time, T timestep) { + const T temp = this->temperatureDriver.getCurrentScalarValue(time); + const T varianceDev = + (2 * BOLTZMANN_CONST * temp * + (this->getAlphaPerpendicular(time) - this->getAlphaParallel(time))) / + (this->volume * this->Ms * GYRO * + pow(this->getAlphaPerpendicular(time), 2)); + return 0 * sqrt(varianceDev) * CVector(this->distributionA); + } + + CVector adiabaticThermalField(T time, T timestep) { + const T temp = this->temperatureDriver.getCurrentScalarValue(time); + // GYRO multiplies in the stochasticTorque for consistency + const T varianceDev = + (2 * BOLTZMANN_CONST * temp * this->getAlphaParallel(time)) / + (GYRO * this->volume * this->Ms); + return 0 * sqrt(varianceDev) * CVector(this->distributionB); + } + + CVector stochasticTorque(const CVector &m, T time, + const CVector &nonAdiabatic, + const CVector &adiabatic) { + /* + This formulation follows: + Axitia, 2015, Fundamentals and applications of the Landau–Lifshitz–Bloch + equation Evans, 2012, Stochastic form of the Landau-Lifshitz-Bloch + equation Read Evans to understand the switch. + + This is more correct than stochasticTorqueOld, and used more recently + */ + const T inv_mlen = pow(1. / m.length(), 2); + const T gamma_p = GYRO / (1 + pow(this->damping, 2)); // LLGS -> LL form + const CVector nonAdiabaticTerm = + c_cross(m, c_cross(m, nonAdiabatic)); + return -gamma_p * inv_mlen * getAlphaPerpendicular(time) * + nonAdiabaticTerm + + gamma_p * adiabatic; + } + + CVector stochasticTorqueOld(const CVector &m, T time, + const CVector &nonAdiabatic, + const CVector &adiabatic) { + /* + This formulation follows: + Atxitia, 2007, Micromagnetic modeling of laser-induced magnetization + dynamics using the Landau-Lifshitz-Bloch equation And classical: Garanin, + 2004, Thermal fluctuations and longitudinal relaxation of single-domain + magnetic particles at elevated temperatures + */ + const T inv_mlen = pow(1. / m.length(), 2); + const T gamma_p = GYRO / (1 + pow(this->damping, 2)); // LLGS -> LL form + const CVector nonAdiabaticTerm = + c_cross(m, c_cross(m, nonAdiabatic)); + const CVector adiabaticTerm = c_dot(m, adiabatic) * m; + return gamma_p * inv_mlen * + (adiabaticTerm * getAlphaParallel(time) - + nonAdiabaticTerm * getAlphaPerpendicular(time)); + } + + // setters + + void setEquilibriumMagnetisation(const T &me) { this->me = me; } + + void setSusceptibility(const T &susceptibility) { + this->susceptibility = susceptibility; + } + + void setTemperatureDriver(const ScalarDriver &driver) { + this->temperatureDriver = driver; + } + + void setExternalFieldDriver(const AxialDriver &driver) { + this->externalFieldDriver = driver; + } + + void setAnisotropyDriver(const ScalarDriver &driver) { + this->anisotropyDriver = driver; + } }; -template -class LLGBJunction -{ +template class LLGBJunction { private: - // friend class LLGBLayer; - const std::vector vectorNames = { "x", "y", "z" }; - std::vector> layers; - std::unordered_map> log; - unsigned int logLength = 0; - unsigned int layerNo = 0; - T time = 0; -public: - explicit LLGBJunction(const std::vector>& layers) { - this->layers = layers; - this->layerNo = layers.size(); - } - - typedef void (LLGBLayer::* scalarDriverSetter)(const ScalarDriver& driver); - typedef void (LLGBLayer::* axialDriverSetter)(const AxialDriver& driver); - void scalarlayerSetter(const std::string& layerID, scalarDriverSetter functor, ScalarDriver driver) - { - bool found = false; - for (auto& l : this->layers) - { - if (l.id == layerID || layerID == "all") - { - (l.*functor)(driver); - found = true; - } - } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); - } - } - void axiallayerSetter(const std::string& layerID, axialDriverSetter functor, AxialDriver driver) - { - bool found = false; - for (auto& l : this->layers) - { - if (l.id == layerID || layerID == "all") - { - (l.*functor)(driver); - found = true; - } - } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); - } - } - void setLayerTemperatureDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &LLGBLayer::setTemperatureDriver, driver); - } - void setLayerExternalFieldDriver(const std::string& layerID, const AxialDriver& driver) - { - axiallayerSetter(layerID, &LLGBLayer::setExternalFieldDriver, driver); - } - void setLayerAnisotropyDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &LLGBLayer::setAnisotropyDriver, driver); - } - void setLayerEquilibriumMagnetisation(const std::string& layerID, const T& me) - { - bool found = false; - for (auto& l : this->layers) - { - if (l.id == layerID || layerID == "all") - { - l.setEquilibriumMagnetisation(me); - found = true; - } - } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); - } - } + // friend class LLGBLayer; + const std::vector vectorNames = {"x", "y", "z"}; + std::vector> layers; + std::unordered_map> log; + unsigned int logLength = 0; + unsigned int layerNo = 0; + T time = 0; - void setLayerSusceptibility(const std::string& layerID, const T& susceptibility) - { - bool found = false; - for (auto& l : this->layers) - { - if (l.id == layerID || layerID == "all") - { - l.setSusceptibility(susceptibility); - found = true; - } - } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); - } - } - - void heunSolverStep(const T& t, const T& timeStep) { - /* - Heun method - y'(t+1) = y(t) + dy(y, t) - y(t+1) = y(t) + 0.5 * (dy(y, t) + dy(y'(t+1), t+1)) - */ - /* - Stochastic Heun method - y_np = y + g(y,t,dW)*dt - g_sp = g(y_np,t+1,dW) - y' = y_n + f_n * dt + g_n * dt - f' = f(y, ) - y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) - */ - std::vector> fn(this->layerNo, CVector()); - std::vector> gn(this->layerNo, CVector()); - std::vector> nonAdiabatic(this->layerNo, CVector()); - std::vector> adiabatic(this->layerNo, CVector()); - std::vector> mNext(this->layerNo, CVector()); - // first approximation - - // make sure that - // 1. Thermal field is added if needed - // 2. One/f noise is added if needed - // 3. The timestep is correctly multiplied - - for (unsigned int i = 0; i < this->layerNo; i++) - { - fn[i] = this->layers[i].calculateLLG( - t, timeStep, this->layers[i].mag); - - // draw the noise for each layer, dW - nonAdiabatic[i] = this->layers[i].nonadiabaticThermalField(t, timeStep); - adiabatic[i] = this->layers[i].adiabaticThermalField(t, timeStep); - gn[i] = this->layers[i].stochasticTorque(this->layers[i].mag, t, nonAdiabatic[i], adiabatic[i]); - - mNext[i] = this->layers[i].mag + fn[i] * timeStep + gn[i] * sqrt(timeStep); - } - // second approximation - for (unsigned int i = 0; i < this->layerNo; i++) - { - // first approximation is already multiplied by timeStep - this->layers[i].mag = this->layers[i].mag + 0.5 * timeStep * ( - fn[i] + this->layers[i].calculateLLG( - t + timeStep, timeStep, mNext[i]) - ) + 0.5 * (gn[i] + this->layers[i].stochasticTorque(mNext[i], t + timeStep, - nonAdiabatic[i], adiabatic[i])) * sqrt(timeStep); - // normalise only in classical - // this->layers[i].mag.normalize(); // LLB doesn't normalise - } - } - void eulerHeunSolverStep(const T& t, const T& timeStep) { - /* - Euler Heun method (stochastic heun) - - y_np = y + g(y,t,dW)*dt - g_sp = g(y_np,t+1,dW) - y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) - - with f being the non-stochastic part and g the stochastic part - */ - // draw the noise for each layer, dW - std::vector> mPrime(this->layerNo, CVector()); - for (unsigned int i = 0; i < this->layerNo; i++) { - // todo: after you're done, double check the thermal magnitude and dt scaling there - const CVector nonAdiabaticTorque = this->layers[i].nonadiabaticThermalField(t, timeStep); - const CVector adiabaticTorque = this->layers[i].adiabaticThermalField(t, timeStep); - - const CVector fnApprox = this->layers[i].calculateLLG( - t, timeStep, this->layers[i].mag); - const CVector gnApprox = this->layers[i].stochasticTorque(this->layers[i].mag, t, - nonAdiabaticTorque, adiabaticTorque); - - // theoretically we have 2 options - // 1. calculate only the stochastic part with the second approximation - // 2. calculate the second approximation of m with the stochastic and non-stochastic - // part and then use if for torque est. - const CVector mNext = this->layers[i].mag + gnApprox * sqrt(timeStep); - const CVector gnPrimeApprox = this->layers[i].stochasticTorque(mNext, t + timeStep, - nonAdiabaticTorque, adiabaticTorque); - mPrime[i] = this->layers[i].mag + fnApprox * timeStep + 0.5 * (gnApprox + gnPrimeApprox) * sqrt(timeStep); - } - - for (unsigned int i = 0; i < this->layerNo; i++) { - this->layers[i].mag = mPrime[i]; - // this->layers[i].mag.normalize(); // LLB doesn't normalise - } - } - - typedef void (LLGBJunction::* runnerFn)(const T& t, const T& timeStep); - std::tuple getSolver(SolverMode mode) { - auto runner = &LLGBJunction::heunSolverStep; - if (mode == HEUN) - runner = &LLGBJunction::heunSolverStep; - else if (mode == EULER_HEUN) - runner = &LLGBJunction::eulerHeunSolverStep; - else - throw std::runtime_error("The solver mode is not supported!"); - return std::make_tuple(runner, mode); - } - - /** - * @brief Log computed layer parameters. - * This function logs all the necessayr parameters of the layers. - * @param t: current time - * @param timeStep: timeStep of the simulation (unsued for now) - * @param calculateEnergies: if true, also include fields for energy computation. - */ - void logLayerParams(T t, const T timeStep) - { - for (const auto& layer : this->layers) - { - const std::string lId = layer.id; - // always save magnetisation - for (int i = 0; i < 3; i++) - { - this->log[lId + "_m" + vectorNames[i]].emplace_back(layer.mag[i]); - } - this->log[lId + "_alpha_parallel"].emplace_back(layer.alpha_par_log); - this->log[lId + "_alpha_perpendicular"].emplace_back(layer.alpha_perp_log); - this->log[lId + "_K"].emplace_back(layer.K_log); - this->log[lId + "_T"].emplace_back(layer.T_log); - this->log[lId + "_me"].emplace_back(layer.me); - this->log[lId + "_Xpar"].emplace_back(layer.susceptibility); - } - this->log["time"].emplace_back(t); - this->logLength++; - } - - - void - saveLogs(const std::string& filename) - { - if (filename == "") - { - // if there's an empty fn, don't save - throw std::runtime_error("The filename may not be empty!"); - } - std::ofstream logFile; - logFile.open(filename); - for (const auto& keyPair : this->log) - { - logFile << keyPair.first << ";"; - } - logFile << "\n"; - for (unsigned int i = 0; i < logLength; i++) - { - for (const auto& keyPair : this->log) - { - logFile << keyPair.second[i] << ";"; - } - logFile << "\n"; - } - logFile.close(); - } - - /** - * Clears the simulation log. - **/ - void clearLog() - { - this->log.clear(); - this->logLength = 0; - this->time = 0; - } - - std::unordered_map>& getLog() - { - return this->log; - } - - /** - * Main run simulation function. Use it to run the simulation. - * @param totalTime: total time of a simulation, give it in seconds. Typical length is in ~couple ns. - * @param timeStep: the integration step of the RK45 method. Default is 1e-13 - * @param writeFrequency: how often is the log saved to? Must be no smaller than `timeStep`. Default is 1e-11. - * @param log: if you want some verbosity like timing the simulation. Default is false - * @param mode: Solver mode EULER_HEUN, RK4 or DORMAND_PRICE - */ - void runSimulation(T totalTime, T timeStep = 1e-13, T writeFrequency = 1e-13, - bool log = false, - SolverMode mode = HEUN) - - { - if (timeStep > writeFrequency) - { - throw std::runtime_error("The time step cannot be larger than write frequency!"); - } - const unsigned int totalIterations = (int)(totalTime / timeStep); - const unsigned int writeEvery = (int)(writeFrequency / timeStep); - std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); - // pick a solver based on drivers - auto [runner, _] = getSolver(mode); - - for (unsigned int i = 0; i < totalIterations; i++) - { - this->time += timeStep; - (*this.*runner)(this->time, timeStep); - - if (!(i % writeEvery)) - { - logLayerParams(this->time, timeStep); - } - } - std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); - if (log) - { - std::cout << "Steps in simulation: " << totalIterations << std::endl; - std::cout << "Write every: " << writeEvery << std::endl; - std::cout << "Simulation time = " << std::chrono::duration_cast(end - begin).count() << "[s]" << std::endl; - } - } +public: + explicit LLGBJunction(const std::vector> &layers) { + this->layers = layers; + this->layerNo = layers.size(); + } + + typedef void (LLGBLayer::*scalarDriverSetter)( + const ScalarDriver &driver); + typedef void (LLGBLayer::*axialDriverSetter)(const AxialDriver &driver); + void scalarlayerSetter(const std::string &layerID, scalarDriverSetter functor, + ScalarDriver driver) { + bool found = false; + for (auto &l : this->layers) { + if (l.id == layerID || layerID == "all") { + (l.*functor)(driver); + found = true; + } + } + if (!found) { + throw std::runtime_error( + "Failed to find a layer with a given id: " + layerID + "!"); + } + } + void axiallayerSetter(const std::string &layerID, axialDriverSetter functor, + AxialDriver driver) { + bool found = false; + for (auto &l : this->layers) { + if (l.id == layerID || layerID == "all") { + (l.*functor)(driver); + found = true; + } + } + if (!found) { + throw std::runtime_error( + "Failed to find a layer with a given id: " + layerID + "!"); + } + } + void setLayerTemperatureDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &LLGBLayer::setTemperatureDriver, driver); + } + void setLayerExternalFieldDriver(const std::string &layerID, + const AxialDriver &driver) { + axiallayerSetter(layerID, &LLGBLayer::setExternalFieldDriver, driver); + } + void setLayerAnisotropyDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &LLGBLayer::setAnisotropyDriver, driver); + } + void setLayerEquilibriumMagnetisation(const std::string &layerID, + const T &me) { + bool found = false; + for (auto &l : this->layers) { + if (l.id == layerID || layerID == "all") { + l.setEquilibriumMagnetisation(me); + found = true; + } + } + if (!found) { + throw std::runtime_error( + "Failed to find a layer with a given id: " + layerID + "!"); + } + } + + void setLayerSusceptibility(const std::string &layerID, + const T &susceptibility) { + bool found = false; + for (auto &l : this->layers) { + if (l.id == layerID || layerID == "all") { + l.setSusceptibility(susceptibility); + found = true; + } + } + if (!found) { + throw std::runtime_error( + "Failed to find a layer with a given id: " + layerID + "!"); + } + } + + void heunSolverStep(const T &t, const T &timeStep) { + /* + Heun method + y'(t+1) = y(t) + dy(y, t) + y(t+1) = y(t) + 0.5 * (dy(y, t) + dy(y'(t+1), t+1)) + */ + /* + Stochastic Heun method + y_np = y + g(y,t,dW)*dt + g_sp = g(y_np,t+1,dW) + y' = y_n + f_n * dt + g_n * dt + f' = f(y, ) + y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) + */ + std::vector> fn(this->layerNo, CVector()); + std::vector> gn(this->layerNo, CVector()); + std::vector> nonAdiabatic(this->layerNo, CVector()); + std::vector> adiabatic(this->layerNo, CVector()); + std::vector> mNext(this->layerNo, CVector()); + // first approximation + + // make sure that + // 1. Thermal field is added if needed + // 2. One/f noise is added if needed + // 3. The timestep is correctly multiplied + + for (unsigned int i = 0; i < this->layerNo; i++) { + fn[i] = this->layers[i].calculateLLG(t, timeStep, this->layers[i].mag); + + // draw the noise for each layer, dW + nonAdiabatic[i] = this->layers[i].nonadiabaticThermalField(t, timeStep); + adiabatic[i] = this->layers[i].adiabaticThermalField(t, timeStep); + gn[i] = this->layers[i].stochasticTorque(this->layers[i].mag, t, + nonAdiabatic[i], adiabatic[i]); + + mNext[i] = + this->layers[i].mag + fn[i] * timeStep + gn[i] * sqrt(timeStep); + } + // second approximation + for (unsigned int i = 0; i < this->layerNo; i++) { + // first approximation is already multiplied by timeStep + this->layers[i].mag = + this->layers[i].mag + + 0.5 * timeStep * + (fn[i] + + this->layers[i].calculateLLG(t + timeStep, timeStep, mNext[i])) + + 0.5 * + (gn[i] + this->layers[i].stochasticTorque(mNext[i], t + timeStep, + nonAdiabatic[i], + adiabatic[i])) * + sqrt(timeStep); + // normalise only in classical + // this->layers[i].mag.normalize(); // LLB doesn't normalise + } + } + void eulerHeunSolverStep(const T &t, const T &timeStep) { + /* + Euler Heun method (stochastic heun) + + y_np = y + g(y,t,dW)*dt + g_sp = g(y_np,t+1,dW) + y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) + + with f being the non-stochastic part and g the stochastic part + */ + // draw the noise for each layer, dW + std::vector> mPrime(this->layerNo, CVector()); + for (unsigned int i = 0; i < this->layerNo; i++) { + // todo: after you're done, double check the thermal magnitude and dt + // scaling there + const CVector nonAdiabaticTorque = + this->layers[i].nonadiabaticThermalField(t, timeStep); + const CVector adiabaticTorque = + this->layers[i].adiabaticThermalField(t, timeStep); + + const CVector fnApprox = + this->layers[i].calculateLLG(t, timeStep, this->layers[i].mag); + const CVector gnApprox = this->layers[i].stochasticTorque( + this->layers[i].mag, t, nonAdiabaticTorque, adiabaticTorque); + + // theoretically we have 2 options + // 1. calculate only the stochastic part with the second approximation + // 2. calculate the second approximation of m with the stochastic and + // non-stochastic + // part and then use if for torque est. + const CVector mNext = this->layers[i].mag + gnApprox * sqrt(timeStep); + const CVector gnPrimeApprox = this->layers[i].stochasticTorque( + mNext, t + timeStep, nonAdiabaticTorque, adiabaticTorque); + mPrime[i] = this->layers[i].mag + fnApprox * timeStep + + 0.5 * (gnApprox + gnPrimeApprox) * sqrt(timeStep); + } + + for (unsigned int i = 0; i < this->layerNo; i++) { + this->layers[i].mag = mPrime[i]; + // this->layers[i].mag.normalize(); // LLB doesn't normalise + } + } + + typedef void (LLGBJunction::*runnerFn)(const T &t, const T &timeStep); + std::tuple getSolver(SolverMode mode) { + auto runner = &LLGBJunction::heunSolverStep; + if (mode == HEUN) + runner = &LLGBJunction::heunSolverStep; + else if (mode == EULER_HEUN) + runner = &LLGBJunction::eulerHeunSolverStep; + else + throw std::runtime_error("The solver mode is not supported!"); + return std::make_tuple(runner, mode); + } + + /** + * @brief Log computed layer parameters. + * This function logs all the necessayr parameters of the layers. + * @param t: current time + * @param timeStep: timeStep of the simulation (unsued for now) + * @param calculateEnergies: if true, also include fields for energy + * computation. + */ + void logLayerParams(T t, const T timeStep) { + for (const auto &layer : this->layers) { + const std::string lId = layer.id; + // always save magnetisation + for (int i = 0; i < 3; i++) { + this->log[lId + "_m" + vectorNames[i]].emplace_back(layer.mag[i]); + } + this->log[lId + "_alpha_parallel"].emplace_back(layer.alpha_par_log); + this->log[lId + "_alpha_perpendicular"].emplace_back( + layer.alpha_perp_log); + this->log[lId + "_K"].emplace_back(layer.K_log); + this->log[lId + "_T"].emplace_back(layer.T_log); + this->log[lId + "_me"].emplace_back(layer.me); + this->log[lId + "_Xpar"].emplace_back(layer.susceptibility); + } + this->log["time"].emplace_back(t); + this->logLength++; + } + + void saveLogs(const std::string &filename) { + if (filename == "") { + // if there's an empty fn, don't save + throw std::runtime_error("The filename may not be empty!"); + } + std::ofstream logFile; + logFile.open(filename); + for (const auto &keyPair : this->log) { + logFile << keyPair.first << ";"; + } + logFile << "\n"; + for (unsigned int i = 0; i < logLength; i++) { + for (const auto &keyPair : this->log) { + logFile << keyPair.second[i] << ";"; + } + logFile << "\n"; + } + logFile.close(); + } + + /** + * Clears the simulation log. + **/ + void clearLog() { + this->log.clear(); + this->logLength = 0; + this->time = 0; + } + + std::unordered_map> &getLog() { + return this->log; + } + + /** + * Main run simulation function. Use it to run the simulation. + * @param totalTime: total time of a simulation, give it in seconds. Typical + * length is in ~couple ns. + * @param timeStep: the integration step of the RK45 method. Default is 1e-13 + * @param writeFrequency: how often is the log saved to? Must be no smaller + * than `timeStep`. Default is 1e-11. + * @param log: if you want some verbosity like timing the simulation. Default + * is false + * @param mode: Solver mode EULER_HEUN, RK4 or DORMAND_PRICE + */ + void runSimulation(T totalTime, T timeStep = 1e-13, T writeFrequency = 1e-13, + bool log = false, SolverMode mode = HEUN) + + { + if (timeStep > writeFrequency) { + throw std::runtime_error( + "The time step cannot be larger than write frequency!"); + } + const unsigned int totalIterations = (int)(totalTime / timeStep); + const unsigned int writeEvery = (int)(writeFrequency / timeStep); + std::chrono::steady_clock::time_point begin = + std::chrono::steady_clock::now(); + // pick a solver based on drivers + auto [runner, _] = getSolver(mode); + + for (unsigned int i = 0; i < totalIterations; i++) { + this->time += timeStep; + (*this.*runner)(this->time, timeStep); + + if (!(i % writeEvery)) { + logLayerParams(this->time, timeStep); + } + } + std::chrono::steady_clock::time_point end = + std::chrono::steady_clock::now(); + if (log) { + std::cout << "Steps in simulation: " << totalIterations << std::endl; + std::cout << "Write every: " << writeEvery << std::endl; + std::cout << "Simulation time = " + << std::chrono::duration_cast(end - begin) + .count() + << "[s]" << std::endl; + } + } }; diff --git a/docs/api/drivers.md b/docs/api/drivers.md new file mode 100644 index 0000000..b7d4c38 --- /dev/null +++ b/docs/api/drivers.md @@ -0,0 +1,78 @@ +--- +author: + - LemurPwned +date: July 2024 +title: Tips and tricks +--- + + +# Drivers API + +The drivers API allows you to freely control the excitation types in `cmtj` library. +There are two types of drivers: + +- `ScalarDriver` -- is a function over time that returns a scalar value. +- `AxialDriver` -- is a driver that returns a 3-vector value. It is a composition of three scalar drivers, one for each component of the vector. + +The library provides a few built-in drivers that can be used to define the scalar values. The built-in `ScalarDrivers` drivers that can also be easily used to define the `AxialDrivers`. The built-in drivers are: + +- `constantDriver`: A driver that returns a constant value at each time step. +- `sineDriver`: A driver that returns a sinusoidal value at each time step. +- `gaussianImpulseDriver`: A driver that returns a Gaussian impulse at a given time. +- `posSineDriver`: A driver that returns a positive sinusoidal value at each time step. +- `pulseDriver`: A driver that returns a pulse at a given time. +- `stepDriver`: A driver that returns a step function at a given time. +- `trapezoidDriver`: A driver that returns a trapezoidal function at a given time. +- `NullDriver`: A driver that returns zero at each time step (no-op driver) + +For more details on the driver parameters see the binding file [here](https://github.com/LemurPwned/cmtj/blob/master/cmtj/__init__.pyi#L14) or [the documentation](../core/#cmtj.constantDriver). + +## How to define your own drivers? + +You can define your own drivers by inheriting from the `ScalarDriver` class. This class has a single method `getCurrentScalarValue` which you need to implement. This method should return the scalar value of the driver at the given time. The time is given in seconds. The driver can be used in the same way as the built-in drivers. Here is an example of a driver that returns a random value at each time step: + +```python +from cmtj import ( + ScalarDriver, + Layer, + Junction, + CVector, + constantDriver, + AxialDriver, + NullDriver, +) +import numpy as np + + +class MyDriver(ScalarDriver): + def getCurrentScalarValue(self, time: float) -> float: + return time * np.random.choice([-1, 1]) + + +driver = MyDriver() +for i in range(10): + print(driver.getCurrentScalarValue(i * 1e-9)) + +demag = [CVector(0, 0, 0), CVector(0, 0, 0), CVector(0, 0, 1)] +layer = Layer( + "free", + mag=CVector(0.1, 0.1, 0.9), + anis=CVector(0.0, 0.0, 1.0), + Ms=1.0, + thickness=3e-9, + cellSurface=0, + demagTensor=demag, + damping=3e-3, +) +layer.setReferenceLayer(CVector(0, 0, 1)) +junction = Junction([layer], 100, 200) +junction.setLayerExternalFieldDriver( + "all", AxialDriver(driver, NullDriver(), NullDriver()) +) +junction.setLayerAnisotropyDriver("all", constantDriver(150e3)) +junction.runSimulation(30e-9, 1e-13, 1e-13) + +``` + +After you've defined the driver these work just like any other driver. diff --git a/docs/index.md b/docs/index.md index 375453c..c87648d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -84,7 +84,7 @@ The package requires (if `utils` subpackage is used): ## Documentation and examples -Documentation: [https://lemurpwned.github.io/cmtj](https://lemurpwned.github.io/cmtj) +Documentation: [https://lemurpwned.github.io/cmtj](https://lemurpwned.github.io/cmtj). There are many examples available, check out the [examples section in the docs](https://lemurpwned.github.io/cmtj/experimental-methods/introduction/) ## Extensions @@ -144,13 +144,16 @@ pre-commit run -a (or --files core/* cmtj/*) ## Documentation builds -There are couple of stages to building the documentation +**Note** +For stub generation add `__init__.py` to the `cmtj` directory. + +There are a couple of stages to building the documentation 1. Build Doxygen documentation ``` doxygen Doxyfile ``` - This is mostly for the C++ documentation. Furture changes may couple C++ and Python docs. + This is mostly for the C++ documentation. Future changes may couple C++ and Python docs. 2. Build stubs The stubgen is `pybind11-stubgen` or `mypy stubgen` with the latter being preferred now. Before running the stubgen, make sure to install the package with: @@ -168,8 +171,7 @@ There are couple of stages to building the documentation ``` More info here: https://mypy.readthedocs.io/en/stable/stubgen.html. 3. Parse stubs to Markdown. - This stage is done by running: - `python3 docs/docgen.py ` + This stage is done by running: `python3 docs/docgen.py ` The deployment of the documentation is done via: ```bash mkdocs gh-deploy diff --git a/docs/tipsandtricks.md b/docs/tipsandtricks.md index 06e23cf..ad2f801 100644 --- a/docs/tipsandtricks.md +++ b/docs/tipsandtricks.md @@ -13,3 +13,4 @@ This is a loose collection of observations and tips that may help you in your wo - Use `utils.Filters` for postprocessing the data. Not only logarithm, but detrending the spectra may help in obtaining a clearer picture. Using a `uniform_filter` from scipy may also help in smoothing the data. - Try out integration times no lower than $10^{-12}$. For large IEC coupling values (in the ballpark of $10^{-4}$ or larger than that) you may need to go even much lower. You can always start up higher and then reduce step size to confirm that it has no effect on the results and convergence. - Use `junction.clearLog()` and `stack.clearLogs()` to clear the log of the junction and stack. This will save you a lot of memory if you're doing a lot of scans and will vastly speed up the processing. +- You can define your own drivers! See the [API documentation](api/drivers.md) for more information. diff --git a/mkdocs.yml b/mkdocs.yml index ec3e48f..20f4540 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,7 @@ nav: - Parallelism: physics/paralellism.md - API: - Core: api/core.md + - Drivers: api/drivers.md - Models: - Smit-Beljers: api/models/sb-general-reference.md - Domain wall: api/models/dw-reference.md diff --git a/python/cmtj.cpp b/python/cmtj.cpp index ec1d8a7..7e1b7c3 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -1,15 +1,15 @@ -#include #include +#include #include #include -#include "../core/reservoir.hpp" -#include "../core/stack.hpp" #include "../core/cvector.hpp" #include "../core/drivers.hpp" #include "../core/junction.hpp" -#include "../core/noise.hpp" #include "../core/llgb.hpp" +#include "../core/noise.hpp" +#include "../core/reservoir.hpp" +#include "../core/stack.hpp" #include #include @@ -31,450 +31,396 @@ using DLLGBLayer = LLGBLayer; using DLLGBJunction = LLGBJunction; #define USING_PY true -PYBIND11_MODULE(cmtj, m) -{ - // helpers - m.def("c_dot", &c_dot); - m.doc() = "Python binding for C++ CMTJ Library."; - - // driver aliases - m.def("constantDriver", - [](double value) { return DScalarDriver::getConstantDriver(value); }, - "value"_a); - m.def("pulseDriver", - [](double constantValue, double amplitude, double period, double cycle) { - return DScalarDriver::getPulseDriver(constantValue, amplitude, period, cycle); - }, - "constantValue"_a, "amplitude"_a, "period"_a, "cycle"_a); - m.def("sineDriver", - [](double constantValue, double amplitude, double frequency, double phase) { - return DScalarDriver::getSineDriver(constantValue, amplitude, frequency, phase); - }, - "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); - m.def("posSineDriver", - [](double constantValue, double amplitude, double frequency, double phase) { - return DScalarDriver::getPosSineDriver(constantValue, amplitude, frequency, phase); - }, - "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); - m.def("stepDriver", - [](double constantValue, double amplitude, double timeStart, double timeStop) { - return DScalarDriver::getStepDriver(constantValue, amplitude, timeStart, timeStop); - }, - "constantValue"_a, "amplitude"_a, "timeStart"_a, "timeStop"_a); - m.def("trapezoidDriver", - [](double constantValue, double amplitude, double timeStart, double edgeTime, double steadyTime) { - return DScalarDriver::getTrapezoidDriver(constantValue, amplitude, timeStart, edgeTime, steadyTime); - }, - "constantValue"_a, "amplitude"_a, "timeStart"_a, "edgeTime"_a, "steadyTime"_a); - m.def("gaussianImpulseDriver", - [](double constantValue, double amplitude, double t0, double sigma) { - return DScalarDriver::getGaussianImpulseDriver(constantValue, amplitude, t0, sigma); - }, - "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); - m.def("gaussianStepDriver", - [](double constantValue, double amplitude, double t0, double sigma) { - return DScalarDriver::getGaussianStepDriver(constantValue, amplitude, t0, sigma); - }, - "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); - - - // CVector - py::class_(m, "CVector") - .def(py::init< - double, double, double>()) - .def_readwrite("x", &DVector::x) - .def_readwrite("y", &DVector::y) - .def_readwrite("z", &DVector::z) - .def("length", [](const DVector& vec) { return vec.length(); }) - .def("normalize", &DVector::normalize) - .def("tolist", &DVector::tolist) - // operators - .def(py::self + py::self) - .def(py::self += py::self) - .def(py::self - py::self) - .def(py::self -= py::self) - .def(py::self *= double()) - .def(py::self == py::self) - .def(py::self != py::self) - .def(double() * py::self) - .def(py::self * double()) - .def("__getitem__", [](const DVector& v, const int key) { return v[key]; }) - .def("__len__", [](const DVector& v) { return 3; }) - .def("__str__", py::overload_cast<>(&DVector::toString)) - .def("__repr__", py::overload_cast<>(&DVector::toString)); +PYBIND11_MODULE(cmtj, m) { + // helpers + m.def("c_dot", &c_dot); + m.doc() = "Python binding for C++ CMTJ Library."; - py::implicitly_convertible, DVector>(); - py::implicitly_convertible, DVector>(); + // driver aliases + m.def( + "constantDriver", + [](double value) { return DScalarDriver::getConstantDriver(value); }, + "value"_a); + m.def( + "pulseDriver", + [](double constantValue, double amplitude, double period, double cycle) { + return DScalarDriver::getPulseDriver(constantValue, amplitude, period, + cycle); + }, + "constantValue"_a, "amplitude"_a, "period"_a, "cycle"_a); + m.def( + "sineDriver", + [](double constantValue, double amplitude, double frequency, + double phase) { + return DScalarDriver::getSineDriver(constantValue, amplitude, frequency, + phase); + }, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); + m.def( + "posSineDriver", + [](double constantValue, double amplitude, double frequency, + double phase) { + return DScalarDriver::getPosSineDriver(constantValue, amplitude, + frequency, phase); + }, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); + m.def( + "stepDriver", + [](double constantValue, double amplitude, double timeStart, + double timeStop) { + return DScalarDriver::getStepDriver(constantValue, amplitude, timeStart, + timeStop); + }, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "timeStop"_a); + m.def( + "trapezoidDriver", + [](double constantValue, double amplitude, double timeStart, + double edgeTime, double steadyTime) { + return DScalarDriver::getTrapezoidDriver( + constantValue, amplitude, timeStart, edgeTime, steadyTime); + }, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "edgeTime"_a, + "steadyTime"_a); + m.def( + "gaussianImpulseDriver", + [](double constantValue, double amplitude, double t0, double sigma) { + return DScalarDriver::getGaussianImpulseDriver(constantValue, amplitude, + t0, sigma); + }, + "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); + m.def( + "gaussianStepDriver", + [](double constantValue, double amplitude, double t0, double sigma) { + return DScalarDriver::getGaussianStepDriver(constantValue, amplitude, + t0, sigma); + }, + "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); - py::enum_(m, "Axis") - .value("xaxis", xaxis) - .value("yaxis", yaxis) - .value("zaxis", zaxis) - .value("all", all) - .value("none", none) - .export_values(); + // CVector + py::class_(m, "CVector") + .def(py::init()) + .def_readwrite("x", &DVector::x) + .def_readwrite("y", &DVector::y) + .def_readwrite("z", &DVector::z) + .def("length", [](const DVector &vec) { return vec.length(); }) + .def("normalize", &DVector::normalize) + .def("tolist", &DVector::tolist) + // operators + .def(py::self + py::self) + .def(py::self += py::self) + .def(py::self - py::self) + .def(py::self -= py::self) + .def(py::self *= double()) + .def(py::self == py::self) + .def(py::self != py::self) + .def(double() * py::self) + .def(py::self * double()) + .def("__getitem__", + [](const DVector &v, const int key) { return v[key]; }) + .def("__len__", [](const DVector &v) { return 3; }) + .def("__str__", py::overload_cast<>(&DVector::toString)) + .def("__repr__", py::overload_cast<>(&DVector::toString)); - py::enum_(m, "Reference") - .value("none", NONE) - .value("fixed", FIXED) - .value("top", TOP) - .value("bottom", BOTTOM) - .export_values(); + py::implicitly_convertible, DVector>(); + py::implicitly_convertible, DVector>(); - py::enum_(m, "SolverMode") - .value("RK4", RK4) - .value("Heun", HEUN) - .value("EulerHeun", EULER_HEUN) - .value("DormandPrice", DORMAND_PRICE) - .export_values(); + py::enum_(m, "Axis") + .value("xaxis", xaxis) + .value("yaxis", yaxis) + .value("zaxis", zaxis) + .value("all", all) + .value("none", none) + .export_values(); - // Driver Class - py::class_(m, "ScalarDriver") - .def(py::self + double()) - .def(py::self += double()) - .def(py::self * double()) - .def(py::self *= double()) - .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, "time"_a) - .def_static("getConstantDriver", - &DScalarDriver::getConstantDriver, - "constantValue"_a) - .def_static("getPulseDriver", - &DScalarDriver::getPulseDriver, - "constantValue"_a, - "amplitude"_a, - "period"_a, - "cycle"_a) - .def_static("getSineDriver", - &DScalarDriver::getSineDriver, - "constantValue"_a, - "amplitude"_a, - "frequency"_a, - "phase"_a) - .def_static("getPosSineDriver", - &DScalarDriver::getPosSineDriver, - "constantValue"_a, - "amplitude"_a, - "frequency"_a, - "phase"_a) - .def_static("getStepDriver", - &DScalarDriver::getStepDriver, - "constantValue"_a, - "amplitude"_a, - "timeStart"_a, - "timeStop"_a) - .def_static("getTrapezoidDriver", - &DScalarDriver::getTrapezoidDriver, - "constantValue"_a, - "amplitude"_a, - "timeStart"_a, - "edgeTime"_a, - "steadyTime"_a) - .def_static("getGaussianImpulseDriver", - &DScalarDriver::getGaussianImpulseDriver, - "constantValue"_a, - "amplitude"_a, - "t0"_a, - "sigma"_a) - .def_static("getGaussianStepDriver", - &DScalarDriver::getGaussianStepDriver, - "constantValue"_a, - "amplitude"_a, - "t0"_a, - "sigma"_a); + py::enum_(m, "Reference") + .value("none", NONE) + .value("fixed", FIXED) + .value("top", TOP) + .value("bottom", BOTTOM) + .export_values(); - py::class_(m, "NullDriver") - .def(py::init<>()) - .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, "time"_a); + py::enum_(m, "SolverMode") + .value("RK4", RK4) + .value("Heun", HEUN) + .value("EulerHeun", EULER_HEUN) + .value("DormandPrice", DORMAND_PRICE) + .export_values(); - py::class_(m, "AxialDriver") - .def(py::init()) - .def(py::init>>()) - .def(py::init()) - .def(py::init()) - .def("getVectorAxialDriver", &DAxialDriver::getVectorAxialDriver) - .def("getCurrentAxialDrivers", - &DAxialDriver::getCurrentAxialDrivers, "time"_a) - .def("applyMask", py::overload_cast(&DAxialDriver::applyMask)) - .def("applyMask", py::overload_cast&>(&DAxialDriver::applyMask)); + // Driver Class + py::class_(m, "ScalarDriver") + .def(py::init<>()) + .def(py::self + double()) + .def(py::self += double()) + .def(py::self * double()) + .def(py::self *= double()) + .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, + "time"_a) + .def_static("getConstantDriver", &DScalarDriver::getConstantDriver, + "constantValue"_a) + .def_static("getPulseDriver", &DScalarDriver::getPulseDriver, + "constantValue"_a, "amplitude"_a, "period"_a, "cycle"_a) + .def_static("getSineDriver", &DScalarDriver::getSineDriver, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a) + .def_static("getPosSineDriver", &DScalarDriver::getPosSineDriver, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a) + .def_static("getStepDriver", &DScalarDriver::getStepDriver, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "timeStop"_a) + .def_static("getTrapezoidDriver", &DScalarDriver::getTrapezoidDriver, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "edgeTime"_a, + "steadyTime"_a) + .def_static("getGaussianImpulseDriver", + &DScalarDriver::getGaussianImpulseDriver, "constantValue"_a, + "amplitude"_a, "t0"_a, "sigma"_a) + .def_static("getGaussianStepDriver", + &DScalarDriver::getGaussianStepDriver, "constantValue"_a, + "amplitude"_a, "t0"_a, "sigma"_a); - py::class_(m, "Layer") - .def(py::init< - std::string, // id - DVector, // mag - DVector, // anis - double, // Ms - double, // thickness - double, // cellSurface - std::vector, // demagTensor - double // damping - >(), - "id"_a, - "mag"_a, - "anis"_a, - "Ms"_a, - "thickness"_a, - "cellSurface"_a, - "demagTensor"_a, - "damping"_a = 0.011) - .def_static("createSOTLayer", &DLayer::LayerSOT, - "id"_a, - "mag"_a, - "anis"_a, - "Ms"_a, - "thickness"_a, - "cellSurface"_a, - "demagTensor"_a, - "damping"_a = 0.011, - "fieldLikeTorque"_a = 0.0, - "dampingLikeTorque"_a = 0.0) - .def_static("createSTTLayer", &DLayer::LayerSTT, - "id"_a, - "mag"_a, - "anis"_a, - "Ms"_a, - "thickness"_a, - "cellSurface"_a, - "demagTensor"_a, - "damping"_a = 0.011, - "SlonczewskiSpacerLayerParameter"_a = 1.0, - "beta"_a = 0.0, - "spinPolarisation"_a = 0.0) - .def("setMagnetisation", &DLayer::setMagnetisation) - .def("setAnisotropyDriver", &DLayer::setAnisotropyDriver) - .def("setExternalFieldDriver", &DLayer::setExternalFieldDriver) - .def("setOerstedFieldDriver", &DLayer::setOerstedFieldDriver) - .def("setHdmiDriver", &DLayer::setHdmiDriver) - // reference layers - .def("setReferenceLayer", py::overload_cast(&DLayer::setReferenceLayer)) - .def("setReferenceLayer", py::overload_cast(&DLayer::setReferenceLayer)) + py::class_(m, "NullDriver") + .def(py::init<>()) + .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, + "time"_a); - .def("setFieldLikeTorqueDriver", &DLayer::setFieldLikeTorqueDriver) - .def("setDampingLikeTorqueDriver", &DLayer::setDampingLikeTorqueDriver) - .def("setTemperatureDriver", &DLayer::setTemperatureDriver) - .def("setTopDipoleTensor", &DLayer::setTopDipoleTensor) - .def("setBottomDipoleTensor", &DLayer::setBottomDipoleTensor) - .def("setKappa", &DLayer::setKappa) - .def("setAlternativeSTT", &DLayer::setAlternativeSTT) - // readonly props - .def_readonly("id", &DLayer::id) - .def_readonly("Ms", &DLayer::Ms) - .def_readonly("thickness", &DLayer::thickness) - .def_readonly("damping", &DLayer::damping) - .def_readonly("cellSurface", &DLayer::cellSurface) - .def_readonly("demagTensor", &DLayer::demagTensor) - // noise - .def("setAlphaNoise", &DLayer::setAlphaNoise) - .def("setOneFNoise", &DLayer::setOneFNoise) - // getters - .def("getId", &DLayer::getId) - .def("getOneFVector", &DLayer::getOneFVector) - .def("createBufferedAlphaNoise", &DLayer::createBufferedAlphaNoise); + py::class_(m, "AxialDriver") + .def(py::init()) + .def(py::init>>()) + .def(py::init()) + .def(py::init()) + .def("getVectorAxialDriver", &DAxialDriver::getVectorAxialDriver) + .def("getCurrentAxialDrivers", &DAxialDriver::getCurrentAxialDrivers, + "time"_a) + .def("applyMask", + py::overload_cast(&DAxialDriver::applyMask)) + .def("applyMask", py::overload_cast &>( + &DAxialDriver::applyMask)); - py::class_(m, "Junction") - .def(py::init>(), - "layers"_a) - .def(py::init, - double, double>(), - "layers"_a, - "Rp"_a = 100, - "Rap"_a = 200) - .def(py::init< - std::vector, - std::vector, - std::vector, - std::vector, - std::vector, - std::vector, - std::vector, - std::vector>(), - "layers"_a, - "Rx0"_a, - "Ry0"_a, - "AMR_X"_a, - "AMR_Y"_a, - "SMR_X"_a, - "SMR_Y"_a, - "AHE"_a) - // log utils - .def("getLog", &DJunction::getLog) - .def("clearLog", &DJunction::clearLog) - .def("saveLog", &DJunction::saveLogs, "filename"_a) - // main run - .def("runSimulation", &DJunction::runSimulation, - "totalTime"_a, - "timeStep"_a = 1e-13, - "writeFrequency"_a = 1e-11, - "log"_a = false, - "calculateEnergies"_a = false, - "solverMode"_a = RK4) + py::class_(m, "Layer") + .def(py::init, // demagTensor + double // damping + >(), + "id"_a, "mag"_a, "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a = 0.011) + .def_static("createSOTLayer", &DLayer::LayerSOT, "id"_a, "mag"_a, + "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a = 0.011, + "fieldLikeTorque"_a = 0.0, "dampingLikeTorque"_a = 0.0) + .def_static("createSTTLayer", &DLayer::LayerSTT, "id"_a, "mag"_a, + "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a = 0.011, + "SlonczewskiSpacerLayerParameter"_a = 1.0, "beta"_a = 0.0, + "spinPolarisation"_a = 0.0) + .def("setMagnetisation", &DLayer::setMagnetisation) + .def("setAnisotropyDriver", &DLayer::setAnisotropyDriver) + .def("setExternalFieldDriver", &DLayer::setExternalFieldDriver) + .def("setOerstedFieldDriver", &DLayer::setOerstedFieldDriver) + .def("setHdmiDriver", &DLayer::setHdmiDriver) + // reference layers + .def("setReferenceLayer", + py::overload_cast(&DLayer::setReferenceLayer)) + .def("setReferenceLayer", + py::overload_cast(&DLayer::setReferenceLayer)) - // driver setters - .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) - .def("setLayerExternalFieldDriver", &DJunction::setLayerExternalFieldDriver) - .def("setLayerCurrentDriver", &DJunction::setLayerCurrentDriver) - .def("setLayerAnisotropyDriver", &DJunction::setLayerAnisotropyDriver) - .def("setIECDriver", &DJunction::setIECDriver) - .def("setQuadIECDriver", &DJunction::setQuadIECDriver) - .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) - .def("setLayerMagnetisation", &DJunction::setLayerMagnetisation) - .def("setLayerHdmiDriver", &DJunction::setLayerHdmiDriver) - // noise - .def("setLayerTemperatureDriver", &DJunction::setLayerTemperatureDriver) - .def("setLayerNonStochasticLangevinDriver", &DJunction::setLayerNonStochasticLangevinDriver) - .def("setLayerOneFNoise", &DJunction::setLayerOneFNoise) - // SOT setters - .def("setLayerFieldLikeTorqueDriver", &DJunction::setLayerFieldLikeTorqueDriver) - .def("setLayerDampingLikeTorqueDriver", &DJunction::setLayerDampingLikeTorqueDriver) - // Reference setters - .def("setLayerReferenceType", &DJunction::setLayerReferenceType) - .def("setLayerReferenceLayer", &DJunction::setLayerReferenceLayer) - // other setters - .def("setLayerAlternativeSTT", &DJunction::setLayerAlternativeSTT) - // junction calculations - .def("getLayerMagnetisation", &DJunction::getLayerMagnetisation) - .def("getMagnetoresistance", &DJunction::getMagnetoresistance) - // getters - .def("getLayerIds", &DJunction::getLayerIds) - // readonly props - .def_readonly("layers", &DJunction::layers); + .def("setFieldLikeTorqueDriver", &DLayer::setFieldLikeTorqueDriver) + .def("setDampingLikeTorqueDriver", &DLayer::setDampingLikeTorqueDriver) + .def("setTemperatureDriver", &DLayer::setTemperatureDriver) + .def("setTopDipoleTensor", &DLayer::setTopDipoleTensor) + .def("setBottomDipoleTensor", &DLayer::setBottomDipoleTensor) + .def("setKappa", &DLayer::setKappa) + .def("setAlternativeSTT", &DLayer::setAlternativeSTT) + // readonly props + .def_readonly("id", &DLayer::id) + .def_readonly("Ms", &DLayer::Ms) + .def_readonly("thickness", &DLayer::thickness) + .def_readonly("damping", &DLayer::damping) + .def_readonly("cellSurface", &DLayer::cellSurface) + .def_readonly("demagTensor", &DLayer::demagTensor) + // noise + .def("setAlphaNoise", &DLayer::setAlphaNoise) + .def("setOneFNoise", &DLayer::setOneFNoise) + // getters + .def("getId", &DLayer::getId) + .def("getOneFVector", &DLayer::getOneFVector) + .def("createBufferedAlphaNoise", &DLayer::createBufferedAlphaNoise); - // stack module - py::module stack_module = m.def_submodule("stack", "A stack submodule for joining MTJ junctions"); + py::class_(m, "Junction") + .def(py::init>(), "layers"_a) + .def(py::init, double, double>(), "layers"_a, + "Rp"_a = 100, "Rap"_a = 200) + .def(py::init, std::vector, + std::vector, std::vector, + std::vector, std::vector, + std::vector, std::vector>(), + "layers"_a, "Rx0"_a, "Ry0"_a, "AMR_X"_a, "AMR_Y"_a, "SMR_X"_a, + "SMR_Y"_a, "AHE"_a) + // log utils + .def("getLog", &DJunction::getLog) + .def("clearLog", &DJunction::clearLog) + .def("saveLog", &DJunction::saveLogs, "filename"_a) + // main run + .def("runSimulation", &DJunction::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11, "log"_a = false, + "calculateEnergies"_a = false, "solverMode"_a = RK4) - py::class_(stack_module, "SeriesStack") - .def(py::init, - std::string, - std::string, double>(), - "junctionList"_a, - "topId_a"_a = "free", - "bottomId"_a = "bottom", - "phaseOffset"_a = 0.0) - .def("runSimulation", &DSeriesStack::runSimulation, - "totalTime"_a, - "timeStep"_a = 1e-13, - "writeFrequency"_a = 1e-11) - .def("setMagnetisation", &DSeriesStack::setMagnetisation, "junction"_a, "layerId"_a, "mag"_a) - .def("getMagnetisation", &DSeriesStack::getMagnetisation, "junction"_a, "layerId"_a) - .def("setCoupledCurrentDriver", &DSeriesStack::setCoupledCurrentDriver, "driver"_a) - .def("setExternalFieldDriver", &DSeriesStack::setExternalFieldDriver, "driver"_a) - .def("setCouplingStrength", py::overload_cast(&DSeriesStack::setCouplingStrength), "coupling"_a) - .def("setCouplingStrength", py::overload_cast&>(&DSeriesStack::setCouplingStrength), "coupling"_a) - .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) - // logging - .def("clearLogs", &DSeriesStack::clearLogs) - .def("getLog", py::overload_cast(&DSeriesStack::getLog)) - .def("getLog", py::overload_cast<>(&DSeriesStack::getLog)); + // driver setters + .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) + .def("setLayerExternalFieldDriver", + &DJunction::setLayerExternalFieldDriver) + .def("setLayerCurrentDriver", &DJunction::setLayerCurrentDriver) + .def("setLayerAnisotropyDriver", &DJunction::setLayerAnisotropyDriver) + .def("setIECDriver", &DJunction::setIECDriver) + .def("setQuadIECDriver", &DJunction::setQuadIECDriver) + .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) + .def("setLayerMagnetisation", &DJunction::setLayerMagnetisation) + .def("setLayerHdmiDriver", &DJunction::setLayerHdmiDriver) + // noise + .def("setLayerTemperatureDriver", &DJunction::setLayerTemperatureDriver) + .def("setLayerNonStochasticLangevinDriver", + &DJunction::setLayerNonStochasticLangevinDriver) + .def("setLayerOneFNoise", &DJunction::setLayerOneFNoise) + // SOT setters + .def("setLayerFieldLikeTorqueDriver", + &DJunction::setLayerFieldLikeTorqueDriver) + .def("setLayerDampingLikeTorqueDriver", + &DJunction::setLayerDampingLikeTorqueDriver) + // Reference setters + .def("setLayerReferenceType", &DJunction::setLayerReferenceType) + .def("setLayerReferenceLayer", &DJunction::setLayerReferenceLayer) + // other setters + .def("setLayerAlternativeSTT", &DJunction::setLayerAlternativeSTT) + // junction calculations + .def("getLayerMagnetisation", &DJunction::getLayerMagnetisation) + .def("getMagnetoresistance", &DJunction::getMagnetoresistance) + // getters + .def("getLayerIds", &DJunction::getLayerIds) + // readonly props + .def_readonly("layers", &DJunction::layers); - py::class_(stack_module, "ParallelStack") - .def(py::init, - std::string, - std::string, double>(), - "junctionList"_a, - "topId_a"_a = "free", - "bottomId"_a = "bottom", - "phaseOffset"_a = 0.0) - .def("runSimulation", &DParallelStack::runSimulation, - "totalTime"_a, - "timeStep"_a = 1e-13, - "writeFrequency"_a = 1e-11) - .def("setMagnetisation", &DParallelStack::setMagnetisation, "junction"_a, "layerId"_a, "mag"_a) - .def("getMagnetisation", &DParallelStack::getMagnetisation, "junction"_a, "layerId"_a) - .def("setCoupledCurrentDriver", &DParallelStack::setCoupledCurrentDriver, "driver"_a) - .def("setExternalFieldDriver", &DParallelStack::setExternalFieldDriver, "driver"_a) - .def("setCouplingStrength", py::overload_cast(&DParallelStack::setCouplingStrength), "coupling"_a) - .def("setCouplingStrength", py::overload_cast&>(&DParallelStack::setCouplingStrength), "coupling"_a) - .def("setDelayed", &DParallelStack::setDelayed, "delayed"_a) - // logging - .def("clearLogs", &ParallelStack::clearLogs) - .def("getLog", py::overload_cast(&ParallelStack::getLog)) - .def("getLog", py::overload_cast<>(&ParallelStack::getLog)); + // stack module + py::module stack_module = + m.def_submodule("stack", "A stack submodule for joining MTJ junctions"); + py::class_(stack_module, "SeriesStack") + .def(py::init, std::string, std::string, double>(), + "junctionList"_a, "topId_a"_a = "free", "bottomId"_a = "bottom", + "phaseOffset"_a = 0.0) + .def("runSimulation", &DSeriesStack::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11) + .def("setMagnetisation", &DSeriesStack::setMagnetisation, "junction"_a, + "layerId"_a, "mag"_a) + .def("getMagnetisation", &DSeriesStack::getMagnetisation, "junction"_a, + "layerId"_a) + .def("setCoupledCurrentDriver", &DSeriesStack::setCoupledCurrentDriver, + "driver"_a) + .def("setExternalFieldDriver", &DSeriesStack::setExternalFieldDriver, + "driver"_a) + .def( + "setCouplingStrength", + py::overload_cast(&DSeriesStack::setCouplingStrength), + "coupling"_a) + .def("setCouplingStrength", + py::overload_cast &>( + &DSeriesStack::setCouplingStrength), + "coupling"_a) + .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) + // logging + .def("clearLogs", &DSeriesStack::clearLogs) + .def("getLog", py::overload_cast(&DSeriesStack::getLog)) + .def("getLog", py::overload_cast<>(&DSeriesStack::getLog)); - // reservoir module - py::module reservoir_module = m.def_submodule("reservoir", "A reservoir submodule for joining MTJ junctions"); - py::class_(reservoir_module, "Reservoir") - .def(py::init(), - "coordinateMatrix"_a, - "layerMatrix"_a) - .def("runSimulation", &Reservoir::runSimulation) - .def("clearLogs", &Reservoir::clearLogs) - .def("saveLogs", &Reservoir::saveLogs) - .def("getLayer", &Reservoir::getLayer) - .def("setAllExternalField", &Reservoir::setAllExternalField) - .def("setLayerAnisotropy", &Reservoir::setLayerAnisotropy) - .def("setLayerExternalField", &Reservoir::setLayerExternalField) - .def("getMagnetisation", &Reservoir::getMagnetisation); + py::class_(stack_module, "ParallelStack") + .def(py::init, std::string, std::string, double>(), + "junctionList"_a, "topId_a"_a = "free", "bottomId"_a = "bottom", + "phaseOffset"_a = 0.0) + .def("runSimulation", &DParallelStack::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11) + .def("setMagnetisation", &DParallelStack::setMagnetisation, "junction"_a, + "layerId"_a, "mag"_a) + .def("getMagnetisation", &DParallelStack::getMagnetisation, "junction"_a, + "layerId"_a) + .def("setCoupledCurrentDriver", &DParallelStack::setCoupledCurrentDriver, + "driver"_a) + .def("setExternalFieldDriver", &DParallelStack::setExternalFieldDriver, + "driver"_a) + .def("setCouplingStrength", + py::overload_cast( + &DParallelStack::setCouplingStrength), + "coupling"_a) + .def("setCouplingStrength", + py::overload_cast &>( + &DParallelStack::setCouplingStrength), + "coupling"_a) + .def("setDelayed", &DParallelStack::setDelayed, "delayed"_a) + // logging + .def("clearLogs", &ParallelStack::clearLogs) + .def("getLog", + py::overload_cast(&ParallelStack::getLog)) + .def("getLog", py::overload_cast<>(&ParallelStack::getLog)); - // generator module - py::module generator_module = m.def_submodule("noise", "Submodule with noise generation functions"); - py::class_>(generator_module, "BufferedAlphaNoise") - .def(py::init(), - "bufferSize"_a, - "alpha"_a, - "std"_a, - "scale"_a) - .def("fillBuffer", &BufferedAlphaNoise::fillBuffer) - .def("tick", &BufferedAlphaNoise::tick); - py::class_>(generator_module, "VectorAlphaNoise") - .def(py::init(), - "bufferSize"_a, - "alpha"_a, - "std"_a, - "scale"_a, - "axis"_a = Axis::all) - .def("tickVector", &VectorAlphaNoise::tickVector) - .def("tick", &VectorAlphaNoise::tick) - .def("getPrevSample", &VectorAlphaNoise::getPrevSample) - .def("getScale", &VectorAlphaNoise::getScale); + // reservoir module + py::module reservoir_module = m.def_submodule( + "reservoir", "A reservoir submodule for joining MTJ junctions"); + py::class_(reservoir_module, "Reservoir") + .def(py::init(), "coordinateMatrix"_a, + "layerMatrix"_a) + .def("runSimulation", &Reservoir::runSimulation) + .def("clearLogs", &Reservoir::clearLogs) + .def("saveLogs", &Reservoir::saveLogs) + .def("getLayer", &Reservoir::getLayer) + .def("setAllExternalField", &Reservoir::setAllExternalField) + .def("setLayerAnisotropy", &Reservoir::setLayerAnisotropy) + .def("setLayerExternalField", &Reservoir::setLayerExternalField) + .def("getMagnetisation", &Reservoir::getMagnetisation); + // generator module + py::module generator_module = + m.def_submodule("noise", "Submodule with noise generation functions"); + py::class_>(generator_module, "BufferedAlphaNoise") + .def(py::init(), "bufferSize"_a, + "alpha"_a, "std"_a, "scale"_a) + .def("fillBuffer", &BufferedAlphaNoise::fillBuffer) + .def("tick", &BufferedAlphaNoise::tick); + py::class_>(generator_module, "VectorAlphaNoise") + .def(py::init(), + "bufferSize"_a, "alpha"_a, "std"_a, "scale"_a, "axis"_a = Axis::all) + .def("tickVector", &VectorAlphaNoise::tickVector) + .def("tick", &VectorAlphaNoise::tick) + .def("getPrevSample", &VectorAlphaNoise::getPrevSample) + .def("getScale", &VectorAlphaNoise::getScale); - // LLGB module - auto llgb_module = m.def_submodule("llgb", "A submodule for LLGB junctions"); - llgb_module.def("MFAWeissCurie", &LLGB::MFAWeissCurie, - "me"_a, "T"_a, "J0"_a, "relax"_a = 0.2, "tolerance"_a = 1e-6, "maxIter"_a = 1000); - llgb_module.def("langevin", &LLGB::langevin); - llgb_module.def("langevinDerivative", &LLGB::langevinDerivative); + // LLGB module + auto llgb_module = m.def_submodule("llgb", "A submodule for LLGB junctions"); + llgb_module.def("MFAWeissCurie", &LLGB::MFAWeissCurie, "me"_a, "T"_a, + "J0"_a, "relax"_a = 0.2, "tolerance"_a = 1e-6, + "maxIter"_a = 1000); + llgb_module.def("langevin", &LLGB::langevin); + llgb_module.def("langevinDerivative", &LLGB::langevinDerivative); - py::class_(llgb_module, "LLGBLayer") - .def(py::init&, - double, - double, - double, - double >(), - "id"_a, - "mag"_a, - "anis"_a, - "Ms"_a, - "thickness"_a, - "cellSurface"_a, - "demagTensor"_a, - "damping"_a, - "Tc"_a, - "susceptibility"_a, - "me"_a) - // setters - .def("setTemperatureDriver", &DLLGBLayer::setTemperatureDriver) - .def("setExternalFieldDriver", &DLLGBLayer::setExternalFieldDriver) - .def("setAnisotropyDriver", &DLLGBLayer::setAnisotropyDriver); + py::class_(llgb_module, "LLGBLayer") + .def(py::init &, double, double, + double, double>(), + "id"_a, "mag"_a, "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a, "Tc"_a, "susceptibility"_a, "me"_a) + // setters + .def("setTemperatureDriver", &DLLGBLayer::setTemperatureDriver) + .def("setExternalFieldDriver", &DLLGBLayer::setExternalFieldDriver) + .def("setAnisotropyDriver", &DLLGBLayer::setAnisotropyDriver); - py::class_(llgb_module, "LLGBJunction") - .def(py::init>(), - "layers"_a) - .def("runSimulation", &DLLGBJunction::runSimulation, - "totalTime"_a, - "timeStep"_a = 1e-13, - "writeFrequency"_a = 1e-11, - "log"_a = false, - "solverMode"_a = HEUN) - .def("setLayerTemperatureDriver", &DLLGBJunction::setLayerTemperatureDriver) - .def("setLayerExternalFieldDriver", &DLLGBJunction::setLayerExternalFieldDriver) - .def("saveLogs", &DLLGBJunction::saveLogs) - .def("getLog", &DLLGBJunction::getLog) - .def("clearLog", &DLLGBJunction::clearLog); + py::class_(llgb_module, "LLGBJunction") + .def(py::init>(), "layers"_a) + .def("runSimulation", &DLLGBJunction::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11, "log"_a = false, + "solverMode"_a = HEUN) + .def("setLayerTemperatureDriver", + &DLLGBJunction::setLayerTemperatureDriver) + .def("setLayerExternalFieldDriver", + &DLLGBJunction::setLayerExternalFieldDriver) + .def("saveLogs", &DLLGBJunction::saveLogs) + .def("getLog", &DLLGBJunction::getLog) + .def("clearLog", &DLLGBJunction::clearLog); } diff --git a/scripts/readme_copy.py b/scripts/readme_copy.py index ce3295d..0b6b479 100644 --- a/scripts/readme_copy.py +++ b/scripts/readme_copy.py @@ -5,9 +5,7 @@ log = logging.getLogger("mkdocs") -KNOWN_REFLINK_MAP = { - "./scripts": "https://github.com/LemurPwned/video-sampler/tree/main/scripts", -} +KNOWN_REFLINK_MAP = {} def on_startup(command, dirty, **kwargs): From 01d27c7555e7defefb204cf1ed993fa8d9b5be9f Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 28 Jul 2024 23:42:47 +0200 Subject: [PATCH 14/44] fixing the order in stack simulation --- cmtj/__init__.pyi | 2 +- core/stack.hpp | 15 ++++++++------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index 99b316f..0909e61 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -38,7 +38,7 @@ def gaussianImpulseDriver( Formula: - $A * \exp(-(t - t_0)^2 / (2\sigma^2))$ + $A \exp(-(t - t_0)^2 / (2\sigma^2))$ :param constantValue: offset of the pulse (vertical) :param amplitude: amplitude that is added on top of the constantValue diff --git a/core/stack.hpp b/core/stack.hpp index 4b1e472..e35a125 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -265,16 +265,17 @@ template class Stack { if (this->delayed) { // accumulate coupling effectiveCoupling *= (1 + this->getEffectiveCouplingStrength( - j - 1, frozenMags[j], - frozenMags[j - 1], frozenPols[j])); + j - 1, frozenMags[j - 1], + frozenMags[j], frozenPols[j - 1])); } else { effectiveCoupling *= - (1 + - this->getEffectiveCouplingStrength( - j - 1, junctionList[j].getLayerMagnetisation(this->topId), - junctionList[j - 1].getLayerMagnetisation(this->topId), - junctionList[j].getLayerMagnetisation(this->bottomId))); + (1 + this->getEffectiveCouplingStrength( + j - 1, + junctionList[j - 1].getLayerMagnetisation(this->topId), + junctionList[j].getLayerMagnetisation(this->topId), + junctionList[j - 1].getLayerMagnetisation( + this->bottomId))); } } From c6874d68c22bcf5d1dbc70f8eac7e2bbaad6e1ac Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sat, 7 Sep 2024 16:18:50 +0200 Subject: [PATCH 15/44] adding idmi --- .pre-commit-config.yaml | 4 +- cmtj/models/general_sb.py | 25 +- core/drivers.hpp | 5 + core/junction.hpp | 3215 ++++++++++++++++++------------------- python/cmtj.cpp | 6 +- 5 files changed, 1625 insertions(+), 1630 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3b3e1f6..e0dad85 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -53,7 +53,7 @@ repos: exclude: third_party/.* - id: clang-format args: [-i] - - id: clang-tidy - args: [-checks=*] + # - id: clang-tidy + # args: [-checks=*] # - id: include-what-you-use # exclude: ^third_party diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index 5ebd694..128ba03 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -1,9 +1,9 @@ import math import time import warnings -from dataclasses import dataclass, field +from dataclasses import dataclass from functools import lru_cache -from typing import Iterable, List, Tuple, Union +from typing import Iterable, List, Literal, Tuple, Union import numpy as np import sympy as sym @@ -302,6 +302,8 @@ class Solver: :param J1: list of interlayer exchange constants. Goes (i)-(i+1), i = 0, 1, 2, ... with i being the index of the layer. :param J2: list of interlayer exchange constants. + :param ilD: list of interlayer DMI vectors, e.g. (0, 0, D)., + ilD * (m1 x m2) :param H: external field. :param Ndipole: list of dipole fields for each layer. Defaults to None. Goes (i)-(i+1), i = 0, 1, 2, ... with i being the index of the layer. @@ -311,6 +313,7 @@ class Solver: J1: List[float] J2: List[float] H: VectorObj = None + ilD: List[VectorObj] = None Ndipole: List[List[VectorObj]] = None def __post_init__(self): @@ -318,7 +321,17 @@ def __post_init__(self): raise ValueError("Number of layers must be 1 more than J1.") if len(self.layers) != len(self.J2) + 1: raise ValueError("Number of layers must be 1 more than J2.") + if self.ilD is None: + # this is optional, if not provided, we assume zero DMI + self.ilD = [ + VectorObj(0, 0, 0) for _ in range(len(self.layers) - 1) + ] + if len(self.layers) != len(self.ilD) + 1: + raise ValueError("Number of layers must be 1 more than ilD.") + if not all(isinstance(d, VectorObj) for d in self.ilD): + raise ValueError("ilD must be a list of VectorObj.") + self.ilD = [sym.ImmutableMatrix(d.get_cartesian()) for d in self.ilD] self.dipoleMatrix: list[sym.Matrix] = None if self.Ndipole is not None: if len(self.layers) != len(self.Ndipole) + 1: @@ -422,10 +435,16 @@ def create_energy(self, for i in range(len(self.layers) - 1): l1m = self.layers[i].get_m_sym() l2m = self.layers[i + 1].get_m_sym() + + # IEC ldot = l1m.dot(l2m) energy -= self.J1[i] * ldot energy -= self.J2[i] * (ldot)**2 + # IDMI, sign is the same J1 + lcross = l1m.cross(l2m) + energy -= self.ilD[i].dot(lcross) + # dipole fields if self.dipoleMatrix is not None: mat = self.dipoleMatrix[i] @@ -700,7 +719,7 @@ def set_H(self, H: VectorObj): def analytical_field_scan( self, Hrange: List[VectorObj], - init_position: List[float] = None, + init_position: Union[List[float], None] = None, max_steps: int = 1e9, learning_rate: float = 1e-4, first_momentum_decay: float = 0.9, diff --git a/core/drivers.hpp b/core/drivers.hpp index 8a1bf3d..b6d9ec0 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -277,6 +277,11 @@ template class ScalarDriver : public Driver { return returnValue; } + CVector getUnitAxis() { + return CVector(1 ? this->constantValue : 0, 1 ? this->constantValue : 0, + 1 ? this->constantValue : 0); + } + // override multiplication operator ScalarDriver operator*(const T &val) { return ScalarDriver(this->update, this->constantValue * val, diff --git a/core/junction.hpp b/core/junction.hpp index 614d88d..5b9adde 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -12,22 +12,22 @@ #define CORE_JUNCTION_HPP_ #define _USE_MATH_DEFINES -#include // for file save -#include // for find_if -#include // for bind, function -#include // for array, array<>::value_type -#include // for seconds, steady_clock, duration -#include // for isnan, M_PI -#include // for string, operator<<, basic_ostream -#include // for mt19937, normal_distribution -#include // for runtime_error, invalid_argument -#include // for operator+, operator==, basic_string -#include // for enable_if<>::type -#include // for unordered_map -#include // for vector, __vector_base<>::value_type -#include "cvector.hpp" // for CVector -#include "drivers.hpp" // for ScalarDriver, AxialDriver -#include "noise.hpp" // for OneFNoise +#include "cvector.hpp" // for CVector +#include "drivers.hpp" // for ScalarDriver, AxialDriver +#include "noise.hpp" // for OneFNoise +#include // for find_if +#include // for array, array<>::value_type +#include // for seconds, steady_clock, duration +#include // for isnan, M_PI +#include // for file save +#include // for bind, function +#include // for string, operator<<, basic_ostream +#include // for mt19937, normal_distribution +#include // for runtime_error, invalid_argument +#include // for operator+, operator==, basic_string +#include // for enable_if<>::type +#include // for unordered_map +#include // for vector, __vector_base<>::value_type #define MAGNETIC_PERMEABILITY 12.57e-7 #define GYRO 220880.0 // rad/Ts converted to m/As @@ -39,1644 +39,1613 @@ typedef CVector DVector; typedef CVector FVector; -double operator"" _ns(unsigned long long timeUnit) -{ - return ((double)timeUnit) / 1e9; -} -double operator"" _ns(long double timeUnit) -{ - return ((double)timeUnit) / 1e9; +double operator"" _ns(unsigned long long timeUnit) { + return ((double)timeUnit) / 1e9; } +double operator"" _ns(long double timeUnit) { return ((double)timeUnit) / 1e9; } -double operator"" _mT(unsigned long long tesla) -{ - return ((double)tesla) / 1000.0; +double operator"" _mT(unsigned long long tesla) { + return ((double)tesla) / 1000.0; } -double operator"" _mT(long double tesla) -{ - return ((double)tesla) / 1000.0; -} +double operator"" _mT(long double tesla) { return ((double)tesla) / 1000.0; } template -inline CVector calculate_tensor_interaction(const CVector& m, - const std::vector>& tensor, - const T& Ms) -{ - CVector res( - tensor[0][0] * m[0] + tensor[0][1] * m[1] + tensor[0][2] * m[2], - tensor[1][0] * m[0] + tensor[1][1] * m[1] + tensor[1][2] * m[2], - tensor[2][0] * m[0] + tensor[2][1] * m[1] + tensor[2][2] * m[2]); - return res * (Ms / MAGNETIC_PERMEABILITY); +inline CVector calculate_tensor_interaction( + const CVector &m, const std::vector> &tensor, const T &Ms) { + CVector res( + tensor[0][0] * m[0] + tensor[0][1] * m[1] + tensor[0][2] * m[2], + tensor[1][0] * m[0] + tensor[1][1] * m[1] + tensor[1][2] * m[2], + tensor[2][0] * m[0] + tensor[2][1] * m[1] + tensor[2][2] * m[2]); + return res * (Ms / MAGNETIC_PERMEABILITY); } template -inline CVector calculate_tensor_interaction(const CVector& m, - const std::array, 3>& tensor, - const T& Ms) -{ - CVector res( - tensor[0][0] * m[0] + tensor[0][1] * m[1] + tensor[0][2] * m[2], - tensor[1][0] * m[0] + tensor[1][1] * m[1] + tensor[1][2] * m[2], - tensor[2][0] * m[0] + tensor[2][1] * m[1] + tensor[2][2] * m[2]); - return res * (Ms / MAGNETIC_PERMEABILITY); +inline CVector calculate_tensor_interaction( + const CVector &m, const std::array, 3> &tensor, const T &Ms) { + CVector res( + tensor[0][0] * m[0] + tensor[0][1] * m[1] + tensor[0][2] * m[2], + tensor[1][0] * m[0] + tensor[1][1] * m[1] + tensor[1][2] * m[2], + tensor[2][0] * m[0] + tensor[2][1] * m[1] + tensor[2][2] * m[2]); + return res * (Ms / MAGNETIC_PERMEABILITY); } template -inline CVector c_cross(const CVector& a, const CVector& b) -{ - CVector res( - a[1] * b[2] - a[2] * b[1], - a[2] * b[0] - a[0] * b[2], - a[0] * b[1] - a[1] * b[0]); - - return res; +inline CVector c_cross(const CVector &a, const CVector &b) { + CVector res(a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], + a[0] * b[1] - a[1] * b[0]); + + return res; } -template -inline T c_dot(const CVector& a, const CVector& b) -{ - return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; +template inline T c_dot(const CVector &a, const CVector &b) { + return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]; } -template -class EnergyDriver -{ +template class EnergyDriver { public: - static T calculateZeemanEnergy(CVector mag, CVector Hext, T cellVolume, T Ms) - { - return -MAGNETIC_PERMEABILITY * Ms * c_dot(mag, Hext) * cellVolume; - } - - static T calculateAnisotropyEnergy(CVector mag, CVector anis, T K, T cellVolume) - { - const T sinSq = 1.0 - pow(c_dot(mag, anis) / (anis.length() * mag.length()), 2); - return K * sinSq * cellVolume; - } - - static T calculateIECEnergy(CVector mag, CVector other, T J, T cellSurface) - { - return -c_dot(mag, other) * J * cellSurface; - } - - static T calculateDemagEnergy(CVector mag, CVector Hdemag, T Ms, T cellVolume) - { - return -0.5 * MAGNETIC_PERMEABILITY * Ms * c_dot(mag, Hdemag) * cellVolume; - } + static T calculateZeemanEnergy(CVector mag, CVector Hext, T cellVolume, + T Ms) { + return -MAGNETIC_PERMEABILITY * Ms * c_dot(mag, Hext) * cellVolume; + } + + static T calculateAnisotropyEnergy(CVector mag, CVector anis, T K, + T cellVolume) { + const T sinSq = + 1.0 - pow(c_dot(mag, anis) / (anis.length() * mag.length()), 2); + return K * sinSq * cellVolume; + } + + static T calculateIECEnergy(CVector mag, CVector other, T J, + T cellSurface) { + return -c_dot(mag, other) * J * cellSurface; + } + + static T calculateDemagEnergy(CVector mag, CVector Hdemag, T Ms, + T cellVolume) { + return -0.5 * MAGNETIC_PERMEABILITY * Ms * c_dot(mag, Hdemag) * + cellVolume; + } }; -enum Reference -{ - NONE = 0, - FIXED, - TOP, - BOTTOM -}; +enum Reference { NONE = 0, FIXED, TOP, BOTTOM }; -enum SolverMode -{ - EULER_HEUN = 0, - RK4 = 1, - DORMAND_PRICE = 2, - HEUN = 3 -}; +enum SolverMode { EULER_HEUN = 0, RK4 = 1, DORMAND_PRICE = 2, HEUN = 3 }; -template -class Layer -{ +template class Layer { private: - - ScalarDriver temperatureDriver; - ScalarDriver IECDriverTop; - ScalarDriver IECDriverBottom; - ScalarDriver IECQuadDriverTop; - ScalarDriver IECQuadDriverBottom; - - ScalarDriver currentDriver; - ScalarDriver anisotropyDriver; - ScalarDriver fieldLikeTorqueDriver; - ScalarDriver dampingLikeTorqueDriver; - AxialDriver externalFieldDriver; - AxialDriver HoeDriver, HdmiDriver; - - bool nonStochasticTempSet = false; - bool nonStochasticOneFSet = true; - bool temperatureSet = false; - bool pinkNoiseSet = false; - bool alternativeSTTSet = false; - Reference referenceType = NONE; - - // the distribution is binded for faster generation - // is also shared between 1/f and Gaussian noise. - std::function distribution = std::bind(std::normal_distribution(0, 1), std::mt19937(std::random_device{}())); - - CVector dWn, dWn2; // one for thermal, one for OneF - Layer( - const std::string& id, - CVector mag, - CVector anis, - T Ms, - T thickness, - T cellSurface, - const std::vector>& demagTensor, - T damping, - T fieldLikeTorque, - T dampingLikeTorque, - T SlonczewskiSpacerLayerParameter, - T beta, - T spinPolarisation) : id(id), - mag(mag), - anis(anis), - Ms(Ms), - thickness(thickness), - cellSurface(cellSurface), - demagTensor(demagTensor), - damping(damping), - fieldLikeTorque(fieldLikeTorque), - dampingLikeTorque(dampingLikeTorque), + ScalarDriver temperatureDriver; + + // CMTJ interaction drivers + ScalarDriver IECDriverTop; + ScalarDriver IECDriverBottom; + ScalarDriver IECQuadDriverTop; + ScalarDriver IECQuadDriverBottom; + AxialDriver IDMIDriverTop; + AxialDriver IDMIDriverBottom; + + // CMTJ Torque & Field drivers + ScalarDriver currentDriver; + ScalarDriver anisotropyDriver; + ScalarDriver fieldLikeTorqueDriver; + ScalarDriver dampingLikeTorqueDriver; + AxialDriver externalFieldDriver; + AxialDriver HoeDriver, HdmiDriver; + + bool nonStochasticTempSet = false; + bool nonStochasticOneFSet = true; + bool temperatureSet = false; + bool pinkNoiseSet = false; + bool alternativeSTTSet = false; + Reference referenceType = NONE; + + // the distribution is binded for faster generation + // is also shared between 1/f and Gaussian noise. + std::function distribution = std::bind( + std::normal_distribution(0, 1), std::mt19937(std::random_device{}())); + + CVector dWn, dWn2; // one for thermal, one for OneF + Layer(const std::string &id, CVector mag, CVector anis, T Ms, + T thickness, T cellSurface, const std::vector> &demagTensor, + T damping, T fieldLikeTorque, T dampingLikeTorque, + T SlonczewskiSpacerLayerParameter, T beta, T spinPolarisation) + : id(id), mag(mag), anis(anis), Ms(Ms), thickness(thickness), + cellSurface(cellSurface), demagTensor(demagTensor), damping(damping), + fieldLikeTorque(fieldLikeTorque), dampingLikeTorque(dampingLikeTorque), SlonczewskiSpacerLayerParameter(SlonczewskiSpacerLayerParameter), - beta(beta), - spinPolarisation(spinPolarisation) - { - if (mag.length() == 0) - { - throw std::runtime_error("Initial magnetisation was set to a zero vector!"); - } - if (anis.length() == 0) - { - throw std::runtime_error("Anisotropy was set to a zero vector!"); - } - // normalise magnetisation - mag.normalize(); - dWn = CVector(this->distribution); - dWn.normalize(); - this->cellVolume = this->cellSurface * this->thickness; - this->ofn = std::shared_ptr>(new OneFNoise(0, 0., 0.)); - } + beta(beta), spinPolarisation(spinPolarisation) { + if (mag.length() == 0) { + throw std::runtime_error( + "Initial magnetisation was set to a zero vector!"); + } + if (anis.length() == 0) { + throw std::runtime_error("Anisotropy was set to a zero vector!"); + } + // normalise magnetisation + mag.normalize(); + dWn = CVector(this->distribution); + dWn.normalize(); + this->cellVolume = this->cellSurface * this->thickness; + this->ofn = std::shared_ptr>(new OneFNoise(0, 0., 0.)); + } public: - struct BufferedNoiseParameters - { - /* data */ - T alphaNoise = 1.0; - T scaleNoise = 0.0; - T stdNoise = 0.0; - Axis axis = Axis::all; - }; - BufferedNoiseParameters noiseParams; - std::shared_ptr> ofn; - std::shared_ptr> bfn; - bool includeSTT = false; - bool includeSOT = false; - - std::string id; - T Ms = 0.0; - - // geometric parameters - T thickness = 0.0; - T cellVolume = 0.0, cellSurface = 0.0; - - CVector H_log, Hoe_log, Hconst, mag, anis, referenceLayer; - CVector Hext, Hdipole, Hdemag, Hoe, HAnis, Hthermal, Hfluctuation, Hdmi; - - CVector Hfl_v, Hdl_v; - - CVector HIEC, HIECtop, HIECbottom; - T Jbottom_log = 0.0, Jtop_log = 0.0; - T J2bottom_log = 0.0, J2top_log = 0.0; - T K_log = 0.0; - T I_log = 0.0; - - // dipole and demag tensors - std::vector> demagTensor; - std::vector> dipoleBottom = std::vector>{ CVector(), CVector(), CVector() }; - std::vector> dipoleTop = std::vector>{ CVector(), CVector(), CVector() }; - - // LLG params - T damping; - - // SOT params - bool dynamicSOT = true; - T fieldLikeTorque; - T dampingLikeTorque; - - // STT params - T SlonczewskiSpacerLayerParameter; - T beta; // usually either set to 0 or to damping - T kappa = 1; // for damping-like off -turning torque - T spinPolarisation; - - T hopt = -1.0; - - Layer() {} - explicit Layer(const std::string& id, - const CVector& mag, - const CVector& anis, - T Ms, - T thickness, - T cellSurface, - const std::vector>& demagTensor, - T damping) : Layer(id, mag, anis, Ms, thickness, cellSurface, - demagTensor, - damping, 0, 0, 0, 0, 0) {} - - /** - * The basic structure is a magnetic layer. - * Its parameters are defined by the constructor and may be altered - * by the drivers during the simulation time. - * If you want STT, remember to set the reference vector for the polarisation of the layer. - * Use `setReferenceLayer` function to do that. - * @param id: identifiable name for a layer -- e.g. "bottom" or "free". - * @param mag: initial magnetisation. Must be normalised (norm of 1). Used for quicker convergence. - * @param anis: anisotropy of the layer. A normalised vector - * @param Ms: magnetisation saturation. Unit: Tesla [T]. - * @param thickness: thickness of the layer. Unit: meter [m]. - * @param cellSurface: surface of the layer, for volume calculation. Unit: meter^2 [m^2]. - * @param demagTensor: demagnetisation tensor of the layer. - * @param damping: often marked as alpha in the LLG equation. Damping of the layer. Default 0.011. Dimensionless. - * @param fieldLikeTorque: [SOT] effective spin Hall angle (spin effectiveness) for Hfl. - * @param dampingLikeTorque: [SOT] effective spin Hall angle (spin effectiveness) for Hdl. - */ - explicit Layer(const std::string& id, - const CVector& mag, - const CVector& anis, - T Ms, - T thickness, - T cellSurface, - const std::vector>& demagTensor, - T damping, - T fieldLikeTorque, - T dampingLikeTorque) : Layer(id, mag, anis, Ms, thickness, cellSurface, - demagTensor, - damping, - fieldLikeTorque, - dampingLikeTorque, 0, 0, 0) - { - this->includeSTT = false; - this->includeSOT = true; - this->dynamicSOT = false; - } - - /** - * The basic structure is a magnetic layer. - * Its parameters are defined by the constructor and may be altered - * by the drivers during the simulation time. - * If you want STT, remember to set the reference vector for the polarisation of the layer. - * Use `setReferenceLayer` function to do that. - * @param id: identifiable name for a layer -- e.g. "bottom" or "free". - * @param mag: initial magnetisation. Must be normalised (norm of 1). Used for quicker convergence. - * @param anis: anisotropy of the layer. A normalised vector - * @param Ms: magnetisation saturation. Unit: Tesla [T]. - * @param thickness: thickness of the layer. Unit: meter [m]. - * @param cellSurface: surface of the layer, for volume calculation. Unit: meter^2 [m^2]. - * @param demagTensor: demagnetisation tensor of the layer. - * @param damping: often marked as alpha in the LLG equation. Damping of the layer. Default 0.011. Dimensionless. - * @param SlomczewskiSpacerLayerParameter: [STT] Slomczewski parameter. Default 1.0. Dimensionless. - * @param beta: [STT] beta parameter for the STT. Default 0.0. Dimensionless. - * @param spinPolarisation: [STT] polarisation ratio while passing through reference layer. - */ - explicit Layer(const std::string& id, - const CVector& mag, - const CVector& anis, - T Ms, - T thickness, - T cellSurface, - const std::vector>& demagTensor, - T damping, - T SlonczewskiSpacerLayerParameter, - T beta, - T spinPolarisation) : Layer(id, mag, anis, Ms, thickness, cellSurface, - demagTensor, - damping, 0, 0, SlonczewskiSpacerLayerParameter, beta, spinPolarisation) - { - this->includeSTT = true; - this->includeSOT = false; - } - - inline static Layer LayerSTT(const std::string& id, - const CVector& mag, - const CVector& anis, - T Ms, - T thickness, - T cellSurface, - const std::vector>& demagTensor, - T damping, - T SlonczewskiSpacerLayerParameter, - T beta, - T spinPolarisation) - { - return Layer( - id, - mag, - anis, - Ms, - thickness, - cellSurface, - demagTensor, - damping, - SlonczewskiSpacerLayerParameter, - beta, - spinPolarisation); - } - - inline static Layer LayerSOT(const std::string& id, - const CVector& mag, - const CVector& anis, - T Ms, - T thickness, - T cellSurface, - const std::vector>& demagTensor, - T damping, - T fieldLikeTorque, - T dampingLikeTorque) - { - return Layer(id, - mag, - anis, - Ms, - thickness, - cellSurface, - demagTensor, - damping, - fieldLikeTorque, - dampingLikeTorque); - } - - /** - * @brief Get the Id object - * - * @return const std::string - */ - const std::string& getId() const { return id; } - /** - * @brief Set the Alternative STT formulation - * - * @param alternativeSTT: True if you want to use the alternative STT formulation. - */ - void setAlternativeSTT(bool alternativeSTT) { this->alternativeSTTSet = alternativeSTT; } - void setKappa(T kappa) { this->kappa = kappa; } - void setTopDipoleTensor(const std::vector>& dipoleTensor) - { - this->dipoleTop = dipoleTensor; - } - - void setBottomDipoleTensor(const std::vector>& dipoleTensor) - { - this->dipoleBottom = dipoleTensor; - } - - const bool hasTemperature() - { - return this->temperatureSet; - } - - void setTemperatureDriver(const ScalarDriver& driver) - { - this->temperatureDriver = driver; - this->temperatureSet = true; - } - - void setNonStochasticLangevinDriver(const ScalarDriver& driver) - { - this->temperatureDriver = driver; - // do not set the SDE flag here - this->temperatureSet = false; - this->nonStochasticTempSet = true; - } - - void setOneFNoise(unsigned int sources, T bias, T scale) { - this->ofn = std::shared_ptr>(new OneFNoise(sources, bias, scale)); - this->pinkNoiseSet = true; - // by default turn it on, but in the stochastic sims, we will have to turn it off - this->nonStochasticOneFSet = true; - } - - void setAlphaNoise(T alpha, T std, T scale, Axis axis = Axis::all) { - if ((alpha < 0) || (alpha > 2)) - throw std::runtime_error("alpha must be between 0 and 2"); - this->noiseParams.alphaNoise = alpha; - this->noiseParams.stdNoise = std; - this->noiseParams.scaleNoise = scale; - this->noiseParams.axis = axis; - this->pinkNoiseSet = true; - } - - void createBufferedAlphaNoise(unsigned int bufferSize) { - if (this->noiseParams.alphaNoise < 0) - throw std::runtime_error("alpha must be set before creating the noise!" - " Use setAlphaNoise function to set the alpha parameter."); - - this->bfn = std::shared_ptr>(new VectorAlphaNoise(bufferSize, - this->noiseParams.alphaNoise, - this->noiseParams.stdNoise, this->noiseParams.scaleNoise, this->noiseParams.axis)); - } - - void setCurrentDriver(const ScalarDriver& driver) - { - this->currentDriver = driver; - } - - void setFieldLikeTorqueDriver(const ScalarDriver& driver) - { - this->includeSOT = true; - if (this->includeSTT) - throw std::runtime_error("includeSTT was on and now setting SOT interaction!"); - if (!this->dynamicSOT) - throw std::runtime_error("used a static SOT definition, now trying to set it dynamically!"); - this->fieldLikeTorqueDriver = driver; - } - - void setDampingLikeTorqueDriver(const ScalarDriver& driver) - { - this->includeSOT = true; - if (this->includeSTT) - throw std::runtime_error("includeSTT was on and now setting SOT interaction!"); - if (!this->dynamicSOT) - throw std::runtime_error("used a static SOT definition, now trying to set it dynamically!"); - this->dampingLikeTorqueDriver = driver; - } - - void setAnisotropyDriver(const ScalarDriver& driver) - { - this->anisotropyDriver = driver; - } - - void setExternalFieldDriver(const AxialDriver& driver) - { - this->externalFieldDriver = driver; - } - void setOerstedFieldDriver(const AxialDriver& driver) - { - this->HoeDriver = driver; - } - - void setMagnetisation(CVector& mag) - { - if (mag.length() == 0) - { - throw std::runtime_error("Initial magnetisation was set to a zero vector!"); - } - this->mag = mag; - this->mag.normalize(); - } - - void setIECDriverBottom(const ScalarDriver& driver) - { - this->IECDriverBottom = driver; - } - - void setIECDriverTop(const ScalarDriver& driver) - { - this->IECDriverTop = driver; - } - - void setQuadIECDriverTop(const ScalarDriver& driver) - { - this->IECQuadDriverTop = driver; - } - - void setQuadIECDriverBottom(const ScalarDriver& driver) - { - this->IECQuadDriverBottom = driver; - } - - void setHdmiDriver(const AxialDriver& driver) - { - this->HdmiDriver = driver; - } - - /** - * @brief Sets reference layer with a custom vector - * Set reference layer parameter. This is for calculating the spin current - * polarisation if `includeSTT` is true. - * @param reference: CVector describing the reference layer. - */ - void setReferenceLayer(const CVector& reference) - { - this->referenceLayer = reference; - this->referenceType = FIXED; - } - - /** - * @brief Set reference layer with enum - * Can be used to refer to other layers in stack as reference - * for this layer. - * @param reference: an enum: FIXED, TOP, BOTTOM, or CUSTOM - */ - void setReferenceLayer(Reference reference) - { - if ((reference == FIXED) && (!this->referenceLayer.length())) - { - throw std::runtime_error("Cannot set fixed polarisation layer to 0!" - " Set reference to NONE to disable reference."); - } - this->referenceType = reference; - } - - - /** - * @brief Get the Reference Layer object - */ - CVector getReferenceLayer() - { - // TODO: return other mags when the reference layer is not fixed. - return this->referenceLayer; - } - - /** - * @brief Get the Reference Layer Type object (enum type is returned) - */ - Reference getReferenceType() - { - return this->referenceType; - } - - const CVector calculateHeff(T time, T timeStep, - const CVector& stepMag, const CVector& bottom, const CVector& top, - const CVector& Hfluctuation = CVector()) - { - this->Hdipole = calculate_tensor_interaction(bottom, this->dipoleBottom, this->Ms) + - calculate_tensor_interaction(top, this->dipoleTop, this->Ms); - return calculateHeffDipoleInjection(time, timeStep, stepMag, bottom, top, this->Hdipole, Hfluctuation); - } - - const CVector calculateHeffDipoleInjection(T time, T timeStep, - const CVector& stepMag, const CVector& bottom, const CVector& top, - const CVector& dipole, const CVector& Hfluctuation) - { - this->Hext = calculateExternalField(time); - this->Hoe = calculateHOeField(time); - - this->Hdemag = calculate_tensor_interaction(stepMag, this->demagTensor, this->Ms); - this->HIEC = calculateIEC(time, stepMag, bottom, top); - this->HAnis = calculateAnisotropy(stepMag, time); - this->Hdmi = calculateHdmiField(time); - const CVector Heff = this->Hext // external - + this->HAnis // anistotropy - + this->HIEC // IEC - + this->Hoe // Oersted field - + this->Hdmi - + Hfluctuation - // demag -- negative contribution - - this->Hdemag - // dipole -- negative contribution - - dipole; - return Heff; - } - - CVector calculateHOeField(const T& time) - { - this->Hoe_log = this->HoeDriver.getCurrentAxialDrivers(time); - return this->Hoe_log; - } - - CVector calculateHdmiField(const T& time) - { - return this->HdmiDriver.getCurrentAxialDrivers(time); - } - - CVector calculateExternalField(const T& time) - { - this->H_log = - this->externalFieldDriver.getCurrentAxialDrivers(time); - return this->H_log; - } - - CVector calculateAnisotropy(const CVector& stepMag, T& time) - { - this->K_log = this->anisotropyDriver.getCurrentScalarValue(time); - const T nom = (2 * this->K_log) * c_dot(this->anis, stepMag) / (this->Ms); - return this->anis * nom; - } - - CVector calculateIEC_(const T J, const T J2, const CVector& stepMag, const CVector& coupledMag) - { - // below an alternative method for computing J -- it's here for reference only. - // const T nom = J / (this->Ms * this->thickness); - // return (coupledMag - stepMag) * nom; // alternative form - // return (coupledMag + coupledMag * 2 * J2 * c_dot(coupledMag, stepMag)) * nom; - return coupledMag * (J + 2 * J2 * c_dot(coupledMag, stepMag)) / (this->Ms * this->thickness); - } - - CVector calculateIEC(T time, const CVector& stepMag, const CVector& bottom, const CVector& top) - { - this->Jbottom_log = this->IECDriverBottom.getCurrentScalarValue(time); - this->Jtop_log = this->IECDriverTop.getCurrentScalarValue(time); - - this->J2bottom_log = this->IECQuadDriverBottom.getCurrentScalarValue(time); - this->J2top_log = this->IECQuadDriverTop.getCurrentScalarValue(time); - - return calculateIEC_(this->Jbottom_log, - this->J2bottom_log, stepMag, bottom) + - calculateIEC_(this->Jtop_log, this->J2top_log, stepMag, top); - } - - - /** - * @brief Main solver function. It is solver-independent (all solvers use this function). - * This function is called by the solver to calculate the next step of the magnetisation. - * It computes implicitly, all torques, given the current magnetisation and effective field. - * @param time the time at which the solver is currently at. - * @param m the current magnetisation (from the solver, may be a semi-step) - * @param timeStep integration time - * @param bottom magnetisation of the layer below - * @param top magnetisation of the layer above - * @param heff the effective field - * @return const CVector magnetisation after the step - */ - const CVector solveLLG(T time, const CVector& m, T timeStep, - const CVector& bottom, const CVector& top, const CVector& heff) - { - const CVector prod = c_cross(m, heff); - const CVector prod2 = c_cross(m, prod); - const T convTerm = 1 / (1 + pow(this->damping, 2)); // LLGS -> LL form - const CVector dmdt = prod + prod2 * this->damping; - CVector reference; - - // decide what is to be the reference for (s)LLG-STT - // dynamically substitute other active layers - switch (this->referenceType) - { - // TODO: add the warning if reference layer is top/bottom and empty - case FIXED: - reference = this->referenceLayer; - break; - case TOP: - reference = top; - break; - case BOTTOM: - reference = bottom; - break; - default: - break; - } - - // extra terms - if (this->includeSTT) - { - this->I_log = this->currentDriver.getCurrentScalarValue(time); - // use standard STT formulation - // see that literature reports Ms/MAGNETIC_PERMEABILITY - // but then the units don't match, we use Ms [T] which works - const T aJ = HBAR * this->I_log / - (ELECTRON_CHARGE * this->Ms * this->thickness); - // field like - T eta = 0; - if (this->alternativeSTTSet) { - // this is simplified - eta = (this->spinPolarisation) / (1 + this->SlonczewskiSpacerLayerParameter * c_dot(m, reference)); - } - else { - // this is more complex model (classical STT) - const T slonSq = pow(this->SlonczewskiSpacerLayerParameter, 2); - eta = (this->spinPolarisation * slonSq) / (slonSq + 1 + (slonSq - 1) * c_dot(m, reference)); - } - const T sttTerm = GYRO * aJ * eta; - const CVector fieldLike = c_cross(m, reference); - // damping like - const CVector dampingLike = c_cross(m, fieldLike); - return (dmdt * -GYRO + dampingLike * -sttTerm * this->kappa + fieldLike * sttTerm * this->beta) * convTerm; - } - else if (this->includeSOT) - { - T Hdl, Hfl; - // I log current density - // use SOT formulation with effective DL and FL fields - if (this->dynamicSOT) - { - // dynamic SOT is set when the driver is present - Hdl = this->dampingLikeTorqueDriver.getCurrentScalarValue(time); - Hfl = this->fieldLikeTorqueDriver.getCurrentScalarValue(time); - } - else - { - this->I_log = this->currentDriver.getCurrentScalarValue(time); - Hdl = this->dampingLikeTorque * this->I_log; - Hfl = this->fieldLikeTorque * this->I_log; - } - this->Hfl_v = reference * (Hfl - this->damping * Hdl); - this->Hdl_v = reference * (Hdl + this->damping * Hfl); - const CVector cm = c_cross(m, reference); - const CVector ccm = c_cross(m, cm); - const CVector flTorque = cm * (Hfl - this->damping * Hdl); - const CVector dlTorque = ccm * (Hdl + this->damping * Hfl); - return (dmdt + flTorque + dlTorque) * -GYRO * convTerm; - } - return dmdt * -GYRO * convTerm; - } - - /** - * @brief Assumes the dW has the scale of sqrt(timeStep). - * - * @param currentMag - * @param dW - stochastic vector already scaled properly - * @return CVector - */ - CVector stochasticTorque(const CVector& currentMag, const CVector& dW) { - - const T convTerm = -GYRO / (1. + pow(this->damping, 2)); - const CVector thcross = c_cross(currentMag, dW); - const CVector thcross2 = c_cross(currentMag, thcross); - return (thcross + thcross2 * this->damping) * convTerm; - } - - const CVector calculateLLGWithFieldTorqueDipoleInjection(T time, const CVector& m, - const CVector& bottom, const CVector& top, - const CVector& dipole, T timeStep, const CVector& Hfluctuation = CVector()) - { - // classic LLG first - const CVector heff = calculateHeffDipoleInjection(time, timeStep, m, bottom, top, dipole, Hfluctuation); - return solveLLG(time, m, timeStep, bottom, top, heff); - } - - /** - * Compute the LLG time step. The efficient field vectors is calculated implicitly here. - * Use the effective spin hall angles formulation for SOT interaction. - * @param time: current simulation time. - * @param m: current RK45 magnetisation. - * @param bottom: layer below the current layer (current layer's magnetisation is m). For IEC interaction. - * @param top: layer above the current layer (current layer's magnetisation is m). For IEC interaction. - * @param timeStep: RK45 integration step. - */ - const CVector calculateLLGWithFieldTorque(T time, const CVector& m, const CVector& bottom, - const CVector& top, T timeStep, const CVector& Hfluctuation = CVector()) - { - // classic LLG first - const CVector heff = calculateHeff(time, timeStep, m, bottom, top, Hfluctuation); - return solveLLG(time, m, timeStep, bottom, top, heff); - } - - - /** - * @brief RK4 step of the LLG equation. - * Compute the LLG time step. The efficient field vectors is calculated implicitly here. - * Use the effective spin hall angles formulation for SOT interaction. - * @param time: current simulation time. - * @param m: current RK45 magnetisation. - * @param bottom: layer below the current layer (current layer's magnetisation is m). For IEC interaction. - * @param top: layer above the current layer (current layer's magnetisation is m). For IEC interaction. - * @param timeStep: RK45 integration step. - */ - void rk4_step(T time, T timeStep, const CVector& bottom, const CVector& top) - { - CVector m_t = this->mag; - const CVector k1 = calculateLLGWithFieldTorque(time, m_t, bottom, top, timeStep) * timeStep; - const CVector k2 = calculateLLGWithFieldTorque(time + 0.5 * timeStep, m_t + k1 * 0.5, bottom, top, timeStep) * timeStep; - const CVector k3 = calculateLLGWithFieldTorque(time + 0.5 * timeStep, m_t + k2 * 0.5, bottom, top, timeStep) * timeStep; - const CVector k4 = calculateLLGWithFieldTorque(time + timeStep, m_t + k3, bottom, top, timeStep) * timeStep; - m_t = m_t + (k1 + (k2 * 2.0) + (k3 * 2.0) + k4) / 6.0; - m_t.normalize(); - this->mag = m_t; - if (isnan(this->mag.x)) - { - throw std::runtime_error("NAN magnetisation"); - } - } - - /** - * @brief RK4 step of the LLG equation if dipole injection is present. - * Compute the LLG time step. The efficient field vectors is calculated implicitly here. - * Use the effective spin hall angles formulation for SOT interaction. - * @param time: current simulation time. - * @param m: current RK45 magnetisation. - * @param bottom: layer below the current layer (current layer's magnetisation is m). For IEC interaction. - * @param top: layer above the current layer (current layer's magnetisation is m). For IEC interaction. - * @param timeStep: RK45 integration step. - */ - void rk4_stepDipoleInjection(T time, T timeStep, const CVector& bottom, const CVector& top, const CVector& dipole) - { - CVector m_t = this->mag; - const CVector k1 = calculateLLGWithFieldTorqueDipoleInjection(time, m_t, bottom, top, dipole, timeStep) * timeStep; - const CVector k2 = calculateLLGWithFieldTorqueDipoleInjection(time + 0.5 * timeStep, m_t + k1 * 0.5, bottom, top, dipole, timeStep) * timeStep; - const CVector k3 = calculateLLGWithFieldTorqueDipoleInjection(time + 0.5 * timeStep, m_t + k2 * 0.5, bottom, top, dipole, timeStep) * timeStep; - const CVector k4 = calculateLLGWithFieldTorqueDipoleInjection(time + timeStep, m_t + k3, bottom, top, dipole, timeStep) * timeStep; - m_t = m_t + (k1 + (k2 * 2.0) + (k3 * 2.0) + k4) / 6.0; - m_t.normalize(); - this->mag = m_t; - } - - - CVector stochastic_llg(const CVector& cm, T time, T timeStep, - const CVector& bottom, const CVector& top, const CVector& dW, const CVector& dW2, const T& HoneF) - { - // compute the Langevin fluctuations -- this is the sigma - const T convTerm = -GYRO / (1 + pow(this->damping, 2)); - const T Hthermal_temp = this->getLangevinStochasticStandardDeviation(time, timeStep); - const CVector thcross = c_cross(cm, dW); - const CVector thcross2 = c_cross(thcross, dW); - const T scalingTh = Hthermal_temp * convTerm; - - // compute 1/f noise term - const CVector onefcross = c_cross(cm, dW2); - const CVector onefcross2 = c_cross(onefcross, dW2); - const T scalingOneF = HoneF * convTerm; - - return (thcross + thcross2 * this->damping) * scalingTh + (onefcross + onefcross2 * this->damping) * scalingOneF; - } - - const T getStochasticOneFNoise(T time) { - if (!this->pinkNoiseSet) - return 0; - else if (this->noiseParams.scaleNoise != 0) { - // use buffered noise if available - return this->bfn->tick(); - } - return this->ofn->tick(); - } - - T getLangevinStochasticStandardDeviation(T time, T timeStep) - { - if (this->cellVolume == 0.0) - throw std::runtime_error("Cell surface cannot be 0 during temp. calculations!"); - const T currentTemp = this->temperatureDriver.getCurrentScalarValue(time); - const T mainFactor = (2 * this->damping * BOLTZMANN_CONST * currentTemp) / (this->Ms * this->cellVolume * GYRO); - return sqrt(mainFactor); - } - - CVector getStochasticLangevinVector(const T& time, const T& timeStep) - { - if (!this->temperatureSet) - return CVector(); - const T Hthermal_temp = this->getLangevinStochasticStandardDeviation(time, timeStep); - const CVector dW = CVector(this->distribution); - return dW * Hthermal_temp; - } - - CVector getOneFVector() { - if (this->noiseParams.scaleNoise != 0) { - // use buffered noise if available - return this->bfn->tickVector(); - } - return CVector(); - } + struct BufferedNoiseParameters { + /* data */ + T alphaNoise = 1.0; + T scaleNoise = 0.0; + T stdNoise = 0.0; + Axis axis = Axis::all; + }; + BufferedNoiseParameters noiseParams; + std::shared_ptr> ofn; + std::shared_ptr> bfn; + bool includeSTT = false; + bool includeSOT = false; + + std::string id; + T Ms = 0.0; + + // geometric parameters + T thickness = 0.0; + T cellVolume = 0.0, cellSurface = 0.0; + + CVector H_log, Hoe_log, Hconst, mag, anis, referenceLayer; + CVector Hext, Hdipole, Hdemag, Hoe, HAnis, Hthermal, Hfluctuation, Hdmi; + + CVector Hfl_v, Hdl_v; + + CVector HIEC, HIECtop, HIECbottom; + T Jbottom_log = 0.0, Jtop_log = 0.0; + T J2bottom_log = 0.0, J2top_log = 0.0; + T K_log = 0.0; + T I_log = 0.0; + + // dipole and demag tensors + std::vector> demagTensor; + std::vector> dipoleBottom = + std::vector>{CVector(), CVector(), CVector()}; + std::vector> dipoleTop = + std::vector>{CVector(), CVector(), CVector()}; + + // LLG params + T damping; + + // SOT params + bool dynamicSOT = true; + T fieldLikeTorque; + T dampingLikeTorque; + + // STT params + T SlonczewskiSpacerLayerParameter; + T beta; // usually either set to 0 or to damping + T kappa = 1; // for damping-like off -turning torque + T spinPolarisation; + + T hopt = -1.0; + + Layer() {} + explicit Layer(const std::string &id, const CVector &mag, + const CVector &anis, T Ms, T thickness, T cellSurface, + const std::vector> &demagTensor, T damping) + : Layer(id, mag, anis, Ms, thickness, cellSurface, demagTensor, damping, + 0, 0, 0, 0, 0) {} + + /** + * The basic structure is a magnetic layer. + * Its parameters are defined by the constructor and may be altered + * by the drivers during the simulation time. + * If you want STT, remember to set the reference vector for the polarisation + * of the layer. Use `setReferenceLayer` function to do that. + * @param id: identifiable name for a layer -- e.g. "bottom" or "free". + * @param mag: initial magnetisation. Must be normalised (norm of 1). Used for + * quicker convergence. + * @param anis: anisotropy of the layer. A normalised vector + * @param Ms: magnetisation saturation. Unit: Tesla [T]. + * @param thickness: thickness of the layer. Unit: meter [m]. + * @param cellSurface: surface of the layer, for volume calculation. Unit: + * meter^2 [m^2]. + * @param demagTensor: demagnetisation tensor of the layer. + * @param damping: often marked as alpha in the LLG equation. Damping of the + * layer. Default 0.011. Dimensionless. + * @param fieldLikeTorque: [SOT] effective spin Hall angle (spin + * effectiveness) for Hfl. + * @param dampingLikeTorque: [SOT] effective spin Hall angle (spin + * effectiveness) for Hdl. + */ + explicit Layer(const std::string &id, const CVector &mag, + const CVector &anis, T Ms, T thickness, T cellSurface, + const std::vector> &demagTensor, T damping, + T fieldLikeTorque, T dampingLikeTorque) + : Layer(id, mag, anis, Ms, thickness, cellSurface, demagTensor, damping, + fieldLikeTorque, dampingLikeTorque, 0, 0, 0) { + this->includeSTT = false; + this->includeSOT = true; + this->dynamicSOT = false; + } + + /** + * The basic structure is a magnetic layer. + * Its parameters are defined by the constructor and may be altered + * by the drivers during the simulation time. + * If you want STT, remember to set the reference vector for the polarisation + * of the layer. Use `setReferenceLayer` function to do that. + * @param id: identifiable name for a layer -- e.g. "bottom" or "free". + * @param mag: initial magnetisation. Must be normalised (norm of 1). Used for + * quicker convergence. + * @param anis: anisotropy of the layer. A normalised vector + * @param Ms: magnetisation saturation. Unit: Tesla [T]. + * @param thickness: thickness of the layer. Unit: meter [m]. + * @param cellSurface: surface of the layer, for volume calculation. Unit: + * meter^2 [m^2]. + * @param demagTensor: demagnetisation tensor of the layer. + * @param damping: often marked as alpha in the LLG equation. Damping of the + * layer. Default 0.011. Dimensionless. + * @param SlomczewskiSpacerLayerParameter: [STT] Slomczewski parameter. + * Default 1.0. Dimensionless. + * @param beta: [STT] beta parameter for the STT. Default 0.0. Dimensionless. + * @param spinPolarisation: [STT] polarisation ratio while passing through + * reference layer. + */ + explicit Layer(const std::string &id, const CVector &mag, + const CVector &anis, T Ms, T thickness, T cellSurface, + const std::vector> &demagTensor, T damping, + T SlonczewskiSpacerLayerParameter, T beta, T spinPolarisation) + : Layer(id, mag, anis, Ms, thickness, cellSurface, demagTensor, damping, + 0, 0, SlonczewskiSpacerLayerParameter, beta, spinPolarisation) { + this->includeSTT = true; + this->includeSOT = false; + } + + inline static Layer LayerSTT(const std::string &id, const CVector &mag, + const CVector &anis, T Ms, T thickness, + T cellSurface, + const std::vector> &demagTensor, + T damping, T SlonczewskiSpacerLayerParameter, + T beta, T spinPolarisation) { + return Layer(id, mag, anis, Ms, thickness, cellSurface, demagTensor, + damping, SlonczewskiSpacerLayerParameter, beta, + spinPolarisation); + } + + inline static Layer LayerSOT(const std::string &id, const CVector &mag, + const CVector &anis, T Ms, T thickness, + T cellSurface, + const std::vector> &demagTensor, + T damping, T fieldLikeTorque, + T dampingLikeTorque) { + return Layer(id, mag, anis, Ms, thickness, cellSurface, demagTensor, + damping, fieldLikeTorque, dampingLikeTorque); + } + + /** + * @brief Get the Id object + * + * @return const std::string + */ + const std::string &getId() const { return id; } + /** + * @brief Set the Alternative STT formulation + * + * @param alternativeSTT: True if you want to use the alternative STT + * formulation. + */ + void setAlternativeSTT(bool alternativeSTT) { + this->alternativeSTTSet = alternativeSTT; + } + void setKappa(T kappa) { this->kappa = kappa; } + void setTopDipoleTensor(const std::vector> &dipoleTensor) { + this->dipoleTop = dipoleTensor; + } + + void setBottomDipoleTensor(const std::vector> &dipoleTensor) { + this->dipoleBottom = dipoleTensor; + } + + const bool hasTemperature() { return this->temperatureSet; } + + void setTemperatureDriver(const ScalarDriver &driver) { + this->temperatureDriver = driver; + this->temperatureSet = true; + } + + void setNonStochasticLangevinDriver(const ScalarDriver &driver) { + this->temperatureDriver = driver; + // do not set the SDE flag here + this->temperatureSet = false; + this->nonStochasticTempSet = true; + } + + void setOneFNoise(unsigned int sources, T bias, T scale) { + this->ofn = + std::shared_ptr>(new OneFNoise(sources, bias, scale)); + this->pinkNoiseSet = true; + // by default turn it on, but in the stochastic sims, we will have to turn + // it off + this->nonStochasticOneFSet = true; + } + + void setAlphaNoise(T alpha, T std, T scale, Axis axis = Axis::all) { + if ((alpha < 0) || (alpha > 2)) + throw std::runtime_error("alpha must be between 0 and 2"); + this->noiseParams.alphaNoise = alpha; + this->noiseParams.stdNoise = std; + this->noiseParams.scaleNoise = scale; + this->noiseParams.axis = axis; + this->pinkNoiseSet = true; + } + + void createBufferedAlphaNoise(unsigned int bufferSize) { + if (this->noiseParams.alphaNoise < 0) + throw std::runtime_error( + "alpha must be set before creating the noise!" + " Use setAlphaNoise function to set the alpha parameter."); + + this->bfn = std::shared_ptr>(new VectorAlphaNoise( + bufferSize, this->noiseParams.alphaNoise, this->noiseParams.stdNoise, + this->noiseParams.scaleNoise, this->noiseParams.axis)); + } + + void setCurrentDriver(const ScalarDriver &driver) { + this->currentDriver = driver; + } + + void setFieldLikeTorqueDriver(const ScalarDriver &driver) { + this->includeSOT = true; + if (this->includeSTT) + throw std::runtime_error( + "includeSTT was on and now setting SOT interaction!"); + if (!this->dynamicSOT) + throw std::runtime_error( + "used a static SOT definition, now trying to set it dynamically!"); + this->fieldLikeTorqueDriver = driver; + } + + void setDampingLikeTorqueDriver(const ScalarDriver &driver) { + this->includeSOT = true; + if (this->includeSTT) + throw std::runtime_error( + "includeSTT was on and now setting SOT interaction!"); + if (!this->dynamicSOT) + throw std::runtime_error( + "used a static SOT definition, now trying to set it dynamically!"); + this->dampingLikeTorqueDriver = driver; + } + + void setAnisotropyDriver(const ScalarDriver &driver) { + this->anisotropyDriver = driver; + } + + void setExternalFieldDriver(const AxialDriver &driver) { + this->externalFieldDriver = driver; + } + void setOerstedFieldDriver(const AxialDriver &driver) { + this->HoeDriver = driver; + } + + void setMagnetisation(CVector &mag) { + if (mag.length() == 0) { + throw std::runtime_error( + "Initial magnetisation was set to a zero vector!"); + } + this->mag = mag; + this->mag.normalize(); + } + + void setIECDriverBottom(const ScalarDriver &driver) { + this->IECDriverBottom = driver; + } + + void setIECDriverTop(const ScalarDriver &driver) { + this->IECDriverTop = driver; + } + + void setQuadIECDriverTop(const ScalarDriver &driver) { + this->IECQuadDriverTop = driver; + } + + void setQuadIECDriverBottom(const ScalarDriver &driver) { + this->IECQuadDriverBottom = driver; + } + + void setIDMIDriverTop(const AxialDriver &driver) { + this->IDMIDriverTop = driver; + } + + void setIDMIDriverBottom(const AxialDriver &driver) { + this->IDMIDriverBottom = driver; + } + + void setHdmiDriver(const AxialDriver &driver) { + this->HdmiDriver = driver; + } + + /** + * @brief Sets reference layer with a custom vector + * Set reference layer parameter. This is for calculating the spin current + * polarisation if `includeSTT` is true. + * @param reference: CVector describing the reference layer. + */ + void setReferenceLayer(const CVector &reference) { + this->referenceLayer = reference; + this->referenceType = FIXED; + } + + /** + * @brief Set reference layer with enum + * Can be used to refer to other layers in stack as reference + * for this layer. + * @param reference: an enum: FIXED, TOP, BOTTOM, or CUSTOM + */ + void setReferenceLayer(Reference reference) { + if ((reference == FIXED) && (!this->referenceLayer.length())) { + throw std::runtime_error("Cannot set fixed polarisation layer to 0!" + " Set reference to NONE to disable reference."); + } + this->referenceType = reference; + } + + /** + * @brief Get the Reference Layer object + */ + CVector getReferenceLayer() { + // TODO: return other mags when the reference layer is not fixed. + return this->referenceLayer; + } + + /** + * @brief Get the Reference Layer Type object (enum type is returned) + */ + Reference getReferenceType() { return this->referenceType; } + + const CVector + calculateHeff(T time, T timeStep, const CVector &stepMag, + const CVector &bottom, const CVector &top, + const CVector &Hfluctuation = CVector()) { + this->Hdipole = + calculate_tensor_interaction(bottom, this->dipoleBottom, this->Ms) + + calculate_tensor_interaction(top, this->dipoleTop, this->Ms); + return calculateHeffDipoleInjection(time, timeStep, stepMag, bottom, top, + this->Hdipole, Hfluctuation); + } + + const CVector + calculateHeffDipoleInjection(T time, T timeStep, const CVector &stepMag, + const CVector &bottom, const CVector &top, + const CVector &dipole, + const CVector &Hfluctuation) { + this->Hext = calculateExternalField(time); + this->Hoe = calculateHOeField(time); + + this->Hdemag = + calculate_tensor_interaction(stepMag, this->demagTensor, this->Ms); + this->HIEC = calculateIEC(time, stepMag, bottom, top); + this->Hdmi = calculateIDMI(time, stepMag, bottom, top); + this->HAnis = calculateAnisotropy(stepMag, time); + this->Hdmi = calculateHdmiField(time); + const CVector Heff = this->Hext // external + + this->HAnis // anistotropy + + this->HIEC // IEC + + this->Hoe // Oersted field + + this->Hdmi + + Hfluctuation + // demag -- negative contribution + - this->Hdemag + // dipole -- negative contribution + - dipole; + return Heff; + } + + CVector calculateHOeField(const T &time) { + this->Hoe_log = this->HoeDriver.getCurrentAxialDrivers(time); + return this->Hoe_log; + } + + CVector calculateHdmiField(const T &time) { + return this->HdmiDriver.getCurrentAxialDrivers(time); + } + + CVector calculateExternalField(const T &time) { + this->H_log = this->externalFieldDriver.getCurrentAxialDrivers(time); + return this->H_log; + } + + CVector calculateAnisotropy(const CVector &stepMag, T &time) { + this->K_log = this->anisotropyDriver.getCurrentScalarValue(time); + const T nom = + (2 * this->K_log) * c_dot(this->anis, stepMag) / (this->Ms); + return this->anis * nom; + } + + CVector calculateIEC_(const T J, const T J2, const CVector &stepMag, + const CVector &coupledMag) { + // below an alternative method for computing J -- it's here for reference + // only. const T nom = J / (this->Ms * this->thickness); return (coupledMag + // - stepMag) * nom; // alternative form return (coupledMag + coupledMag * 2 + // * J2 * c_dot(coupledMag, stepMag)) * nom; + return coupledMag * (J + 2 * J2 * c_dot(coupledMag, stepMag)) / + (this->Ms * this->thickness); + } + + CVector calculateIEC(T time, const CVector &stepMag, + const CVector &bottom, const CVector &top) { + this->Jbottom_log = this->IECDriverBottom.getCurrentScalarValue(time); + this->Jtop_log = this->IECDriverTop.getCurrentScalarValue(time); + + this->J2bottom_log = this->IECQuadDriverBottom.getCurrentScalarValue(time); + this->J2top_log = this->IECQuadDriverTop.getCurrentScalarValue(time); + + return calculateIEC_(this->Jbottom_log, this->J2bottom_log, stepMag, + bottom) + + calculateIEC_(this->Jtop_log, this->J2top_log, stepMag, top); + } + + CVector calculateIDMI_(const CVector &Dvalue, const CVector &stepMag, + const CVector &coupledMag) { + const CVector Dunit(1 ? Dvalue.x : 0, 1 ? Dvalue.y : 0, + 1 ? Dvalue.z : 0); + return Dunit * c_dot(Dvalue, c_cross(CVector(1., 1., 1.), stepMag)); + } + + CVector calculateIDMI(T time, const CVector &stepMag, + const CVector &bottom, const CVector &top) { + return calculateIDMI_(this->IDMIDriverBottom.getCurrentAxialDrivers(time), + stepMag, bottom) + + calculateIDMI_(this->IDMIDriverTop.getCurrentAxialDrivers(time), + stepMag, top); + } + + /** + * @brief Main solver function. It is solver-independent (all solvers use this + * function). This function is called by the solver to calculate the next step + * of the magnetisation. It computes implicitly, all torques, given the + * current magnetisation and effective field. + * @param time the time at which the solver is currently at. + * @param m the current magnetisation (from the solver, may be a semi-step) + * @param timeStep integration time + * @param bottom magnetisation of the layer below + * @param top magnetisation of the layer above + * @param heff the effective field + * @return const CVector magnetisation after the step + */ + const CVector solveLLG(T time, const CVector &m, T timeStep, + const CVector &bottom, const CVector &top, + const CVector &heff) { + const CVector prod = c_cross(m, heff); + const CVector prod2 = c_cross(m, prod); + const T convTerm = 1 / (1 + pow(this->damping, 2)); // LLGS -> LL form + const CVector dmdt = prod + prod2 * this->damping; + CVector reference; + + // decide what is to be the reference for (s)LLG-STT + // dynamically substitute other active layers + switch (this->referenceType) { + // TODO: add the warning if reference layer is top/bottom and empty + case FIXED: + reference = this->referenceLayer; + break; + case TOP: + reference = top; + break; + case BOTTOM: + reference = bottom; + break; + default: + break; + } + + // extra terms + if (this->includeSTT) { + this->I_log = this->currentDriver.getCurrentScalarValue(time); + // use standard STT formulation + // see that literature reports Ms/MAGNETIC_PERMEABILITY + // but then the units don't match, we use Ms [T] which works + const T aJ = + HBAR * this->I_log / (ELECTRON_CHARGE * this->Ms * this->thickness); + // field like + T eta = 0; + if (this->alternativeSTTSet) { + // this is simplified + eta = (this->spinPolarisation) / + (1 + + this->SlonczewskiSpacerLayerParameter * c_dot(m, reference)); + } else { + // this is more complex model (classical STT) + const T slonSq = pow(this->SlonczewskiSpacerLayerParameter, 2); + eta = (this->spinPolarisation * slonSq) / + (slonSq + 1 + (slonSq - 1) * c_dot(m, reference)); + } + const T sttTerm = GYRO * aJ * eta; + const CVector fieldLike = c_cross(m, reference); + // damping like + const CVector dampingLike = c_cross(m, fieldLike); + return (dmdt * -GYRO + dampingLike * -sttTerm * this->kappa + + fieldLike * sttTerm * this->beta) * + convTerm; + } else if (this->includeSOT) { + T Hdl, Hfl; + // I log current density + // use SOT formulation with effective DL and FL fields + if (this->dynamicSOT) { + // dynamic SOT is set when the driver is present + Hdl = this->dampingLikeTorqueDriver.getCurrentScalarValue(time); + Hfl = this->fieldLikeTorqueDriver.getCurrentScalarValue(time); + } else { + this->I_log = this->currentDriver.getCurrentScalarValue(time); + Hdl = this->dampingLikeTorque * this->I_log; + Hfl = this->fieldLikeTorque * this->I_log; + } + this->Hfl_v = reference * (Hfl - this->damping * Hdl); + this->Hdl_v = reference * (Hdl + this->damping * Hfl); + const CVector cm = c_cross(m, reference); + const CVector ccm = c_cross(m, cm); + const CVector flTorque = cm * (Hfl - this->damping * Hdl); + const CVector dlTorque = ccm * (Hdl + this->damping * Hfl); + return (dmdt + flTorque + dlTorque) * -GYRO * convTerm; + } + return dmdt * -GYRO * convTerm; + } + + /** + * @brief Assumes the dW has the scale of sqrt(timeStep). + * + * @param currentMag + * @param dW - stochastic vector already scaled properly + * @return CVector + */ + CVector stochasticTorque(const CVector ¤tMag, + const CVector &dW) { + + const T convTerm = -GYRO / (1. + pow(this->damping, 2)); + const CVector thcross = c_cross(currentMag, dW); + const CVector thcross2 = c_cross(currentMag, thcross); + return (thcross + thcross2 * this->damping) * convTerm; + } + + const CVector calculateLLGWithFieldTorqueDipoleInjection( + T time, const CVector &m, const CVector &bottom, + const CVector &top, const CVector &dipole, T timeStep, + const CVector &Hfluctuation = CVector()) { + // classic LLG first + const CVector heff = calculateHeffDipoleInjection( + time, timeStep, m, bottom, top, dipole, Hfluctuation); + return solveLLG(time, m, timeStep, bottom, top, heff); + } + + /** + * Compute the LLG time step. The efficient field vectors is calculated + * implicitly here. Use the effective spin hall angles formulation for SOT + * interaction. + * @param time: current simulation time. + * @param m: current RK45 magnetisation. + * @param bottom: layer below the current layer (current layer's magnetisation + * is m). For IEC interaction. + * @param top: layer above the current layer (current layer's magnetisation is + * m). For IEC interaction. + * @param timeStep: RK45 integration step. + */ + const CVector + calculateLLGWithFieldTorque(T time, const CVector &m, + const CVector &bottom, const CVector &top, + T timeStep, + const CVector &Hfluctuation = CVector()) { + // classic LLG first + const CVector heff = + calculateHeff(time, timeStep, m, bottom, top, Hfluctuation); + return solveLLG(time, m, timeStep, bottom, top, heff); + } + + /** + * @brief RK4 step of the LLG equation. + * Compute the LLG time step. The efficient field vectors is calculated + * implicitly here. Use the effective spin hall angles formulation for SOT + * interaction. + * @param time: current simulation time. + * @param m: current RK45 magnetisation. + * @param bottom: layer below the current layer (current layer's magnetisation + * is m). For IEC interaction. + * @param top: layer above the current layer (current layer's magnetisation is + * m). For IEC interaction. + * @param timeStep: RK45 integration step. + */ + void rk4_step(T time, T timeStep, const CVector &bottom, + const CVector &top) { + CVector m_t = this->mag; + const CVector k1 = + calculateLLGWithFieldTorque(time, m_t, bottom, top, timeStep) * + timeStep; + const CVector k2 = + calculateLLGWithFieldTorque(time + 0.5 * timeStep, m_t + k1 * 0.5, + bottom, top, timeStep) * + timeStep; + const CVector k3 = + calculateLLGWithFieldTorque(time + 0.5 * timeStep, m_t + k2 * 0.5, + bottom, top, timeStep) * + timeStep; + const CVector k4 = calculateLLGWithFieldTorque(time + timeStep, m_t + k3, + bottom, top, timeStep) * + timeStep; + m_t = m_t + (k1 + (k2 * 2.0) + (k3 * 2.0) + k4) / 6.0; + m_t.normalize(); + this->mag = m_t; + if (isnan(this->mag.x)) { + throw std::runtime_error("NAN magnetisation"); + } + } + + /** + * @brief RK4 step of the LLG equation if dipole injection is present. + * Compute the LLG time step. The efficient field vectors is calculated + * implicitly here. Use the effective spin hall angles formulation for SOT + * interaction. + * @param time: current simulation time. + * @param m: current RK45 magnetisation. + * @param bottom: layer below the current layer (current layer's magnetisation + * is m). For IEC interaction. + * @param top: layer above the current layer (current layer's magnetisation is + * m). For IEC interaction. + * @param timeStep: RK45 integration step. + */ + void rk4_stepDipoleInjection(T time, T timeStep, const CVector &bottom, + const CVector &top, + const CVector &dipole) { + CVector m_t = this->mag; + const CVector k1 = calculateLLGWithFieldTorqueDipoleInjection( + time, m_t, bottom, top, dipole, timeStep) * + timeStep; + const CVector k2 = calculateLLGWithFieldTorqueDipoleInjection( + time + 0.5 * timeStep, m_t + k1 * 0.5, bottom, + top, dipole, timeStep) * + timeStep; + const CVector k3 = calculateLLGWithFieldTorqueDipoleInjection( + time + 0.5 * timeStep, m_t + k2 * 0.5, bottom, + top, dipole, timeStep) * + timeStep; + const CVector k4 = + calculateLLGWithFieldTorqueDipoleInjection( + time + timeStep, m_t + k3, bottom, top, dipole, timeStep) * + timeStep; + m_t = m_t + (k1 + (k2 * 2.0) + (k3 * 2.0) + k4) / 6.0; + m_t.normalize(); + this->mag = m_t; + } + + CVector stochastic_llg(const CVector &cm, T time, T timeStep, + const CVector &bottom, const CVector &top, + const CVector &dW, const CVector &dW2, + const T &HoneF) { + // compute the Langevin fluctuations -- this is the sigma + const T convTerm = -GYRO / (1 + pow(this->damping, 2)); + const T Hthermal_temp = + this->getLangevinStochasticStandardDeviation(time, timeStep); + const CVector thcross = c_cross(cm, dW); + const CVector thcross2 = c_cross(thcross, dW); + const T scalingTh = Hthermal_temp * convTerm; + + // compute 1/f noise term + const CVector onefcross = c_cross(cm, dW2); + const CVector onefcross2 = c_cross(onefcross, dW2); + const T scalingOneF = HoneF * convTerm; + + return (thcross + thcross2 * this->damping) * scalingTh + + (onefcross + onefcross2 * this->damping) * scalingOneF; + } + + const T getStochasticOneFNoise(T time) { + if (!this->pinkNoiseSet) + return 0; + else if (this->noiseParams.scaleNoise != 0) { + // use buffered noise if available + return this->bfn->tick(); + } + return this->ofn->tick(); + } + + T getLangevinStochasticStandardDeviation(T time, T timeStep) { + if (this->cellVolume == 0.0) + throw std::runtime_error( + "Cell surface cannot be 0 during temp. calculations!"); + const T currentTemp = this->temperatureDriver.getCurrentScalarValue(time); + const T mainFactor = (2 * this->damping * BOLTZMANN_CONST * currentTemp) / + (this->Ms * this->cellVolume * GYRO); + return sqrt(mainFactor); + } + + CVector getStochasticLangevinVector(const T &time, const T &timeStep) { + if (!this->temperatureSet) + return CVector(); + const T Hthermal_temp = + this->getLangevinStochasticStandardDeviation(time, timeStep); + const CVector dW = CVector(this->distribution); + return dW * Hthermal_temp; + } + + CVector getOneFVector() { + if (this->noiseParams.scaleNoise != 0) { + // use buffered noise if available + return this->bfn->tickVector(); + } + return CVector(); + } }; -template -class Junction -{ - friend class Layer; - const std::vector vectorNames = { "x", "y", "z" }; +template class Junction { + friend class Layer; + const std::vector vectorNames = {"x", "y", "z"}; public: - enum MRmode - { - NONE = 0, - CLASSIC = 1, - STRIP = 2 - }; - - MRmode MR_mode; - std::vector> layers; - T Rp, Rap = 0.0; - - std::vector Rx0, Ry0, AMR_X, AMR_Y, SMR_X, SMR_Y, AHE; - std::unordered_map> log; - - unsigned int logLength = 0; - unsigned int layerNo; - std::string Rtag = "R"; - - Junction() {} - - /** - * @brief Create a plain junction. - * No magnetoresistance is calculated. - * @param layersToSet: layers that compose the junction - */ - explicit Junction(const std::vector>& layersToSet) - { - this->MR_mode = NONE; - this->layers = layersToSet; - this->layerNo = this->layers.size(); - if (this->layerNo == 0) - { - throw std::invalid_argument("Passed a zero length Layer vector!"); - } - } - explicit Junction(const std::vector>& layersToSet, T Rp, T Rap) : Junction( - layersToSet) - { - if (this->layerNo == 1) - { - // we need to check if this layer has a reference layer. - if (!this->layers[0].referenceLayer.length()) - { - throw std::invalid_argument("MTJ with a single layer must have" - " a pinning (referenceLayer) set!"); - } - } - if (this->layerNo > 2) - { - throw std::invalid_argument("This constructor supports only bilayers!" - " Choose the other one with the strip resistance!"); - } - this->Rp = Rp; - this->Rap = Rap; - this->MR_mode = CLASSIC; - // A string representing the tag for the junction's resistance value. - if (this->layerNo == 2) - this->Rtag = "R_" + this->layers[0].id + "_" + this->layers[1].id; - } - - /** - * Creates a junction with a STRIP magnetoresistance. - * Each of the Rx0, Ry, AMR, AMR and SMR is list matching the - * length of the layers passed (they directly correspond to each layer). - * Calculates the magnetoresistance as per: __see reference__: - * Spin Hall magnetoresistance in metallic bilayers by Kim, J. et al. - * @param Rx0 - * @param Ry0 - * @param AMR_X - * @param AMR_Y - * @param SMR_X - * @param SMR_Y - * @param AHE - */ - explicit Junction(const std::vector>& layersToSet, - std::vector Rx0, - std::vector Ry0, - std::vector AMR_X, - std::vector AMR_Y, - std::vector SMR_X, - std::vector SMR_Y, - std::vector AHE) : Rx0(std::move(Rx0)), - Ry0(std::move(Ry0)), - AMR_X(std::move(AMR_X)), - AMR_Y(std::move(AMR_Y)), - SMR_X(std::move(SMR_X)), - SMR_Y(std::move(SMR_Y)), - AHE(std::move(AHE)) - - { - this->layers = std::move(layersToSet); - this->layerNo = this->layers.size(); - if (this->layerNo == 0) - { - throw std::invalid_argument("Passed a zero length Layer vector!"); - } - if ((this->layerNo != (unsigned int)this->Rx0.size()) || - (this->layerNo != (unsigned int)this->Ry0.size()) || - (this->layerNo != (unsigned int)this->AMR_X.size()) || - (this->layerNo != (unsigned int)this->AMR_Y.size()) || - (this->layerNo != (unsigned int)this->AHE.size()) || - (this->layerNo != (unsigned int)this->SMR_X.size()) || - (this->layerNo != (unsigned int)this->SMR_Y.size())) - { - throw std::invalid_argument("Layers and Rx0, Ry, AMR, AMR and SMR must be of the same size!"); - } - // this->fileSave = std::move(filename); - this->MR_mode = STRIP; - } - - /** - * @brief Get Ids of the layers in the junction. - * @return vector of layer ids. - */ - const std::vector getLayerIds() const - { - std::vector ids; - std::transform(this->layers.begin(), this->layers.end(), std::back_inserter(ids), - [](const Layer& layer) { return layer.id; }); - return ids; - } - - /** - * Clears the simulation log. - **/ - void clearLog() - { - this->log.clear(); - this->logLength = 0; - } - - std::unordered_map>& getLog() - { - return this->log; - } - - typedef void (Layer::* scalarDriverSetter)(const ScalarDriver& driver); - typedef void (Layer::* axialDriverSetter)(const AxialDriver& driver); - void scalarlayerSetter(const std::string& layerID, scalarDriverSetter functor, ScalarDriver driver) - { - bool found = false; - for (auto& l : this->layers) - { - if (l.id == layerID || layerID == "all") - { - (l.*functor)(driver); - found = true; - } - } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); - } - } - void axiallayerSetter(const std::string& layerID, axialDriverSetter functor, AxialDriver driver) - { - bool found = false; - for (auto& l : this->layers) - { - if (l.id == layerID || layerID == "all") - { - (l.*functor)(driver); - found = true; - } - } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); - } - } - void setLayerTemperatureDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &Layer::setTemperatureDriver, driver); - } - void setLayerNonStochasticLangevinDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &Layer::setNonStochasticLangevinDriver, driver); - } - void setLayerAnisotropyDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &Layer::setAnisotropyDriver, driver); - } - void setLayerExternalFieldDriver(const std::string& layerID, const AxialDriver& driver) - { - axiallayerSetter(layerID, &Layer::setExternalFieldDriver, driver); - } - void setLayerOerstedFieldDriver(const std::string& layerID, const AxialDriver& driver) - { - axiallayerSetter(layerID, &Layer::setOerstedFieldDriver, driver); - } - void setLayerCurrentDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &Layer::setCurrentDriver, driver); - } - void setLayerDampingLikeTorqueDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &Layer::setDampingLikeTorqueDriver, driver); - } - void setLayerFieldLikeTorqueDriver(const std::string& layerID, const ScalarDriver& driver) - { - scalarlayerSetter(layerID, &Layer::setFieldLikeTorqueDriver, driver); - } - - void setLayerHdmiDriver(const std::string& layerID, const AxialDriver& driver) - { - axiallayerSetter(layerID, &Layer::setHdmiDriver, driver); - } - - void setLayerAlternativeSTT(const std::string& layerID, const bool alternative) - { - if (layerID == "all") - { - for (auto& l : this->layers) - { - l.setAlternativeSTT(alternative); - } - } - else - getLayer(layerID).setAlternativeSTT(alternative); - } - - void setLayerOneFNoise(const std::string& layerID, unsigned int sources, T bias, T scale) { - - if (layerID == "all") - { - for (auto& l : this->layers) - { - l.setOneFNoise(sources, bias, scale); - } - } - else - getLayer(layerID).setOneFNoise(sources, bias, scale); - } - - /** - * Set IEC interaction between two layers. - * The names of the params are only for convention. The IEC will be set - * between bottomLayer or topLayer, order is irrelevant. - * @param bottomLayer: the first layer id - * @param topLayer: the second layer id - */ - void setIECDriver(const std::string& bottomLayer, const std::string& topLayer, const ScalarDriver& driver) - { - bool found = false; - for (unsigned int i = 0; i < this->layerNo - 1; i++) - { - // check if the layer above is actually top layer the user specified - if ((this->layers[i].id == bottomLayer) && (this->layers[i + 1].id == topLayer)) - { - this->layers[i].setIECDriverTop(driver); - this->layers[i + 1].setIECDriverBottom(driver); - found = true; - break; - } - else if ((this->layers[i].id == topLayer) && (this->layers[i + 1].id == bottomLayer)) - { - this->layers[i].setIECDriverTop(driver); - this->layers[i + 1].setIECDriverBottom(driver); - found = true; - break; - } - } - if (!found) - { - throw std::runtime_error("Failed to match the layer order or find layer ids: " + bottomLayer + " and " + topLayer + "!"); - } - } - - void setQuadIECDriver(const std::string& bottomLayer, const std::string& topLayer, const ScalarDriver& driver) - { - bool found = false; - for (unsigned int i = 0; i < this->layerNo - 1; i++) - { - // check if the layer above is actually top layer the user specified - if ((this->layers[i].id == bottomLayer) && (this->layers[i + 1].id == topLayer)) - { - this->layers[i].setQuadIECDriverTop(driver); - this->layers[i + 1].setQuadIECDriverBottom(driver); - found = true; - break; - } - else if ((this->layers[i].id == topLayer) && (this->layers[i + 1].id == bottomLayer)) - { - this->layers[i].setQuadIECDriverTop(driver); - this->layers[i + 1].setQuadIECDriverBottom(driver); - found = true; - break; - } - } - if (!found) - { - throw std::runtime_error("Failed to match the layer order or find layer ids: " + bottomLayer + " and " + topLayer + "!"); - } - } - - void setLayerMagnetisation(const std::string& layerID, CVector& mag) - { - bool found = false; - for (auto& l : this->layers) - { - if (l.id == layerID || layerID == "all") - { - l.setMagnetisation(mag); - found = true; - } - } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); - } - } - - CVector getLayerMagnetisation(const std::string& layerID) - { - return getLayer(layerID).mag; - } - - Reference getLayerReferenceType(const std::string& layerID) - { - return getLayer(layerID).referenceType; - } - - void setLayerReferenceLayer(const std::string& layerID, const CVector& referenceLayer) - { - if (layerID == "all") - { - for (auto& l : this->layers) - { - l.setReferenceLayer(referenceLayer); - } - } - else - getLayer(layerID).setReferenceLayer(referenceLayer); - } - - void setLayerReferenceType(const std::string& layerID, Reference referenceType) - { - if (layerID == "all") - { - for (auto& l : this->layers) - { - l.setReferenceLayer(referenceType); - } - } - else - getLayer(layerID).setReferenceLayer(referenceType); - } - - Layer& getLayer(const std::string& layerID) - { - const auto res = std::find_if( - this->layers.begin(), this->layers.end(), - [layerID](const auto& l) -> bool {return (l.id == layerID);} - ); - if (res != this->layers.end()) { - return *res; - } - throw std::runtime_error("Failed to find a layer with a given id " + layerID + "!"); - } - - /** - * @brief Log computed layer parameters. - * This function logs all the necessayr parameters of the layers. - * @param t: current time - * @param timeStep: timeStep of the simulation (unsued for now) - * @param calculateEnergies: if true, also include fields for energy computation. - */ - void logLayerParams(T& t, T timeStep, bool calculateEnergies = false) - { - for (const auto& layer : this->layers) - { - const std::string lId = layer.id; - - if (calculateEnergies) - { - // TODO: avoid recomputation at a cost of a slight error - // recompute the current Heff to avoid shadow persistence of the layer parameters - // const CVector heff = calculateHeff(t, timeStep, layer.m, layer.bottom, layer.top); - this->log[lId + "_K"].emplace_back(layer.K_log); - this->log[lId + "_Jbottom"].emplace_back(layer.Jbottom_log); - this->log[lId + "_Jtop"].emplace_back(layer.Jtop_log); - this->log[lId + "_I"].emplace_back(layer.I_log); - for (int i = 0; i < 3; i++) - { - this->log[lId + "_Hext" + vectorNames[i]].emplace_back(layer.Hext[i]); - this->log[lId + "_Hiec" + vectorNames[i]].emplace_back(layer.HIEC[i]); - this->log[lId + "_Hanis" + vectorNames[i]].emplace_back(layer.HAnis[i]); - this->log[lId + "_Hdemag" + vectorNames[i]].emplace_back(layer.Hdemag[i]); - this->log[lId + "_Hth" + vectorNames[i]].emplace_back(layer.Hfluctuation[i]); - if (layer.includeSOT) - { - this->log[lId + "_Hfl" + vectorNames[i]].emplace_back(layer.Hfl_v[i]); - this->log[lId + "_Hdl" + vectorNames[i]].emplace_back(layer.Hdl_v[i]); - } - } - if (layer.includeSTT | layer.includeSOT) - this->log[lId + "_I"].emplace_back(layer.I_log); - } - // always save magnetisation - for (int i = 0; i < 3; i++) - { - this->log[lId + "_m" + vectorNames[i]].emplace_back(layer.mag[i]); - } - } - if (this->MR_mode == CLASSIC && this->layerNo == 1) - { - this->log["R"].emplace_back(calculateMagnetoresistance(c_dot(layers[0].mag, layers[0].referenceLayer))); - } - else if (MR_mode == CLASSIC && this->layerNo > 1) - { - const auto magnetoresistance = calculateMagnetoresistance(c_dot(this->layers[0].mag, - this->layers[1].mag)); - this->log[this->Rtag].emplace_back(magnetoresistance); - } - else if (MR_mode == STRIP) - { - const auto magnetoresistance = stripMagnetoResistance(this->Rx0, - this->Ry0, - this->AMR_X, - this->SMR_X, - this->AMR_Y, - this->SMR_Y, - this->AHE); - this->log["Rx"].emplace_back(magnetoresistance[0]); - this->log["Ry"].emplace_back(magnetoresistance[1]); - this->log["Rz"].emplace_back(magnetoresistance[2]); - } - this->log["time"].emplace_back(t); - this->logLength++; - } - - void - saveLogs(const std::string& filename) - { - if (filename == "") - { - // if there's an empty fn, don't save - throw std::runtime_error("The filename may not be empty!"); - } - std::ofstream logFile; - logFile.open(filename); - for (const auto& keyPair : this->log) - { - logFile << keyPair.first << ";"; - } - logFile << "\n"; - for (unsigned int i = 0; i < logLength; i++) - { - for (const auto& keyPair : this->log) - { - logFile << keyPair.second[i] << ";"; - } - logFile << "\n"; - } - logFile.close(); - } - - typedef void (Layer::* solverFn)(T t, T timeStep, const CVector& bottom, const CVector& top); - typedef void (Junction::* runnerFn)(solverFn& functor, T& t, T& timeStep); - /** - * @brief Run Euler-Heun or RK4 method for a single layer. - * - * The Euler-Heun method should only be used - * for stochastic simulations where the temperature - * driver is set. - * @param functor: solver function. - * @param t: current time - * @param timeStep: integration step - */ - void runSingleLayerSolver(solverFn& functor, T& t, T& timeStep) - { - CVector null; - (this->layers[0].*functor)( - t, timeStep, null, null); - } - - /** - * @brief Select a solver based on the setup. - * - * Multilayer layer solver iteration. - * @param functor: solver function. - * @param t: current time - * @param timeStep: integration step - * */ - void runMultiLayerSolver(solverFn& functor, T& t, T& timeStep) - { - // initialise with 0 CVectors - std::vector> magCopies(this->layerNo + 2, CVector()); - // the first and the last layer get 0 vector coupled - for (unsigned int i = 0; i < this->layerNo; i++) - { - magCopies[i + 1] = this->layers[i].mag; - } - - for (unsigned int i = 0; i < this->layerNo; i++) - { - (this->layers[i].*functor)( - t, timeStep, magCopies[i], magCopies[i + 2]); - } - } - - void eulerHeunSolverStep(solverFn& functor, T& t, T& timeStep) { - /* - Euler Heun method (stochastic heun) - - y_np = y + g(y,t,dW)*dt - g_sp = g(y_np,t+1,dW) - y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) - - with f being the non-stochastic part and g the stochastic part - */ - // draw the noise for each layer, dW - std::vector> mPrime(this->layerNo, CVector()); - for (unsigned int i = 0; i < this->layerNo; i++) { - // todo: after you're done, double check the thermal magnitude and dt scaling there - const CVector dW = this->layers[i].getStochasticLangevinVector(t, timeStep) + this->layers[i].getOneFVector(); - const CVector bottom = (i == 0) ? CVector() : this->layers[i - 1].mag; - const CVector top = (i == this->layerNo - 1) ? CVector() : this->layers[i + 1].mag; - - const CVector fnApprox = this->layers[i].calculateLLGWithFieldTorque( - t, this->layers[i].mag, bottom, top, timeStep); - const CVector gnApprox = this->layers[i].stochasticTorque(this->layers[i].mag, dW); - - // theoretically we have 2 options - // 1. calculate only the stochastic part with the second approximation - // 2. calculate the second approximation of m with the stochastic and non-stochastic - // part and then use if for torque est. - const CVector mNext = this->layers[i].mag + gnApprox * sqrt(timeStep); - const CVector gnPrimeApprox = this->layers[i].stochasticTorque(mNext, dW); - mPrime[i] = this->layers[i].mag + fnApprox * timeStep + 0.5 * (gnApprox + gnPrimeApprox) * sqrt(timeStep); - } - - for (unsigned int i = 0; i < this->layerNo; i++) { - this->layers[i].mag = mPrime[i]; - this->layers[i].mag.normalize(); - } - } - - - void heunSolverStep(solverFn& functor, T& t, T& timeStep) { - /* - Heun method - y'(t+1) = y(t) + dy(y, t) - y(t+1) = y(t) + 0.5 * (dy(y, t) + dy(y'(t+1), t+1)) - */ - /* - Stochastic Heun method - y_np = y + g(y,t,dW)*dt - g_sp = g(y_np,t+1,dW) - y' = y_n + f_n * dt + g_n * dt - f' = f(y, ) - y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) - */ - std::vector> fn(this->layerNo, CVector()); - std::vector> gn(this->layerNo, CVector()); - std::vector> dW(this->layerNo, CVector()); - std::vector> mNext(this->layerNo, CVector()); - // first approximation - - // make sure that - // 1. Thermal field is added if needed - // 2. One/f noise is added if needed - // 3. The timestep is correctly multiplied - - for (unsigned int i = 0; i < this->layerNo; i++) - { - const CVector bottom = (i == 0) ? CVector() : this->layers[i - 1].mag; - const CVector top = (i == this->layerNo - 1) ? CVector() : this->layers[i + 1].mag; - - fn[i] = this->layers[i].calculateLLGWithFieldTorque( - t, this->layers[i].mag, bottom, top, timeStep); - - // draw the noise for each layer, dW - dW[i] = this->layers[i].getStochasticLangevinVector(t, timeStep) + this->layers[i].getOneFVector(); - gn[i] = this->layers[i].stochasticTorque(this->layers[i].mag, dW[i]); - - mNext[i] = this->layers[i].mag + fn[i] * timeStep + gn[i] * sqrt(timeStep); - } - // second approximation - for (unsigned int i = 0; i < this->layerNo; i++) - { - const CVector bottom = (i == 0) ? CVector() : mNext[i - 1]; - const CVector top = (i == this->layerNo - 1) ? CVector() : mNext[i + 1]; - // first approximation is already multiplied by timeStep - this->layers[i].mag = this->layers[i].mag + 0.5 * timeStep * ( - fn[i] + this->layers[i].calculateLLGWithFieldTorque( - t + timeStep, mNext[i], - bottom, - top, timeStep) - ) + 0.5 * (gn[i] + this->layers[i].stochasticTorque(mNext[i], dW[i])) * sqrt(timeStep); - // normalise - this->layers[i].mag.normalize(); - } - - } - - /** - * @brief Calculate strip magnetoresistance for multilayer. - * - * Used when MR_MODE == STRIP - * Magnetoresistance as per: - * Spin Hall magnetoresistance in metallic bilayers by Kim, J. et al. - * Each of the Rx0, Ry, AMR, AMR and SMR is list matching the - * length of the layers passed (they directly correspond to each layer). - * Calculates the magnetoresistance as per: __see reference__: - * Spin Hall magnetoresistance in metallic bilayers by Kim, J. et al. - * @param Rx0 - * @param Ry0 - * @param AMR_X - * @param AMR_Y - * @param SMR_X - * @param SMR_Y - * @param AHE - */ - std::vector stripMagnetoResistance(const std::vector& Rx0, - const std::vector& Ry0, - const std::vector& AMR_X, - const std::vector& SMR_X, - const std::vector& AMR_Y, - const std::vector& SMR_Y, - const std::vector& AHE) - { - T Rx_acc = 0.0; - T Ry_acc = 0.0; - - for (unsigned int i = 0; i < this->layers.size(); i++) - { - const T Rx = Rx0[i] + AMR_X[i] * pow(this->layers[i].mag.x, 2) + SMR_X[i] * pow(this->layers[i].mag.y, 2); - const T Ry = Ry0[i] + 0.5 * AHE[i] * this->layers[i].mag.z + - (AMR_Y[i] + SMR_Y[i]) * this->layers[i].mag.x * this->layers[i].mag.y; - Rx_acc += 1. / Rx; - Ry_acc += 1. / Ry; - } - - return { 1 / Rx_acc, 1 / Ry_acc, 0. }; - } - - /** - * Calculate classic magnetoresistance. - * Only for bilayer structures. - * used when MR_MODE == CLASSIC - * @param cosTheta: cosine between two layers. - */ - T calculateMagnetoresistance(T cosTheta) - { - return this->Rp + (((this->Rap - this->Rp) / 2.0) * (1.0 - cosTheta)); - } - - std::vector getMagnetoresistance() - { - // this is classical bilayer case - if (this->MR_mode == CLASSIC && this->layerNo == 2) - { - return { calculateMagnetoresistance(c_dot(layers[0].mag, layers[1].mag)) }; + enum MRmode { NONE = 0, CLASSIC = 1, STRIP = 2 }; + + MRmode MR_mode; + std::vector> layers; + T Rp, Rap = 0.0; + + std::vector Rx0, Ry0, AMR_X, AMR_Y, SMR_X, SMR_Y, AHE; + std::unordered_map> log; + + unsigned int logLength = 0; + unsigned int layerNo; + std::string Rtag = "R"; + + Junction() {} + + /** + * @brief Create a plain junction. + * No magnetoresistance is calculated. + * @param layersToSet: layers that compose the junction + */ + explicit Junction(const std::vector> &layersToSet) { + this->MR_mode = NONE; + this->layers = layersToSet; + this->layerNo = this->layers.size(); + if (this->layerNo == 0) { + throw std::invalid_argument("Passed a zero length Layer vector!"); + } + } + explicit Junction(const std::vector> &layersToSet, T Rp, T Rap) + : Junction(layersToSet) { + if (this->layerNo == 1) { + // we need to check if this layer has a reference layer. + if (!this->layers[0].referenceLayer.length()) { + throw std::invalid_argument("MTJ with a single layer must have" + " a pinning (referenceLayer) set!"); + } + } + if (this->layerNo > 2) { + throw std::invalid_argument( + "This constructor supports only bilayers!" + " Choose the other one with the strip resistance!"); + } + this->Rp = Rp; + this->Rap = Rap; + this->MR_mode = CLASSIC; + // A string representing the tag for the junction's resistance value. + if (this->layerNo == 2) + this->Rtag = "R_" + this->layers[0].id + "_" + this->layers[1].id; + } + + /** + * Creates a junction with a STRIP magnetoresistance. + * Each of the Rx0, Ry, AMR, AMR and SMR is list matching the + * length of the layers passed (they directly correspond to each layer). + * Calculates the magnetoresistance as per: __see reference__: + * Spin Hall magnetoresistance in metallic bilayers by Kim, J. et al. + * @param Rx0 + * @param Ry0 + * @param AMR_X + * @param AMR_Y + * @param SMR_X + * @param SMR_Y + * @param AHE + */ + explicit Junction(const std::vector> &layersToSet, + std::vector Rx0, std::vector Ry0, + std::vector AMR_X, std::vector AMR_Y, + std::vector SMR_X, std::vector SMR_Y, + std::vector AHE) + : Rx0(std::move(Rx0)), Ry0(std::move(Ry0)), AMR_X(std::move(AMR_X)), + AMR_Y(std::move(AMR_Y)), SMR_X(std::move(SMR_X)), + SMR_Y(std::move(SMR_Y)), AHE(std::move(AHE)) + + { + this->layers = std::move(layersToSet); + this->layerNo = this->layers.size(); + if (this->layerNo == 0) { + throw std::invalid_argument("Passed a zero length Layer vector!"); + } + if ((this->layerNo != (unsigned int)this->Rx0.size()) || + (this->layerNo != (unsigned int)this->Ry0.size()) || + (this->layerNo != (unsigned int)this->AMR_X.size()) || + (this->layerNo != (unsigned int)this->AMR_Y.size()) || + (this->layerNo != (unsigned int)this->AHE.size()) || + (this->layerNo != (unsigned int)this->SMR_X.size()) || + (this->layerNo != (unsigned int)this->SMR_Y.size())) { + throw std::invalid_argument( + "Layers and Rx0, Ry, AMR, AMR and SMR must be of the same size!"); + } + // this->fileSave = std::move(filename); + this->MR_mode = STRIP; + } + + /** + * @brief Get Ids of the layers in the junction. + * @return vector of layer ids. + */ + const std::vector getLayerIds() const { + std::vector ids; + std::transform(this->layers.begin(), this->layers.end(), + std::back_inserter(ids), + [](const Layer &layer) { return layer.id; }); + return ids; + } + + /** + * Clears the simulation log. + **/ + void clearLog() { + this->log.clear(); + this->logLength = 0; + } + + std::unordered_map> &getLog() { + return this->log; + } + + typedef void (Layer::*scalarDriverSetter)(const ScalarDriver &driver); + typedef void (Layer::*axialDriverSetter)(const AxialDriver &driver); + void scalarlayerSetter(const std::string &layerID, scalarDriverSetter functor, + ScalarDriver driver) { + bool found = false; + for (auto &l : this->layers) { + if (l.id == layerID || layerID == "all") { + (l.*functor)(driver); + found = true; + } + } + if (!found) { + throw std::runtime_error( + "Failed to find a layer with a given id: " + layerID + "!"); + } + } + + void axiallayerSetter(const std::string &layerID, axialDriverSetter functor, + AxialDriver driver) { + bool found = false; + for (auto &l : this->layers) { + if (l.id == layerID || layerID == "all") { + (l.*functor)(driver); + found = true; + } + } + if (!found) { + throw std::runtime_error( + "Failed to find a layer with a given id: " + layerID + "!"); + } + } + + /** + * Set coupling between two layers. + * The names of the params are only for convention. The coupling will be set + * between bottomLayer or topLayer, order is irrelevant. + * @param bottomLayer: the first layer id + * @param topLayer: the second layer id + */ + void setCouplingDriver( + const std::string &bottomLayer, const std::string &topLayer, + const ScalarDriver &driver, + void (Layer::*setDriverFuncTop)(const ScalarDriver &), + void (Layer::*setDriverFuncBottom)(const ScalarDriver &)) { + bool found = false; + for (unsigned int i = 0; i < this->layerNo - 1; i++) { + // check if the layer above is actually top layer the user specified + if ((this->layers[i].id == bottomLayer) && + (this->layers[i + 1].id == topLayer)) { + (this->layers[i].*setDriverFuncTop)(driver); + (this->layers[i + 1].*setDriverFuncBottom)(driver); + found = true; + break; + } else if ((this->layers[i].id == topLayer) && + (this->layers[i + 1].id == bottomLayer)) { + (this->layers[i].*setDriverFuncTop)(driver); + (this->layers[i + 1].*setDriverFuncBottom)(driver); + found = true; + break; + } + } + if (!found) { + throw std::runtime_error( + "Failed to match the layer order or find layer ids: " + bottomLayer + + " and " + topLayer + "!"); + } + } + + /** + * Set coupling between two layers with an AxialDriver + * The names of the params are only for convention. The coupling will be set + * between bottomLayer or topLayer, order is irrelevant. + * @param bottomLayer: the first layer id + * @param topLayer: the second layer id + */ + void setCouplingDriverAxial( + const std::string &bottomLayer, const std::string &topLayer, + const AxialDriver &driver, + void (Layer::*setDriverFuncTop)(const AxialDriver &), + void (Layer::*setDriverFuncBottom)(const AxialDriver &)) { + bool found = false; + for (unsigned int i = 0; i < this->layerNo - 1; i++) { + // check if the layer above is actually top layer the user specified + if ((this->layers[i].id == bottomLayer) && + (this->layers[i + 1].id == topLayer)) { + (this->layers[i].*setDriverFuncTop)(driver); + (this->layers[i + 1].*setDriverFuncBottom)(driver); + found = true; + break; + } else if ((this->layers[i].id == topLayer) && + (this->layers[i + 1].id == bottomLayer)) { + (this->layers[i].*setDriverFuncTop)(driver); + (this->layers[i + 1].*setDriverFuncBottom)(driver); + found = true; + break; + } + } + if (!found) { + throw std::runtime_error( + "Failed to match the layer order or find layer ids: " + bottomLayer + + " and " + topLayer + "!"); + } + } + + void setLayerTemperatureDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &Layer::setTemperatureDriver, driver); + } + void setLayerNonStochasticLangevinDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &Layer::setNonStochasticLangevinDriver, + driver); + } + void setLayerAnisotropyDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &Layer::setAnisotropyDriver, driver); + } + void setLayerExternalFieldDriver(const std::string &layerID, + const AxialDriver &driver) { + axiallayerSetter(layerID, &Layer::setExternalFieldDriver, driver); + } + void setLayerOerstedFieldDriver(const std::string &layerID, + const AxialDriver &driver) { + axiallayerSetter(layerID, &Layer::setOerstedFieldDriver, driver); + } + void setLayerCurrentDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &Layer::setCurrentDriver, driver); + } + void setLayerDampingLikeTorqueDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &Layer::setDampingLikeTorqueDriver, driver); + } + void setLayerFieldLikeTorqueDriver(const std::string &layerID, + const ScalarDriver &driver) { + scalarlayerSetter(layerID, &Layer::setFieldLikeTorqueDriver, driver); + } + + void setLayerHdmiDriver(const std::string &layerID, + const AxialDriver &driver) { + axiallayerSetter(layerID, &Layer::setHdmiDriver, driver); + } + + void setLayerAlternativeSTT(const std::string &layerID, + const bool alternative) { + if (layerID == "all") { + for (auto &l : this->layers) { + l.setAlternativeSTT(alternative); + } + } else + getLayer(layerID).setAlternativeSTT(alternative); + } + + void setLayerOneFNoise(const std::string &layerID, unsigned int sources, + T bias, T scale) { + + if (layerID == "all") { + for (auto &l : this->layers) { + l.setOneFNoise(sources, bias, scale); + } + } else + getLayer(layerID).setOneFNoise(sources, bias, scale); + } + + /** + * Set IDMI interaction between two layers. + * The names of the params are only for convention. The IDMI will be set + * between bottomLayer or topLayer, order is irrelevant. + * See Arregi et al, Nat. Comm. 2022: Large interlayer Dzyaloshinskii-Moriya + * interactions across Ag-layers + * @param bottomLayer: the first layer id + * @param topLayer: the second layer id + */ + void setIDMIDriver(const std::string &bottomLayer, + const std::string &topLayer, + const AxialDriver &driver) { + setCouplingDriverAxial(bottomLayer, topLayer, driver, + &Layer::setIDMIDriverTop, + &Layer::setIDMIDriverBottom); + } + + /** + * Set biquadratic IEC interaction between two layers. + * The names of the params are only for convention. The IEC will be set + * between bottomLayer or topLayer, order is irrelevant. + * @param bottomLayer: the first layer id + * @param topLayer: the second layer id + */ + void setQuadIECDriver(const std::string &bottomLayer, + const std::string &topLayer, + const ScalarDriver &driver) { + setCouplingDriver(bottomLayer, topLayer, driver, + &Layer::setQuadIECDriverTop, + &Layer::setQuadIECDriverBottom); + } + + /** + * Set blilinear IEC interaction between two layers. + * The names of the params are only for convention. The IEC will be set + * between bottomLayer or topLayer, order is irrelevant. + * @param bottomLayer: the first layer id + * @param topLayer: the second layer id + */ + void setIECDriver(const std::string &bottomLayer, const std::string &topLayer, + const ScalarDriver &driver) { + setCouplingDriver(bottomLayer, topLayer, driver, &Layer::setIECDriverTop, + &Layer::setIECDriverBottom); + } + + void setLayerMagnetisation(const std::string &layerID, CVector &mag) { + bool found = false; + for (auto &l : this->layers) { + if (l.id == layerID || layerID == "all") { + l.setMagnetisation(mag); + found = true; + } + } + if (!found) { + throw std::runtime_error( + "Failed to find a layer with a given id: " + layerID + "!"); + } + } + + CVector getLayerMagnetisation(const std::string &layerID) { + return getLayer(layerID).mag; + } + + Reference getLayerReferenceType(const std::string &layerID) { + return getLayer(layerID).referenceType; + } + + void setLayerReferenceLayer(const std::string &layerID, + const CVector &referenceLayer) { + if (layerID == "all") { + for (auto &l : this->layers) { + l.setReferenceLayer(referenceLayer); + } + } else + getLayer(layerID).setReferenceLayer(referenceLayer); + } + + void setLayerReferenceType(const std::string &layerID, + Reference referenceType) { + if (layerID == "all") { + for (auto &l : this->layers) { + l.setReferenceLayer(referenceType); + } + } else + getLayer(layerID).setReferenceLayer(referenceType); + } + + Layer &getLayer(const std::string &layerID) { + const auto res = std::find_if( + this->layers.begin(), this->layers.end(), + [layerID](const auto &l) -> bool { return (l.id == layerID); }); + if (res != this->layers.end()) { + return *res; + } + throw std::runtime_error("Failed to find a layer with a given id " + + layerID + "!"); + } + + /** + * @brief Log computed layer parameters. + * This function logs all the necessayr parameters of the layers. + * @param t: current time + * @param timeStep: timeStep of the simulation (unsued for now) + * @param calculateEnergies: if true, also include fields for energy + * computation. + */ + void logLayerParams(T &t, T timeStep, bool calculateEnergies = false) { + for (const auto &layer : this->layers) { + const std::string lId = layer.id; + + if (calculateEnergies) { + // TODO: avoid recomputation at a cost of a slight error + // recompute the current Heff to avoid shadow persistence of the layer + // parameters const CVector heff = calculateHeff(t, timeStep, + // layer.m, layer.bottom, layer.top); + this->log[lId + "_K"].emplace_back(layer.K_log); + this->log[lId + "_Jbottom"].emplace_back(layer.Jbottom_log); + this->log[lId + "_Jtop"].emplace_back(layer.Jtop_log); + this->log[lId + "_I"].emplace_back(layer.I_log); + for (int i = 0; i < 3; i++) { + this->log[lId + "_Hext" + vectorNames[i]].emplace_back(layer.Hext[i]); + this->log[lId + "_Hiec" + vectorNames[i]].emplace_back(layer.HIEC[i]); + this->log[lId + "_Hanis" + vectorNames[i]].emplace_back( + layer.HAnis[i]); + this->log[lId + "_Hdemag" + vectorNames[i]].emplace_back( + layer.Hdemag[i]); + this->log[lId + "_Hth" + vectorNames[i]].emplace_back( + layer.Hfluctuation[i]); + if (layer.includeSOT) { + this->log[lId + "_Hfl" + vectorNames[i]].emplace_back( + layer.Hfl_v[i]); + this->log[lId + "_Hdl" + vectorNames[i]].emplace_back( + layer.Hdl_v[i]); + } } - // this is the case when we use the pinning layer - else if (this->MR_mode == CLASSIC && this->layerNo == 1) - { - return { calculateMagnetoresistance(c_dot(layers[0].mag, layers[0].referenceLayer)) }; + if (layer.includeSTT | layer.includeSOT) + this->log[lId + "_I"].emplace_back(layer.I_log); + } + // always save magnetisation + for (int i = 0; i < 3; i++) { + this->log[lId + "_m" + vectorNames[i]].emplace_back(layer.mag[i]); + } + } + if (this->MR_mode == CLASSIC && this->layerNo == 1) { + this->log["R"].emplace_back(calculateMagnetoresistance( + c_dot(layers[0].mag, layers[0].referenceLayer))); + } else if (MR_mode == CLASSIC && this->layerNo > 1) { + const auto magnetoresistance = calculateMagnetoresistance( + c_dot(this->layers[0].mag, this->layers[1].mag)); + this->log[this->Rtag].emplace_back(magnetoresistance); + } else if (MR_mode == STRIP) { + const auto magnetoresistance = + stripMagnetoResistance(this->Rx0, this->Ry0, this->AMR_X, this->SMR_X, + this->AMR_Y, this->SMR_Y, this->AHE); + this->log["Rx"].emplace_back(magnetoresistance[0]); + this->log["Ry"].emplace_back(magnetoresistance[1]); + this->log["Rz"].emplace_back(magnetoresistance[2]); + } + this->log["time"].emplace_back(t); + this->logLength++; + } + + void saveLogs(const std::string &filename) { + if (filename == "") { + // if there's an empty fn, don't save + throw std::runtime_error("The filename may not be empty!"); + } + std::ofstream logFile; + logFile.open(filename); + for (const auto &keyPair : this->log) { + logFile << keyPair.first << ";"; + } + logFile << "\n"; + for (unsigned int i = 0; i < logLength; i++) { + for (const auto &keyPair : this->log) { + logFile << keyPair.second[i] << ";"; + } + logFile << "\n"; + } + logFile.close(); + } + + typedef void (Layer::*solverFn)(T t, T timeStep, const CVector &bottom, + const CVector &top); + typedef void (Junction::*runnerFn)(solverFn &functor, T &t, T &timeStep); + /** + * @brief Run Euler-Heun or RK4 method for a single layer. + * + * The Euler-Heun method should only be used + * for stochastic simulations where the temperature + * driver is set. + * @param functor: solver function. + * @param t: current time + * @param timeStep: integration step + */ + void runSingleLayerSolver(solverFn &functor, T &t, T &timeStep) { + CVector null; + (this->layers[0].*functor)(t, timeStep, null, null); + } + + /** + * @brief Select a solver based on the setup. + * + * Multilayer layer solver iteration. + * @param functor: solver function. + * @param t: current time + * @param timeStep: integration step + * */ + void runMultiLayerSolver(solverFn &functor, T &t, T &timeStep) { + // initialise with 0 CVectors + std::vector> magCopies(this->layerNo + 2, CVector()); + // the first and the last layer get 0 vector coupled + for (unsigned int i = 0; i < this->layerNo; i++) { + magCopies[i + 1] = this->layers[i].mag; + } + + for (unsigned int i = 0; i < this->layerNo; i++) { + (this->layers[i].*functor)(t, timeStep, magCopies[i], magCopies[i + 2]); + } + } + + void eulerHeunSolverStep(solverFn &functor, T &t, T &timeStep) { + /* + Euler Heun method (stochastic heun) + + y_np = y + g(y,t,dW)*dt + g_sp = g(y_np,t+1,dW) + y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) + + with f being the non-stochastic part and g the stochastic part + */ + // draw the noise for each layer, dW + std::vector> mPrime(this->layerNo, CVector()); + for (unsigned int i = 0; i < this->layerNo; i++) { + // todo: after you're done, double check the thermal magnitude and dt + // scaling there + const CVector dW = + this->layers[i].getStochasticLangevinVector(t, timeStep) + + this->layers[i].getOneFVector(); + const CVector bottom = + (i == 0) ? CVector() : this->layers[i - 1].mag; + const CVector top = + (i == this->layerNo - 1) ? CVector() : this->layers[i + 1].mag; + + const CVector fnApprox = this->layers[i].calculateLLGWithFieldTorque( + t, this->layers[i].mag, bottom, top, timeStep); + const CVector gnApprox = + this->layers[i].stochasticTorque(this->layers[i].mag, dW); + + // theoretically we have 2 options + // 1. calculate only the stochastic part with the second approximation + // 2. calculate the second approximation of m with the stochastic and + // non-stochastic + // part and then use if for torque est. + const CVector mNext = this->layers[i].mag + gnApprox * sqrt(timeStep); + const CVector gnPrimeApprox = + this->layers[i].stochasticTorque(mNext, dW); + mPrime[i] = this->layers[i].mag + fnApprox * timeStep + + 0.5 * (gnApprox + gnPrimeApprox) * sqrt(timeStep); + } + + for (unsigned int i = 0; i < this->layerNo; i++) { + this->layers[i].mag = mPrime[i]; + this->layers[i].mag.normalize(); + } + } + + void heunSolverStep(solverFn &functor, T &t, T &timeStep) { + /* + Heun method + y'(t+1) = y(t) + dy(y, t) + y(t+1) = y(t) + 0.5 * (dy(y, t) + dy(y'(t+1), t+1)) + */ + /* + Stochastic Heun method + y_np = y + g(y,t,dW)*dt + g_sp = g(y_np,t+1,dW) + y' = y_n + f_n * dt + g_n * dt + f' = f(y, ) + y(t+1) = y + dt*f(y,t) + .5*(g(y,t,dW)+g_sp)*sqrt(dt) + */ + std::vector> fn(this->layerNo, CVector()); + std::vector> gn(this->layerNo, CVector()); + std::vector> dW(this->layerNo, CVector()); + std::vector> mNext(this->layerNo, CVector()); + // first approximation + + // make sure that + // 1. Thermal field is added if needed + // 2. One/f noise is added if needed + // 3. The timestep is correctly multiplied + + for (unsigned int i = 0; i < this->layerNo; i++) { + const CVector bottom = + (i == 0) ? CVector() : this->layers[i - 1].mag; + const CVector top = + (i == this->layerNo - 1) ? CVector() : this->layers[i + 1].mag; + + fn[i] = this->layers[i].calculateLLGWithFieldTorque( + t, this->layers[i].mag, bottom, top, timeStep); + + // draw the noise for each layer, dW + dW[i] = this->layers[i].getStochasticLangevinVector(t, timeStep) + + this->layers[i].getOneFVector(); + gn[i] = this->layers[i].stochasticTorque(this->layers[i].mag, dW[i]); + + mNext[i] = + this->layers[i].mag + fn[i] * timeStep + gn[i] * sqrt(timeStep); + } + // second approximation + for (unsigned int i = 0; i < this->layerNo; i++) { + const CVector bottom = (i == 0) ? CVector() : mNext[i - 1]; + const CVector top = + (i == this->layerNo - 1) ? CVector() : mNext[i + 1]; + // first approximation is already multiplied by timeStep + this->layers[i].mag = + this->layers[i].mag + + 0.5 * timeStep * + (fn[i] + this->layers[i].calculateLLGWithFieldTorque( + t + timeStep, mNext[i], bottom, top, timeStep)) + + 0.5 * (gn[i] + this->layers[i].stochasticTorque(mNext[i], dW[i])) * + sqrt(timeStep); + // normalise + this->layers[i].mag.normalize(); + } + } + + /** + * @brief Calculate strip magnetoresistance for multilayer. + * + * Used when MR_MODE == STRIP + * Magnetoresistance as per: + * Spin Hall magnetoresistance in metallic bilayers by Kim, J. et al. + * Each of the Rx0, Ry, AMR, AMR and SMR is list matching the + * length of the layers passed (they directly correspond to each layer). + * Calculates the magnetoresistance as per: __see reference__: + * Spin Hall magnetoresistance in metallic bilayers by Kim, J. et al. + * @param Rx0 + * @param Ry0 + * @param AMR_X + * @param AMR_Y + * @param SMR_X + * @param SMR_Y + * @param AHE + */ + std::vector stripMagnetoResistance(const std::vector &Rx0, + const std::vector &Ry0, + const std::vector &AMR_X, + const std::vector &SMR_X, + const std::vector &AMR_Y, + const std::vector &SMR_Y, + const std::vector &AHE) { + T Rx_acc = 0.0; + T Ry_acc = 0.0; + + for (unsigned int i = 0; i < this->layers.size(); i++) { + const T Rx = Rx0[i] + AMR_X[i] * pow(this->layers[i].mag.x, 2) + + SMR_X[i] * pow(this->layers[i].mag.y, 2); + const T Ry = + Ry0[i] + 0.5 * AHE[i] * this->layers[i].mag.z + + (AMR_Y[i] + SMR_Y[i]) * this->layers[i].mag.x * this->layers[i].mag.y; + Rx_acc += 1. / Rx; + Ry_acc += 1. / Ry; + } + + return {1 / Rx_acc, 1 / Ry_acc, 0.}; + } + + /** + * Calculate classic magnetoresistance. + * Only for bilayer structures. + * used when MR_MODE == CLASSIC + * @param cosTheta: cosine between two layers. + */ + T calculateMagnetoresistance(T cosTheta) { + return this->Rp + (((this->Rap - this->Rp) / 2.0) * (1.0 - cosTheta)); + } + + std::vector getMagnetoresistance() { + // this is classical bilayer case + if (this->MR_mode == CLASSIC && this->layerNo == 2) { + return { + calculateMagnetoresistance(c_dot(layers[0].mag, layers[1].mag))}; + } + // this is the case when we use the pinning layer + else if (this->MR_mode == CLASSIC && this->layerNo == 1) { + return {calculateMagnetoresistance( + c_dot(layers[0].mag, layers[0].referenceLayer))}; + } + // this is strip magnetoresistance + else if (this->MR_mode == STRIP) { + return stripMagnetoResistance(this->Rx0, this->Ry0, this->AMR_X, + this->SMR_X, this->AMR_Y, this->SMR_Y, + this->AHE); + } else { + throw std::runtime_error( + "Magnetisation calculation is not supported for this structure!"); + } + } + + std::tuple + getSolver(SolverMode mode, unsigned int totalIterations) { + SolverMode localMode = mode; + for (auto &l : this->layers) { + if (l.hasTemperature()) { + // if at least one temp. driver is set + // then use heun for consistency + if (localMode != HEUN && localMode != EULER_HEUN) { + std::cout << "[WARNING] Solver automatically changed to Euler Heun " + "for stochastic calculation." + << std::endl; + localMode = EULER_HEUN; } - // this is strip magnetoresistance - else if (this->MR_mode == STRIP) - { - return stripMagnetoResistance(this->Rx0, - this->Ry0, - this->AMR_X, - this->SMR_X, - this->AMR_Y, - this->SMR_Y, - this->AHE); - } - else - { - throw std::runtime_error("Magnetisation calculation is not supported for this structure!"); - } - } - - std::tuple getSolver(SolverMode mode, unsigned int totalIterations) { - SolverMode localMode = mode; - for (auto& l : this->layers) - { - if (l.hasTemperature()) - { - // if at least one temp. driver is set - // then use heun for consistency - if (localMode != HEUN && localMode != EULER_HEUN) { - std::cout << "[WARNING] Solver automatically changed to Euler Heun for stochastic calculation." << std::endl; - localMode = EULER_HEUN; - } - } - if (l.noiseParams.scaleNoise != 0) { - // if at least one temp. driver is set - // then use heun for consistency - if (localMode != HEUN && localMode != EULER_HEUN) { - std::cout << "[WARNING] Solver automatically changed to Euler Heun for stochastic calculation." << std::endl; - localMode = EULER_HEUN; - } - // create a buffer - l.createBufferedAlphaNoise(totalIterations); - } + } + if (l.noiseParams.scaleNoise != 0) { + // if at least one temp. driver is set + // then use heun for consistency + if (localMode != HEUN && localMode != EULER_HEUN) { + std::cout << "[WARNING] Solver automatically changed to Euler Heun " + "for stochastic calculation." + << std::endl; + localMode = EULER_HEUN; } - auto solver = &Layer::rk4_step; - - // assign a runner function pointer from junction - auto runner = &Junction::runMultiLayerSolver; - if (this->layerNo == 1) - runner = &Junction::runSingleLayerSolver; - if (localMode == HEUN) - runner = &Junction::heunSolverStep; - else if (localMode == EULER_HEUN) - runner = &Junction::eulerHeunSolverStep; - - return std::make_tuple(runner, solver, localMode); - } - - - /** - * Main run simulation function. Use it to run the simulation. - * @param totalTime: total time of a simulation, give it in seconds. Typical length is in ~couple ns. - * @param timeStep: the integration step of the RK45 method. Default is 1e-13 - * @param writeFrequency: how often is the log saved to? Must be no smaller than `timeStep`. Default is 1e-11. - * @param persist: whether to save to the filename specified in the Junction constructor. Default is true - * @param log: if you want some verbosity like timing the simulation. Default is false - * @param calculateEnergies: [WORK IN PROGRESS] log energy values to the log. Default is false. - * @param mode: Solver mode EULER_HEUN, RK4 or DORMAND_PRICE - */ - void runSimulation(T totalTime, T timeStep = 1e-13, T writeFrequency = 1e-11, - bool log = false, bool calculateEnergies = false, - SolverMode mode = RK4) - - { - if (timeStep > writeFrequency) - { - throw std::runtime_error("The time step cannot be larger than write frequency!"); - } - const unsigned int totalIterations = (int)(totalTime / timeStep); - const unsigned int writeEvery = (int)(writeFrequency / timeStep); - std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); - // pick a solver based on drivers - auto [runner, solver, _] = getSolver(mode, totalIterations); - - for (unsigned int i = 0; i < totalIterations; i++) - { - T t = i * timeStep; - (*this.*runner)(solver, t, timeStep); - - if (!(i % writeEvery)) - { - logLayerParams(t, timeStep, calculateEnergies); - } - } - std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); - if (log) - { - std::cout << "Steps in simulation: " << totalIterations << std::endl; - std::cout << "Write every: " << writeEvery << std::endl; - std::cout << "Simulation time = " << std::chrono::duration_cast(end - begin).count() << "[s]" << std::endl; - } - } + // create a buffer + l.createBufferedAlphaNoise(totalIterations); + } + } + auto solver = &Layer::rk4_step; + + // assign a runner function pointer from junction + auto runner = &Junction::runMultiLayerSolver; + if (this->layerNo == 1) + runner = &Junction::runSingleLayerSolver; + if (localMode == HEUN) + runner = &Junction::heunSolverStep; + else if (localMode == EULER_HEUN) + runner = &Junction::eulerHeunSolverStep; + + return std::make_tuple(runner, solver, localMode); + } + + /** + * Main run simulation function. Use it to run the simulation. + * @param totalTime: total time of a simulation, give it in seconds. Typical + * length is in ~couple ns. + * @param timeStep: the integration step of the RK45 method. Default is 1e-13 + * @param writeFrequency: how often is the log saved to? Must be no smaller + * than `timeStep`. Default is 1e-11. + * @param persist: whether to save to the filename specified in the Junction + * constructor. Default is true + * @param log: if you want some verbosity like timing the simulation. Default + * is false + * @param calculateEnergies: [WORK IN PROGRESS] log energy values to the log. + * Default is false. + * @param mode: Solver mode EULER_HEUN, RK4 or DORMAND_PRICE + */ + void runSimulation(T totalTime, T timeStep = 1e-13, T writeFrequency = 1e-11, + bool log = false, bool calculateEnergies = false, + SolverMode mode = RK4) + + { + if (timeStep > writeFrequency) { + throw std::runtime_error( + "The time step cannot be larger than write frequency!"); + } + const unsigned int totalIterations = (int)(totalTime / timeStep); + const unsigned int writeEvery = (int)(writeFrequency / timeStep); + std::chrono::steady_clock::time_point begin = + std::chrono::steady_clock::now(); + // pick a solver based on drivers + auto [runner, solver, _] = getSolver(mode, totalIterations); + + for (unsigned int i = 0; i < totalIterations; i++) { + T t = i * timeStep; + (*this.*runner)(solver, t, timeStep); + + if (!(i % writeEvery)) { + logLayerParams(t, timeStep, calculateEnergies); + } + } + std::chrono::steady_clock::time_point end = + std::chrono::steady_clock::now(); + if (log) { + std::cout << "Steps in simulation: " << totalIterations << std::endl; + std::cout << "Write every: " << writeEvery << std::endl; + std::cout << "Simulation time = " + << std::chrono::duration_cast(end - begin) + .count() + << "[s]" << std::endl; + } + } }; -#endif // CORE_JUNCTION_HPP_ +#endif // CORE_JUNCTION_HPP_ diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 7e1b7c3..a944e18 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -272,11 +272,13 @@ PYBIND11_MODULE(cmtj, m) { &DJunction::setLayerExternalFieldDriver) .def("setLayerCurrentDriver", &DJunction::setLayerCurrentDriver) .def("setLayerAnisotropyDriver", &DJunction::setLayerAnisotropyDriver) - .def("setIECDriver", &DJunction::setIECDriver) - .def("setQuadIECDriver", &DJunction::setQuadIECDriver) .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) .def("setLayerMagnetisation", &DJunction::setLayerMagnetisation) .def("setLayerHdmiDriver", &DJunction::setLayerHdmiDriver) + // interaction setters + .def("setIECDriver", &DJunction::setIECDriver) + .def("setQuadIECDriver", &DJunction::setQuadIECDriver) + .def("setIDMIDriver", &DJunction::setIDMIDriver) // noise .def("setLayerTemperatureDriver", &DJunction::setLayerTemperatureDriver) .def("setLayerNonStochasticLangevinDriver", From aeb868827598b2583cbf3da860744325801ec57a Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 16:15:25 +0200 Subject: [PATCH 16/44] get junction now correctly returns a reference instead of a copy --- CHANGELOG.md | 2 ++ core/stack.hpp | 10 ++++++++++ python/cmtj.cpp | 10 ++++++++++ 3 files changed, 22 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ef75cf..9189ab1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ - Extended the `Stack` models allowing for non-symmetric coupling between devices. - `Stack` current drivers can now be of any type are adequately scaled. - Custom definition of the `ScalarDriver` is now possible and documented. +- Fixed a bug in the `Stack` class which inverted the connection order of in-series connections. +- Exposed IDMI interaction to Layer and Junction classes. # 1.5.0-1.5.4 diff --git a/core/stack.hpp b/core/stack.hpp index e35a125..4bd9f6c 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -90,6 +90,16 @@ template class Stack { return this->junctionList[junctionId].getLayerMagnetisation(layerId); } + Junction &getJunction(unsigned int junctionId) { + return this->junctionList.at(junctionId); + } + + void setJunctionAnisotropyDriver(unsigned int junctionId, + const std::string &layerId, + const ScalarDriver &k) { + this->junctionList[junctionId].setLayerAnisotropyDriver(layerId, k); + } + void setOerstedFieldDriver(const AxialDriver &oDriver) { for (auto &j : this->junctionList) { j.setLayerOerstedFieldDriver("all", oDriver); diff --git a/python/cmtj.cpp b/python/cmtj.cpp index a944e18..19aa239 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -329,6 +329,11 @@ PYBIND11_MODULE(cmtj, m) { &DSeriesStack::setCouplingStrength), "coupling"_a) .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) + .def("getJunction", &DParallelStack::getJunction, "junctionId"_a, + py::return_value_policy::reference) + .def("setJunctionAnisotropyDriver", + &DSeriesStack::setJunctionAnisotropyDriver, "junctionId"_a, + "layerId"_a, "k"_a) // logging .def("clearLogs", &DSeriesStack::clearLogs) .def("getLog", py::overload_cast(&DSeriesStack::getLog)) @@ -357,6 +362,11 @@ PYBIND11_MODULE(cmtj, m) { &DParallelStack::setCouplingStrength), "coupling"_a) .def("setDelayed", &DParallelStack::setDelayed, "delayed"_a) + .def("getJunction", &DParallelStack::getJunction, "junctionId"_a, + py::return_value_policy::reference) + .def("setJunctionAnisotropyDriver", + &DSeriesStack::setJunctionAnisotropyDriver, "junctionId"_a, + "layerId"_a, "k"_a) // logging .def("clearLogs", &ParallelStack::clearLogs) .def("getLog", From 0a4cb199e3240588796e5a6a42df45d82ce4b89e Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 16:19:03 +0200 Subject: [PATCH 17/44] IDMI now renormalised like IEC --- core/junction.hpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/core/junction.hpp b/core/junction.hpp index 5b9adde..2d81dff 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -584,7 +584,8 @@ template class Layer { const CVector &coupledMag) { const CVector Dunit(1 ? Dvalue.x : 0, 1 ? Dvalue.y : 0, 1 ? Dvalue.z : 0); - return Dunit * c_dot(Dvalue, c_cross(CVector(1., 1., 1.), stepMag)); + return Dunit * c_dot(Dvalue, c_cross(CVector(1., 1., 1.), stepMag)) / + (this->Ms * this->thickness); } CVector calculateIDMI(T time, const CVector &stepMag, From db75eaa8e297adee20ee702e7b4b37bcc3bd8a43 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 21:14:07 +0200 Subject: [PATCH 18/44] return a layer reference --- .pre-commit-config.yaml | 3 ++- CHANGELOG.md | 1 + cmtj/__init__.pyi | 6 ++++++ cmtj/stack/__init__.pyi | 18 +++++++++++++++++- core/junction.hpp | 2 +- python/cmtj.cpp | 2 ++ 6 files changed, 29 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e0dad85..d95d321 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,9 +50,10 @@ repos: hooks: - id: cppcheck args: ["--check-level=exhaustive"] - exclude: third_party/.* + files: cmtj/.*\.(cpp|hpp)$ - id: clang-format args: [-i] + exclude: ^third_party/ # - id: clang-tidy # args: [-checks=*] # - id: include-what-you-use diff --git a/CHANGELOG.md b/CHANGELOG.md index 9189ab1..75da526 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Custom definition of the `ScalarDriver` is now possible and documented. - Fixed a bug in the `Stack` class which inverted the connection order of in-series connections. - Exposed IDMI interaction to Layer and Junction classes. +- Added `getLayer` method to the `Junction` class and `getJunction` method to the `Stack` class that return a reference to the object. # 1.5.0-1.5.4 diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index 0909e61..2cf2d8b 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -409,6 +409,12 @@ class Junction: """ ... + def getLayer(self, layerId: str) -> Layer: + """Get a specific layer from the junction. Returns a reference. + :param layerId: the id of the layer (string) as passed in the init. + """ + ... + class Layer: def __init__( self, diff --git a/cmtj/stack/__init__.pyi b/cmtj/stack/__init__.pyi index 4338f02..adb9324 100644 --- a/cmtj/stack/__init__.pyi +++ b/cmtj/stack/__init__.pyi @@ -89,7 +89,17 @@ class ParallelStack: """ ... - def getMagnetisation(self, junction: int, layerId: str) -> cmtj.CVector: ... + def getMagnetisation(self, junction: int, layerId: str) -> cmtj.CVector: + """Get the magnetisation of a specific layer in a specific junction. + :param junction: the id of the junction (int) as passed in the init. + :param layerId: the string id of the layer in the junction.""" + ... + + def getJunction(self, junctionId: int) -> cmtj.Junction: + """Get a specific junction from the stack. Returns a reference. + :param junctionId: the id of the junction (int) as passed in the init. + """ + ... class SeriesStack: def __init__( @@ -194,3 +204,9 @@ class SeriesStack: :param junction: the id of the junction (int) as passed in the init. :param layerId: the string id of the layer in the junction.""" ... + + def getJunction(self, junctionId: int) -> cmtj.Junction: + """Get a specific junction from the stack. Returns a reference. + :param junctionId: the id of the junction (int) as passed in the init. + """ + ... diff --git a/core/junction.hpp b/core/junction.hpp index 2d81dff..3d23d57 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -1246,7 +1246,7 @@ template class Junction { Layer &getLayer(const std::string &layerID) { const auto res = std::find_if( this->layers.begin(), this->layers.end(), - [layerID](const auto &l) -> bool { return (l.id == layerID); }); + [&layerID](const auto &l) -> bool { return (l.id == layerID); }); if (res != this->layers.end()) { return *res; } diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 19aa239..036c0cd 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -299,6 +299,8 @@ PYBIND11_MODULE(cmtj, m) { .def("getMagnetoresistance", &DJunction::getMagnetoresistance) // getters .def("getLayerIds", &DJunction::getLayerIds) + .def("getLayer", &DJunction::getLayer, "layerId"_a, + py::return_value_policy::reference) // readonly props .def_readonly("layers", &DJunction::layers); From dd8e1db13bcf62094186338bb3576fda9ded726e Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 21:14:31 +0200 Subject: [PATCH 19/44] return a layer reference --- core/stack.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/core/stack.hpp b/core/stack.hpp index 4bd9f6c..bf8de4f 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -62,7 +62,7 @@ template class Stack { stackSize = this->junctionList.size(); } - T getCoupling(const unsigned int &order) const { + T getCoupling(const unsigned int &order) const2 { if (this->couplingStrength.empty()) { throw std::runtime_error("Coupling strength is not set!"); } From 3da8527027576aacab660d71761979a7d8e1da0d Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 21:14:44 +0200 Subject: [PATCH 20/44] return a layer reference --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d95d321..a0fd5d8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -50,7 +50,7 @@ repos: hooks: - id: cppcheck args: ["--check-level=exhaustive"] - files: cmtj/.*\.(cpp|hpp)$ + files: core/.*\.(cpp|hpp)$ - id: clang-format args: [-i] exclude: ^third_party/ From 97bcf4d4b6370d45069810a0135cf3da7bb80b80 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 21:34:23 +0200 Subject: [PATCH 21/44] trying out new cpp config --- .pre-commit-config.yaml | 28 +- core/compute.hpp | 388 +++++++++++++------------ core/cvector.hpp | 466 +++++++++++++----------------- core/noise.hpp | 502 +++++++++++++++++---------------- core/reservoir.hpp | 464 +++++++++++++++--------------- core/stack.hpp | 2 +- deprecated/experiment.cpp | 394 +++++++++++++------------- examples/stt-fields/torque.cpp | 349 ++++++++++++----------- 8 files changed, 1262 insertions(+), 1331 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a0fd5d8..a1e03f4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,17 +2,19 @@ # See https://pre-commit.com/hooks.html for more hooks exclude: "^\ - (third_party/.*)|\ + (third_party/kissfft)|\ (build/.*)|\ (.github/.*)|\ (.vscode/.*)|\ (^tests)|\ - (docs/api/.*) + (docs/api/.*)|\ + (core/compute.hpp)|\ + (defaults.cfg) " repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v4.1.0 + rev: v4.6.0 hooks: - id: check-added-large-files # prevents giant files from being committed. - id: check-case-conflict # checks for files that would conflict in case-insensitive filesystems. @@ -26,14 +28,14 @@ repos: - id: trailing-whitespace # trims trailing whitespace. - repo: https://github.com/pre-commit/mirrors-prettier - rev: v2.5.1 + rev: v4.0.0-alpha.8 hooks: - id: prettier files: \.(html|json|markdown|md|yaml|yml)$ exclude: (^docs/api/.*) - repo: https://github.com/pycqa/isort - rev: 5.10.1 + rev: 5.13.2 hooks: - id: isort name: isort (python) @@ -46,15 +48,17 @@ repos: args: [--in-place, --recursive] - repo: https://github.com/pocc/pre-commit-hooks - rev: master + rev: v1.3.5 hooks: - id: cppcheck args: ["--check-level=exhaustive"] - files: core/.*\.(cpp|hpp)$ + files: ^(cmtj|core)/.*\.(cpp|hpp)$ + exclude: ^third_party/ | ^core/compute.hpp - id: clang-format args: [-i] - exclude: ^third_party/ - # - id: clang-tidy - # args: [-checks=*] - # - id: include-what-you-use - # exclude: ^third_party + files: ^(cmtj|core)/.*\.(cpp|hpp)$ + exclude: ^third_party/ | ^core/compute.hpp + - id: clang-tidy + args: [-checks=*] + files: ^(cmtj|core)/.*\.(cpp|hpp)$ + exclude: ^third_party/ | ^core/compute.hpp diff --git a/core/compute.hpp b/core/compute.hpp index e52b31f..b8f5456 100644 --- a/core/compute.hpp +++ b/core/compute.hpp @@ -3,12 +3,12 @@ #define COMPUTE_FUNCTIONS_H #define _USE_MATH_DEFINES -#include -#include #include -#include +#include #include +#include #include +#include #include #include #include @@ -19,205 +19,201 @@ * Provides a static interface for computing useful magnetic properties * such as Voltage Spin Diode Effect or FFT on the magnetoresistance. */ -template -class ComputeFunctions -{ +template class ComputeFunctions { public: - /** - * Computes the Voltage Spin diode. - * @param log: This is the log from the simulation. - * @param resTag: Tag to fetch the resistance from the simulation. - * @param frequency: excitation frequency for the current. - * @param power: power assumed in the system (this is somewhat an arbitrary value). - * @param minTime: time after which to take the log, preferably when the magnetisation is stable. - */ - static std::unordered_map calculateVoltageSpinDiode( - std::unordered_map> &log, - const std::string &resTag, - T frequency, T power = 10e-6, const T minTime = 10e-9) - { - if (log.empty()) - { - throw std::invalid_argument("Empty log! Cannot proceed without running a simulation!"); - } - - if (log.find(resTag) == log.end()) - { - // not found - throw std::invalid_argument("Tag was not found in the junction log: " + resTag); - } - const T omega = 2 * M_PI * frequency; - std::vector &resistance = log[resTag]; - auto it = std::find_if(log["time"].begin(), log["time"].end(), - [&minTime](const auto &value) - { return value >= minTime; }); - // turn into index - const int thresIdx = (int)(it - log["time"].begin()); - const int cutSize = log["time"].size() - thresIdx; - - // Rpp - const T RppMax = *std::max_element(resistance.begin() + thresIdx, resistance.end()); - const T RppMin = *std::min_element(resistance.begin() + thresIdx, resistance.end()); - const T avgR = std::accumulate(resistance.begin() + thresIdx, resistance.end(), 0.0) / cutSize; - const T Iampl = sqrt(power / avgR); - std::vector voltage, current; - std::transform( - log["time"].begin() + thresIdx, log["time"].end(), - std::back_inserter(current), - [&Iampl, &omega](const T &time) - { return Iampl * sin(omega * time); }); - - for (int i = 0; i < cutSize; i++) - { - voltage.push_back(resistance[thresIdx + i] * current[i]); - } - const T Vmix = std::accumulate(voltage.begin(), voltage.end(), 0.0) / voltage.size(); - std::unordered_map mRes = {{"Vmix", Vmix}, {"RMax", RppMax}, {"RMin", RppMin}, {"Rpp", (RppMax - RppMin)}}; - return mRes; + /** + * Computes the Voltage Spin diode. + * @param log: This is the log from the simulation. + * @param resTag: Tag to fetch the resistance from the simulation. + * @param frequency: excitation frequency for the current. + * @param power: power assumed in the system (this is somewhat an arbitrary + * value). + * @param minTime: time after which to take the log, preferably when the + * magnetisation is stable. + */ + static std::unordered_map calculateVoltageSpinDiode( + std::unordered_map> &log, + const std::string &resTag, T frequency, T power = 10e-6, + const T minTime = 10e-9) { + if (log.empty()) { + throw std::invalid_argument( + "Empty log! Cannot proceed without running a simulation!"); + } + + if (log.find(resTag) == log.end()) { + // not found + throw std::invalid_argument("Tag was not found in the junction log: " + + resTag); + } + const T omega = 2 * M_PI * frequency; + std::vector &resistance = log[resTag]; + auto it = std::find_if( + log["time"].begin(), log["time"].end(), + [&minTime](const auto &value) { return value >= minTime; }); + // turn into index + const int thresIdx = (int)(it - log["time"].begin()); + const int cutSize = log["time"].size() - thresIdx; + + // Rpp + const T RppMax = + *std::max_element(resistance.begin() + thresIdx, resistance.end()); + const T RppMin = + *std::min_element(resistance.begin() + thresIdx, resistance.end()); + const T avgR = + std::accumulate(resistance.begin() + thresIdx, resistance.end(), 0.0) / + cutSize; + const T Iampl = sqrt(power / avgR); + std::vector voltage, current; + std::transform( + log["time"].begin() + thresIdx, log["time"].end(), + std::back_inserter(current), + [&Iampl, &omega](const T &time) { return Iampl * sin(omega * time); }); + + for (int i = 0; i < cutSize; i++) { + voltage.push_back(resistance[thresIdx + i] * current[i]); + } + const T Vmix = + std::accumulate(voltage.begin(), voltage.end(), 0.0) / voltage.size(); + std::unordered_map mRes = {{"Vmix", Vmix}, + {"RMax", RppMax}, + {"RMin", RppMin}, + {"Rpp", (RppMax - RppMin)}}; + return mRes; + } + + /** + * Computes the FFT on a given tag. + * @param log: This is the log from the simulation. + * @param fftIds: a vector of ids (log keys) for which FFT is to be computed. + * @param minTime: minimum waiting time (10e-9) by default. Set it so that + * non-harmonic oscillations are not included into FFT computation. + * @param timeStep: integration step (1e-11) by default . + */ + static std::unordered_map> + spectralFFT(std::unordered_map> &log, + const std::vector &fftIds, T minTime = 10.0e-9, + T timeStep = 1e-11) { + + if (minTime >= log["time"][log["time"].size() - 1]) { + throw std::invalid_argument( + "The minTime parameter is larger than the simulation time!"); + } + if (log.empty()) { + throw std::invalid_argument( + "Empty log! Cannot proceed without running a simulation!"); } - /** - * Computes the FFT on a given tag. - * @param log: This is the log from the simulation. - * @param fftIds: a vector of ids (log keys) for which FFT is to be computed. - * @param minTime: minimum waiting time (10e-9) by default. Set it so that non-harmonic - * oscillations are not included into FFT computation. - * @param timeStep: integration step (1e-11) by default . - */ - static std::unordered_map> - spectralFFT(std::unordered_map> &log, - const std::vector &fftIds, - T minTime = 10.0e-9, T timeStep = 1e-11) - { - - if (minTime >= log["time"][log["time"].size() - 1]) - { - throw std::invalid_argument("The minTime parameter is larger than the simulation time!"); - } - if (log.empty()) - { - throw std::invalid_argument("Empty log! Cannot proceed without running a simulation!"); - } - - auto it = std::find_if(log["time"].begin(), log["time"].end(), - [&minTime](const auto &value) - { return value >= minTime; }); - const int thresIdx = (int)(it - log["time"].begin()); - const int cutSize = log["time"].size() - thresIdx; - // plan creation is not thread safe - const T normalizer = timeStep * cutSize; - const int maxIt = (cutSize % 2) ? cutSize / 2 : (cutSize - 1) / 2; - std::vector frequencySteps(maxIt); - for (int i = 1; i <= maxIt; i++) - { - frequencySteps[i - 1] = (i - 1) / normalizer; - } - // plan creation is not thread safe - std::unordered_map> spectralFFTResult; - spectralFFTResult["frequencies"] = std::move(frequencySteps); - - for (const auto &tag : fftIds) - { - if (log.find(tag) == log.end()) - { - // not found - throw std::invalid_argument("FFT id tag was not found in the junction log: " + tag); - } - std::vector cutMag(log[tag].begin() + thresIdx, log[tag].end()); - // define FFT plan - std::complex *out = new std::complex[cutMag.size()]; - fftw_plan plan = fftw_plan_dft_r2c_1d(cutMag.size(), - cutMag.data(), - reinterpret_cast(out), - FFTW_ESTIMATE); // here it's weird, FFT_FORWARD produces an empty plan - - if (plan == NULL) - { - throw std::runtime_error("Plan creation for fftw failed, cannot proceed"); - } - fftw_execute(plan); - const int outBins = (cutMag.size() + 1) / 2; - std::vector amplitudes, phases; - const double norm = (double)cutSize / 2; - amplitudes.push_back(out[0].real()); - phases.push_back(0.); - for (int i = 1; i < outBins - 1; i++) - { - const auto tandem = out[i]; - T real = tandem.real() / norm; // [0]; - T img = tandem.imag() / norm; // [1]; - amplitudes.push_back(sqrt(pow(real, 2) + pow(img, 2))); - phases.push_back(atan2(img, real)); - } - spectralFFTResult[tag + "_amplitude"] = std::move(amplitudes); - spectralFFTResult[tag + "_phase"] = std::move(phases); - fftw_destroy_plan(plan); - } - return spectralFFTResult; + auto it = std::find_if( + log["time"].begin(), log["time"].end(), + [&minTime](const auto &value) { return value >= minTime; }); + const int thresIdx = (int)(it - log["time"].begin()); + const int cutSize = log["time"].size() - thresIdx; + // plan creation is not thread safe + const T normalizer = timeStep * cutSize; + const int maxIt = (cutSize % 2) ? cutSize / 2 : (cutSize - 1) / 2; + std::vector frequencySteps(maxIt); + for (int i = 1; i <= maxIt; i++) { + frequencySteps[i - 1] = (i - 1) / normalizer; + } + // plan creation is not thread safe + std::unordered_map> spectralFFTResult; + spectralFFTResult["frequencies"] = std::move(frequencySteps); + + for (const auto &tag : fftIds) { + if (log.find(tag) == log.end()) { + // not found + throw std::invalid_argument( + "FFT id tag was not found in the junction log: " + tag); + } + std::vector cutMag(log[tag].begin() + thresIdx, log[tag].end()); + // define FFT plan + std::complex *out = new std::complex[cutMag.size()]; + fftw_plan plan = fftw_plan_dft_r2c_1d( + cutMag.size(), cutMag.data(), reinterpret_cast(out), + FFTW_ESTIMATE); // here it's weird, FFT_FORWARD produces an empty plan + + if (plan == NULL) { + throw std::runtime_error( + "Plan creation for fftw failed, cannot proceed"); + } + fftw_execute(plan); + const int outBins = (cutMag.size() + 1) / 2; + std::vector amplitudes, phases; + const double norm = (double)cutSize / 2; + amplitudes.push_back(out[0].real()); + phases.push_back(0.); + for (int i = 1; i < outBins - 1; i++) { + const auto tandem = out[i]; + T real = tandem.real() / norm; // [0]; + T img = tandem.imag() / norm; // [1]; + amplitudes.push_back(sqrt(pow(real, 2) + pow(img, 2))); + phases.push_back(atan2(img, real)); + } + spectralFFTResult[tag + "_amplitude"] = std::move(amplitudes); + spectralFFTResult[tag + "_phase"] = std::move(phases); + fftw_destroy_plan(plan); + } + return spectralFFTResult; + } + + static std::unordered_map> + spectralFFTMixed(std::unordered_map> &log, + const std::vector &tagsToMix, + T timeStep = 1e-11) { + const int cutSize = log["time"].size(); + if (log.empty()) { + throw std::invalid_argument( + "Empty log! Cannot proceed without running a simulation!"); + } + // plan creation is not thread safe + const T normalizer = timeStep * cutSize; + const int maxIt = (cutSize % 2) ? cutSize / 2 : (cutSize - 1) / 2; + std::vector frequencySteps(maxIt); + frequencySteps[0] = 0; + for (int i = 1; i <= maxIt; i++) { + frequencySteps[i - 1] = (i - 1) / normalizer; } + // plan creation is not thread safe + std::unordered_map> spectralFFTResult; + spectralFFTResult["frequencies"] = std::move(frequencySteps); + + std::vector mixedSignal(log["time"].size(), 0); + for (const auto &tag : tagsToMix) { + if (log.find(tag) == log.end()) + // not found + throw std::invalid_argument( + "FFT id tag was not found in the junction log: " + tag); + for (unsigned int i = 0; i < log["time"].size(); i++) { + mixedSignal[i] += log[tag][i]; + } + } + + // define FFT plan + std::complex *out = new std::complex[mixedSignal.size()]; + fftw_plan plan = fftw_plan_dft_r2c_1d( + mixedSignal.size(), mixedSignal.data(), + reinterpret_cast(out), + FFTW_ESTIMATE); // here it's weird, FFT_FORWARD produces an empty plan - static std::unordered_map> - spectralFFTMixed(std::unordered_map> &log, - const std::vector &tagsToMix, T timeStep = 1e-11) - { - const int cutSize = log["time"].size(); - if (log.empty()) - { - throw std::invalid_argument("Empty log! Cannot proceed without running a simulation!"); - } - // plan creation is not thread safe - const T normalizer = timeStep * cutSize; - const int maxIt = (cutSize % 2) ? cutSize / 2 : (cutSize - 1) / 2; - std::vector frequencySteps(maxIt); - frequencySteps[0] = 0; - for (int i = 1; i <= maxIt; i++) - { - frequencySteps[i - 1] = (i - 1) / normalizer; - } - // plan creation is not thread safe - std::unordered_map> spectralFFTResult; - spectralFFTResult["frequencies"] = std::move(frequencySteps); - - std::vector mixedSignal(log["time"].size(), 0); - for (const auto &tag : tagsToMix) - { - if (log.find(tag) == log.end()) - // not found - throw std::invalid_argument("FFT id tag was not found in the junction log: " + tag); - for (unsigned int i = 0; i < log["time"].size(); i++) - { - mixedSignal[i] += log[tag][i]; - } - } - - // define FFT plan - std::complex *out = new std::complex[mixedSignal.size()]; - fftw_plan plan = fftw_plan_dft_r2c_1d(mixedSignal.size(), - mixedSignal.data(), - reinterpret_cast(out), - FFTW_ESTIMATE); // here it's weird, FFT_FORWARD produces an empty plan - - if (plan == NULL) - { - throw std::runtime_error("Plan creation for fftw failed, cannot proceed"); - } - fftw_execute(plan); - const int outBins = (mixedSignal.size() + 1) / 2; - std::vector amplitudes; - amplitudes.push_back(out[0].real()); - const double norm = (double)cutSize / 2; - for (int i = 1; i < outBins; i++) - { - const auto tandem = out[i]; - T real = tandem.real() / norm; // [0]; - T img = tandem.imag() / norm; // [1]; - amplitudes.push_back(sqrt(pow(real, 2) + pow(img, 2))); - } - spectralFFTResult["mixed_amplitude"] = std::move(amplitudes); - fftw_destroy_plan(plan); - - return spectralFFTResult; + if (plan == NULL) { + throw std::runtime_error("Plan creation for fftw failed, cannot proceed"); } + fftw_execute(plan); + const int outBins = (mixedSignal.size() + 1) / 2; + std::vector amplitudes; + amplitudes.push_back(out[0].real()); + const double norm = (double)cutSize / 2; + for (int i = 1; i < outBins; i++) { + const auto tandem = out[i]; + T real = tandem.real() / norm; // [0]; + T img = tandem.imag() / norm; // [1]; + amplitudes.push_back(sqrt(pow(real, 2) + pow(img, 2))); + } + spectralFFTResult["mixed_amplitude"] = std::move(amplitudes); + fftw_destroy_plan(plan); + + return spectralFFTResult; + } }; #endif diff --git a/core/cvector.hpp b/core/cvector.hpp index 14158ca..4a4bc90 100644 --- a/core/cvector.hpp +++ b/core/cvector.hpp @@ -1,282 +1,206 @@ #ifndef CORE_CVECTOR_HPP_ #define CORE_CVECTOR_HPP_ -#include // for function -#include // for operator<<, ostream -#include // for runtime_error -#include // for allocator, vector -#include // for char_traits, basic_stringstream, basic_os.. +#include // for function +#include // for operator<<, ostream +#include // for char_traits, basic_stringstream, basic_os.. +#include // for runtime_error +#include // for allocator, vector /// @brief A simple enum to represent the axis -enum Axis -{ - xaxis, - yaxis, - zaxis, - all, - none -}; +enum Axis { xaxis, yaxis, zaxis, all, none }; -template -class CVector -{ +template class CVector { public: - T x, y, z; - CVector() - { - this->x = 0.0; - this->y = 0.0; - this->z = 0.0; - } - - explicit CVector(const std::vector& vec) - { - if (vec.size() != 3) - { - throw std::runtime_error("Failed to create vector -- passed list was not of len 3!"); - } - this->x = vec[0]; - this->y = vec[1]; - this->z = vec[2]; - } - - CVector(T x, T y, T z) - { - this->x = x; - this->y = y; - this->z = z; - } - - CVector(const CVector& v) - { - this->x = v.x; - this->y = v.y; - this->z = v.z; - } - - explicit CVector(const std::function& generator) - { - // the noise should be independent in each direction - this->x = generator(); - this->y = generator(); - this->z = generator(); - } - - CVector& operator+=(const CVector& v) - { - this->x += v.x; - this->y += v.y; - this->z += v.z; - return *this; - } - - CVector& operator-=(const CVector& v) - { - this->x -= v.x; - this->y -= v.y; - this->z -= v.z; - return *this; - } - - CVector operator+(CVector v) - { - CVector res( - x + v.x, - y + v.y, - z + v.z); - - return res; - }; - - CVector operator+(const CVector& v) const - { - CVector res( - x + v.x, - y + v.y, - z + v.z); - - return res; - }; - - CVector operator+(const T& val) const - { - CVector res( - x + val, - y + val, - z + val); - return res; - } - - CVector operator-(CVector v) - { - CVector res( - x - v.x, - y - v.y, - z - v.z); - - return res; - }; - CVector operator-(const CVector& v) const - { - CVector res( - x - v.x, - y - v.y, - z - v.z); - - return res; - }; - - void operator=(CVector v) - { - x = v.x; - y = v.y; - z = v.z; - } - - bool operator==(const CVector& v) - { - if ( - (x == v.x) && (y == v.y) && (z == v.z)) - return true; - return false; - }; - - bool operator==(const CVector& v) const - { - if ( - (x == v.x) && (y == v.y) && (z == v.z)) - return true; - return false; - }; - - bool operator!=(const CVector& v) - { - if ( - (x == v.x) && (y == v.y) && (z == v.z)) - return false; - return true; - }; - - bool operator!=(const CVector& v) const - { - if ( - (x == v.x) && (y == v.y) && (z == v.z)) - return false; - return true; - }; - - CVector operator*(const T& val) - { - CVector res( - x * val, - y * val, - z * val); - return res; - }; - - CVector operator*(const T& val) const - { - const CVector res( - x * val, - y * val, - z * val); - return res; - } - - friend CVector operator*(const T& val, const CVector& v) { - return CVector(val * v.x, val * v.y, val * v.z); - } - - CVector& operator*=(T v) { x *= v; y *= v; z *= v; return *this; } - - CVector operator/(T val) - { - CVector res( - x / val, - y / val, - z / val); - return res; - }; - - T operator[](const int& i) - { - if (i == 0) - return x; - else if (i == 1) - return y; - else - return z; - } - - T operator[](const int& i) const - { - if (i == 0) - return x; - else if (i == 1) - return y; - else - return z; - } - - T length() - { - return sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); - }; // Magnitude - - T length() const - { - return sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); - }; // Magnitude - - void normalize() - { - const T mag = this->length(); - if (mag != 0) - { - x = x / mag; - y = y / mag; - z = z / mag; - } - }; - void setX(const T& vx) - { - this->x = vx; - } - void setY(const T& vy) - { - this->y = vy; - } - void setZ(const T& vz) - { - this->z = vz; - } - - std::vector tolist() - { - return { - this->x, this->y, this->z }; - } - - friend std::ostream& operator<<(std::ostream& o, const CVector& obj) - { - o << obj.toString(); - return o; - } - - std::string toString() - { - std::stringstream ss; - ss << "[x:" << this->x << ", y:" << this->y << ", z:" << this->z << "]"; - return ss.str(); - } - - const std::string toString() const - { - std::stringstream ss; - ss << "[x:" << this->x << ", y:" << this->y << ", z:" << this->z << "]"; - return ss.str(); - } + T x, y, z; + CVector() { + this->x = 0.0; + this->y = 0.0; + this->z = 0.0; + } + + explicit CVector(const std::vector &vec) { + if (vec.size() != 3) { + throw std::runtime_error( + "Failed to create vector -- passed list was not of len 3!"); + } + this->x = vec[0]; + this->y = vec[1]; + this->z = vec[2]; + } + + CVector(T x, T y, T z) { + this->x = x; + this->y = y; + this->z = z; + } + + CVector(const CVector &v) { + this->x = v.x; + this->y = v.y; + this->z = v.z; + } + + explicit CVector(const std::function &generator) { + // the noise should be independent in each direction + this->x = generator(); + this->y = generator(); + this->z = generator(); + } + + CVector &operator+=(const CVector &v) { + this->x += v.x; + this->y += v.y; + this->z += v.z; + return *this; + } + + CVector &operator-=(const CVector &v) { + this->x -= v.x; + this->y -= v.y; + this->z -= v.z; + return *this; + } + + CVector operator+(CVector v) { + CVector res(x + v.x, y + v.y, z + v.z); + + return res; + }; + + CVector operator+(const CVector &v) const { + CVector res(x + v.x, y + v.y, z + v.z); + + return res; + }; + + CVector operator+(const T &val) const { + CVector res(x + val, y + val, z + val); + return res; + } + + CVector operator-(CVector v) { + CVector res(x - v.x, y - v.y, z - v.z); + + return res; + }; + CVector operator-(const CVector &v) const { + CVector res(x - v.x, y - v.y, z - v.z); + + return res; + }; + + void operator=(CVector v) { + x = v.x; + y = v.y; + z = v.z; + } + + bool operator==(const CVector &v) { + if ((x == v.x) && (y == v.y) && (z == v.z)) + return true; + return false; + }; + + bool operator==(const CVector &v) const { + if ((x == v.x) && (y == v.y) && (z == v.z)) + return true; + return false; + }; + + bool operator!=(const CVector &v) { + if ((x == v.x) && (y == v.y) && (z == v.z)) + return false; + return true; + }; + + bool operator!=(const CVector &v) const { + if ((x == v.x) && (y == v.y) && (z == v.z)) + return false; + return true; + }; + + CVector operator*(const T &val) { + CVector res(x * val, y * val, z * val); + return res; + }; + + CVector operator*(const T &val) const { + const CVector res(x * val, y * val, z * val); + return res; + } + + friend CVector operator*(const T &val, const CVector &v) { + return CVector(val * v.x, val * v.y, val * v.z); + } + + CVector &operator*=(T v) { + x *= v; + y *= v; + z *= v; + return *this; + } + + CVector operator/(T val) { + CVector res(x / val, y / val, z / val); + return res; + }; + + T operator[](const int &i) { + if (i == 0) + return x; + else if (i == 1) + return y; + else + return z; + } + + T operator[](const int &i) const { + if (i == 0) + return x; + else if (i == 1) + return y; + else + return z; + } + + T length() { return sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); }; // Magnitude + + T length() const { + return sqrt(pow(x, 2) + pow(y, 2) + pow(z, 2)); + }; // Magnitude + + void normalize() { + const T mag = this->length(); + if (mag != 0) { + x = x / mag; + y = y / mag; + z = z / mag; + } + }; + void setX(const T &vx) { this->x = vx; } + void setY(const T &vy) { this->y = vy; } + void setZ(const T &vz) { this->z = vz; } + + std::vector tolist() { return {this->x, this->y, this->z}; } + + friend std::ostream &operator<<(std::ostream &o, const CVector &obj) { + o << obj.toString(); + return o; + } + + std::string toString() { + std::stringstream ss; + ss << "[x:" << this->x << ", y:" << this->y << ", z:" << this->z << "]"; + return ss.str(); + } + + const std::string toString() const { + std::stringstream ss; + ss << "[x:" << this->x << ", y:" << this->y << ", z:" << this->z << "]"; + return ss.str(); + } }; #endif // CORE_CVECTOR_HPP_ diff --git a/core/noise.hpp b/core/noise.hpp index 2cd7f1e..689f447 100644 --- a/core/noise.hpp +++ b/core/noise.hpp @@ -3,7 +3,8 @@ * @author Jakub * @brief One F generator, based on the Pink Noise generator from the Music DSP * https://www.musicdsp.org/en/latest/Synthesis/220-trammell-pink-noise-c-class.html - * Second version is custom and gives better results, but builds on the initial one. + * Second version is custom and gives better results, but builds on the initial + * one. * @version 1.0 * @date 2022-03-22 * @@ -14,288 +15,295 @@ #ifndef _PinkNoise_H #define _PinkNoise_H -#include // for generate, sort, unique -#include // for distance -#include // for rand, srand, NULL, RAND_MAX, size_t -#include // for time -#include // for accumulate -#include // for uniform_real_distribution, geometr... -#include // for vector +#include "../third_party/kissfft/kissfft.hh" +#include "cvector.hpp" +#include // for generate, sort, unique #include +#include // for rand, srand, NULL, RAND_MAX, size_t +#include // for time +#include // for distance #include -#include "cvector.hpp" -#include "../third_party/kissfft/kissfft.hh" +#include // for accumulate +#include // for uniform_real_distribution, geometr... +#include // for vector #define PINK_NOISE_NUM_STAGES 3 -template -class PinkNoise { +template class PinkNoise { public: - PinkNoise() { - srand(time(NULL)); // initialize random generator - clear(); - } - - void clear() { - for (size_t i = 0; i < PINK_NOISE_NUM_STAGES; i++) - state[i] = 0.0; - } - - T tick() { - static const T RMI2 = 2.0 / T(RAND_MAX); // + 1.0; // change for range [0,1) - static const T offset = A[0] + A[1] + A[2]; - - // unrolled loop - T temp = T(rand()); - state[0] = P[0] * (state[0] - temp) + temp; - temp = T(rand()); - state[1] = P[1] * (state[1] - temp) + temp; - temp = T(rand()); - state[2] = P[2] * (state[2] - temp) + temp; - return (A[0] * state[0] + A[1] * state[1] + A[2] * state[2]) * RMI2 - offset; - } + PinkNoise() { + srand(time(NULL)); // initialize random generator + clear(); + } + + void clear() { + for (size_t i = 0; i < PINK_NOISE_NUM_STAGES; i++) + state[i] = 0.0; + } + + T tick() { + static const T RMI2 = 2.0 / T(RAND_MAX); // + 1.0; // change for range [0,1) + static const T offset = A[0] + A[1] + A[2]; + + // unrolled loop + T temp = T(rand()); + state[0] = P[0] * (state[0] - temp) + temp; + temp = T(rand()); + state[1] = P[1] * (state[1] - temp) + temp; + temp = T(rand()); + state[2] = P[2] * (state[2] - temp) + temp; + return (A[0] * state[0] + A[1] * state[1] + A[2] * state[2]) * RMI2 - + offset; + } protected: - T state[PINK_NOISE_NUM_STAGES]; - static constexpr T A[PINK_NOISE_NUM_STAGES] = { 0.02109238, 0.07113478, 0.68873558 }; - static constexpr T P[PINK_NOISE_NUM_STAGES] = { 0.3190, 0.7756, 0.9613 }; + T state[PINK_NOISE_NUM_STAGES]; + static constexpr T A[PINK_NOISE_NUM_STAGES] = {0.02109238, 0.07113478, + 0.68873558}; + static constexpr T P[PINK_NOISE_NUM_STAGES] = {0.3190, 0.7756, 0.9613}; }; - -template -class NullTicker { +template class NullTicker { public: - explicit NullTicker() {} - ~NullTicker() {} - virtual T tick() { - return 0; - } + explicit NullTicker() {} + ~NullTicker() {} + virtual T tick() { return 0; } }; -template -class OneFNoise { +template class OneFNoise { private: - int sources; - std::vector state; - std::geometric_distribution geom_distr; - // Mersenne twister is higher quality than the default one - std::mt19937 generator; - std::uniform_real_distribution float_dist; - std::vector trials; - - T scale = 1; - T sumTrack = 0; + int sources; + std::vector state; + std::geometric_distribution geom_distr; + // Mersenne twister is higher quality than the default one + std::mt19937 generator; + std::uniform_real_distribution float_dist; + std::vector trials; + + T scale = 1; + T sumTrack = 0; + public: - OneFNoise(int sources, T bias, T scale) : sources(sources), geom_distr(bias), scale(scale) { - this->state.resize(sources); // fill it with 0s - this->trials.resize(sources); - this->float_dist = std::uniform_real_distribution(0, 1); - // start off with random values in the state - std::generate(this->state.begin(), this->state.end(), [&] { return this->float_dist(generator);}); - // try out the binding stuff - } - /** - * @brief This function works faster if p is a large number (p > 0.5) - * - * @return T sum of the state - */ - T tick() { - std::generate(this->trials.begin(), this->trials.end(), [&] { return this->geom_distr(generator);}); - std::sort(this->trials.begin(), this->trials.end()); - const auto uniq = std::unique(this->trials.begin(), this->trials.end()); - // compute the distance of the last unique element - // this basically takes only the unique elements of the trials - // because if we repeatedly change the same index, we don't get any advantage - const auto lastIndx = std::distance(this->trials.begin(), uniq); - for (int i = 0; i < lastIndx; ++i) { - const auto t = this->trials[i]; - if (t < this->sources) { - this->state[t] = this->float_dist(generator); - } - } - return this->scale * std::accumulate(state.begin(), state.end(), 0.); + OneFNoise(int sources, T bias, T scale) + : sources(sources), geom_distr(bias), scale(scale) { + this->state.resize(sources); // fill it with 0s + this->trials.resize(sources); + this->float_dist = std::uniform_real_distribution(0, 1); + // start off with random values in the state + std::generate(this->state.begin(), this->state.end(), + [&] { return this->float_dist(generator); }); + // try out the binding stuff + } + /** + * @brief This function works faster if p is a large number (p > 0.5) + * + * @return T sum of the state + */ + T tick() { + std::generate(this->trials.begin(), this->trials.end(), + [&] { return this->geom_distr(generator); }); + std::sort(this->trials.begin(), this->trials.end()); + const auto uniq = std::unique(this->trials.begin(), this->trials.end()); + // compute the distance of the last unique element + // this basically takes only the unique elements of the trials + // because if we repeatedly change the same index, we don't get any + // advantage + const auto lastIndx = std::distance(this->trials.begin(), uniq); + for (int i = 0; i < lastIndx; ++i) { + const auto t = this->trials[i]; + if (t < this->sources) { + this->state[t] = this->float_dist(generator); + } } - - /** - * @brief This function works faster if the p is a small number (p < 0.5) - * - * @return T sum of the state - */ - T tick2() { - std::generate(this->trials.begin(), this->trials.end(), [&] { return this->geom_distr(generator);}); - for (const auto& t : this->trials) { - if (t < this->sources) { - this->state[t] = this->float_dist(generator); - } - } - return this->scale * std::accumulate(state.begin(), state.end(), 0.); + return this->scale * std::accumulate(state.begin(), state.end(), 0.); + } + + /** + * @brief This function works faster if the p is a small number (p < 0.5) + * + * @return T sum of the state + */ + T tick2() { + std::generate(this->trials.begin(), this->trials.end(), + [&] { return this->geom_distr(generator); }); + for (const auto &t : this->trials) { + if (t < this->sources) { + this->state[t] = this->float_dist(generator); + } } + return this->scale * std::accumulate(state.begin(), state.end(), 0.); + } }; std::mt19937 generator(std::random_device{}()); -template -class BufferedAlphaNoise : public NullTicker { +template class BufferedAlphaNoise : public NullTicker { protected: - std::vector> bufferWhite, bufferColoured; - std::vector> bufferWhiteComplex, bufferColouredComplex; - std::vector result; - unsigned int bufferSize; - std::function gaussPDF; - T alpha = 1.; - T scale = 1.; - std::shared_ptr> fwd, inv; // configs for forward and inverse real fft - unsigned int internalCounter = 0; - unsigned int refills = 0; -public: + std::vector> bufferWhite, bufferColoured; + std::vector> bufferWhiteComplex, bufferColouredComplex; + std::vector result; + unsigned int bufferSize; + std::function gaussPDF; + T alpha = 1.; + T scale = 1.; + std::shared_ptr> fwd, + inv; // configs for forward and inverse real fft + unsigned int internalCounter = 0; + unsigned int refills = 0; - /** - * @brief Construct a new Buffered Alpha Noise object - * - * @param bufferSize the size of the buffer - * @param alpha the alpha parameter 1/f^alpha - * @param std the standard deviation of the gaussian - * @param scale the scaling parameter - */ - BufferedAlphaNoise(unsigned int bufferSize, T alpha, T std, T scale) : bufferSize(bufferSize), alpha(alpha), scale(scale) { - - this->bufferColoured.resize(2 * bufferSize); - this->bufferWhite.resize(2 * bufferSize); - this->result.resize(bufferSize); - this->bufferColouredComplex.resize(2 * bufferSize); - this->bufferWhiteComplex.resize(2 * bufferSize); - - // these are the filter weights -- we only have to fill it once per alpha and N - this->bufferColoured[0] = 1.0; - for (unsigned int i = 1; i < this->bufferSize; ++i) { - const float weight = (float)(0.5 * alpha + ( - (float)(i - 1) - )) / ((float)i); - this->bufferColoured[i] = this->bufferColoured[i - 1] * weight; - } - - this->gaussPDF = std::bind(std::normal_distribution(0, std), std::ref(generator)); - - this->fwd = std::shared_ptr>(new kissfft(2 * this->bufferSize, false)); - this->inv = std::shared_ptr>(new kissfft(2 * this->bufferSize, true)); +public: + /** + * @brief Construct a new Buffered Alpha Noise object + * + * @param bufferSize the size of the buffer + * @param alpha the alpha parameter 1/f^alpha + * @param std the standard deviation of the gaussian + * @param scale the scaling parameter + */ + BufferedAlphaNoise(unsigned int bufferSize, T alpha, T std, T scale) + : bufferSize(bufferSize), alpha(alpha), scale(scale) { + + this->bufferColoured.resize(2 * bufferSize); + this->bufferWhite.resize(2 * bufferSize); + this->result.resize(bufferSize); + this->bufferColouredComplex.resize(2 * bufferSize); + this->bufferWhiteComplex.resize(2 * bufferSize); + + // these are the filter weights -- we only have to fill it once per alpha + // and N + this->bufferColoured[0] = 1.0; + for (unsigned int i = 1; i < this->bufferSize; ++i) { + const float weight = (float)(0.5 * alpha + ((float)(i - 1))) / ((float)i); + this->bufferColoured[i] = this->bufferColoured[i - 1] * weight; } - ~BufferedAlphaNoise() { - } + this->gaussPDF = + std::bind(std::normal_distribution(0, std), std::ref(generator)); - void fillBuffer() { - // this is actual generation function - // generate random white as a baseline, only N values, rest is 0 padded - std::generate(this->bufferWhite.begin(), this->bufferWhite.begin() + this->bufferSize, this->gaussPDF); - - for (unsigned int i = this->bufferSize; i < 2 * this->bufferSize; ++i) { - this->bufferColoured[i] = 0; - this->bufferWhite[i] = 0; - } - // perform the fft - this->fwd->transform(&this->bufferWhite[0], &this->bufferWhiteComplex[0]); - this->fwd->transform(&this->bufferColoured[0], &this->bufferColouredComplex[0]); - - // multiply the two - for (unsigned int i = 0; i < this->bufferSize; ++i) { - this->bufferColouredComplex[i] = this->bufferColouredComplex[i] * this->bufferWhiteComplex[i]; - } - // invert - this->bufferColouredComplex[0] = this->bufferColouredComplex[0] / std::complex(2.0, 0); - this->bufferColouredComplex[this->bufferSize - 1] = this->bufferColouredComplex[this->bufferSize - 1] / std::complex(2.0, 0); - for (unsigned int i = this->bufferSize; i < 2 * this->bufferSize; ++i) { - this->bufferColouredComplex[i] = 0.; - } - this->inv->transform(&this->bufferColouredComplex[0], &this->bufferWhiteComplex[0]); - - std::transform( - this->bufferWhiteComplex.begin(), - this->bufferWhiteComplex.begin() + this->bufferSize, - this->result.begin(), - [&](std::complex x) { return x.real() / (this->bufferSize); } - ); - } + this->fwd = std::shared_ptr>( + new kissfft(2 * this->bufferSize, false)); + this->inv = std::shared_ptr>( + new kissfft(2 * this->bufferSize, true)); + } - const std::vector& getFullBuffer() { - return this->result; - } + ~BufferedAlphaNoise() {} - // overload from null ticker - T tick() override { - // we measure only up to a buffer size, not 2x buffer size - if (this->internalCounter == 0) { - this->fillBuffer(); - } - const auto ret = this->result[this->internalCounter]; - this->internalCounter = (this->internalCounter + 1) % this->bufferSize; - return this->scale * ret; - } + void fillBuffer() { + // this is actual generation function + // generate random white as a baseline, only N values, rest is 0 padded + std::generate(this->bufferWhite.begin(), + this->bufferWhite.begin() + this->bufferSize, this->gaussPDF); + for (unsigned int i = this->bufferSize; i < 2 * this->bufferSize; ++i) { + this->bufferColoured[i] = 0; + this->bufferWhite[i] = 0; + } + // perform the fft + this->fwd->transform(&this->bufferWhite[0], &this->bufferWhiteComplex[0]); + this->fwd->transform(&this->bufferColoured[0], + &this->bufferColouredComplex[0]); + + // multiply the two + for (unsigned int i = 0; i < this->bufferSize; ++i) { + this->bufferColouredComplex[i] = + this->bufferColouredComplex[i] * this->bufferWhiteComplex[i]; + } + // invert + this->bufferColouredComplex[0] = + this->bufferColouredComplex[0] / std::complex(2.0, 0); + this->bufferColouredComplex[this->bufferSize - 1] = + this->bufferColouredComplex[this->bufferSize - 1] / + std::complex(2.0, 0); + for (unsigned int i = this->bufferSize; i < 2 * this->bufferSize; ++i) { + this->bufferColouredComplex[i] = 0.; + } + this->inv->transform(&this->bufferColouredComplex[0], + &this->bufferWhiteComplex[0]); + + std::transform(this->bufferWhiteComplex.begin(), + this->bufferWhiteComplex.begin() + this->bufferSize, + this->result.begin(), [&](std::complex x) { + return x.real() / (this->bufferSize); + }); + } + + const std::vector &getFullBuffer() { return this->result; } + + // overload from null ticker + T tick() override { + // we measure only up to a buffer size, not 2x buffer size + if (this->internalCounter == 0) { + this->fillBuffer(); + } + const auto ret = this->result[this->internalCounter]; + this->internalCounter = (this->internalCounter + 1) % this->bufferSize; + return this->scale * ret; + } }; -template -class VectorAlphaNoise { +template class VectorAlphaNoise { private: - T scale = 1.; - // 3 components of type BufferedAlphaNoise, or NullTicker - std::unique_ptr> components_x, components_y, components_z; - CVector prevSample, currentSample; - bool normalized = true; + T scale = 1.; + // 3 components of type BufferedAlphaNoise, or NullTicker + std::unique_ptr> components_x, components_y, components_z; + CVector prevSample, currentSample; + bool normalized = true; + public: - VectorAlphaNoise(unsigned int bufferSize, T alpha, T std, T scale, Axis axis = Axis::all) : scale(scale) { - // initialize the as null tickers - this->components_x = std::unique_ptr>(new NullTicker()); - this->components_y = std::unique_ptr>(new NullTicker()); - this->components_z = std::unique_ptr>(new NullTicker()); - - switch (axis) - { - case Axis::all: - this->components_x = std::unique_ptr>(new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); - this->components_y = std::unique_ptr>(new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); - this->components_z = std::unique_ptr>(new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); - this->normalized = true; - break; - case Axis::xaxis: - this->components_x = std::unique_ptr>(new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); - this->normalized = false; - break; - case Axis::yaxis: - this->components_y = std::unique_ptr>(new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); - this->normalized = false; - break; - case Axis::zaxis: - this->components_z = std::unique_ptr>(new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); - this->normalized = false; - break; - default: - throw std::runtime_error("Invalid axis specified: " + std::to_string(static_cast(axis))); - } + VectorAlphaNoise(unsigned int bufferSize, T alpha, T std, T scale, + Axis axis = Axis::all) + : scale(scale) { + // initialize the as null tickers + this->components_x = std::unique_ptr>(new NullTicker()); + this->components_y = std::unique_ptr>(new NullTicker()); + this->components_z = std::unique_ptr>(new NullTicker()); + + switch (axis) { + case Axis::all: + this->components_x = std::unique_ptr>( + new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); + this->components_y = std::unique_ptr>( + new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); + this->components_z = std::unique_ptr>( + new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); + this->normalized = true; + break; + case Axis::xaxis: + this->components_x = std::unique_ptr>( + new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); + this->normalized = false; + break; + case Axis::yaxis: + this->components_y = std::unique_ptr>( + new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); + this->normalized = false; + break; + case Axis::zaxis: + this->components_z = std::unique_ptr>( + new BufferedAlphaNoise(bufferSize, alpha, std, 1.)); + this->normalized = false; + break; + default: + throw std::runtime_error("Invalid axis specified: " + + std::to_string(static_cast(axis))); } + } - CVector tickVector() { - // TODO -- if normalized, generate only 2 values and compute the third from the normalization - this->prevSample = this->currentSample; - this->currentSample = CVector( - this->components_x->tick(), - this->components_y->tick(), - this->components_z->tick() - ); - if (this->normalized) - this->currentSample.normalize(); - return this->currentSample * this->scale; - } + CVector tickVector() { + // TODO -- if normalized, generate only 2 values and compute the third from + // the normalization + this->prevSample = this->currentSample; + this->currentSample = + CVector(this->components_x->tick(), this->components_y->tick(), + this->components_z->tick()); + if (this->normalized) + this->currentSample.normalize(); + return this->currentSample * this->scale; + } - T tick() { - return this->components_x->tick() * this->scale; - } + T tick() { return this->components_x->tick() * this->scale; } + CVector getPrevSample() { return this->prevSample; } - CVector getPrevSample() { - return this->prevSample; - } - - T getScale() { - return this->scale; - } + T getScale() { return this->scale; } }; #endif diff --git a/core/reservoir.hpp b/core/reservoir.hpp index f720d13..05134b0 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -1,14 +1,13 @@ #ifndef RESERVOIR_H #define RESERVOIR_H -#include -#include -#include -#include -#include #include "cvector.hpp" #include "junction.hpp" - +#include +#include +#include +#include +#include /** * @brief Computes the combinations @@ -16,273 +15,268 @@ * @param N size of the set * @param K combination size */ -void comb(int N, int K) -{ - std::string bitmask(K, 1); // K leading 1's - bitmask.resize(N, 0); // N-K trailing 0's - // print integers and permute bitmask - do +void comb(int N, int K) { + std::string bitmask(K, 1); // K leading 1's + bitmask.resize(N, 0); // N-K trailing 0's + // print integers and permute bitmask + do { + for (int i = 0; i < N; ++i) // [0..N-1] integers { - for (int i = 0; i < N; ++i) // [0..N-1] integers - { - if (bitmask[i]) - std::cout << " " << i; - } - std::cout << std::endl; - } while (std::prev_permutation(bitmask.begin(), bitmask.end())); + if (bitmask[i]) + std::cout << " " << i; + } + std::cout << std::endl; + } while (std::prev_permutation(bitmask.begin(), bitmask.end())); } typedef std::array, 3> tensor; // typedef std::vector> tensorMatrix; typedef std::vector tensorList; -class Reservoir -{ +class Reservoir { private: - // log stuff - const std::string intendedKeys = { - "m_" }; - std::vector logKeys; - std::unordered_map> reservoirLog; + // log stuff + const std::string intendedKeys = {"m_"}; + std::vector logKeys; + std::unordered_map> reservoirLog; - // reservoir matrices - std::vector> coordinateMatrix; - std::vector frozenMMatrix; - std::vector MsMatrix, volumeMatrix; - std::vector> reservoirDipoleTensor; - std::vector>> layerMatrix; + // reservoir matrices + std::vector> coordinateMatrix; + std::vector frozenMMatrix; + std::vector MsMatrix, volumeMatrix; + std::vector> reservoirDipoleTensor; + std::vector>> layerMatrix; - std::vector> computeReservoirDipoleMatrix(std::vector>> coordinateMatrix) - { - std::vector> localReservoirDipoleTensor; - // reserve some place here - localReservoirDipoleTensor.resize(this->noElements); + std::vector> computeReservoirDipoleMatrix( + std::vector>> coordinateMatrix) { + std::vector> localReservoirDipoleTensor; + // reserve some place here + localReservoirDipoleTensor.resize(this->noElements); - // given a coordinate matrix, create a flat index of all indexed - std::string bitmask(2, 1); // K leading 1's - bitmask.resize(this->noElements, 0); // N-K trailing 0's - // print integers and permute bitmask - do - { - std::array consideredPair; - int asgn = 0; - for (unsigned int i = 0; i < this->noElements; ++i) // [0..N-1] integers - { - if (bitmask[i]) - consideredPair[asgn++] = i; // currently selected index in the combination - } - // compute matrix position from index - // this is row-first (row-major) ordering! - const auto elIndx0 = getMatrixCoordinates(consideredPair[0]); - const auto elIndx1 = getMatrixCoordinates(consideredPair[1]); - // const unsigned int i0 = (int)consideredPair[0] / cols; // first position of the first element -- row - // const unsigned int i1 = consideredPair[0] % cols; // second position of the first element -- col - const unsigned int i0 = std::get<0>(elIndx0); - const unsigned int i1 = std::get<1>(elIndx0); - const unsigned int j0 = std::get<0>(elIndx1); - const unsigned int j1 = std::get<1>(elIndx1); - std::cout << "i0: " << i0 << " i1: " << i1 << " j0: " << j0 << " j1: " << j1 << std::endl; - // // const unsigned int j0 = (int)consideredPair[1] / cols; // first position of the second element - // // const unsigned int j1 = consideredPair[1] % cols; // second position of the second element + // given a coordinate matrix, create a flat index of all indexed + std::string bitmask(2, 1); // K leading 1's + bitmask.resize(this->noElements, 0); // N-K trailing 0's + // print integers and permute bitmask + do { + std::array consideredPair; + int asgn = 0; + for (unsigned int i = 0; i < this->noElements; ++i) // [0..N-1] integers + { + if (bitmask[i]) + consideredPair[asgn++] = + i; // currently selected index in the combination + } + // compute matrix position from index + // this is row-first (row-major) ordering! + const auto elIndx0 = getMatrixCoordinates(consideredPair[0]); + const auto elIndx1 = getMatrixCoordinates(consideredPair[1]); + // const unsigned int i0 = (int)consideredPair[0] / cols; // first + // position of the first element -- row const unsigned int i1 = + // consideredPair[0] % cols; // second position of the first element + // -- col + const unsigned int i0 = std::get<0>(elIndx0); + const unsigned int i1 = std::get<1>(elIndx0); + const unsigned int j0 = std::get<0>(elIndx1); + const unsigned int j1 = std::get<1>(elIndx1); + std::cout << "i0: " << i0 << " i1: " << i1 << " j0: " << j0 + << " j1: " << j1 << std::endl; + // // const unsigned int j0 = (int)consideredPair[1] / cols; // first + // position of the second element + // // const unsigned int j1 = consideredPair[1] % cols; // second + // position of the second element - tensor dipTensor1 = this->getDipoleTensorFromRelPositions(coordinateMatrix[i0][i1], coordinateMatrix[j0][j1]); - tensor dipTensor2 = this->getDipoleTensorFromRelPositions(coordinateMatrix[j0][j1], coordinateMatrix[i0][i1]); - // dipole tensors should be symmetric (or anti-symmetric) - // TODO: make sure this is the case - // localReservoirDipoleTensor[i0][i1].push_back(dipTensor1); - // localReservoirDipoleTensor[j0][j1].push_back(dipTensor2); - localReservoirDipoleTensor[consideredPair[0]].push_back(dipTensor1); - localReservoirDipoleTensor[consideredPair[1]].push_back(dipTensor2); + tensor dipTensor1 = this->getDipoleTensorFromRelPositions( + coordinateMatrix[i0][i1], coordinateMatrix[j0][j1]); + tensor dipTensor2 = this->getDipoleTensorFromRelPositions( + coordinateMatrix[j0][j1], coordinateMatrix[i0][i1]); + // dipole tensors should be symmetric (or anti-symmetric) + // TODO: make sure this is the case + // localReservoirDipoleTensor[i0][i1].push_back(dipTensor1); + // localReservoirDipoleTensor[j0][j1].push_back(dipTensor2); + localReservoirDipoleTensor[consideredPair[0]].push_back(dipTensor1); + localReservoirDipoleTensor[consideredPair[1]].push_back(dipTensor2); - } while (std::prev_permutation(bitmask.begin(), bitmask.end())); + } while (std::prev_permutation(bitmask.begin(), bitmask.end())); - return localReservoirDipoleTensor; - } + return localReservoirDipoleTensor; + } - std::tuple getMatrixCoordinates(int elementIndx) - { - // this is row-major convention - return std::make_tuple( - (int)(elementIndx / this->cols), - elementIndx % this->cols); - } + std::tuple getMatrixCoordinates(int elementIndx) { + // this is row-major convention + return std::make_tuple((int)(elementIndx / this->cols), + elementIndx % this->cols); + } - const tensor getDipoleTensorFromRelPositions(const CVector& r1, const CVector& r2) - { - const CVector rij = r2 - r1; // 1-2 distance vector - const double r_mag = pow(rij.length(), 2); - const double mult = 3 / (4 * M_PI * pow(rij.length(), 5)); - const tensor dipoleTensor = { - CVector(pow(rij.x, 2) - (r_mag / 3), rij.x * rij.y, rij.x * rij.z) * mult, - CVector(rij.x * rij.y, pow(rij.y, 2) - (r_mag / 3), rij.y * rij.z) * mult, - CVector(rij.x * rij.z, rij.y * rij.z, pow(rij.z, 2) - (r_mag / 3)) * mult }; - // print dipole tensor - // std::cout << "Dipole tensor: " << std::endl; - // for (auto& row : dipoleTensor) - // { - // std::cout << row << " " << std::endl; - // } - return dipoleTensor; - } + const tensor getDipoleTensorFromRelPositions(const CVector &r1, + const CVector &r2) { + const CVector rij = r2 - r1; // 1-2 distance vector + const double r_mag = pow(rij.length(), 2); + const double mult = 3 / (4 * M_PI * pow(rij.length(), 5)); + const tensor dipoleTensor = {CVector(pow(rij.x, 2) - (r_mag / 3), + rij.x * rij.y, rij.x * rij.z) * + mult, + CVector(rij.x * rij.y, + pow(rij.y, 2) - (r_mag / 3), + rij.y * rij.z) * + mult, + CVector(rij.x * rij.z, rij.y * rij.z, + pow(rij.z, 2) - (r_mag / 3)) * + mult}; + // print dipole tensor + // std::cout << "Dipole tensor: " << std::endl; + // for (auto& row : dipoleTensor) + // { + // std::cout << row << " " << std::endl; + // } + return dipoleTensor; + } - CVector computeDipoleInteraction(int currentIndx, double volumeNormaliser) - { - CVector HdipoleEff; - for (unsigned int i = 0; i < this->reservoirDipoleTensor[currentIndx].size(); i++) - { - HdipoleEff += calculate_tensor_interaction(this->frozenMMatrix[i], - this->reservoirDipoleTensor[currentIndx][i], - this->MsMatrix[i]); - } - return HdipoleEff * volumeNormaliser; + CVector computeDipoleInteraction(int currentIndx, + double volumeNormaliser) { + CVector HdipoleEff; + for (unsigned int i = 0; + i < this->reservoirDipoleTensor[currentIndx].size(); i++) { + HdipoleEff += calculate_tensor_interaction( + this->frozenMMatrix[i], this->reservoirDipoleTensor[currentIndx][i], + this->MsMatrix[i]); } + return HdipoleEff * volumeNormaliser; + } public: - unsigned int rows, cols; - unsigned int noElements; + unsigned int rows, cols; + unsigned int noElements; - Reservoir(std::vector> coordinateMatrix, std::vector>> layerMatrix) : coordinateMatrix(std::move(coordinateMatrix)), - layerMatrix(std::move(layerMatrix)) - { - this->rows = this->coordinateMatrix.size(); - this->cols = this->coordinateMatrix[0].size(); - this->noElements = this->rows * this->cols; - this->frozenMMatrix.resize(this->noElements); - this->MsMatrix.reserve(this->noElements); - this->volumeMatrix.reserve(this->noElements); - for (unsigned int i = 0; i < this->rows; i++) - { - for (unsigned j = 0; j < this->cols; j++) - { - // must be multiplied by volume to avoid overflow - this->volumeMatrix.push_back(this->layerMatrix[i][j].thickness * this->layerMatrix[i][j].cellSurface); - this->MsMatrix.push_back(this->layerMatrix[i][j].Ms); - } - } - this->reservoirDipoleTensor = this->computeReservoirDipoleMatrix(this->coordinateMatrix); + Reservoir(std::vector> coordinateMatrix, + std::vector>> layerMatrix) + : coordinateMatrix(std::move(coordinateMatrix)), + layerMatrix(std::move(layerMatrix)) { + this->rows = this->coordinateMatrix.size(); + this->cols = this->coordinateMatrix[0].size(); + this->noElements = this->rows * this->cols; + this->frozenMMatrix.resize(this->noElements); + this->MsMatrix.reserve(this->noElements); + this->volumeMatrix.reserve(this->noElements); + for (unsigned int i = 0; i < this->rows; i++) { + for (unsigned j = 0; j < this->cols; j++) { + // must be multiplied by volume to avoid overflow + this->volumeMatrix.push_back(this->layerMatrix[i][j].thickness * + this->layerMatrix[i][j].cellSurface); + this->MsMatrix.push_back(this->layerMatrix[i][j].Ms); + } } + this->reservoirDipoleTensor = + this->computeReservoirDipoleMatrix(this->coordinateMatrix); + } - std::vector> collectFrozenMMatrix() - { - for (unsigned int i = 0; i < this->noElements; i++) - { - const auto coords = this->getMatrixCoordinates(i); - const unsigned int i0 = std::get<0>(coords); - const unsigned int i1 = std::get<1>(coords); - this->frozenMMatrix[i] = this->layerMatrix[i0][i1].mag; - } - return this->frozenMMatrix; + std::vector> collectFrozenMMatrix() { + for (unsigned int i = 0; i < this->noElements; i++) { + const auto coords = this->getMatrixCoordinates(i); + const unsigned int i0 = std::get<0>(coords); + const unsigned int i1 = std::get<1>(coords); + this->frozenMMatrix[i] = this->layerMatrix[i0][i1].mag; } + return this->frozenMMatrix; + } - void runSolver(double time, double timeStep, bool parallel = false) - { - // collect all frozen states - collectFrozenMMatrix(); - CVector nullVec; - for (unsigned int i = 0; i < this->noElements; i++) - { - const auto dipoleVector = computeDipoleInteraction(i, this->volumeMatrix[i]); - const auto coords = this->getMatrixCoordinates(i); - const unsigned int i0 = std::get<0>(coords); - const unsigned int i1 = std::get<1>(coords); - // std::cout << dipoleVector.x << " " << dipoleVector.y << " " << dipoleVector.z << std::endl; - layerMatrix[i0][i1].rk4_stepDipoleInjection(time, timeStep, nullVec, nullVec, dipoleVector); - } + void runSolver(double time, double timeStep, bool parallel = false) { + // collect all frozen states + collectFrozenMMatrix(); + CVector nullVec; + for (unsigned int i = 0; i < this->noElements; i++) { + const auto dipoleVector = + computeDipoleInteraction(i, this->volumeMatrix[i]); + const auto coords = this->getMatrixCoordinates(i); + const unsigned int i0 = std::get<0>(coords); + const unsigned int i1 = std::get<1>(coords); + // std::cout << dipoleVector.x << " " << dipoleVector.y << " " << + // dipoleVector.z << std::endl; + layerMatrix[i0][i1].rk4_stepDipoleInjection(time, timeStep, nullVec, + nullVec, dipoleVector); } + } - void logReservoirkData(double t) - { - this->reservoirLog["time"].push_back(t); - for (unsigned int i = 0; i < this->noElements; i++) - { - const auto coords = this->getMatrixCoordinates(i); - const unsigned int i0 = std::get<0>(coords); - const unsigned int i1 = std::get<1>(coords); - this->reservoirLog["m_" + std::to_string(i0) + "_" + std::to_string(i1) + "_x"].push_back(this->layerMatrix[i0][i1].mag.x); - this->reservoirLog["m_" + std::to_string(i0) + "_" + std::to_string(i1) + "_y"].push_back(this->layerMatrix[i0][i1].mag.y); - this->reservoirLog["m_" + std::to_string(i0) + "_" + std::to_string(i1) + "_z"].push_back(this->layerMatrix[i0][i1].mag.z); - } + void logReservoirkData(double t) { + this->reservoirLog["time"].push_back(t); + for (unsigned int i = 0; i < this->noElements; i++) { + const auto coords = this->getMatrixCoordinates(i); + const unsigned int i0 = std::get<0>(coords); + const unsigned int i1 = std::get<1>(coords); + this->reservoirLog["m_" + std::to_string(i0) + "_" + std::to_string(i1) + + "_x"] + .push_back(this->layerMatrix[i0][i1].mag.x); + this->reservoirLog["m_" + std::to_string(i0) + "_" + std::to_string(i1) + + "_y"] + .push_back(this->layerMatrix[i0][i1].mag.y); + this->reservoirLog["m_" + std::to_string(i0) + "_" + std::to_string(i1) + + "_z"] + .push_back(this->layerMatrix[i0][i1].mag.z); } + } - Layer& getLayer(unsigned int index) - { - const auto coords = getMatrixCoordinates(index); - const unsigned int i0 = std::get<0>(coords); - const unsigned int i1 = std::get<1>(coords); - return this->layerMatrix[i0][i1]; - } + Layer &getLayer(unsigned int index) { + const auto coords = getMatrixCoordinates(index); + const unsigned int i0 = std::get<0>(coords); + const unsigned int i1 = std::get<1>(coords); + return this->layerMatrix[i0][i1]; + } - void setAllExternalField(const AxialDriver& hdriver) - { - for (auto& r : this->layerMatrix) - { - for (auto& l : r) - { - l.setExternalFieldDriver(hdriver); - } - } + void setAllExternalField(const AxialDriver &hdriver) { + for (auto &r : this->layerMatrix) { + for (auto &l : r) { + l.setExternalFieldDriver(hdriver); + } } + } - void setLayerExternalField(unsigned int index, const AxialDriver& hDriver) - { - this->getLayer(index).setExternalFieldDriver(hDriver); - } + void setLayerExternalField(unsigned int index, + const AxialDriver &hDriver) { + this->getLayer(index).setExternalFieldDriver(hDriver); + } - void setLayerAnisotropy(unsigned int index, const ScalarDriver& anisotropyDriver) - { - this->getLayer(index).setAnisotropyDriver(anisotropyDriver); - } + void setLayerAnisotropy(unsigned int index, + const ScalarDriver &anisotropyDriver) { + this->getLayer(index).setAnisotropyDriver(anisotropyDriver); + } - CVector getMagnetisation(unsigned int index) - { - const auto coords = getMatrixCoordinates(index); - const unsigned int i0 = std::get<0>(coords); - const unsigned int i1 = std::get<1>(coords); - return this->layerMatrix[i0][i1].mag; - } + CVector getMagnetisation(unsigned int index) { + const auto coords = getMatrixCoordinates(index); + const unsigned int i0 = std::get<0>(coords); + const unsigned int i1 = std::get<1>(coords); + return this->layerMatrix[i0][i1].mag; + } - void - saveLogs(const std::string& fileSave) - { - if (fileSave == "") - { - // if there's an empty fn, don't save - std::cout << "Ignoring file save to an empty filename" << std::endl; - return; - } - std::ofstream logFile; - logFile.open(fileSave); - for (const auto& keyPair : this->reservoirLog) - { - logFile << keyPair.first << ";"; - } - logFile << "\n"; - for (unsigned int i = 0; i < this->reservoirLog["time"].size(); i++) - { - for (const auto& keyPair : this->reservoirLog) - { - logFile << keyPair.second[i] << ";"; - } - logFile << "\n"; - } - logFile.close(); + void saveLogs(const std::string &fileSave) { + if (fileSave == "") { + // if there's an empty fn, don't save + std::cout << "Ignoring file save to an empty filename" << std::endl; + return; } - void clearLogs() - { - this->reservoirLog.clear(); + std::ofstream logFile; + logFile.open(fileSave); + for (const auto &keyPair : this->reservoirLog) { + logFile << keyPair.first << ";"; } + logFile << "\n"; + for (unsigned int i = 0; i < this->reservoirLog["time"].size(); i++) { + for (const auto &keyPair : this->reservoirLog) { + logFile << keyPair.second[i] << ";"; + } + logFile << "\n"; + } + logFile.close(); + } + void clearLogs() { this->reservoirLog.clear(); } - void runSimulation(double totalTime, double timeStep) - { - const double totalIterations = (int)(totalTime / timeStep); - // this->clearLogs(); - // this->prepareLog(totalIterations); - for (unsigned int i = 0; i < totalIterations; i++) - { - double t = i * timeStep; - runSolver(t, timeStep); - logReservoirkData(t); - } + void runSimulation(double totalTime, double timeStep) { + const double totalIterations = (int)(totalTime / timeStep); + // this->clearLogs(); + // this->prepareLog(totalIterations); + for (unsigned int i = 0; i < totalIterations; i++) { + double t = i * timeStep; + runSolver(t, timeStep); + logReservoirkData(t); } + } }; #endif diff --git a/core/stack.hpp b/core/stack.hpp index bf8de4f..4bd9f6c 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -62,7 +62,7 @@ template class Stack { stackSize = this->junctionList.size(); } - T getCoupling(const unsigned int &order) const2 { + T getCoupling(const unsigned int &order) const { if (this->couplingStrength.empty()) { throw std::runtime_error("Coupling strength is not set!"); } diff --git a/deprecated/experiment.cpp b/deprecated/experiment.cpp index cdba015..f03c827 100644 --- a/deprecated/experiment.cpp +++ b/deprecated/experiment.cpp @@ -1,209 +1,215 @@ #include "junction.hpp" -void threadedSimulation(Junction cjx, double minField, double maxField, int numberOfPoints, std::ofstream &vsdFile) -{ - const int threadNum = std::thread::hardware_concurrency() - 2; - std::vector>>> threadResults; - threadResults.reserve(threadNum); - - const int pointsPerThread = numberOfPoints / threadNum + 1; - const double spacing = (maxField - minField) / numberOfPoints; - for (int i = 0; i < threadNum; i++) - { - const double threadMinField = pointsPerThread * i * spacing; - const double threadMaxField = pointsPerThread * (i + 1) * spacing; - threadResults.emplace_back(std::async([cjx, threadMinField, threadMaxField, spacing]() mutable { - std::vector> - resAcc; - const double freq = 7e9; - for (double field = threadMinField; field < threadMaxField; - field += spacing) - { - cjx.setConstantExternalField((field / 1000) * TtoAm, xaxis); - // cjx.setLayerAnisotropyUpdate("free", 12000, freq, 0); - // cjx.setLayerAnisotropyUpdate("bottom", 12000, freq, 0); - cjx.setLayerCoupling("free", -3e-6); - cjx.setLayerCoupling("bottom", -3e-6); - cjx.setLayerIECUpdate("free", 1e-6, freq, 0); - cjx.setLayerIECUpdate("bottom", 1e-6, freq, 0); - cjx.runSimulation(20e-9); - std::map vsd = cjx.calculateVoltageSpinDiode(freq); - resAcc.push_back({field, vsd["Vmix"]}); - cjx.log.clear(); - } - - return resAcc; - })); +void threadedSimulation(Junction cjx, double minField, double maxField, + int numberOfPoints, std::ofstream &vsdFile) { + const int threadNum = std::thread::hardware_concurrency() - 2; + std::vector>>> + threadResults; + threadResults.reserve(threadNum); + + const int pointsPerThread = numberOfPoints / threadNum + 1; + const double spacing = (maxField - minField) / numberOfPoints; + for (int i = 0; i < threadNum; i++) { + const double threadMinField = pointsPerThread * i * spacing; + const double threadMaxField = pointsPerThread * (i + 1) * spacing; + threadResults.emplace_back(std::async([cjx, threadMinField, threadMaxField, + spacing]() mutable { + std::vector> resAcc; + const double freq = 7e9; + for (double field = threadMinField; field < threadMaxField; + field += spacing) { + cjx.setConstantExternalField((field / 1000) * TtoAm, xaxis); + // cjx.setLayerAnisotropyUpdate("free", 12000, freq, 0); + // cjx.setLayerAnisotropyUpdate("bottom", 12000, freq, 0); + cjx.setLayerCoupling("free", -3e-6); + cjx.setLayerCoupling("bottom", -3e-6); + cjx.setLayerIECUpdate("free", 1e-6, freq, 0); + cjx.setLayerIECUpdate("bottom", 1e-6, freq, 0); + cjx.runSimulation(20e-9); + std::map vsd = cjx.calculateVoltageSpinDiode(freq); + resAcc.push_back({field, vsd["Vmix"]}); + cjx.log.clear(); + } + + return resAcc; + })); + } + + vsdFile << "H;Vmix\n"; + for (auto &result : threadResults) { + for (const auto [field, vsdVal] : result.get()) { + vsdFile << field << ";" << vsdVal << "\n"; } - - vsdFile << "H;Vmix\n"; - for (auto &result : threadResults) - { - for (const auto [field, vsdVal] : result.get()) - { - vsdFile << field << ";" << vsdVal << "\n"; - } - }; + }; } -void parameterScanVSD(Junction junction, std::string filename) -{ - double minField = 000.0; - double maxField = 500.0; - int numPoints = 50; - double spacing = (maxField - minField) / numPoints; - std::ofstream vsdFile; - vsdFile.open(filename); - vsdFile << "F;J;eJ;H;Vmix;Rpp\n"; - std::vector eJs = {1e-6, 9e-7, 8e-7, 7e-7, 6e-7, 5e-7, 4e-7, 3e-7, 2e-7, 1e-7}; - std::vector Js = {-2e-6, -3e-6, -4e-6, -5e-6, -6e-6}; - std::vector Fs = {5e9, 5.5e9, 6e9, 6.5e9, 7e9, 7.5e9, 8e9}; - // std::vector Fs = {7e9}; - - for (const double &F : Fs) - { - for (const double &J : Js) - { - for (const double &eJ : eJs) - { - for (double field = minField; field < maxField; field += spacing) - { - junction.setConstantExternalField((field / 1000) * TtoAm, xaxis); - junction.setLayerCoupling("free", J); - junction.setLayerCoupling("bottom", J); - junction.setLayerIECUpdate("free", eJ, F, 0); - junction.setLayerIECUpdate("bottom", eJ, F, 0); - junction.runSimulation(20e-9, 1e-13, false, false); - std::map vsd = junction.calculateVoltageSpinDiode(F); - vsdFile << F << ";" << J << ";" << eJ << ";" << field << ";" << vsd["Vmix"] << ";" << vsd["Rpp"] << std::endl; - junction.log.clear(); - } - } +void parameterScanVSD(Junction junction, std::string filename) { + double minField = 000.0; + double maxField = 500.0; + int numPoints = 50; + double spacing = (maxField - minField) / numPoints; + std::ofstream vsdFile; + vsdFile.open(filename); + vsdFile << "F;J;eJ;H;Vmix;Rpp\n"; + std::vector eJs = {1e-6, 9e-7, 8e-7, 7e-7, 6e-7, + 5e-7, 4e-7, 3e-7, 2e-7, 1e-7}; + std::vector Js = {-2e-6, -3e-6, -4e-6, -5e-6, -6e-6}; + std::vector Fs = {5e9, 5.5e9, 6e9, 6.5e9, 7e9, 7.5e9, 8e9}; + // std::vector Fs = {7e9}; + + for (const double &F : Fs) { + for (const double &J : Js) { + for (const double &eJ : eJs) { + for (double field = minField; field < maxField; field += spacing) { + junction.setConstantExternalField((field / 1000) * TtoAm, xaxis); + junction.setLayerCoupling("free", J); + junction.setLayerCoupling("bottom", J); + junction.setLayerIECUpdate("free", eJ, F, 0); + junction.setLayerIECUpdate("bottom", eJ, F, 0); + junction.runSimulation(20e-9, 1e-13, false, false); + std::map vsd = + junction.calculateVoltageSpinDiode(F); + vsdFile << F << ";" << J << ";" << eJ << ";" << field << ";" + << vsd["Vmix"] << ";" << vsd["Rpp"] << std::endl; + junction.log.clear(); } + } } + } - vsdFile.close(); + vsdFile.close(); } -void parameterScanFFT(Junction &junction, std::string filename) -{ - // std::vector Js = {-4e-6, -3e-4, -2e-4, -1e-4, 0.0, 1e-4, 2e-4, 3e-4, 4e-4}; - // std::vector Js = {-9e-6, -8e-6, -7e-6, -6e-6, -5e-6, -4e-6, -3e-6, -2e-6, -1e-6, 0.0, - // 1e-6, 2e-6, 3e-6, 4e-6, 5e-6, 6e-6, 7e-6, 8e-6, 9e-6}; - std::vector Js = {-9e-5, -8e-5, -7e-5, -6e-5, -5e-5, -4e-5, -3e-5, -2e-5, -1e-5, 0.0, - 1e-5, 2e-5, 3e-5, 4e-5, 5e-5, 6e-5, 7e-5, 8e-5, 9e-5}; - std::vector Ks = {900e3, 950e3, 1000e3, 1050e3, 1100e3, 1150e3}; - std::vector fixedFields = {150_mT, 200_mT, 250_mT, - 300_mT, 350_mT, 400_mT, 450_mT}; - - std::ofstream fftFile; - fftFile.open(filename); - fftFile << "K;J;H;Mx;Ax;Px;My;Ay;Py;Mz;Az;Pz;\n"; - for (const double &J : Js) - { - for (const double &K : Ks) - { - for (const double &H : fixedFields) - { - junction.setConstantExternalField(H * TtoAm, xaxis); - junction.setLayerCoupling("free", J); - junction.setLayerCoupling("bottom", J); - junction.setLayerAnisotropy("free", K); - junction.setLayerStepUpdate("free", 1e-4 * TtoAm, 5.0_ns, 5.001_ns, xaxis); - junction.setLayerStepUpdate("bottom", 1e-4 * TtoAm, 5.0_ns, 5.001_ns, xaxis); - junction.runSimulation(20_ns, 1e-13, false, false); - auto resMap = junction.calculateFFT(5e-9, 1e-11); - junction.log.clear(); - fftFile << K << ";" << J << ";" << H << ";"; - for (const std::string majorKey : {"x", "y", "z"}) - { - for (const std::string minorKey : {"_resonant", "_amplitude", "_phase"}) - { - fftFile << resMap[majorKey + minorKey] << ";"; - } - } - fftFile << "\n"; - } +void parameterScanFFT(Junction &junction, std::string filename) { + // std::vector Js = {-4e-6, -3e-4, -2e-4, -1e-4, 0.0, 1e-4, 2e-4, + // 3e-4, 4e-4}; std::vector Js = {-9e-6, -8e-6, -7e-6, -6e-6, -5e-6, + // -4e-6, -3e-6, -2e-6, -1e-6, 0.0, + // 1e-6, 2e-6, 3e-6, 4e-6, 5e-6, 6e-6, 7e-6, 8e-6, + // 9e-6}; + std::vector Js = {-9e-5, -8e-5, -7e-5, -6e-5, -5e-5, -4e-5, -3e-5, + -2e-5, -1e-5, 0.0, 1e-5, 2e-5, 3e-5, 4e-5, + 5e-5, 6e-5, 7e-5, 8e-5, 9e-5}; + std::vector Ks = {900e3, 950e3, 1000e3, 1050e3, 1100e3, 1150e3}; + std::vector fixedFields = {150_mT, 200_mT, 250_mT, 300_mT, + 350_mT, 400_mT, 450_mT}; + + std::ofstream fftFile; + fftFile.open(filename); + fftFile << "K;J;H;Mx;Ax;Px;My;Ay;Py;Mz;Az;Pz;\n"; + for (const double &J : Js) { + for (const double &K : Ks) { + for (const double &H : fixedFields) { + junction.setConstantExternalField(H * TtoAm, xaxis); + junction.setLayerCoupling("free", J); + junction.setLayerCoupling("bottom", J); + junction.setLayerAnisotropy("free", K); + junction.setLayerStepUpdate("free", 1e-4 * TtoAm, 5.0_ns, 5.001_ns, + xaxis); + junction.setLayerStepUpdate("bottom", 1e-4 * TtoAm, 5.0_ns, 5.001_ns, + xaxis); + junction.runSimulation(20_ns, 1e-13, false, false); + auto resMap = junction.calculateFFT(5e-9, 1e-11); + junction.log.clear(); + fftFile << K << ";" << J << ";" << H << ";"; + for (const std::string majorKey : {"x", "y", "z"}) { + for (const std::string minorKey : + {"_resonant", "_amplitude", "_phase"}) { + fftFile << resMap[majorKey + minorKey] << ";"; + } } + fftFile << "\n"; + } } - fftFile.close(); + } + fftFile.close(); } -int main(void) -{ - - std::vector dipoleTensor = { - {6.8353909454237E-4, 0., 0.}, - {0., 0.00150694452305927, 0.}, - {0., 0., 0.99780951638608}}; - std::vector demagTensor = { - {5.57049776248663E-4, 0., 0.}, - {0., 0.00125355500286346, 0.}, - {0., 0.0, -0.00181060482770131}}; - - Layer l1("free", // id - CVector(0., 0., 1.), // mag - CVector(0, 0., 1.), // anis - 900e3, // K - 1200e3, // Ms - -2.5e-6, // J - 1.4e-9, // thickness - 7e-10 * 7e-10, // surface - demagTensor, // demag - dipoleTensor); - Layer l2("bottom", // id - CVector(0., 0., 1.), // mag - CVector(0, 0., 1.), // anis - 1500e3, // K - 1000e3, // Ms - -2.5e-6, // J - 7e-9, // thickness - 7e-10 * 7e-10, // surface - demagTensor, // demag - dipoleTensor); - - Junction mtj( - {l1, l2}, "test2.csv"); - - // double minField = 000.0; - // double maxField = 600.0; - // int numPoints = 50; - // double spacing = (maxField - minField) / numPoints; - // std::cout << spacing << std::endl; - // std::ofstream vsdFile; - // std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); - - // auto r = ComputeUtil::parallelFieldScan(mtj, minField, maxField, numPoints, - // [](Junction &mtj, const double field) mutable { - // mtj.log.clear(); - // const double freq = 7e9; - // mtj.setConstantExternalField((field / 1000) * TtoAm, xaxis); - // mtj.setLayerCoupling("free", -3e-6); - // mtj.setLayerCoupling("bottom", -3e-6); - // mtj.setLayerIECUpdate("free", 1e-6, freq, 0); - // mtj.setLayerIECUpdate("bottom", 1e-6, freq, 0); - // mtj.runSimulation(20e-9); - // std::map vsd = mtj.calculateVoltageSpinDiode(freq); - // mtj.log.clear(); - // return std::make_tuple(field, vsd); - // }); - - // ComputeUtil::customResultMap(r, "VSD-anisotropy.csv"); - // vsdFile.close(); - // mtj.setLayerAnisotropy("free", 200); - // std::cout << mtj.layers[0].K << std::endl; - // parameterScanFFT(mtj, "Jhigh_wide_H2_scan.csv"); - // parameterScanVSD(mtj, "VSD_scan_benchmark.csv"); - - // vsdFile.open("VSD-IEC2.csv"); - // threadedSimulation(mtj, minField, maxField, numPoints, vsdFile); - // vsdFile.close(); - std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); - std::cout << "Simulation time = " << std::chrono::duration_cast(end - begin).count() << "[s]" << std::endl; - // parameterScanVSD(mtj, "VSD-anisotropy-lowIEC2.csv"); - - // end = std::chrono::steady_clock::now(); - // std::cout << "Simulation time = " << std::chrono::duration_cast(end - begin).count() << "[s]" << std::endl; - - return 0; +int main(void) { + + std::vector dipoleTensor = {{6.8353909454237E-4, 0., 0.}, + {0., 0.00150694452305927, 0.}, + {0., 0., 0.99780951638608}}; + std::vector demagTensor = {{5.57049776248663E-4, 0., 0.}, + {0., 0.00125355500286346, 0.}, + {0., 0.0, -0.00181060482770131}}; + + Layer l1("free", // id + CVector(0., 0., 1.), // mag + CVector(0, 0., 1.), // anis + 900e3, // K + 1200e3, // Ms + -2.5e-6, // J + 1.4e-9, // thickness + 7e-10 * 7e-10, // surface + demagTensor, // demag + dipoleTensor); + Layer l2("bottom", // id + CVector(0., 0., 1.), // mag + CVector(0, 0., 1.), // anis + 1500e3, // K + 1000e3, // Ms + -2.5e-6, // J + 7e-9, // thickness + 7e-10 * 7e-10, // surface + demagTensor, // demag + dipoleTensor); + + Junction mtj({l1, l2}, "test2.csv"); + + // double minField = 000.0; + // double maxField = 600.0; + // int numPoints = 50; + // double spacing = (maxField - minField) / numPoints; + // std::cout << spacing << std::endl; + // std::ofstream vsdFile; + // std::chrono::steady_clock::time_point begin = + // std::chrono::steady_clock::now(); + + // auto r = ComputeUtil::parallelFieldScan(mtj, minField, maxField, numPoints, + // [](Junction &mtj, const double + // field) mutable { + // mtj.log.clear(); + // const double freq = 7e9; + // mtj.setConstantExternalField((field + // / 1000) * TtoAm, xaxis); + // mtj.setLayerCoupling("free", + // -3e-6); + // mtj.setLayerCoupling("bottom", + // -3e-6); + // mtj.setLayerIECUpdate("free", + // 1e-6, freq, 0); + // mtj.setLayerIECUpdate("bottom", + // 1e-6, freq, 0); + // mtj.runSimulation(20e-9); + // std::map + // vsd = + // mtj.calculateVoltageSpinDiode(freq); + // mtj.log.clear(); + // return std::make_tuple(field, + // vsd); + // }); + + // ComputeUtil::customResultMap(r, "VSD-anisotropy.csv"); + // vsdFile.close(); + // mtj.setLayerAnisotropy("free", 200); + // std::cout << mtj.layers[0].K << std::endl; + // parameterScanFFT(mtj, "Jhigh_wide_H2_scan.csv"); + // parameterScanVSD(mtj, "VSD_scan_benchmark.csv"); + + // vsdFile.open("VSD-IEC2.csv"); + // threadedSimulation(mtj, minField, maxField, numPoints, vsdFile); + // vsdFile.close(); + std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); + std::cout + << "Simulation time = " + << std::chrono::duration_cast(end - begin).count() + << "[s]" << std::endl; + // parameterScanVSD(mtj, "VSD-anisotropy-lowIEC2.csv"); + + // end = std::chrono::steady_clock::now(); + // std::cout << "Simulation time = " << + // std::chrono::duration_cast(end - begin).count() << + // "[s]" << std::endl; + + return 0; } diff --git a/examples/stt-fields/torque.cpp b/examples/stt-fields/torque.cpp index 47f2b67..55b78ef 100644 --- a/examples/stt-fields/torque.cpp +++ b/examples/stt-fields/torque.cpp @@ -6,183 +6,182 @@ typedef Layer DLayer; -std::vector generateRange(double start, double stop, double step, bool back) -{ - std::vector ranges; - double current = start; - while (current < stop) - { - ranges.push_back(current); - current += step; +std::vector generateRange(double start, double stop, double step, + bool back) { + std::vector ranges; + double current = start; + while (current < stop) { + ranges.push_back(current); + current += step; + } + if (back) { + current = stop; + while (current > start) { + ranges.push_back(current); + current -= step; } - if (back) - { - current = stop; - while (current > start) - { - ranges.push_back(current); - current -= step; - } - } - return ranges; + } + return ranges; } -int main(void) -{ - - std::vector demagTensor = { - {0.00024164288391924, 2.71396011566517e-10, 5.95503928124313e-14}, - {2.71396011566517e-10, 0.000160046006320031, 1.32504057070646e-14}, - {5.95503928124313e-14, 1.32504057070646e-14, 0.999598310229469}}; - // std::vector demagTensor = { - // {0.0, 0., 0.}, - // {0., 0.0, 0.}, - // {0., 0., 0.98}}; - - double damping = 0.004; - double surface = 1; - double Ms = 1.5; // 0.54 0.52 - double thickness = 1.45e-9; - - const double Irf = 5e-3; // 0.0065 / 2.1 - const double Hdl = -600; // 1200 - const double Hfl = -447; // 430 - std::cout << "Hdl: " << Hdl << " Hfl: " << Hfl << std::endl; - - DLayer l1("free", // id - DVector(.0, 0., 1.), // mag - DVector(0.0, .0, 1.), // 0.94 // 0.85 - Ms, // Ms - thickness, // thickness - surface, // surface - demagTensor, // demag - damping // damping - ); - - DVector p(0, 1, 0); - l1.setReferenceLayer(p); - const double l = 2e-5; - const double w = 3e-5; - const double ratio = w / l; - - // Junction mtj( - // {l1}, - // "", - // {186}, // Rx0 - // {100}, // Rxy - // {-0.02}, // AMR_X - // {-0.02 * -ratio}, // AMR_Y - // {-0.25}, // SMR_X - // {-0.25 * ratio}, // SMR_y - // {-2.3} // AHE - // ); - - Junction mtj( - {l1}, - "", - {304.7}, // Rx0 - {3}, // Rxy - {-0.466}, // AMR_X - {-0.466 * -ratio}, // AMR_Y - {-0.053}, // SMR_X - {-0.053 * ratio}, // SMR_y - {-5.7} // AHE - ); - - double Ku = 1.e6; // 1.8e5 0.85 - mtj.setLayerAnisotropyDriver("free", ScalarDriver::getConstantDriver(Ku)); - - const double hmin = -700e3; - const double hmax = -hmin; - const int hsteps = 80; - - const double theta = 89 * M_PI / 180; - const double phi = 89 * M_PI / 180; - - const double tStart = 000e-9; - const double time = 1200e-9; - const double tStep = 1e-11; - std::ofstream saveFile; - saveFile.open("Torque_res.csv"); - saveFile << "H;Vmix;phase\n"; - // saveFile << "H;Vmix;indx\n"; - - std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); - const auto frequencies = {0.8e9}; - auto Hdist = generateRange(hmin, hmax, (hmax - hmin) / hsteps, false); - - const std::string resTag = "Ry"; - - std::cout << "Generated frequency range" << std::endl; - // bottom, top mag - // std::reverse(Hdist.begin(), Hdist.end()); - for (auto &f : frequencies) - { - std::cout << "Computing " << f << std::endl; - for (auto &H : Hdist) - { - mtj.clearLog(); - const AxialDriver HDriver( - ScalarDriver::getConstantDriver(H * sin(theta) * cos(phi)), - ScalarDriver::getConstantDriver(H * sin(theta) * sin(phi)), - ScalarDriver::getConstantDriver(H * cos(theta))); - // const AxialDriver HoeDriver( - // ScalarDriver::getSineDriver(0, 1000, f, 0), NullDriver(), NullDriver()); - // mtj.setLayerOerstedFieldDriver("all", HoeDriver); - // mtj.setLayerCurrentDriver("all", - // ScalarDriver::getSineDriver( - // 0, jrf, f, 0)); - - mtj.setLayerDampingLikeTorqueDriver( - "free", ScalarDriver::getSineDriver( - 0, Hdl, f, 0)); - mtj.setLayerFieldLikeTorqueDriver( - "free", ScalarDriver::getSineDriver( - 0, Hfl, f, 0)); - - mtj.setLayerExternalFieldDriver( - "all", - HDriver); - - mtj.runSimulation( - time, - tStep, tStep, false, false, false); - - auto log = mtj.getLog(); - // compute the mixing voltage - std::vector mixingVoltage; - for (std::size_t i = 0; i < log[resTag].size(); ++i) - { - const double multiplierHann = 0.5 * (1 - cos(2 * M_PI * i / (log[resTag].size() - 1))); - const double v = log[resTag][i] * Irf * sin(2 * M_PI * f * log["time"][i]); - mixingVoltage.push_back(v*multiplierHann); - // mixingVoltage.push_back(v); - // saveFile << -H << ';' << v << ";" << i << std::endl; - } - - // const double maxV = *std::max_element(mixingVoltage.begin(), mixingVoltage.end()); - // const double maxR = *std::max_element(log[resTag].begin(), log[resTag].end()); - // std::cout << "Max V: " << maxV << "; Max R: " << maxR << std::endl; - log["mixing_voltage"] = mixingVoltage; - // calculate the FFT - auto spectrum = ComputeFunctions::spectralFFT( - log, {"mixing_voltage"}, tStart, tStep); - - // find 1f and 2f spectra - auto it1f = std::lower_bound(spectrum["frequencies"].begin(), spectrum["frequencies"].end(), f); - auto it2f = std::lower_bound(spectrum["frequencies"].begin(), spectrum["frequencies"].end(), 2 * f); - if (it1f == spectrum["frequencies"].end() || it2f == spectrum["frequencies"].end()) - { - throw std::runtime_error("Increase T to fit in 2f and 1f frequencies!"); - } - const int indx1f = it1f - spectrum["frequencies"].begin(); - const int indx2f = it2f - spectrum["frequencies"].begin(); - - saveFile << H << ";" << spectrum["mixing_voltage_amplitude"][indx1f] << ";" << spectrum["mixing_voltage_amplitude"][indx2f] * cos(spectrum["mixing_voltage_phase"][indx2f]) << std::endl; - } +int main(void) { + + std::vector demagTensor = { + {0.00024164288391924, 2.71396011566517e-10, 5.95503928124313e-14}, + {2.71396011566517e-10, 0.000160046006320031, 1.32504057070646e-14}, + {5.95503928124313e-14, 1.32504057070646e-14, 0.999598310229469}}; + // std::vector demagTensor = { + // {0.0, 0., 0.}, + // {0., 0.0, 0.}, + // {0., 0., 0.98}}; + + double damping = 0.004; + double surface = 1; + double Ms = 1.5; // 0.54 0.52 + double thickness = 1.45e-9; + + const double Irf = 5e-3; // 0.0065 / 2.1 + const double Hdl = -600; // 1200 + const double Hfl = -447; // 430 + std::cout << "Hdl: " << Hdl << " Hfl: " << Hfl << std::endl; + + DLayer l1("free", // id + DVector(.0, 0., 1.), // mag + DVector(0.0, .0, 1.), // 0.94 // 0.85 + Ms, // Ms + thickness, // thickness + surface, // surface + demagTensor, // demag + damping // damping + ); + + DVector p(0, 1, 0); + l1.setReferenceLayer(p); + const double l = 2e-5; + const double w = 3e-5; + const double ratio = w / l; + + // Junction mtj( + // {l1}, + // "", + // {186}, // Rx0 + // {100}, // Rxy + // {-0.02}, // AMR_X + // {-0.02 * -ratio}, // AMR_Y + // {-0.25}, // SMR_X + // {-0.25 * ratio}, // SMR_y + // {-2.3} // AHE + // ); + + Junction mtj({l1}, "", {304.7}, // Rx0 + {3}, // Rxy + {-0.466}, // AMR_X + {-0.466 * -ratio}, // AMR_Y + {-0.053}, // SMR_X + {-0.053 * ratio}, // SMR_y + {-5.7} // AHE + ); + + double Ku = 1.e6; // 1.8e5 0.85 + mtj.setLayerAnisotropyDriver("free", + ScalarDriver::getConstantDriver(Ku)); + + const double hmin = -700e3; + const double hmax = -hmin; + const int hsteps = 80; + + const double theta = 89 * M_PI / 180; + const double phi = 89 * M_PI / 180; + + const double tStart = 000e-9; + const double time = 1200e-9; + const double tStep = 1e-11; + std::ofstream saveFile; + saveFile.open("Torque_res.csv"); + saveFile << "H;Vmix;phase\n"; + // saveFile << "H;Vmix;indx\n"; + + std::chrono::steady_clock::time_point begin = + std::chrono::steady_clock::now(); + const auto frequencies = {0.8e9}; + auto Hdist = generateRange(hmin, hmax, (hmax - hmin) / hsteps, false); + + const std::string resTag = "Ry"; + + std::cout << "Generated frequency range" << std::endl; + // bottom, top mag + // std::reverse(Hdist.begin(), Hdist.end()); + for (auto &f : frequencies) { + std::cout << "Computing " << f << std::endl; + for (auto &H : Hdist) { + mtj.clearLog(); + const AxialDriver HDriver( + ScalarDriver::getConstantDriver(H * sin(theta) * cos(phi)), + ScalarDriver::getConstantDriver(H * sin(theta) * sin(phi)), + ScalarDriver::getConstantDriver(H * cos(theta))); + // const AxialDriver HoeDriver( + // ScalarDriver::getSineDriver(0, 1000, f, 0), + // NullDriver(), NullDriver()); + // mtj.setLayerOerstedFieldDriver("all", HoeDriver); + // mtj.setLayerCurrentDriver("all", + // ScalarDriver::getSineDriver( + // 0, jrf, f, 0)); + + mtj.setLayerDampingLikeTorqueDriver( + "free", ScalarDriver::getSineDriver(0, Hdl, f, 0)); + mtj.setLayerFieldLikeTorqueDriver( + "free", ScalarDriver::getSineDriver(0, Hfl, f, 0)); + + mtj.setLayerExternalFieldDriver("all", HDriver); + + mtj.runSimulation(time, tStep, tStep, false, false, false); + + auto log = mtj.getLog(); + // compute the mixing voltage + std::vector mixingVoltage; + for (std::size_t i = 0; i < log[resTag].size(); ++i) { + const double multiplierHann = + 0.5 * (1 - cos(2 * M_PI * i / (log[resTag].size() - 1))); + const double v = + log[resTag][i] * Irf * sin(2 * M_PI * f * log["time"][i]); + mixingVoltage.push_back(v * multiplierHann); + // mixingVoltage.push_back(v); + // saveFile << -H << ';' << v << ";" << i << std::endl; + } + + // const double maxV = *std::max_element(mixingVoltage.begin(), + // mixingVoltage.end()); const double maxR = + // *std::max_element(log[resTag].begin(), log[resTag].end()); std::cout << + // "Max V: " << maxV << "; Max R: " << maxR << std::endl; + log["mixing_voltage"] = mixingVoltage; + // calculate the FFT + auto spectrum = ComputeFunctions::spectralFFT( + log, {"mixing_voltage"}, tStart, tStep); + + // find 1f and 2f spectra + auto it1f = std::lower_bound(spectrum["frequencies"].begin(), + spectrum["frequencies"].end(), f); + auto it2f = std::lower_bound(spectrum["frequencies"].begin(), + spectrum["frequencies"].end(), 2 * f); + if (it1f == spectrum["frequencies"].end() || + it2f == spectrum["frequencies"].end()) { + throw std::runtime_error("Increase T to fit in 2f and 1f frequencies!"); + } + const int indx1f = it1f - spectrum["frequencies"].begin(); + const int indx2f = it2f - spectrum["frequencies"].begin(); + + saveFile << H << ";" << spectrum["mixing_voltage_amplitude"][indx1f] + << ";" + << spectrum["mixing_voltage_amplitude"][indx2f] * + cos(spectrum["mixing_voltage_phase"][indx2f]) + << std::endl; } - - saveFile.close(); - std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); - std::cout << "Total result retrieval time = " << std::chrono::duration_cast(end - begin).count() << std::endl; + } + + saveFile.close(); + std::chrono::steady_clock::time_point end = std::chrono::steady_clock::now(); + std::cout + << "Total result retrieval time = " + << std::chrono::duration_cast(end - begin).count() + << std::endl; } From 699e2768b568fd01313ce19bee08223b6d813eed Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 21:57:10 +0200 Subject: [PATCH 22/44] ruff transition --- .pre-commit-config.yaml | 22 ++-- cmtj/__init__.pyi | 126 ++++++------------- cmtj/llgb/__init__.pyi | 18 +-- cmtj/models/domain_dynamics.py | 148 ++++++++++++---------- cmtj/models/drivers.py | 2 +- cmtj/models/ensemble.py | 5 +- cmtj/models/general_sb.py | 222 +++++++++++++++------------------ cmtj/models/noise.py | 40 ++---- cmtj/models/oersted.py | 105 ++++++---------- cmtj/noise/__init__.pyi | 4 +- cmtj/stack/__init__.pyi | 32 ++--- cmtj/utils/__init__.py | 8 +- cmtj/utils/energy.py | 32 ++--- cmtj/utils/filters.py | 20 +-- cmtj/utils/general.py | 3 +- cmtj/utils/linear.py | 25 ++-- cmtj/utils/optimization.py | 20 ++- cmtj/utils/parallel.py | 15 +-- cmtj/utils/plotting.py | 62 +++------ cmtj/utils/procedures.py | 114 ++++++++--------- cmtj/utils/resistance.py | 66 +++++----- cmtj/utils/solvers.py | 11 +- 22 files changed, 459 insertions(+), 641 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index a1e03f4..873661d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,18 +34,18 @@ repos: files: \.(html|json|markdown|md|yaml|yml)$ exclude: (^docs/api/.*) - - repo: https://github.com/pycqa/isort - rev: 5.13.2 + - repo: https://github.com/astral-sh/ruff-pre-commit + # Ruff version. + rev: v0.6.4 hooks: - - id: isort - name: isort (python) - - - repo: https://github.com/pre-commit/mirrors-yapf - rev: v0.32.0 - hooks: - - id: yapf - files: "^cmtj" - args: [--in-place, --recursive] + # Run the linter. + - id: ruff + files: ^cmtj + types_or: [python, pyi] + # Run the formatter. + - id: ruff-format + files: ^cmtj + types_or: [python, pyi] - repo: https://github.com/pocc/pre-commit-hooks rev: v1.3.5 diff --git a/cmtj/__init__.pyi b/cmtj/__init__.pyi index 2cf2d8b..4d4312a 100644 --- a/cmtj/__init__.pyi +++ b/cmtj/__init__.pyi @@ -1,5 +1,5 @@ import typing -from typing import Any, ClassVar, Dict, List, overload +from typing import Any, ClassVar, overload xaxis: Axis yaxis: Axis @@ -18,9 +18,7 @@ def constantDriver(constant: float) -> ScalarDriver: """ ... -def sineDriver( - constantValue: float, amplitude: float, frequency: float, phase: float -) -> ScalarDriver: +def sineDriver(constantValue: float, amplitude: float, frequency: float, phase: float) -> ScalarDriver: """ Produces a sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. :param constantValue: vertical offset. The sine will oscillate around this value. @@ -30,9 +28,7 @@ def sineDriver( """ ... -def gaussianImpulseDriver( - constantValue: float, amplitude: float, t0: float, sigma: float -) -> ScalarDriver: +def gaussianImpulseDriver(constantValue: float, amplitude: float, t0: float, sigma: float) -> ScalarDriver: """ Gaussian impulse driver. It starts with an max amplitude at t0 and falls off with sigma. @@ -47,9 +43,7 @@ def gaussianImpulseDriver( """ ... -def gaussianStepDriver( - constantValue: float, amplitude: float, t0: float, sigma: float -) -> ScalarDriver: +def gaussianStepDriver(constantValue: float, amplitude: float, t0: float, sigma: float) -> ScalarDriver: """Gaussian step driver (erf function). It starts at t0 and falls off with sigma. Formula: @@ -63,9 +57,7 @@ def gaussianStepDriver( """ ... -def posSineDriver( - constantValue: float, amplitude: float, frequency: float, phase: float -) -> ScalarDriver: +def posSineDriver(constantValue: float, amplitude: float, frequency: float, phase: float) -> ScalarDriver: """Produces a positive sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. :param constantValue: vertical offset. The sine will oscillate around this value. :param amplitude: amplitude of the sine wave @@ -74,9 +66,7 @@ def posSineDriver( """ ... -def pulseDriver( - constantValue: float, amplitude: float, period: float, cycle: float -) -> ScalarDriver: +def pulseDriver(constantValue: float, amplitude: float, period: float, cycle: float) -> ScalarDriver: """ Produces a square pulse of certain period and cycle :param constantValue: offset (vertical) of the pulse. The pulse amplitude will be added to this. @@ -86,9 +76,7 @@ def pulseDriver( """ ... -def stepDriver( - constantValue: float, amplitude: float, timeStart: float, timeStop: float -) -> ScalarDriver: +def stepDriver(constantValue: float, amplitude: float, timeStart: float, timeStop: float) -> ScalarDriver: """ Get a step driver. It has amplitude between timeStart and timeStop and 0 elsewhere :param constantValue: offset of the pulse (vertical) @@ -127,7 +115,7 @@ class AxialDriver: ... @overload - def __init__(self, axialDrivers: List[ScalarDriver]) -> None: + def __init__(self, axialDrivers: list[ScalarDriver]) -> None: """Create an axial driver with a list of scalar drivers. :param axialDrivers: list of scalar drivers """ @@ -158,7 +146,7 @@ class AxialDriver: ... @overload - def applyMask(self, mask: List[int]) -> None: + def applyMask(self, mask: list[int]) -> None: """Apply mask to the driver. :param mask: mask to be applied""" ... @@ -209,7 +197,7 @@ class CVector: """Normalizes the vector.""" ... - def tolist(self) -> List[float]: + def tolist(self) -> list[float]: """Converts the vector to a list.""" ... @@ -240,12 +228,12 @@ class CVector: class Junction: @overload - def __init__(self, layers: List[Layer]) -> None: + def __init__(self, layers: list[Layer]) -> None: """""" ... @overload - def __init__(self, layers: List[Layer], Rp: float = ..., Rap: float = ...) -> None: + def __init__(self, layers: list[Layer], Rp: float = ..., Rap: float = ...) -> None: """Creates a junction with a magnetoresistance. :param layers: list of layers @@ -257,14 +245,14 @@ class Junction: @overload def __init__( self, - layers: List[Layer], - Rx0: List[float], - Ry0: List[float], - AMR_X: List[float], - AMR_Y: List[float], - SMR_X: List[float], - SMR_Y: List[float], - AHE: List[float], + layers: list[Layer], + Rx0: list[float], + Ry0: list[float], + AMR_X: list[float], + AMR_Y: list[float], + SMR_X: list[float], + SMR_Y: list[float], + AHE: list[float], ) -> None: """Creates a junction with a STRIP magnetoresistance. Each of the Rx0, Ry, AMR, AMR and SMR is list matching the @@ -283,7 +271,7 @@ class Junction: @overload def __init__(*args, **kwargs) -> Any: ... - def clearLog(self) -> Dict[str, Any]: + def clearLog(self) -> dict[str, Any]: """Reset current simulation state.""" ... @@ -292,11 +280,11 @@ class Junction: :param layerId: the layer id""" ... - def getLog(self) -> Dict[str, List[float]]: + def getLog(self) -> dict[str, list[float]]: """Retrieve the simulation log [data].""" ... - def getMagnetoresistance(self) -> List[float]: ... + def getMagnetoresistance(self) -> list[float]: ... def runSimulation( self, totalTime: float, @@ -317,9 +305,7 @@ class Junction: """ ... - def setIECDriver( - self, bottomLayer: str, topLayer: str, driver: ScalarDriver - ) -> None: + def setIECDriver(self, bottomLayer: str, topLayer: str, driver: ScalarDriver) -> None: """Set IEC interaction between two layers. The names of the params are only for convention. The IEC will be set between bottomLyaer or topLayer, order is irrelevant. @@ -328,9 +314,7 @@ class Junction: """ ... - def setQuadIECDriver( - self, bottomLayer: str, topLayer: str, driver: ScalarDriver - ) -> None: + def setQuadIECDriver(self, bottomLayer: str, topLayer: str, driver: ScalarDriver) -> None: """Set secondary (biquadratic term) IEC interaction between two layers. The names of the params are only for convention. The IEC will be set between bottomLyaer or topLayer, order is irrelevant. @@ -382,9 +366,7 @@ class Junction: """ ... - def setLayerDampingLikeTorqueDriver( - self, layerId: str, driver: ScalarDriver - ) -> None: + def setLayerDampingLikeTorqueDriver(self, layerId: str, driver: ScalarDriver) -> None: """Set the damping like torque driver for a layer. :param layerId: the layer id :param driver: the driver @@ -398,9 +380,7 @@ class Junction: """ ... - def setLayerOneFNoise( - self, layerId: str, sources: int, bias: float, scale: float - ) -> None: + def setLayerOneFNoise(self, layerId: str, sources: int, bias: float, scale: float) -> None: """Set 1/f noise for a layer. :param layerId: the layer id :param sources: the number of generation sources (the more the slower, but more acc.) @@ -424,7 +404,7 @@ class Layer: Ms: float, thickness: float, cellSurface: float, - demagTensor: List[CVector], + demagTensor: list[CVector], temperature: float = ..., damping: float = ..., ) -> Layer: @@ -452,7 +432,7 @@ class Layer: Ms: float, thickness: float, cellSurface: float, - demagTensor: List[CVector], + demagTensor: list[CVector], damping: float = 0.11, fieldLikeTorque: float = 0, dampingLikeTorque: float = 0, @@ -478,7 +458,7 @@ class Layer: Ms: float, thickness: float, cellSurface: float, - demagTensor: List[CVector], + demagTensor: list[CVector], damping: float = 0.011, SlonczewskiSpacerLayerParameter: float = 1.0, beta: float = 0.0, @@ -536,20 +516,20 @@ class Layer: ... @overload - def setReferenceLayer(self, ref: "Reference") -> None: + def setReferenceLayer(self, ref: Reference) -> None: # noqa: F811 """Set a reference layer for the STT. The reference can be FIXED, BOTTOM or TOP. YOu can use another layer as reference to this one. :param ref: the reference layer vector.""" ... - def setTopDipoleTensor(self, tensor: List[CVector]) -> None: + def setTopDipoleTensor(self, tensor: list[CVector]) -> None: """Set a dipole tensor from the top layer. :param tensor: the dipole tensor to be set. """ ... - def setBottomDipoleTensor(self, tensor: List[CVector]) -> None: + def setBottomDipoleTensor(self, tensor: list[CVector]) -> None: """Set a dipole tensor from the bottom layer. :param tensor: the dipole tensor to be set. """ @@ -592,7 +572,7 @@ class ScalarDriver: ... @staticmethod - def getConstantDriver(constantValue: float) -> "ScalarDriver": + def getConstantDriver(constantValue: float) -> ScalarDriver: """ Constant driver produces a constant signal of a fixed amplitude. :param constantValue: constant value of the driver (constant offset/amplitude) @@ -600,23 +580,18 @@ class ScalarDriver: ... @staticmethod - def getPulseDriver( - constantValue: float, amplitude: "ScalarDriver", period: float, cycle: float - ) -> Any: + def getPulseDriver(constantValue: float, amplitude: float, period: float, cycle: float) -> ScalarDriver: """ Produces a square pulse of certain period and cycle :param constantValue: offset (vertical) of the pulse. The pulse amplitude will be added to this. :param amplitude: amplitude of the pulse signal :param period: period of the signal in seconds :param cycle: duty cycle of the signal -- a fraction between [0 and 1]. - """ ... @staticmethod - def getSineDriver( - constantValue: float, amplitude: "ScalarDriver", frequency: float, phase: float - ) -> Any: + def getSineDriver(constantValue: float, amplitude: ScalarDriver, frequency: float, phase: float) -> Any: """ Produces a sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. :param constantValue: vertical offset. The sine will oscillate around this value. @@ -627,9 +602,7 @@ class ScalarDriver: ... @staticmethod - def getStepDriver( - constantValue: float, amplitude: float, timeStart: float, timeStop: float - ) -> ScalarDriver: + def getStepDriver(constantValue: float, amplitude: float, timeStart: float, timeStop: float) -> ScalarDriver: """ Get a step driver. It has amplitude between timeStart and timeStop and 0 elsewhere :param constantValue: offset of the pulse (vertical) @@ -657,9 +630,7 @@ class ScalarDriver: ... @staticmethod - def getGaussianImpulseDriver( - constantValue: float, amplitude: float, t0: float, sigma: float - ) -> ScalarDriver: + def getGaussianImpulseDriver(constantValue: float, amplitude: float, t0: float, sigma: float) -> ScalarDriver: """Gaussian impulse driver. It has amplitude starts at t0 and falls off with sigma. Formula: @@ -673,9 +644,7 @@ class ScalarDriver: ... @staticmethod - def getGaussianStepDriver( - constantValue: float, amplitude: float, t0: float, sigma: float - ) -> ScalarDriver: + def getGaussianStepDriver(constantValue: float, amplitude: float, t0: float, sigma: float) -> ScalarDriver: """Gaussian step driver (erf function). It has amplitude starts at t0 and falls off with sigma. Formula: @@ -689,9 +658,7 @@ class ScalarDriver: ... @staticmethod - def getPosSineDriver( - constantValue: float, amplitude: float, frequency: float, phase: float - ) -> ScalarDriver: + def getPosSineDriver(constantValue: float, amplitude: float, frequency: float, phase: float) -> ScalarDriver: """Produces a positive sinusoidal signal with some offset (constantValue), amplitude frequency and phase offset. :param constantValue: vertical offset. The sine will oscillate around this value. :param amplitude: amplitude of the sine wave @@ -700,19 +667,6 @@ class ScalarDriver: """ ... - @staticmethod - def getPulseDriver( - constantValue: float, amplitude: float, period: float, cycle: float - ) -> ScalarDriver: - """ - Produces a square pulse of certain period and cycle - :param constantValue: offset (vertical) of the pulse. The pulse amplitude will be added to this. - :param amplitude: amplitude of the pulse signal - :param period: period of the signal in seconds - :param cycle: duty cycle of the signal -- a fraction between [0 and 1]. - """ - ... - class SolverMode: """SolverMode Indicator""" diff --git a/cmtj/llgb/__init__.pyi b/cmtj/llgb/__init__.pyi index fac6e3b..3033bf9 100644 --- a/cmtj/llgb/__init__.pyi +++ b/cmtj/llgb/__init__.pyi @@ -1,11 +1,9 @@ -from typing import Dict, List, Tuple - import cmtj class LLGBJunction: """LLGB Junction class.""" - def __init__(self, layers: List[LLGBLayer]) -> None: + def __init__(self, layers: list[LLGBLayer]) -> None: """Initialises a LLGB junction with layers. :param layers: list of LLGB layers.""" ... @@ -14,7 +12,7 @@ class LLGBJunction: """Clears the simulation log of the junction.""" ... - def getLog(self) -> Dict[str, List[float]]: + def getLog(self) -> dict[str, list[float]]: """Returns the simulation log of the junction.""" ... @@ -40,17 +38,13 @@ class LLGBJunction: :param arg0: file path.""" ... - def setLayerExternalFieldDriver( - self, layerId: str, driver: cmtj.AxialDriver - ) -> None: + def setLayerExternalFieldDriver(self, layerId: str, driver: cmtj.AxialDriver) -> None: """Set an external field driver for a layer. :param layerId: the id of the layer. :param driver: the field driver to be set.""" ... - def setLayerTemperatureDriver( - self, layerId: str, driver: cmtj.ScalarDriver - ) -> None: + def setLayerTemperatureDriver(self, layerId: str, driver: cmtj.ScalarDriver) -> None: """Set a temperature driver for a layer. :param layerId: the id of the layer. :param driver: the temperature driver to be set. @@ -68,7 +62,7 @@ class LLGBLayer: Ms: float, thickness: float, cellSurface: float, - demagTensor: List[cmtj.CVector], + demagTensor: list[cmtj.CVector], damping: float, Tc: float, susceptibility: float, @@ -111,7 +105,7 @@ def MFAWeissCurie( relax: float = ..., tolerance: float = ..., maxIter: int = ..., -) -> Tuple[float, float]: +) -> tuple[float, float]: """Mean Field Approximation for Weiss Curie temperature. :param me: equilibrium magnetisation. :param T: temperature. diff --git a/cmtj/models/domain_dynamics.py b/cmtj/models/domain_dynamics.py index 5870c3e..0379780 100644 --- a/cmtj/models/domain_dynamics.py +++ b/cmtj/models/domain_dynamics.py @@ -1,7 +1,7 @@ import math from collections import defaultdict from dataclasses import dataclass, field -from typing import Callable, List, Literal +from typing import Callable, Literal from numba import njit from scipy.integrate import RK45 @@ -10,15 +10,16 @@ from ..utils.general import VectorObj gyro = gyromagnetic_ratio -pi2 = math.pi / 2. +pi2 = math.pi / 2.0 class DW: """Initial conditions for the phi of DW equation.""" + NEEL_RIGHT = 0 NEEL_LEFT = math.pi - BLOCH_UP = math.pi / 2. - BLOCH_DOWN = 3. * math.pi / 2. + BLOCH_UP = math.pi / 2.0 + BLOCH_DOWN = 3.0 * math.pi / 2.0 class DWRelax: @@ -32,7 +33,7 @@ def get_pinning_field(X, Ms, pinning, Ly, Lz, V0_pin): arg = X * math.pi / pinning dVdx = 2 * math.pi * V0_pin * math.sin(arg) * math.cos(arg) denom = 2 * mu0 * Ms * Lz * Ly - return -(1. / denom) * dVdx + return -(1.0 / denom) * dVdx @njit @@ -40,65 +41,77 @@ def get_edge_field(X, Lx, V0_edge): c = Lx / 2 arg = (X - (Lx / 2)) / c p = 6 - return -p * V0_edge * (math.sinh(arg) * math.cosh(arg)**(p - 1) / c) + return -p * V0_edge * (math.sinh(arg) * math.cosh(arg) ** (p - 1) / c) @njit -def get_field_contribution(X, phi, hx, hy, hz, alpha, dw, Ms, V0_pin, pinning, - Ly, Lz): - - pinning = get_pinning_field(X, - Ms=Ms, - pinning=pinning, - Ly=Ly, - Lz=Lz, - V0_pin=V0_pin) - dxdt = alpha * gyro * dw * (hz + pinning) + gyro * dw * pi2 * ( - -hy * math.cos(phi) + hx * math.sin(phi)) - dphidt = gyro * (hz + pinning) + alpha * gyro * pi2 * (hy * math.cos(phi) - - hx * math.sin(phi)) +def get_field_contribution(X, phi, hx, hy, hz, alpha, dw, Ms, V0_pin, pinning, Ly, Lz): + pinning = get_pinning_field(X, Ms=Ms, pinning=pinning, Ly=Ly, Lz=Lz, V0_pin=V0_pin) + dxdt = alpha * gyro * dw * (hz + pinning) + gyro * dw * pi2 * (-hy * math.cos(phi) + hx * math.sin(phi)) + dphidt = gyro * (hz + pinning) + alpha * gyro * pi2 * (hy * math.cos(phi) - hx * math.sin(phi)) return dxdt, dphidt @njit def compute_gamma_a(X, phi, Q, dw, hk, hx, hy, hdmi, bj, IECterm): - pi2 = math.pi / 2. - fact_gamma = -0.5 * hk * math.sin( - 2 * phi) - pi2 * hy * math.cos(phi) + pi2 * hx * math.sin( - phi) + Q * pi2 * hdmi * math.sin(phi) + IECterm + pi2 = math.pi / 2.0 + fact_gamma = ( + -0.5 * hk * math.sin(2 * phi) + - pi2 * hy * math.cos(phi) + + pi2 * hx * math.sin(phi) + + Q * pi2 * hdmi * math.sin(phi) + + IECterm + ) fact_stt = bj / dw return gyro * fact_gamma + fact_stt @njit -def compute_gamma_b(X, phi, Q, dw, hshe, hz, hr, beta, bj, Ms, Lx, Ly, Lz, - V0_pin, V0_edge, pinning): +def compute_gamma_b(X, phi, Q, dw, hshe, hz, hr, beta, bj, Ms, Lx, Ly, Lz, V0_pin, V0_edge, pinning): pi2 = math.pi / 2 - hp = get_pinning_field(X, - Ms=Ms, - pinning=pinning, - Ly=Ly, - Lz=Lz, - V0_pin=V0_pin) + hp = get_pinning_field(X, Ms=Ms, pinning=pinning, Ly=Ly, Lz=Lz, V0_pin=V0_pin) he = get_edge_field(X, Lx, V0_edge) - fact_gamma = Q * (he + hz + hp + pi2 * hshe * - math.cos(phi)) - beta * pi2 * hr * math.cos(phi) + fact_gamma = Q * (he + hz + hp + pi2 * hshe * math.cos(phi)) - beta * pi2 * hr * math.cos(phi) fact_stt = beta * bj / dw return gyro * fact_gamma + fact_stt @njit -def compute_dynamics(X, phi, delta, alpha, Q, hx, hy, hz, hk, hdmi, hr, hshe, - beta, bj, Ms, Lx, Ly, Lz, V0_pin, V0_edge, pinning, - IECterm, thickness, A, Ku, Kp): +def compute_dynamics( + X, + phi, + delta, + alpha, + Q, + hx, + hy, + hz, + hk, + hdmi, + hr, + hshe, + beta, + bj, + Ms, + Lx, + Ly, + Lz, + V0_pin, + V0_edge, + pinning, + IECterm, + thickness, + A, + Ku, + Kp, +): gamma_a = compute_gamma_a(X, phi, Q, delta, hk, hx, hy, hdmi, bj, IECterm) - gamma_b = compute_gamma_b(X, phi, Q, delta, hshe, hz, hr, beta, bj, Ms, Lx, - Ly, Lz, V0_pin, V0_edge, pinning) + gamma_b = compute_gamma_b(X, phi, Q, delta, hshe, hz, hr, beta, bj, Ms, Lx, Ly, Lz, V0_pin, V0_edge, pinning) dXdt = delta * (gamma_a + alpha * gamma_b) dPhidt = -alpha * gamma_a + gamma_b pref = gyro / (alpha * mu0 * Ms * thickness) # domain width relaxation from Thiaville - dDeltadt = pref * (A / delta - delta * (Ku + Kp * math.sin(phi)**2)) + dDeltadt = pref * (A / delta - delta * (Ku + Kp * math.sin(phi) ** 2)) # dDeltadt = 0 return dXdt, dPhidt, dDeltadt @@ -130,6 +143,7 @@ class DomainWallDynamics: For classical formulation see: Current-driven dynamics of chiral ferromagnetic domain walls, Emori et al, 2013 """ + H: VectorObj alpha: float Ms: float @@ -157,7 +171,7 @@ def __post_init__(self): # in post init we already have p self.bj = bohr_magneton * self.p / (echarge * self.Ms) self.je_driver = lambda t: 0 - denom = (2 * self.Ms * mu0 * echarge * self.thickness) + denom = 2 * self.Ms * mu0 * echarge * self.thickness self.Hshe = hbar * self.SHE_angle / denom self.hx, self.hy, self.hz = self.H.get_cartesian() self.dw0 = self.get_unrelaxed_domain_width() @@ -195,7 +209,7 @@ def get_inplane_anisotropy_field(self): @dataclass class MultilayerWallDynamics: - layers: List[DomainWallDynamics] + layers: list[DomainWallDynamics] J: float = 0 vector_size: int = 3 # 3 for X, phi, delta @@ -214,7 +228,7 @@ def multilayer_dw_llg(self, t, vec): new_vec = [] for i, layer in enumerate(self.layers): je_at_t = layer.je_driver(t=t) - reduced_alpha = (1. + layer.alpha**2) + reduced_alpha = 1.0 + layer.alpha**2 lx = vec[self.vector_size * i] lphi = vec[(self.vector_size * i) + 1] ldomain_width = vec[(self.vector_size * i) + 2] @@ -252,7 +266,8 @@ def multilayer_dw_llg(self, t, vec): A=layer.A, Ku=layer.Ku, Kp=layer.Kp, - thickness=layer.thickness) + thickness=layer.thickness, + ) dXdt = dXdt / reduced_alpha dPhidt = dPhidt / reduced_alpha if layer.relax_dw != DWRelax.DYNAMIC: @@ -260,45 +275,46 @@ def multilayer_dw_llg(self, t, vec): new_vec.extend([dXdt, dPhidt, dDeltadt]) return new_vec - def run(self, - sim_time: float, - starting_conditions: List[float], - max_step: float = 1e-10): + def run(self, sim_time: float, starting_conditions: list[float], max_step: float = 1e-10): """Run simulation of DW dynamics. :param sim_time: total simulation time (simulation units). :param starting_conditions: starting position and angle of the DW. :param max_step: maximum allowed step of the RK45 method. """ - integrator = RK45(fun=self.multilayer_dw_llg, - t0=0., - first_step=1e-16, - max_step=max_step, - y0=starting_conditions, - rtol=1e-12, - t_bound=sim_time) + integrator = RK45( + fun=self.multilayer_dw_llg, + t0=0.0, + first_step=1e-16, + max_step=max_step, + y0=starting_conditions, + rtol=1e-12, + t_bound=sim_time, + ) result = defaultdict(list) while True: integrator.step() - if integrator.status == 'failed': + if integrator.status == "failed": print("Failed to converge") break layer_vecs = integrator.y - result['t'].append(integrator.t) + result["t"].append(integrator.t) for i, layer in enumerate(self.layers): - x, phi, dw = layer_vecs[self.vector_size * i], layer_vecs[ - self.vector_size * i + - 1], layer_vecs[self.vector_size * i + 2] + x, phi, dw = ( + layer_vecs[self.vector_size * i], + layer_vecs[self.vector_size * i + 1], + layer_vecs[self.vector_size * i + 2], + ) # static relaxation Thiaville if layer.relax_dw == DWRelax.STATIC: ratio = layer.Kp / layer.Ku - dw = layer.dw0 / math.sqrt(1 + ratio * math.sin(phi)**2) + dw = layer.dw0 / math.sqrt(1 + ratio * math.sin(phi) ** 2) vel = (x - integrator.y_old[2 * i]) / integrator.step_size - result[f'dw_{i}'].append(dw) - result[f'v_{i}'].append(vel) - result[f'x_{i}'].append(x) - result[f'phi_{i}'].append(phi) - result[f'je_{i}'].append(layer.je_driver(t=integrator.t)) - if integrator.status == 'finished': + result[f"dw_{i}"].append(dw) + result[f"v_{i}"].append(vel) + result[f"x_{i}"].append(x) + result[f"phi_{i}"].append(phi) + result[f"je_{i}"].append(layer.je_driver(t=integrator.t)) + if integrator.status == "finished": break return result diff --git a/cmtj/models/drivers.py b/cmtj/models/drivers.py index e30be83..057413f 100644 --- a/cmtj/models/drivers.py +++ b/cmtj/models/drivers.py @@ -19,4 +19,4 @@ def decay_driver(t, amp, tau): @njit def gaussian_driver(t, amp, sigma): - return amp * math.exp(-t**2 / sigma**2) + return amp * math.exp(-(t**2) / sigma**2) diff --git a/cmtj/models/ensemble.py b/cmtj/models/ensemble.py index 73c1151..f2ebdbf 100644 --- a/cmtj/models/ensemble.py +++ b/cmtj/models/ensemble.py @@ -23,7 +23,7 @@ def symmetric_lorentz(H, dH, Hr, Vs): :param Hr: resonance field in A/m """ dH2 = dH**2 - return Vs * dH2 / ((H - Hr)**2 + dH2) + return Vs * dH2 / ((H - Hr) ** 2 + dH2) def antisymmetric_lorentz(H, dH, Hr, Vas): @@ -47,5 +47,4 @@ def mixed_lorentz(H, dH, Hr, Va, Vas): :param Va: amplitude of symmetric Lorentzian :param Vas: amplitude of antisymmetric Lorentzian """ - return symmetric_lorentz(H, dH, Hr, Va) + antisymmetric_lorentz( - H, dH, Hr, Vas) + return symmetric_lorentz(H, dH, Hr, Va) + antisymmetric_lorentz(H, dH, Hr, Vas) diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index 128ba03..56bbca7 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -1,9 +1,10 @@ import math import time import warnings +from collections.abc import Iterable from dataclasses import dataclass from functools import lru_cache -from typing import Iterable, List, Literal, Tuple, Union +from typing import Union import numpy as np import sympy as sym @@ -44,8 +45,7 @@ def general_hessian_functional(N: int): for i in range(N): # indx_i = str(i + 1) # for display purposes indx_i = str(i) - all_symbols.extend( - (sym.Symbol(r"\theta_" + indx_i), sym.Symbol(r"\phi_" + indx_i))) + all_symbols.extend((sym.Symbol(r"\theta_" + indx_i), sym.Symbol(r"\phi_" + indx_i))) energy_functional_expr = sym.Function("E")(*all_symbols) return ( get_hessian_from_energy_expr(N, energy_functional_expr), @@ -69,9 +69,12 @@ def get_hessian_from_energy_expr(N: int, energy_functional_expr: sym.Expr): indx_i = str(i) # z = sym.Symbol("Z") # these here must match the Ms symbols! - z = (sym.Symbol(r"\omega") * sym.Symbol(r"M_{" + indx_i + "}") * - sym.sin(sym.Symbol(r"\theta_" + indx_i)) * - sym.Symbol(r"t_{" + indx_i + "}")) + z = ( + sym.Symbol(r"\omega") + * sym.Symbol(r"M_{" + indx_i + "}") + * sym.sin(sym.Symbol(r"\theta_" + indx_i)) + * sym.Symbol(r"t_{" + indx_i + "}") + ) for j in range(i, N): # indx_j = str(j + 1) # for display purposes indx_j = str(j) @@ -105,7 +108,7 @@ def get_hessian_from_energy_expr(N: int, energy_functional_expr: sym.Expr): return sym.Matrix(hessian) -@lru_cache() +@lru_cache def solve_for_determinant(N: int): """Solve for the determinant of the hessian functional. :param N: number of layers. @@ -126,9 +129,7 @@ def find_analytical_roots(N: int): return solutions, energy_functional_expr -def get_all_second_derivatives(energy_functional_expr, - energy_expression, - subs=None): +def get_all_second_derivatives(energy_functional_expr, energy_expression, subs=None): """Get all second derivatives of the energy expression. :param energy_functional_expr: symbolic energy_functional expression :param energy_expression: symbolic energy expression (from solver) @@ -142,11 +143,9 @@ def get_all_second_derivatives(energy_functional_expr, if i <= j: org_diff = sym.diff(energy_functional_expr, s1, s2) if subs is not None: - second_derivatives[org_diff] = sym.diff( - energy_expression, s1, s2).subs(subs) + second_derivatives[org_diff] = sym.diff(energy_expression, s1, s2).subs(subs) else: - second_derivatives[org_diff] = sym.diff( - energy_expression, s1, s2) + second_derivatives[org_diff] = sym.diff(energy_expression, s1, s2) return second_derivatives @@ -165,9 +164,7 @@ class LayerSB: Kv: VectorObj Ks: float Ms: float - Hdmi: VectorObj = ( - None # TODO: change when we support py3.10 upwards (field(kw_only=True, default=None)) - ) + Hdmi: VectorObj = None # TODO: change when we support py3.10 upwards (field(kw_only=True, default=None)) def __post_init__(self): if self._id > 9: @@ -178,11 +175,13 @@ def __post_init__(self): self.Hdmi = sym.ImmutableMatrix(self.Hdmi.get_cartesian()) self.theta = sym.Symbol(r"\theta_" + str(self._id)) self.phi = sym.Symbol(r"\phi_" + str(self._id)) - self.m = sym.ImmutableMatrix([ - sym.sin(self.theta) * sym.cos(self.phi), - sym.sin(self.theta) * sym.sin(self.phi), - sym.cos(self.theta), - ]) + self.m = sym.ImmutableMatrix( + [ + sym.sin(self.theta) * sym.cos(self.phi), + sym.sin(self.theta) * sym.sin(self.phi), + sym.cos(self.theta), + ] + ) def get_coord_sym(self): """Returns the symbolic coordinates of the layer.""" @@ -192,7 +191,7 @@ def get_m_sym(self): """Returns the magnetisation vector.""" return self.m - @lru_cache(3) + @lru_cache(3) # noqa: B019 def symbolic_layer_energy( self, H: sym.ImmutableMatrix, @@ -214,13 +213,12 @@ def symbolic_layer_energy( if top_layer is not None: other_m = top_layer.get_m_sym() - top_iec_energy = (-(J1top / self.thickness) * m.dot(other_m) - - (J2top / self.thickness) * m.dot(other_m)**2) + top_iec_energy = -(J1top / self.thickness) * m.dot(other_m) - (J2top / self.thickness) * m.dot(other_m) ** 2 if down_layer is not None: other_m = down_layer.get_m_sym() bottom_iec_energy = ( - -(J1bottom / self.thickness) * m.dot(other_m) - - (J2bottom / self.thickness) * m.dot(other_m)**2) + -(J1bottom / self.thickness) * m.dot(other_m) - (J2bottom / self.thickness) * m.dot(other_m) ** 2 + ) return eng_non_interaction + top_iec_energy + bottom_iec_energy def no_iec_symbolic_layer_energy(self, H: sym.ImmutableMatrix): @@ -228,15 +226,12 @@ def no_iec_symbolic_layer_energy(self, H: sym.ImmutableMatrix): Coupling contribution comes only from the bottom layer (top-down crawl)""" m = self.get_m_sym() - alpha = sym.ImmutableMatrix( - [sym.cos(self.Kv.phi), - sym.sin(self.Kv.phi), 0]) + alpha = sym.ImmutableMatrix([sym.cos(self.Kv.phi), sym.sin(self.Kv.phi), 0]) field_energy = -mu0 * self.Ms * m.dot(H) hdmi_energy = -mu0 * self.Ms * m.dot(self.Hdmi) - surface_anistropy = (-self.Ks + - (1.0 / 2.0) * mu0 * self.Ms**2) * (m[-1]**2) - volume_anisotropy = -self.Kv.mag * (m.dot(alpha)**2) + surface_anistropy = (-self.Ks + (1.0 / 2.0) * mu0 * self.Ms**2) * (m[-1] ** 2) + volume_anisotropy = -self.Kv.mag * (m.dot(alpha) ** 2) return field_energy + surface_anistropy + volume_anisotropy + hdmi_energy def sb_correction(self): @@ -247,9 +242,13 @@ def __hash__(self) -> int: return hash(str(self)) def __eq__(self, __value: "LayerSB") -> bool: - return (self._id == __value._id and self.thickness == __value.thickness - and self.Kv == __value.Kv and self.Ks == __value.Ks - and self.Ms == __value.Ms) + return ( + self._id == __value._id + and self.thickness == __value.thickness + and self.Kv == __value.Kv + and self.Ks == __value.Ks + and self.Ms == __value.Ms + ) @dataclass @@ -278,13 +277,13 @@ def rhs_llg( down_layer=down_layer, ) # sum all components - prefac = gamma_rad / (1.0 + self.alpha)**2 + prefac = gamma_rad / (1.0 + self.alpha) ** 2 inv_sin = 1.0 / (sym.sin(self.theta) + EPS) dUdtheta = sym.diff(U, self.theta) dUdphi = sym.diff(U, self.phi) dtheta = -inv_sin * dUdphi - self.alpha * dUdtheta - dphi = inv_sin * dUdtheta - self.alpha * dUdphi * (inv_sin)**2 + dphi = inv_sin * dUdtheta - self.alpha * dUdphi * (inv_sin) ** 2 return prefac * sym.ImmutableMatrix([dtheta, dphi]) / self.Ms def __eq__(self, __value: "LayerDynamic") -> bool: @@ -309,12 +308,12 @@ class Solver: Goes (i)-(i+1), i = 0, 1, 2, ... with i being the index of the layer. """ - layers: List[Union[LayerSB, LayerDynamic]] - J1: List[float] - J2: List[float] + layers: list[Union[LayerSB, LayerDynamic]] + J1: list[float] + J2: list[float] H: VectorObj = None - ilD: List[VectorObj] = None - Ndipole: List[List[VectorObj]] = None + ilD: list[VectorObj] = None + Ndipole: list[list[VectorObj]] = None def __post_init__(self): if len(self.layers) != len(self.J1) + 1: @@ -323,9 +322,7 @@ def __post_init__(self): raise ValueError("Number of layers must be 1 more than J2.") if self.ilD is None: # this is optional, if not provided, we assume zero DMI - self.ilD = [ - VectorObj(0, 0, 0) for _ in range(len(self.layers) - 1) - ] + self.ilD = [VectorObj(0, 0, 0) for _ in range(len(self.layers) - 1)] if len(self.layers) != len(self.ilD) + 1: raise ValueError("Number of layers must be 1 more than ilD.") if not all(isinstance(d, VectorObj) for d in self.ilD): @@ -335,21 +332,15 @@ def __post_init__(self): self.dipoleMatrix: list[sym.Matrix] = None if self.Ndipole is not None: if len(self.layers) != len(self.Ndipole) + 1: - raise ValueError( - "Number of layers must be 1 more than number of tensors.") + raise ValueError("Number of layers must be 1 more than number of tensors.") if isinstance(self.layers[0], LayerDynamic): - raise ValueError( - "Dipole coupling is not yet supported for LayerDynamic.") - self.dipoleMatrix = [ - sym.Matrix([d.get_cartesian() for d in dipole]) - for dipole in self.Ndipole - ] + raise ValueError("Dipole coupling is not yet supported for LayerDynamic.") + self.dipoleMatrix = [sym.Matrix([d.get_cartesian() for d in dipole]) for dipole in self.Ndipole] id_sets = {layer._id for layer in self.layers} ideal_set = set(range(len(self.layers))) if id_sets != ideal_set: - raise ValueError("Layer ids must be 0, 1, 2, ... and unique." - "Ids must start from 0.") + raise ValueError("Layer ids must be 0, 1, 2, ... and unique." "Ids must start from 0.") def get_layer_references(self, layer_indx, interaction_constant): """Returns the references to the layers above and below the layer @@ -357,11 +348,9 @@ def get_layer_references(self, layer_indx, interaction_constant): if len(self.layers) == 1: return None, None, 0, 0 if layer_indx == 0: - return None, self.layers[layer_indx + - 1], 0, interaction_constant[0] + return None, self.layers[layer_indx + 1], 0, interaction_constant[0] elif layer_indx == len(self.layers) - 1: - return self.layers[layer_indx - - 1], None, interaction_constant[-1], 0 + return self.layers[layer_indx - 1], None, interaction_constant[-1], 0 return ( self.layers[layer_indx - 1], self.layers[layer_indx + 1], @@ -378,18 +367,13 @@ def compose_llg_jacobian(self, H: VectorObj): symbols, fns = [], [] for i, layer in enumerate(self.layers): symbols.extend((layer.theta, layer.phi)) - top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references( - i, self.J1) + top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references(i, self.J1) _, _, J2top, J2bottom = self.get_layer_references(i, self.J2) - fns.append( - layer.rhs_llg(H, Jtop, Jbottom, J2top, J2bottom, top_layer, - bottom_layer)) + fns.append(layer.rhs_llg(H, Jtop, Jbottom, J2top, J2bottom, top_layer, bottom_layer)) jac = sym.ImmutableMatrix(fns).jacobian(symbols) return jac, symbols - def create_energy(self, - H: Union[VectorObj, sym.ImmutableMatrix] = None, - volumetric: bool = False): + def create_energy(self, H: Union[VectorObj, sym.ImmutableMatrix] = None, volumetric: bool = False): """Creates the symbolic energy expression. Due to problematic nature of coupling, there is an issue of @@ -406,16 +390,13 @@ def create_energy(self, if volumetric: # volumetric energy -- DO NOT USE IN GENERAL for i, layer in enumerate(self.layers): - top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references( - i, self.J1) + top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references(i, self.J1) _, _, J2top, J2bottom = self.get_layer_references(i, self.J2) ratio_top, ratio_bottom = 0, 0 if top_layer: - ratio_top = top_layer.thickness / (top_layer.thickness + - layer.thickness) + ratio_top = top_layer.thickness / (top_layer.thickness + layer.thickness) if bottom_layer: - ratio_bottom = bottom_layer.thickness / ( - layer.thickness + bottom_layer.thickness) + ratio_bottom = bottom_layer.thickness / (layer.thickness + bottom_layer.thickness) energy += layer.symbolic_layer_energy( H, Jtop * ratio_top, @@ -429,8 +410,7 @@ def create_energy(self, # surface energy for correct angular gradient for layer in self.layers: # to avoid dividing J by thickness - energy += layer.no_iec_symbolic_layer_energy( - H) * layer.thickness + energy += layer.no_iec_symbolic_layer_energy(H) * layer.thickness for i in range(len(self.layers) - 1): l1m = self.layers[i].get_m_sym() @@ -439,7 +419,7 @@ def create_energy(self, # IEC ldot = l1m.dot(l2m) energy -= self.J1[i] * ldot - energy -= self.J2[i] * (ldot)**2 + energy -= self.J2[i] * (ldot) ** 2 # IDMI, sign is the same J1 lcross = l1m.cross(l2m) @@ -449,15 +429,23 @@ def create_energy(self, if self.dipoleMatrix is not None: mat = self.dipoleMatrix[i] # is positive, just like demag - energy += ((mu0 / 2.0) * l1m.dot(mat * l2m) * - self.layers[i].Ms * self.layers[i + 1].Ms * - self.layers[i].thickness) - energy += ((mu0 / 2.0) * l2m.dot(mat * l1m) * - self.layers[i].Ms * self.layers[i + 1].Ms * - self.layers[i + 1].thickness) + energy += ( + (mu0 / 2.0) + * l1m.dot(mat * l2m) + * self.layers[i].Ms + * self.layers[i + 1].Ms + * self.layers[i].thickness + ) + energy += ( + (mu0 / 2.0) + * l2m.dot(mat * l1m) + * self.layers[i].Ms + * self.layers[i + 1].Ms + * self.layers[i + 1].thickness + ) return energy - def create_energy_hessian(self, equilibrium_position: List[float]): + def create_energy_hessian(self, equilibrium_position: list[float]): """Creates the symbolic hessian of the energy expression.""" energy = self.create_energy(volumetric=False) subs = self.get_subs(equilibrium_position) @@ -503,8 +491,7 @@ def get_gradient_expr(self, accel="math"): symbols = [] for layer in self.layers: (theta, phi) = layer.get_coord_sym() - grad_vector.extend((sym.diff(energy, theta), sym.diff(energy, - phi))) + grad_vector.extend((sym.diff(energy, theta), sym.diff(energy, phi))) symbols.extend((theta, phi)) return sym.lambdify(symbols, grad_vector, accel) @@ -540,12 +527,10 @@ def adam_gradient_descent( step += 1 grad = np.asarray(gradfn(*current_position)) m = first_momentum_decay * m + (1.0 - first_momentum_decay) * grad - v = second_momentum_decay * v + (1.0 - - second_momentum_decay) * grad**2 + v = second_momentum_decay * v + (1.0 - second_momentum_decay) * grad**2 m_hat = m / (1.0 - first_momentum_decay**step) v_hat = v / (1.0 - second_momentum_decay**step) - new_position = current_position - learning_rate * m_hat / ( - np.sqrt(v_hat) + eps) + new_position = current_position - learning_rate * m_hat / (np.sqrt(v_hat) + eps) if step > max_steps: break if fast_norm(current_position - new_position) < tol: @@ -570,8 +555,7 @@ def single_layer_resonance(self, layer_indx: int, eq_position: np.ndarray): d2Edthetaphi = sym.diff(sym.diff(energy, theta), phi).subs(subs) vareps = 1e-18 - fmr = (d2Edtheta2 * d2Edphi2 - d2Edthetaphi**2) / np.power( - np.sin(theta_eq + vareps) * layer.Ms, 2) + fmr = (d2Edtheta2 * d2Edphi2 - d2Edthetaphi**2) / np.power(np.sin(theta_eq + vareps) * layer.Ms, 2) fmr = np.sqrt(float(fmr)) * gamma_rad / (2 * np.pi) return fmr @@ -614,8 +598,7 @@ def solve( :return: equilibrium position and frequencies in [GHz] (and eigenvectors if LayerDynamic instead of LayerSB). """ if self.H is None: - raise ValueError( - "H must be set before solving the system numerically.") + raise ValueError("H must be set before solving the system numerically.") eq = self.adam_gradient_descent( init_position=init_position, max_steps=max_steps, @@ -639,7 +622,7 @@ def solve( return eq, frequencies return self.num_solve(eq, ftol=ftol, max_freq=max_freq) - def dynamic_layer_solve(self, eq: List[float]): + def dynamic_layer_solve(self, eq: list[float]): """Return the FMR frequencies and modes for N layers using the dynamic RHS model :param eq: the equilibrium position of the system. @@ -653,10 +636,7 @@ def dynamic_layer_solve(self, eq: List[float]): indx = np.argwhere(eigvals_im > 0).ravel() return eigvals_im[indx], eigvecs[indx] - def num_solve(self, - eq: List[float], - ftol: float = 0.01e9, - max_freq: float = 80e9): + def num_solve(self, eq: list[float], ftol: float = 0.01e9, max_freq: float = 80e9): hes = self.create_energy_hessian(eq) omega = sym.Symbol(r"\omega") if len(self.layers) <= 3: @@ -676,25 +656,26 @@ def analytical_roots(self): Returns a list of solutions. Ineffecient for more than 2 layers (can try though). """ - Hsym = sym.Matrix([ - sym.Symbol(r"H_{x}"), - sym.Symbol(r"H_{y}"), - sym.Symbol(r"H_{z}"), - ]) + Hsym = sym.Matrix( + [ + sym.Symbol(r"H_{x}"), + sym.Symbol(r"H_{y}"), + sym.Symbol(r"H_{z}"), + ] + ) N = len(self.layers) if N > 2: warnings.warn( - "Analytical solutions for over 2 layers may be computationally expensive." + "Analytical solutions for over 2 layers may be computationally expensive.", + stacklevel=2, ) system_energy = self.create_energy(H=Hsym, volumetric=False) root_expr, energy_functional_expr = find_analytical_roots(N) - subs = get_all_second_derivatives(energy_functional_expr, - energy_expression=system_energy, - subs={}) + subs = get_all_second_derivatives(energy_functional_expr, energy_expression=system_energy, subs={}) subs.update(self.get_ms_subs()) return [s.subs(subs) for s in root_expr] - def get_subs(self, equilibrium_position: List[float]): + def get_subs(self, equilibrium_position: list[float]): """Returns the substitution dictionary for the energy expression.""" subs = {} for i in range(len(self.layers)): @@ -706,10 +687,7 @@ def get_subs(self, equilibrium_position: List[float]): def get_ms_subs(self): """Returns a dictionary of substitutions for the Ms symbols.""" a = {r"M_{" + str(layer._id) + "}": layer.Ms for layer in self.layers} - b = { - r"t_{" + str(layer._id) + r"}": layer.thickness - for layer in self.layers - } + b = {r"t_{" + str(layer._id) + r"}": layer.thickness for layer in self.layers} return a | b def set_H(self, H: VectorObj): @@ -718,14 +696,14 @@ def set_H(self, H: VectorObj): def analytical_field_scan( self, - Hrange: List[VectorObj], - init_position: Union[List[float], None] = None, + Hrange: list[VectorObj], + init_position: Union[list[float], None] = None, max_steps: int = 1e9, learning_rate: float = 1e-4, first_momentum_decay: float = 0.9, second_momentum_decay: float = 0.999, disable_tqdm: bool = False, - ) -> Iterable[Tuple[List[float], List[float], VectorObj]]: + ) -> Iterable[tuple[list[float], list[float], VectorObj]]: """Performs a field scan using the analytical solutions. :param Hrange: the range of fields to scan. :param init_position: the initial position for the gradient descent. @@ -749,11 +727,13 @@ def analytical_field_scan( # align with the first field for _ in self.layers: init_position.extend([start.theta, start.phi]) - Hsym = sym.Matrix([ - sym.Symbol(r"H_{x}"), - sym.Symbol(r"H_{y}"), - sym.Symbol(r"H_{z}"), - ]) + Hsym = sym.Matrix( + [ + sym.Symbol(r"H_{x}"), + sym.Symbol(r"H_{y}"), + sym.Symbol(r"H_{z}"), + ] + ) current_position = init_position for Hvalue in tqdm(Hrange, disable=disable_tqdm): self.set_H(Hvalue) diff --git a/cmtj/models/noise.py b/cmtj/models/noise.py index fa1b073..06f4454 100644 --- a/cmtj/models/noise.py +++ b/cmtj/models/noise.py @@ -107,11 +107,9 @@ def noise_model( triggers = 0 def _oscillations(i: int): - return amplitude * np.sin(2 * np.pi * freqs_osc * i * time_scale + - phases).reshape(-1, 1) + return amplitude * np.sin(2 * np.pi * freqs_osc * i * time_scale + phases).reshape(-1, 1) def _background_noise(i: int): - return rng.normal(0, background_thermal_noise_std, dims) if enable_oscillations and background_thermal_noise_std > 0: @@ -131,10 +129,8 @@ def _background_noise(i: int): f_counts[freq_mask] += 1 if fsum > 0: triggers += 1 - vector_values[freq_mask] = rng.normal(0, thermal_noise_std, - (fsum, dims)) - m_values[i - offset] += np.sum(volumes * vector_values, - axis=0) + osc_vals + vector_values[freq_mask] = rng.normal(0, thermal_noise_std, (fsum, dims)) + m_values[i - offset] += np.sum(volumes * vector_values, axis=0) + osc_vals else: m_values[i - offset] = osc_vals + m_values[i - offset - 1] @@ -159,9 +155,9 @@ def autocorrelation(x, dT): """ xp = x - np.mean(x) f = np.fft.fft(xp) - p = np.abs(f)**2 + p = np.abs(f) ** 2 pi = np.fft.ifft(p) - autocorr = np.real(pi)[:x.size // 2] / np.sum(xp**2) + autocorr = np.real(pi)[: x.size // 2] / np.sum(xp**2) # Create a lag array lag = np.arange(0, len(autocorr)) * dT @@ -169,8 +165,7 @@ def autocorrelation(x, dT): return lag, autocorr -def plot_noise_data(m_values: np.ndarray, volumes: np.ndarray, - freqs: np.ndarray, time_scale: float): +def plot_noise_data(m_values: np.ndarray, volumes: np.ndarray, freqs: np.ndarray, time_scale: float): """ Plot noise data: - Autocorrelation @@ -203,20 +198,16 @@ def plot_noise_data(m_values: np.ndarray, volumes: np.ndarray, ax2.plot(volumes, freqs / 1000, color="crimson") # histogram of volumes ax25 = ax2.twinx() - ax25.hist(volumes, - bins=min(100, len(volumes)), - color="navy", - alpha=0.5, - label="Count") + ax25.hist(volumes, bins=min(100, len(volumes)), color="navy", alpha=0.5, label="Count") ax25.set_ylabel("Count", rotation=-90, labelpad=10) ax25.legend() ax2.set_xlabel("Area (a.u.)") ax2.set_ylabel("Modulo step activation (1000x)") y = np.fft.fft(m_values, axis=0) y = np.power(np.abs(y), 2) - y = y[:int(k // 2)] + y = y[: int(k // 2)] x = np.fft.fftfreq(int(k), time_scale) - x = x[:int(k // 2)] + x = x[: int(k // 2)] ax3.plot(x, y, color="royalblue") ax3.set_xscale("log") ax3.set_yscale("log") @@ -233,8 +224,7 @@ def plot_noise_data(m_values: np.ndarray, volumes: np.ndarray, for label, ax in zip("abcd", (ax1, ax2, ax3, ax4)): # label physical distance in and down: - trans = mtransforms.ScaledTranslation(10 / 72, -5 / 72, - fig.dpi_scale_trans) + trans = mtransforms.ScaledTranslation(10 / 72, -5 / 72, fig.dpi_scale_trans) ax.text( 0.0, 1.0, @@ -243,10 +233,7 @@ def plot_noise_data(m_values: np.ndarray, volumes: np.ndarray, # fontsize="medium", verticalalignment="top", color="black", - bbox=dict(facecolor="none", - alpha=0.4, - edgecolor="none", - pad=3.0), + bbox=dict(facecolor="none", alpha=0.4, edgecolor="none", pad=3.0), ) return fig @@ -274,7 +261,6 @@ def create_noise_animation( rng = np.random.default_rng(seed=42) vector_values = np.asarray(vector_values).squeeze() - vector_values.shape v = volumes.ravel() v = v / v.sum() n = 1000 @@ -283,8 +269,8 @@ def create_noise_animation( volume_masks = [] for i, volume in enumerate(v): x0, y0 = rng.integers(0, n, 2) - shape = (xx - x0)**2 + (yy - y0)**2 - mask = shape <= (volume / np.pi) * ((n / 2)**2) + shape = (xx - x0) ** 2 + (yy - y0) ** 2 + mask = shape <= (volume / np.pi) * ((n / 2) ** 2) values[mask] = vector_values[105, i] volume_masks.append(mask) diff --git a/cmtj/models/oersted.py b/cmtj/models/oersted.py index 9fe44ff..d42230d 100644 --- a/cmtj/models/oersted.py +++ b/cmtj/models/oersted.py @@ -12,7 +12,7 @@ @njit def distance(p1, p2): - return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2) + return math.sqrt((p1[0] - p2[0]) ** 2 + (p1[1] - p2[1]) ** 2) @njit @@ -33,16 +33,15 @@ class Block: I: float = 0 def __post_init__(self): - self.x = (self.ix + 1) * self.dx / 2. # compute center point - self.y = (self.iy + 1) * self.dy / 2. # compute center point - self.z = (self.iz + 1) * self.dz / 2. # compute center point + self.x = (self.ix + 1) * self.dx / 2.0 # compute center point + self.y = (self.iy + 1) * self.dy / 2.0 # compute center point + self.z = (self.iz + 1) * self.dz / 2.0 # compute center point self.area = self.dx * self.dy self.dl = self.dz return self.dz - def distance_sqr_from(self, other_block: 'Block'): - return (self.x - other_block.x)**2 + (self.y - other_block.y)**2 + ( - self.z - other_block.z)**2 + def distance_sqr_from(self, other_block: "Block"): + return (self.x - other_block.x) ** 2 + (self.y - other_block.y) ** 2 + (self.z - other_block.z) ** 2 def set_I(self, I): self.I = I @@ -53,31 +52,22 @@ def set_j(self, j): def add_H(self, H): self.Hlocal += H - def biot_savart(self, other_block: 'Block'): + def biot_savart(self, other_block: "Block"): r = distance((self.x, self.y), (other_block.x, other_block.y)) if r < 1e-15: - return 0. + return 0.0 H = other_block.I * other_block.area * self.dl / r**2 return H / (4 * math.pi) - def ampere_law(self, other_block: 'Block'): - return ampere_law(other_block.I, (self.x, self.y), - (other_block.x, other_block.y)) + def ampere_law(self, other_block: "Block"): + return ampere_law(other_block.I, (self.x, self.y), (other_block.x, other_block.y)) - def __eq__(self, __o: 'Block') -> bool: + def __eq__(self, __o: "Block") -> bool: return self.ix == __o.ix and self.iy == __o.iy and self.iz == __o.iz class Structure: - - def __init__(self, - maxX, - maxY, - maxZ, - dx, - dy, - dz, - method: Literal['ampere', 'biot-savart'] = 'ampere') -> None: + def __init__(self, maxX, maxY, maxZ, dx, dy, dz, method: Literal["ampere", "biot-savart"] = "ampere") -> None: self.maxX = maxX self.maxY = maxY self.maxZ = maxZ @@ -88,7 +78,7 @@ def __init__(self, self.Ysize = math.ceil(maxY / dy) self.Zsize = max(math.ceil(maxZ / dz), 1) print(f"Creating {self.Xsize}x{self.Ysize}x{self.Zsize} blocks") - if (method == 'ampere') and (self.Zsize > 1): + if (method == "ampere") and (self.Zsize > 1): raise ValueError("Wasting compute with z dim non-zero!") self.blocks = self.init_blocks() @@ -98,8 +88,7 @@ def __init__(self, def set_region_I_idx(self, I, min_y_indx, max_y_indx=-1): if max_y_indx == -1: max_y_indx = self.Ysize - I_mag = I / (self.Xsize * - (min(max_y_indx + 1, self.Ysize) - min_y_indx)) + I_mag = I / (self.Xsize * (min(max_y_indx + 1, self.Ysize) - min_y_indx)) # print(f"Setting I={I*1e3:.2f}mA in region {min_y_indx}:{max_y_indx}") # print(f"Unit I={I_mag*1e6:.2f}uA") for yindx in prange(min_y_indx, min(max_y_indx, self.Ysize)): @@ -116,13 +105,11 @@ def set_region_I(self, I, min_y, max_y=-1, label=None): self.set_region_I_idx(I, min_y_indx, max_y_indx) def init_blocks(self): - null_blocks = np.empty((self.Xsize, self.Ysize, self.Zsize), - dtype=Block) + null_blocks = np.empty((self.Xsize, self.Ysize, self.Zsize), dtype=Block) for ix in prange(self.Xsize): for iy in range(self.Ysize): for iz in range(self.Zsize): - null_blocks[ix, iy, iz] = Block(ix, iy, iz, self.dx, - self.dy, self.dz) + null_blocks[ix, iy, iz] = Block(ix, iy, iz, self.dx, self.dy, self.dz) return null_blocks def reset(self): @@ -142,10 +129,7 @@ def compute_blocks(self): def __ycoords2indx(self, min_coord_y, max_coord_y): min_y_indx = math.ceil(min_coord_y / self.dy) - if max_coord_y == -1: - max_y_indx = self.maxY - else: - max_y_indx = math.ceil(max_coord_y / self.dy) + max_y_indx = self.maxY if max_coord_y == -1 else math.ceil(max_coord_y / self.dy) return min_y_indx, max_y_indx def get_region_contributions_idx(self, min_y_indx, max_y_indx=-1): @@ -158,29 +142,21 @@ def get_region_contributions_idx(self, min_y_indx, max_y_indx=-1): H += self.blocks[x, yindx, zindx].Hlocal return H - def compute_region_contribution(self, source_min_y, source_max_y, - target_min_y, target_max_y): - source_min_y_indx, source_max_y_indx = self.__ycoords2indx( - source_min_y, source_max_y) - target_min_y_indx, target_max_y_indx = self.__ycoords2indx( - target_min_y, target_max_y) - - return self.compute_region_contribution_idx(source_min_y_indx, - source_max_y_indx, - target_min_y_indx, - target_max_y_indx) - - def compute_region_contribution_idx(self, source_min_y_indx, - source_max_y_indx, target_min_y_indx, - target_max_y_indx): - print( - f"Computing H from {source_min_y_indx}:{source_max_y_indx} to {target_min_y_indx}:{target_max_y_indx}" + def compute_region_contribution(self, source_min_y, source_max_y, target_min_y, target_max_y): + source_min_y_indx, source_max_y_indx = self.__ycoords2indx(source_min_y, source_max_y) + target_min_y_indx, target_max_y_indx = self.__ycoords2indx(target_min_y, target_max_y) + + return self.compute_region_contribution_idx( + source_min_y_indx, source_max_y_indx, target_min_y_indx, target_max_y_indx ) + + def compute_region_contribution_idx( + self, source_min_y_indx, source_max_y_indx, target_min_y_indx, target_max_y_indx + ): + print(f"Computing H from {source_min_y_indx}:{source_max_y_indx} to {target_min_y_indx}:{target_max_y_indx}") total_H = 0 - block_src = self.blocks[:, source_min_y_indx: - source_max_y_indx, :].flatten() - block_targ = self.blocks[:, target_min_y_indx: - target_max_y_indx, :].flatten() + block_src = self.blocks[:, source_min_y_indx:source_max_y_indx, :].flatten() + block_targ = self.blocks[:, target_min_y_indx:target_max_y_indx, :].flatten() print(f"Source blocks: {block_src.shape}") print(f"Target blocks: {block_targ.shape}") @@ -196,26 +172,17 @@ def show_field(self, log=False): field = np.zeros((self.Xsize, self.Ysize)) for block in self.blocks.flatten(): field[block.ix, block.iy] = block.Hlocal - with plt.style.context(['nature', 'science']): + with plt.style.context(["nature", "science"]): fig, ax = plt.subplots(dpi=300) - if log: - img = ax.pcolormesh(np.log(field).T, cmap='viridis') - else: - img = ax.pcolormesh(field.T, cmap='viridis') + img = ax.pcolormesh(np.log(field).T, cmap="viridis") if log else ax.pcolormesh(field.T, cmap="viridis") # add colorbar - ax.set_xticklabels([ - f"{x*1e9:.2f}" - for x in np.linspace(0, self.Xsize * self.dx, self.Xsize) - ]) - ax.set_yticklabels([ - f"{y*1e9:.2f}" - for y in np.linspace(0, self.Ysize * self.dy, self.Ysize) - ]) + ax.set_xticklabels([f"{x*1e9:.2f}" for x in np.linspace(0, self.Xsize * self.dx, self.Xsize)]) + ax.set_yticklabels([f"{y*1e9:.2f}" for y in np.linspace(0, self.Ysize * self.dy, self.Ysize)]) ax.set_xlabel("x (nm)") ax.set_ylabel("y (nm)") # add colorbar - for unq_border, label in zip(self.borders, self.labels): - ax.axhline(unq_border / self.dy, color='crimson') + for unq_border, _label in zip(self.borders, self.labels): + ax.axhline(unq_border / self.dy, color="crimson") fig.colorbar(img, ax=ax) return field diff --git a/cmtj/noise/__init__.pyi b/cmtj/noise/__init__.pyi index aa5c242..596e85f 100644 --- a/cmtj/noise/__init__.pyi +++ b/cmtj/noise/__init__.pyi @@ -3,9 +3,7 @@ import cmtj class BufferedAlphaNoise: """Create a buffer of alpha noise generator. Alpha can be in [0, 2].""" - def __init__( - self, bufferSize: int, alpha: float, std: float, scale: float - ) -> None: ... + def __init__(self, bufferSize: int, alpha: float, std: float, scale: float) -> None: ... def fillBuffer(self) -> None: """Fill the buffer with the noise. This method is called only once.""" ... diff --git a/cmtj/stack/__init__.pyi b/cmtj/stack/__init__.pyi index adb9324..11aecab 100644 --- a/cmtj/stack/__init__.pyi +++ b/cmtj/stack/__init__.pyi @@ -1,11 +1,11 @@ -from typing import Dict, List, overload +from typing import overload import cmtj class ParallelStack: def __init__( self, - junctionList: List[cmtj.Junction], + junctionList: list[cmtj.Junction], topId: str = "free", bottomId: str = "bottom", phaseOffset: float = 0, @@ -28,7 +28,7 @@ class ParallelStack: ... @overload - def getLog(self, junctionId: int) -> Dict[str, List[float]]: + def getLog(self, junctionId: int) -> dict[str, list[float]]: """ Get the logs of a specific junction -- integer id from the `junctionList`. @@ -37,15 +37,13 @@ class ParallelStack: ... @overload - def getLog(self) -> Dict[str, List[float]]: + def getLog(self) -> dict[str, list[float]]: """ Get the logs of the stack """ ... - def runSimulation( - self, totalTime: float, timeStep: float = ..., writeFrequency: float = ... - ) -> None: + def runSimulation(self, totalTime: float, timeStep: float = ..., writeFrequency: float = ...) -> None: """ Run the simulation of the stack. :param totalTime: total time of a simulation, give it in seconds. Typical length is in ~couple ns. @@ -78,9 +76,7 @@ class ParallelStack: """ ... - def setMagnetisation( - self, junctionId: int, layerId: str, mag: cmtj.CVector - ) -> None: + def setMagnetisation(self, junctionId: int, layerId: str, mag: cmtj.CVector) -> None: """ Set magnetisation on a specific layer in a specific junction. :param junctionId: the id of the junction (int) as passed in the init. @@ -104,7 +100,7 @@ class ParallelStack: class SeriesStack: def __init__( self, - junctionList: List[cmtj.Junction], + junctionList: list[cmtj.Junction], topId: str = "free", bottomId: str = "bottom", phaseOffset: float = 0, @@ -127,7 +123,7 @@ class SeriesStack: ... @overload - def getLog(self, junctionId: int) -> Dict[str, List[float]]: + def getLog(self, junctionId: int) -> dict[str, list[float]]: """ Get the logs of a specific junction -- integer id from the `junctionList`. @@ -136,15 +132,13 @@ class SeriesStack: ... @overload - def getLog(self) -> Dict[str, List[float]]: + def getLog(self) -> dict[str, list[float]]: """ Get the logs of the stack """ ... - def runSimulation( - self, totalTime: float, timeStep: float = ..., writeFrequency: float = ... - ) -> None: + def runSimulation(self, totalTime: float, timeStep: float = ..., writeFrequency: float = ...) -> None: """ Run the simulation of the stack. :param totalTime: total time of a simulation, give it in seconds. Typical length is in ~couple ns. @@ -172,7 +166,7 @@ class SeriesStack: ... @overload - def setCouplingStrength(self, coupling: List[float]) -> None: + def setCouplingStrength(self, coupling: list[float]) -> None: """ Coupling constant that represents the energy losses as the current passes through the stack. @@ -188,9 +182,7 @@ class SeriesStack: """ ... - def setMagnetisation( - self, junctionId: int, layerId: str, mag: cmtj.CVector - ) -> None: + def setMagnetisation(self, junctionId: int, layerId: str, mag: cmtj.CVector) -> None: """ Set magnetisation on a specific layer in a specific junction. :param junctionId: the id of the junction (int) as passed in the init. diff --git a/cmtj/utils/__init__.py b/cmtj/utils/__init__.py index 61e363f..bcf21a4 100644 --- a/cmtj/utils/__init__.py +++ b/cmtj/utils/__init__.py @@ -3,7 +3,13 @@ from .filters import Filters from .general import VectorObj, box_muller_random, perturb_position from .linear import FieldScan -from .resistance import * +from .resistance import ( + calculate_magnetoresistance, + calculate_resistance_parallel, + calculate_resistance_series, + compute_resistance, + compute_sd, +) # constants OetoAm = 79.57747 diff --git a/cmtj/utils/energy.py b/cmtj/utils/energy.py index 9dc7181..615d96d 100644 --- a/cmtj/utils/energy.py +++ b/cmtj/utils/energy.py @@ -1,13 +1,10 @@ -from typing import Dict, List - import numpy as np class EnergyCompute: """Energy density in [J/m^3] computing functions""" - def __init__(self, cell_surface: float, thickness: float, - log: Dict[str, List[float]]) -> None: + def __init__(self, cell_surface: float, thickness: float, log: dict[str, list[float]]) -> None: """Initialise energy computation class :param cell_surface: surface of the cell in [m^2] :param thickness: thickness of the cell in [m] @@ -17,19 +14,14 @@ def __init__(self, cell_surface: float, thickness: float, self.cell_volumne = self.cell_surface * thickness self.log = log - def compute_from_log(self) -> Dict[str, List[float]]: + def compute_from_log(self) -> dict[str, list[float]]: """ Computes a log of energies over time and returns it in the same form of the """ field_keys = list({k[:-1] for k in self.log if "_H" in k}) mag_k = (k.replace("_mx", "") for k in self.log if "_mx" in k) - mag_vectors = { - k: np.asarray([ - self.log[f"{k}_mx"], self.log[f"{k}_my"], self.log[f"{k}_mz"] - ]) - for k in mag_k - } + mag_vectors = {k: np.asarray([self.log[f"{k}_mx"], self.log[f"{k}_my"], self.log[f"{k}_mz"]]) for k in mag_k} energy_data = {} for field_key in field_keys: if "J_" in field_key: @@ -39,17 +31,18 @@ def compute_from_log(self) -> Dict[str, List[float]]: m_key = field_key.split("_")[0] # get m key m = mag_vectors[m_key] - field_series = np.asarray([ - self.log[f"{field_key}x"], - self.log[f"{field_key}y"], - self.log[f"{field_key}z"], - ]) + field_series = np.asarray( + [ + self.log[f"{field_key}x"], + self.log[f"{field_key}y"], + self.log[f"{field_key}z"], + ] + ) energy_data[f"energy_{field_key}"] = eng_fn(m, field_series) return energy_data - def calculate_energy_from_field(self, m: np.ndarray, - field_vector: np.ndarray) -> np.ndarray: + def calculate_energy_from_field(self, m: np.ndarray, field_vector: np.ndarray) -> np.ndarray: """ :param m: magnetisation :param field_vector: magnetic field vector (can be external, Oersted etc.) @@ -59,8 +52,7 @@ def calculate_energy_from_field(self, m: np.ndarray, """ return -np.sum(m * field_vector, axis=0) / self.cell_volumne - def calculate_energy_from_field_interfacial( - self, m: np.ndarray, field_vector: np.ndarray) -> np.ndarray: + def calculate_energy_from_field_interfacial(self, m: np.ndarray, field_vector: np.ndarray) -> np.ndarray: """ :param m: magnetisation :param field_vector: magnetic field vector (can be IEC etc.) diff --git a/cmtj/utils/filters.py b/cmtj/utils/filters.py index 355c01a..8213ebc 100644 --- a/cmtj/utils/filters.py +++ b/cmtj/utils/filters.py @@ -1,16 +1,10 @@ -from typing import Tuple - import numpy as np from scipy.signal import butter, lfilter class Filters: - @staticmethod - def butter_bandpass_filter(data: np.ndarray, - pass_freq: Tuple[float, float], - fs: float, - order: int = 5): + def butter_bandpass_filter(data: np.ndarray, pass_freq: tuple[float, float], fs: float, order: int = 5): """Basic bandpass (notch) butterworth filter. :param data: input data. :param pass_freq: the tuple of (low, high) band frequencies. @@ -29,20 +23,14 @@ def butter_bandpass_filter(data: np.ndarray, analog=False, ) except ValueError as e: - print(fs, pass_freq, nyq, 0.9 * pass_freq / nyq, - pass_freq / nyq) + print(fs, pass_freq, nyq, 0.9 * pass_freq / nyq, pass_freq / nyq) raise ValueError("Error in filtering") from e elif isinstance(pass_freq, tuple): - b, a = butter(order, [pass_freq[0], pass_freq[1]], - btype="bandpass", - analog=False) + b, a = butter(order, [pass_freq[0], pass_freq[1]], btype="bandpass", analog=False) return lfilter(b, a, data, zi=None) @staticmethod - def butter_lowpass_filter(data: np.ndarray, - cutoff: float, - fs: float, - order: int = 5): + def butter_lowpass_filter(data: np.ndarray, cutoff: float, fs: float, order: int = 5): """Low pass digital filter. :param data: data to be filtered. :param cutoff: cutoff frequency of the filter. diff --git a/cmtj/utils/general.py b/cmtj/utils/general.py index 3910b69..8bb4958 100644 --- a/cmtj/utils/general.py +++ b/cmtj/utils/general.py @@ -42,8 +42,7 @@ def __hash__(self) -> int: return hash(str(self)) def __eq__(self, __value: "VectorObj") -> bool: - return (self.theta == __value.theta and self.phi == __value.phi - and self.mag == __value.mag) + return self.theta == __value.theta and self.phi == __value.phi and self.mag == __value.mag def _componentwise_mul(self, other): coors = self.get_cartesian() diff --git a/cmtj/utils/linear.py b/cmtj/utils/linear.py index 48ae568..46931b7 100644 --- a/cmtj/utils/linear.py +++ b/cmtj/utils/linear.py @@ -1,14 +1,11 @@ -from typing import Tuple - import numpy as np from cmtj import CVector class FieldScan: - @staticmethod - def _trig_compute(theta, phi) -> Tuple: + def _trig_compute(theta, phi) -> tuple: """Compute trigonometric functions for theta and phi. :param theta: theta angle in [deg]. :param phi: phi angle in [deg]. @@ -34,7 +31,7 @@ def angle2vector(theta, phi, amplitude=1) -> CVector: ) @staticmethod - def vector2angle(x, y, z) -> Tuple: + def vector2angle(x, y, z) -> tuple: """Convert cartesian coordinates to spherical coordinates. :param x: x coordinate of the vector. :param y: y coordinate of the vector. @@ -48,7 +45,7 @@ def vector2angle(x, y, z) -> Tuple: return theta, phi, r @staticmethod - def cvector2angle(vector: CVector) -> Tuple: + def cvector2angle(vector: CVector) -> tuple: """ :param vector: cartesian vector. :returns (theta, phi, r) @@ -64,7 +61,7 @@ def amplitude_scan( theta: float, phi: float, back: bool = False, - ) -> Tuple[np.ndarray, np.ndarray]: + ) -> tuple[np.ndarray, np.ndarray]: """ Compute a linear magnitude sweep. Angles given in deg. :param start: start of the sweep @@ -82,14 +79,13 @@ def amplitude_scan( if back: forward = np.vstack((Hx, Hy, Hz)).T back = forward[::-1] - return np.concatenate((Hspan, Hspan[::-1]), - axis=0), np.concatenate((forward, back), - axis=0) + return np.concatenate((Hspan, Hspan[::-1]), axis=0), np.concatenate((forward, back), axis=0) return Hspan, np.vstack((Hx, Hy, Hz)).T @staticmethod - def theta_scan(start: float, stop: float, steps: int, amplitude: float, - phi: float) -> Tuple[np.ndarray, np.ndarray]: + def theta_scan( + start: float, stop: float, steps: int, amplitude: float, phi: float + ) -> tuple[np.ndarray, np.ndarray]: """ Compute a linear theta angle sweep. Angles given in deg. :param start: polar angle start of the sweep @@ -106,8 +102,9 @@ def theta_scan(start: float, stop: float, steps: int, amplitude: float, return theta_span, np.vstack((Hx, Hy, Hz)).T @staticmethod - def phi_scan(start: float, stop: float, steps: int, amplitude: float, - theta: float) -> Tuple[np.ndarray, np.ndarray]: + def phi_scan( + start: float, stop: float, steps: int, amplitude: float, theta: float + ) -> tuple[np.ndarray, np.ndarray]: """ Compute a linear phi angle sweep. Angles given in deg. :param start: azimuthal angle start of the sweep diff --git a/cmtj/utils/optimization.py b/cmtj/utils/optimization.py index 7c10553..7d49d4e 100644 --- a/cmtj/utils/optimization.py +++ b/cmtj/utils/optimization.py @@ -1,12 +1,12 @@ from concurrent.futures import ProcessPoolExecutor -from typing import Callable, Dict, List +from typing import Callable import numpy as np from tqdm import tqdm def coordinate_descent( - operating_point: Dict[str, float], + operating_point: dict[str, float], fn: Callable, best_mse: float = float("-inf"), granularity: int = 10, @@ -24,10 +24,9 @@ def coordinate_descent( for k, org_v in tqdm(operating_point.items(), desc="Coordinate descent"): new_params = operating_point.copy() for v in tqdm( - np.linspace((1 - percentage) * org_v, (1 + percentage) * org_v, - granularity), - desc=f"Optimising {k}", - leave=False, + np.linspace((1 - percentage) * org_v, (1 + percentage) * org_v, granularity), + desc=f"Optimising {k}", + leave=False, ): new_params[k] = v mse = fn(**new_params) @@ -40,7 +39,7 @@ def coordinate_descent( def multiprocess_simulate( fn: Callable, error_fn: Callable, - suggestions: List[dict], + suggestions: list[dict], target: np.ndarray, fixed_parameters: dict, ): @@ -50,7 +49,8 @@ def multiprocess_simulate( fn, **fixed_parameters, **suggestion, - ) for suggestion in suggestions + ) + for suggestion in suggestions ] errors = np.zeros(len(suggestions)) for j, future in enumerate(futures): @@ -82,9 +82,7 @@ def hebo_optimization_loop( from hebo.design_space.design_space import DesignSpace from hebo.optimizers.hebo import HEBO except ImportError as e: - raise ImportError( - "HEBO is not installed. Please install it with `pip install HEBO`" - ) from e + raise ImportError("HEBO is not installed. Please install it with `pip install HEBO`") from e space = DesignSpace().parse(cfg) opt = HEBO(space) best_mse = float("inf") diff --git a/cmtj/utils/parallel.py b/cmtj/utils/parallel.py index f195776..10e38a4 100644 --- a/cmtj/utils/parallel.py +++ b/cmtj/utils/parallel.py @@ -1,5 +1,5 @@ from itertools import product -from typing import Callable, List +from typing import Callable import numpy as np from multiprocess import Pool @@ -8,10 +8,7 @@ __all__ = ["distribute"] -def distribute(simulation_fn: Callable, - spaces: List[List[float]], - n_cores: int = None, - shuffle: bool = False): +def distribute(simulation_fn: Callable, spaces: list[list[float]], n_cores: int = None, shuffle: bool = False): """ Distribute a function over a list of parameters in parallel. :param simulation_fn: function to be distributed @@ -25,10 +22,7 @@ def distribute(simulation_fn: Callable, spaces = [np.asarray(space) for space in spaces] def _get_index(values): - return [ - np.argwhere(space == values[i]).ravel()[0] - for i, space in enumerate(spaces) - ] + return [np.argwhere(space == values[i]).ravel()[0] for i, space in enumerate(spaces)] iterables = list(product(*spaces)) indexes = [_get_index(val) for val in iterables] @@ -44,8 +38,7 @@ def func_wrapper(iterable): return iterable, simulation_fn(*iterable) with Pool(processes=n_cores) as pool: - for result in tqdm(pool.imap_unordered(func_wrapper, iterables), - total=len(iterables)): + for result in tqdm(pool.imap_unordered(func_wrapper, iterables), total=len(iterables)): iterable, output = result indx = indexes[iterables.index(iterable)] yield indx, output diff --git a/cmtj/utils/plotting.py b/cmtj/utils/plotting.py index ec421a1..2656ab4 100644 --- a/cmtj/utils/plotting.py +++ b/cmtj/utils/plotting.py @@ -11,7 +11,7 @@ def get_sphere(): pi = np.pi cos = np.cos sin = np.sin - phi, theta = np.mgrid[0.0:pi:100j, 0.0:2.0 * pi:100j] + phi, theta = np.mgrid[0.0:pi:100j, 0.0 : 2.0 * pi : 100j] xs = r * sin(phi) * cos(theta) ys = r * sin(phi) * sin(theta) zs = r * cos(phi) @@ -52,14 +52,7 @@ def plot_trajectory_sphere(x, y, z, color="blue", alpha=1, ax=None): else: ax.plot3D(m[0], m[1], m[2], color=color, alpha=alpha) ax.set_axis_off() - ax.plot_surface(xs, - ys, - zs, - rstride=2, - cstride=2, - color="azure", - alpha=0.1, - linewidth=0.1) + ax.plot_surface(xs, ys, zs, rstride=2, cstride=2, color="azure", alpha=0.1, linewidth=0.1) ax.scatter([0], [0], [1], color="crimson", alpha=1.0) @@ -99,14 +92,7 @@ def plot_coloured_trajectory(x, y, z, colormap="plasma", ax=None): ax.add_collection(Line3DCollection(segs, colors=colors, alpha=1)) else: ax.set_axis_off() - ax.plot_surface(xs, - ys, - zs, - rstride=2, - cstride=2, - color="azure", - alpha=0.1, - linewidth=0.1) + ax.plot_surface(xs, ys, zs, rstride=2, cstride=2, color="azure", alpha=0.1, linewidth=0.1) ax.add_collection(Line3DCollection(segs, colors=colors, alpha=1)) @@ -130,11 +116,7 @@ def unpack_ndim_map(map, axes): return ax_lists, value_list -def create_coordinates_plot(axes, - ax_names, - result_map, - sample=0, - alpha_black=0.01): +def create_coordinates_plot(axes, ax_names, result_map, sample=0, alpha_black=0.01): """Create parallel coordinates plot for multidimensional parameter space. Modified from: https://stackoverflow.com/questions/8230638/parallel-coordinates-plot-in-matplotlib @@ -153,9 +135,7 @@ def create_coordinates_plot(axes, fig, host = plt.subplots(dpi=400) ax_lists, value_list = unpack_ndim_map(result_map, axes) - norm = matplotlib.colors.Normalize(vmin=min(value_list), - vmax=max(value_list), - clip=True) + norm = matplotlib.colors.Normalize(vmin=min(value_list), vmax=max(value_list), clip=True) mapper = cm.ScalarMappable(norm=norm, cmap=cm.magma) # organize the data @@ -185,8 +165,7 @@ def create_coordinates_plot(axes, if ax != host: ax.spines["left"].set_visible(False) ax.yaxis.set_ticks_position("right") - ax.spines["right"].set_position( - ("axes", i / (ys.shape[1] - 1))) + ax.spines["right"].set_position(("axes", i / (ys.shape[1] - 1))) host.set_xlim(0, ys.shape[1] - 1) host.set_xticks(range(ys.shape[1])) @@ -204,16 +183,12 @@ def create_coordinates_plot(axes, # y-coordinate: repeat every point three times, except the first and last only twice verts = list( zip( - list( - np.linspace(0, - len(ys) - 1, - len(ys) * 3 - 2, - endpoint=True)), + list(np.linspace(0, len(ys) - 1, len(ys) * 3 - 2, endpoint=True)), np.repeat(zs[j, :], 3)[1:-1], - )) + ) + ) # for x,y in verts: host.plot(x, y, 'go') # to show the control points of the beziers - codes = [Path.MOVETO - ] + [Path.CURVE4 for _ in range(len(verts) - 1)] + codes = [Path.MOVETO] + [Path.CURVE4 for _ in range(len(verts) - 1)] path = Path(verts, codes) alpha = alpha_black if ys[j, -1] == 0 else 0.8 patch = patches.PathPatch( @@ -227,8 +202,7 @@ def create_coordinates_plot(axes, def rotation_matrix(theta): - return np.array([[np.cos(theta), -np.sin(theta)], - [np.sin(theta), np.cos(theta)]]) + return np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]]) def create_stack( @@ -271,15 +245,8 @@ def create_stack( colors = colors[::-1] angles = angles[::-1] labels = labels[::-1] - for i, (height, angle, color, - label) in enumerate(zip(heights, angles, colors, labels)): - ax.add_patch( - patches.Rectangle((offset_x, offset_y), - width, - height, - fill=True, - color=color, - zorder=10)) + for _i, (height, angle, color, label) in enumerate(zip(heights, angles, colors, labels)): + ax.add_patch(patches.Rectangle((offset_x, offset_y), width, height, fill=True, color=color, zorder=10)) ax.text( offset_x - labelpad_left, offset_y + height / 2, @@ -303,7 +270,8 @@ def create_stack( lw=lw_arrow, color="black", zorder=10, - )) + ) + ) offset_y += height ax.set_ylim([first_offset - max(heights) / 2, offset_y + max(heights) / 2]) ax.set_xlim([offset_x - width / 2, offset_x + width + width / 2]) diff --git a/cmtj/utils/procedures.py b/cmtj/utils/procedures.py index 093f502..6ec9286 100644 --- a/cmtj/utils/procedures.py +++ b/cmtj/utils/procedures.py @@ -1,7 +1,7 @@ import math from collections import defaultdict from dataclasses import dataclass -from typing import Any, Callable, Dict, List, Tuple +from typing import Any, Callable import numpy as np from scipy.fft import fft, fftfreq @@ -25,13 +25,12 @@ class ResistanceParameters: l: float = 0 # length -def compute_spectrum_strip(input_m: np.ndarray, int_step: float, - max_frequency: float): +def compute_spectrum_strip(input_m: np.ndarray, int_step: float, max_frequency: float): """Compute the spectrum of a given magnetization trajectory.""" yf = np.abs(fft(input_m)) freqs = fftfreq(len(yf), int_step) - freqs = freqs[:len(freqs) // 2] - yf = yf[:len(yf) // 2] + freqs = freqs[: len(freqs) // 2] + yf = yf[: len(yf) // 2] findx = np.argwhere(freqs <= max_frequency) freqs = freqs[findx] @@ -44,7 +43,7 @@ def PIMM_procedure( junction: "Junction", Hvecs: np.ndarray, int_step: float, - resistance_params: List[ResistanceParameters], + resistance_params: list[ResistanceParameters], Hoe_direction: Axis = Axis.zaxis, Hoe_excitation: float = 50, Hoe_duration: int = 3, @@ -57,7 +56,7 @@ def PIMM_procedure( full_output: bool = False, disable_tqdm: bool = False, static_only: bool = False, -) -> Tuple[np.ndarray, np.ndarray, Dict[str, Any]]: +) -> tuple[np.ndarray, np.ndarray, dict[str, Any]]: """Procedure for computing Pulse Induced Microwave Magnetometry. It computes both PIMM and Resistance (for instance AHE loops). Set `static_only` to True to only compute the static resistance. @@ -94,22 +93,19 @@ def PIMM_procedure( oedriver = AxialDriver( NullDriver(), NullDriver(), - ScalarDriver.getStepDriver(0, Hoe_excitation, 0, - int_step * Hoe_duration), + ScalarDriver.getStepDriver(0, Hoe_excitation, 0, int_step * Hoe_duration), ) elif Hoe_direction == Axis.yaxis: extraction_m_component = "y" oedriver = AxialDriver( NullDriver(), - ScalarDriver.getStepDriver(0, Hoe_excitation, 0, - int_step * Hoe_duration), + ScalarDriver.getStepDriver(0, Hoe_excitation, 0, int_step * Hoe_duration), NullDriver(), ) else: extraction_m_component = "x" oedriver = AxialDriver( - ScalarDriver.getStepDriver(0, Hoe_excitation, 0, - int_step * Hoe_duration), + ScalarDriver.getStepDriver(0, Hoe_excitation, 0, int_step * Hoe_duration), NullDriver(), NullDriver(), ) @@ -117,12 +113,9 @@ def PIMM_procedure( # get layer strings layer_ids = junction.getLayerIds() if len(layer_ids) != len(resistance_params): - raise ValueError( - "The number of layers in the junction must match the number of resistance parameters!" - ) + raise ValueError("The number of layers in the junction must match the number of resistance parameters!") output = defaultdict(list) - normalising_factor = np.sum( - [layer.thickness * layer.Ms for layer in junction.layers]) + normalising_factor = np.sum([layer.thickness * layer.Ms for layer in junction.layers]) freqs = None # in case of static_only for H in tqdm(Hvecs, desc="Computing PIMM", disable=disable_tqdm): junction.clearLog() @@ -148,16 +141,22 @@ def PIMM_procedure( junction.runSimulation(simulation_duration, int_step, int_step) log = junction.getLog() indx = np.argwhere(np.asarray(log["time"]) >= wait_time).ravel() - m_traj = np.asarray([ - np.asarray([ - log[f"{layer.id}_mx"], - log[f"{layer.id}_my"], - log[f"{layer.id}_mz"], - ]) * layer.thickness * layer.Ms / normalising_factor - for layer in junction.layers - ]) - m = m_traj[:, :, - -take_last_n:] # all layers, all x, y, z, last 100 steps + m_traj = np.asarray( + [ + np.asarray( + [ + log[f"{layer.id}_mx"], + log[f"{layer.id}_my"], + log[f"{layer.id}_mz"], + ] + ) + * layer.thickness + * layer.Ms + / normalising_factor + for layer in junction.layers + ] + ) + m = m_traj[:, :, -take_last_n:] # all layers, all x, y, z, last 100 steps Rx, Ry = resistance_fn( [r.Rxx0 for r in resistance_params], [r.Rxy0 for r in resistance_params], @@ -169,14 +168,17 @@ def PIMM_procedure( w=[r.w for r in resistance_params], ) if not static_only: - mixed = np.asarray([ - np.asarray(log[f"{layer.id}_m{extraction_m_component}"])[indx] - * layer.thickness * layer.Ms / normalising_factor - for layer in junction.layers - ]) + mixed = np.asarray( + [ + np.asarray(log[f"{layer.id}_m{extraction_m_component}"])[indx] + * layer.thickness + * layer.Ms + / normalising_factor + for layer in junction.layers + ] + ) mixed_sum = mixed.sum(axis=0) - yf, freqs = compute_spectrum_strip(mixed_sum, int_step, - max_frequency) + yf, freqs = compute_spectrum_strip(mixed_sum, int_step, max_frequency) spectrum.append(yf) @@ -188,8 +190,7 @@ def PIMM_procedure( if full_output and not static_only: output["m_traj"].append(m_traj) for li, layer_id in enumerate(layer_ids): - y, _ = compute_spectrum_strip(mixed[li], int_step, - max_frequency) + y, _ = compute_spectrum_strip(mixed[li], int_step, max_frequency) output[layer_id].append(y) spectrum = np.squeeze(np.asarray(spectrum)) if full_output: @@ -203,7 +204,7 @@ def VSD_procedure( Hvecs: np.ndarray, frequencies: np.ndarray, int_step: float, - resistance_params: List[ResistanceParameters] = [], + resistance_params: list[ResistanceParameters] = None, Hoe_direction: Axis = Axis.yaxis, Hoe_excitation: float = 50, simulation_duration: float = 30e-9, @@ -228,17 +229,15 @@ def VSD_procedure( :param Rtype: type of resistance to be used. (Rx Ry or Rz) :param disable_tqdm: if True, disable tqdm progress bar. """ + if resistance_params is None: + resistance_params = [] layer_ids = junction.getLayerIds() if Rtype == "Rz" and len(layer_ids) > 2: - raise ValueError( - "Rz can only be used for 2 layer junctions. Use Rx or Ry instead.") + raise ValueError("Rz can only be used for 2 layer junctions. Use Rx or Ry instead.") elif len(resistance_params) != len(layer_ids): - raise ValueError( - "The number of layers in the junction must match the number of resistance parameters!" - ) + raise ValueError("The number of layers in the junction must match the number of resistance parameters!") - def simulate_VSD(H: np.ndarray, frequency: float, - resistance_params: ResistanceParameters): + def simulate_VSD(H: np.ndarray, frequency: float, resistance_params: ResistanceParameters): if Hoe_direction == Axis.zaxis: oedriver = AxialDriver( NullDriver(), @@ -280,16 +279,19 @@ def simulate_VSD(H: np.ndarray, frequency: float, junction.setLayerMagnetisation(layer_id, new_mag) junction.runSimulation(simulation_duration, int_step, int_step) log = junction.getLog() - m_traj = np.asarray([[ - log[f"{layer_ids[i]}_mx"], - log[f"{layer_ids[i]}_my"], - log[f"{layer_ids[i]}_mz"], - ] for i in range(len(layer_ids))]) + m_traj = np.asarray( + [ + [ + log[f"{layer_ids[i]}_mx"], + log[f"{layer_ids[i]}_my"], + log[f"{layer_ids[i]}_mz"], + ] + for i in range(len(layer_ids)) + ] + ) if Rtype == "Rz": if len(layer_ids) > 2: - raise ValueError( - "Rz can only be used for 2 layer junctions. One layer can be fictisious." - ) + raise ValueError("Rz can only be used for 2 layer junctions. One layer can be fictisious.") elif len(layer_ids) == 2: R = log[f"R_{layer_ids[0]}_{layer_ids[1]}"] elif len(layer_ids) == 1: @@ -299,7 +301,8 @@ def simulate_VSD(H: np.ndarray, frequency: float, "Resistance definition ambiguous!" "If you want to use Rz, you must provide" "a single resistance parameter set or set Rp Rap" - " at junction creation.") + " at junction creation." + ) else: Rx, Ry = resistance_fn( [r.Rxx0 for r in resistance_params], @@ -322,8 +325,7 @@ def simulate_VSD(H: np.ndarray, frequency: float, return vmix spectrum = np.zeros((len(Hvecs), len(frequencies))) - for hindx, H in enumerate( - tqdm(Hvecs, "Computing VSD", disable=disable_tqdm)): + for hindx, H in enumerate(tqdm(Hvecs, "Computing VSD", disable=disable_tqdm)): for findx, f in enumerate(frequencies): spectrum[hindx, findx] = simulate_VSD(H, f, resistance_params) return spectrum diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index a131ab6..3b71f24 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -1,12 +1,11 @@ -from typing import List, Union +from typing import Union, list import numpy as np from .filters import Filters -def compute_sd(dynamic_r: np.ndarray, dynamic_i: np.ndarray, - integration_step: float) -> np.ndarray: +def compute_sd(dynamic_r: np.ndarray, dynamic_i: np.ndarray, integration_step: float) -> np.ndarray: """Computes the SD voltage. :param dynamic_r: magnetoresistance from log :param dynamic_i: excitation current @@ -19,14 +18,14 @@ def compute_sd(dynamic_r: np.ndarray, dynamic_i: np.ndarray, def compute_resistance( - Rx0: List[float], - Ry0: List[float], - AMR: List[float], - AHE: List[float], - SMR: List[float], - m: Union[List[float], np.ndarray], - l: List[float], - w: List[float], + Rx0: list[float], + Ry0: list[float], + AMR: list[float], + AHE: list[float], + SMR: list[float], + m: Union[list[float], np.ndarray], + l: list[float], + w: list[float], ): """Computes the resistance of the system. If you want to compute the resistance for an entire time series, pass m as a 3D array @@ -37,8 +36,8 @@ def compute_resistance( if not isinstance(m, np.ndarray): m = np.asarray(m) if m.ndim == 2: - SxAll = np.zeros((number_of_layers, )) - SyAll = np.zeros((number_of_layers, )) + SxAll = np.zeros((number_of_layers,)) + SyAll = np.zeros((number_of_layers,)) elif m.ndim == 3: SxAll = np.zeros((number_of_layers, m.shape[2])) @@ -46,9 +45,8 @@ def compute_resistance( for i in range(number_of_layers): w_l = w[i] / l[i] - SxAll[i] = Rx0[i] + (AMR[i] * m[i, 0]**2 + SMR[i] * m[i, 1]**2) - SyAll[i] = (Ry0[i] + 0.5 * AHE[i] * m[i, 2] + (w_l) * - (SMR[i] - AMR[i]) * m[i, 0] * m[i, 1]) + SxAll[i] = Rx0[i] + (AMR[i] * m[i, 0] ** 2 + SMR[i] * m[i, 1] ** 2) + SyAll[i] = Ry0[i] + 0.5 * AHE[i] * m[i, 2] + (w_l) * (SMR[i] - AMR[i]) * m[i, 0] * m[i, 1] return SxAll, SyAll @@ -70,21 +68,19 @@ def calculate_magnetoresistance(Rp: float, Rap: float, m: np.ndarray): if not isinstance(m, np.ndarray): m = np.asarray(m) if m.shape[0] != 2: - raise ValueError( - "The magnetoresistance can only be computed for 2 layers" - f". Current shape {m.shape}") + raise ValueError("The magnetoresistance can only be computed for 2 layers" f". Current shape {m.shape}") return Rp + 0.5 * (Rap - Rp) * np.sum(m[0] * m[1], axis=0) def calculate_resistance_parallel( - Rx0: List[float], - Ry0: List[float], - AMR: List[float], - AHE: List[float], - SMR: List[float], - m: List[float], - l: List[float], - w: List[float], + Rx0: list[float], + Ry0: list[float], + AMR: list[float], + AHE: list[float], + SMR: list[float], + m: list[float], + l: list[float], + w: list[float], ): """Calculates the resistance of the system in parallel. If you want to compute the resistance for an entire time series, pass m as a 3D array. @@ -108,14 +104,14 @@ def calculate_resistance_parallel( def calculate_resistance_series( - Rx0: List[float], - Ry0: List[float], - AMR: List[float], - AHE: List[float], - SMR: List[float], - m: List[float], - l: List[float], - w: List[float], + Rx0: list[float], + Ry0: list[float], + AMR: list[float], + AHE: list[float], + SMR: list[float], + m: list[float], + l: list[float], + w: list[float], ): """Calculates the resistance of the system in series. If you want to compute the resistance for an entire time series, pass m as a 3D array. diff --git a/cmtj/utils/solvers.py b/cmtj/utils/solvers.py index 4c7402a..ed5bce5 100644 --- a/cmtj/utils/solvers.py +++ b/cmtj/utils/solvers.py @@ -1,17 +1,11 @@ import numpy as np -from scipy.optimize import fsolve, root +from scipy.optimize import root class RootFinder: """Adopted from: https://stackoverflow.com/a/65185377/3588442""" - def __init__(self, - start, - stop, - step=0.01, - root_dtype="float32", - xtol=1e-9): - + def __init__(self, start, stop, step=0.01, root_dtype="float32", xtol=1e-9): self.start = start self.stop = stop self.step = step @@ -19,7 +13,6 @@ def __init__(self, self.roots = np.array([], dtype=root_dtype) def add_to_roots(self, x): - if (x < self.start) or (x > self.stop): return # outside range if any(abs(self.roots - x) < self.xtol): From 01d43d8c483b39e77f574238fcf7258b8825c6fe Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 8 Sep 2024 22:02:13 +0200 Subject: [PATCH 23/44] fixing typing --- cmtj/utils/resistance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index 3b71f24..64a1451 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -1,4 +1,4 @@ -from typing import Union, list +from typing import Union import numpy as np From 0b793c12a022953df97b0c324c87668826f3d5a8 Mon Sep 17 00:00:00 2001 From: Jakub Date: Mon, 9 Sep 2024 00:00:20 +0200 Subject: [PATCH 24/44] Update CHANGELOG.md Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75da526..130e5e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,7 @@ # 1.5.5 - Extended the `Stack` models allowing for non-symmetric coupling between devices. -- `Stack` current drivers can now be of any type are adequately scaled. + `Stack` current drivers can now be of any type and are adequately scaled. - Custom definition of the `ScalarDriver` is now possible and documented. - Fixed a bug in the `Stack` class which inverted the connection order of in-series connections. - Exposed IDMI interaction to Layer and Junction classes. From 6ef4989739be4b33f80d85c660c0344cf4e0b6c5 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 9 Sep 2024 09:59:44 +0200 Subject: [PATCH 25/44] reworking IDMI macro formula --- .pre-commit-config.yaml | 1 + cmtj/utils/__init__.py | 2 ++ core/cvector.hpp | 11 +++++++++++ core/junction.hpp | 34 +++++++++++++++++++++------------- tests/CMakeLists.txt | 4 +++- 5 files changed, 38 insertions(+), 14 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 873661d..1bd61c9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,6 +41,7 @@ repos: # Run the linter. - id: ruff files: ^cmtj + args: ["--fix"] types_or: [python, pyi] # Run the formatter. - id: ruff-format diff --git a/cmtj/utils/__init__.py b/cmtj/utils/__init__.py index bcf21a4..9ebd4f8 100644 --- a/cmtj/utils/__init__.py +++ b/cmtj/utils/__init__.py @@ -7,6 +7,7 @@ calculate_magnetoresistance, calculate_resistance_parallel, calculate_resistance_series, + compute_gmr, compute_resistance, compute_sd, ) @@ -37,4 +38,5 @@ "VectorObj", "box_muller_random", "perturb_position", + "compute_gmr", ] diff --git a/core/cvector.hpp b/core/cvector.hpp index 4a4bc90..301a207 100644 --- a/core/cvector.hpp +++ b/core/cvector.hpp @@ -143,6 +143,17 @@ template class CVector { } CVector operator/(T val) { + if (val == 0) { + throw std::runtime_error("Failed to divide vector by zero!"); + } + CVector res(x / val, y / val, z / val); + return res; + }; + + CVector operator/(T val) const { + if (val == 0) { + throw std::runtime_error("Failed to divide vector by zero!"); + } CVector res(x / val, y / val, z / val); return res; }; diff --git a/core/junction.hpp b/core/junction.hpp index 3d23d57..ec046ee 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -191,7 +191,8 @@ template class Layer { T cellVolume = 0.0, cellSurface = 0.0; CVector H_log, Hoe_log, Hconst, mag, anis, referenceLayer; - CVector Hext, Hdipole, Hdemag, Hoe, HAnis, Hthermal, Hfluctuation, Hdmi; + CVector Hext, Hdipole, Hdemag, Hoe, HAnis, Hthermal, Hfluctuation, Hdmi, + Hidmi; CVector Hfl_v, Hdl_v; @@ -520,15 +521,16 @@ template class Layer { this->Hdemag = calculate_tensor_interaction(stepMag, this->demagTensor, this->Ms); this->HIEC = calculateIEC(time, stepMag, bottom, top); - this->Hdmi = calculateIDMI(time, stepMag, bottom, top); + this->Hidmi = calculateIDMI(time, stepMag, bottom, top); this->HAnis = calculateAnisotropy(stepMag, time); this->Hdmi = calculateHdmiField(time); - const CVector Heff = this->Hext // external - + this->HAnis // anistotropy - + this->HIEC // IEC - + this->Hoe // Oersted field - + this->Hdmi + - Hfluctuation + const CVector Heff = this->Hext // external + + this->HAnis // anistotropy + + this->HIEC // IEC + + this->Hidmi // IDMI + + this->Hoe // Oersted field + + this->Hdmi // regular DMI + + Hfluctuation // fluctuations // demag -- negative contribution - this->Hdemag // dipole -- negative contribution @@ -580,12 +582,18 @@ template class Layer { calculateIEC_(this->Jtop_log, this->J2top_log, stepMag, top); } - CVector calculateIDMI_(const CVector &Dvalue, const CVector &stepMag, + CVector calculateIDMI_(const CVector &Dvector, + const CVector &stepMag, const CVector &coupledMag) { - const CVector Dunit(1 ? Dvalue.x : 0, 1 ? Dvalue.y : 0, - 1 ? Dvalue.z : 0); - return Dunit * c_dot(Dvalue, c_cross(CVector(1., 1., 1.), stepMag)) / - (this->Ms * this->thickness); + // D * [(dm1/dm1x x m2) + (m1 x dm2/dm2x)] + // dm1/dm1x x m2 = (0, -mz, my) + // dm1/dm1y x m2 = (mz, 0, -mx) + // dm1/dm1z x m2 = (-my, mx, 0) + const CVector dm1crossm2( + c_dot(Dvector, CVector(0, -coupledMag.z, coupledMag.y)), + c_dot(Dvector, CVector(coupledMag.z, 0, -coupledMag.x)), + c_dot(Dvector, CVector(-coupledMag.y, coupledMag.x, 0))); + return dm1crossm2 / (this->Ms * this->thickness); } CVector calculateIDMI(T time, const CVector &stepMag, diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index b85f582..06f38ea 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -48,4 +48,6 @@ endmacro() # target_link_libraries(test_reservoir Eigen3::Eigen) # package_add_test(test_noise test_noise.cpp) -package_add_test(test_llgb test_llgb.cpp) +# package_add_test(test_llgb test_llgb.cpp) +# package_add_test(test_interaction test_interaction.cpp) +package_add_test(test_junction test_junction.cpp) From 672a42689750498aa4f9f2ad0656e3d280de65b1 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Tue, 10 Sep 2024 21:27:56 +0200 Subject: [PATCH 26/44] adding ruff spec --- ruff.toml | 90 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) create mode 100644 ruff.toml diff --git a/ruff.toml b/ruff.toml new file mode 100644 index 0000000..ca84b11 --- /dev/null +++ b/ruff.toml @@ -0,0 +1,90 @@ +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +# Same as Black. +line-length = 120 + +# Assume Python 3.9 +target-version = "py39" + +[lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = [ + # pycodestyle + "E", + # Pyflakes + "F", + # pyupgrade + "UP", + # flake8-bugbear + "B", + # flake8-simplify + "SIM", + # isort + "I", +] + +ignore = ["E741"] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] + +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" From 37d6b4b847870030853ab7d72ff74e910f02d9da Mon Sep 17 00:00:00 2001 From: Jakub Date: Tue, 10 Sep 2024 21:38:19 +0200 Subject: [PATCH 27/44] Update core/junction.hpp Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- core/junction.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/junction.hpp b/core/junction.hpp index ec046ee..ae3fe08 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -1520,8 +1520,8 @@ template class Junction { T Ry_acc = 0.0; for (unsigned int i = 0; i < this->layers.size(); i++) { - const T Rx = Rx0[i] + AMR_X[i] * pow(this->layers[i].mag.x, 2) + - SMR_X[i] * pow(this->layers[i].mag.y, 2); + const T Rx = Rx0[i] + AMR_X[i] * (this->layers[i].mag.x * this->layers[i].mag.x) + + SMR_X[i] * (this->layers[i].mag.y * this->layers[i].mag.y); const T Ry = Ry0[i] + 0.5 * AHE[i] * this->layers[i].mag.z + (AMR_Y[i] + SMR_Y[i]) * this->layers[i].mag.x * this->layers[i].mag.y; From c30b3a80be5d0a2419ebcd79b7207b4e67e7dcd6 Mon Sep 17 00:00:00 2001 From: Jakub Date: Tue, 10 Sep 2024 21:38:37 +0200 Subject: [PATCH 28/44] Update core/junction.hpp Co-authored-by: sourcery-ai[bot] <58596630+sourcery-ai[bot]@users.noreply.github.com> --- core/junction.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/core/junction.hpp b/core/junction.hpp index ae3fe08..0b61252 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -1629,8 +1629,8 @@ template class Junction { throw std::runtime_error( "The time step cannot be larger than write frequency!"); } - const unsigned int totalIterations = (int)(totalTime / timeStep); - const unsigned int writeEvery = (int)(writeFrequency / timeStep); + const unsigned int totalIterations = static_cast(totalTime / timeStep); + const unsigned int writeEvery = static_cast(writeFrequency / timeStep); std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); // pick a solver based on drivers From 39a959edfaef509bdbdc9cd86a3c7d95ca034b7b Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Fri, 11 Oct 2024 12:35:54 +0200 Subject: [PATCH 29/44] extra grad term --- cmtj/models/general_sb.py | 60 +++++++++++++++++++++++++++++++++++++-- cmtj/utils/parallel.py | 7 ++++- cmtj/utils/resistance.py | 6 ++-- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index 56bbca7..30481ba 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -157,6 +157,8 @@ class LayerSB: :param Ks: surface anisotropy (out-of plane, or perpendicular) value [J/m^3]. :param Ms: magnetisation saturation value in [A/m]. :param Hdmi: DMI field in the layer. Defaults to [0, 0, 0]. + :param Ndemag: demagnetisation tensor diagonal. Defaults to [0, 0, 1] (thin film). + for sphere, use [1/3, 1/3, 1/3]. """ _id: int @@ -165,6 +167,7 @@ class LayerSB: Ks: float Ms: float Hdmi: VectorObj = None # TODO: change when we support py3.10 upwards (field(kw_only=True, default=None)) + Ndemag: VectorObj = VectorObj.from_cartesian(0, 0, 1) def __post_init__(self): if self._id > 9: @@ -173,6 +176,8 @@ def __post_init__(self): self.Hdmi = sym.Matrix([0, 0, 0]) else: self.Hdmi = sym.ImmutableMatrix(self.Hdmi.get_cartesian()) + self.Ndemag = sym.ImmutableMatrix(self.Ndemag.get_cartesian()) + self.theta = sym.Symbol(r"\theta_" + str(self._id)) self.phi = sym.Symbol(r"\phi_" + str(self._id)) self.m = sym.ImmutableMatrix( @@ -230,9 +235,14 @@ def no_iec_symbolic_layer_energy(self, H: sym.ImmutableMatrix): field_energy = -mu0 * self.Ms * m.dot(H) hdmi_energy = -mu0 * self.Ms * m.dot(self.Hdmi) - surface_anistropy = (-self.Ks + (1.0 / 2.0) * mu0 * self.Ms**2) * (m[-1] ** 2) + # old surface anisotropy only took into account the thin slab demag + # surface_anistropy = (-self.Ks + (1.0 / 2.0) * mu0 * self.Ms**2) * (m[-1] ** 2) + surface_anistropy = -self.Ks * (m[-1] ** 2) volume_anisotropy = -self.Kv.mag * (m.dot(alpha) ** 2) - return field_energy + surface_anistropy + volume_anisotropy + hdmi_energy + m_2 = sym.ImmutableMatrix([m_i**2 for m_i in m]) + demagnetisation_energy = 0.5 * mu0 * (self.Ms**2) * m_2.dot(self.Ndemag) + + return field_energy + surface_anistropy + volume_anisotropy + hdmi_energy + demagnetisation_energy def sb_correction(self): omega = sym.Symbol(r"\omega") @@ -281,7 +291,7 @@ def rhs_llg( inv_sin = 1.0 / (sym.sin(self.theta) + EPS) dUdtheta = sym.diff(U, self.theta) dUdphi = sym.diff(U, self.phi) - + # TODO: check if dtheta, dphi terms with inv_sin have correct isgn dtheta = -inv_sin * dUdphi - self.alpha * dUdtheta dphi = inv_sin * dUdtheta - self.alpha * dUdphi * (inv_sin) ** 2 return prefac * sym.ImmutableMatrix([dtheta, dphi]) / self.Ms @@ -323,6 +333,8 @@ def __post_init__(self): if self.ilD is None: # this is optional, if not provided, we assume zero DMI self.ilD = [VectorObj(0, 0, 0) for _ in range(len(self.layers) - 1)] + elif isinstance(self.layers[0], LayerDynamic): + raise ValueError("interlayer DMI coupling is not yet supported for LayerDynamic.") if len(self.layers) != len(self.ilD) + 1: raise ValueError("Number of layers must be 1 more than ilD.") if not all(isinstance(d, VectorObj) for d in self.ilD): @@ -540,6 +552,48 @@ def adam_gradient_descent( # return np.asarray(current_position), np.asarray(history) return np.asarray(current_position) + def amsgrad_gradient_descent( + self, + init_position: np.ndarray, + max_steps: int, + tol: float = 1e-8, + learning_rate: float = 1e-4, + first_momentum_decay: float = 0.9, + second_momentum_decay: float = 0.999, + perturbation: float = 1e-6, + ): + """ + A naive implementation of AMSGrad gradient descent. + See: On the Convergence of Adam and Beyond, Reddi et al., 2018 + :param max_steps: maximum number of gradient steps. + :param tol: tolerance of the solution. + :param learning_rate: the learning rate (descent speed). + :param first_momentum_decay: constant for the first momentum. + :param second_momentum_decay: constant for the second momentum. + """ + step = 0 + gradfn = self.get_gradient_expr() + current_position = init_position + if perturbation: + current_position = perturb_position(init_position, perturbation) + m = np.zeros_like(current_position) + v = np.zeros_like(current_position) + v_hat = np.zeros_like(current_position) + eps = 1e-12 + while True: + step += 1 + grad = np.asarray(gradfn(*current_position)) + m = first_momentum_decay * m + (1.0 - first_momentum_decay) * grad + v = second_momentum_decay * v + (1.0 - second_momentum_decay) * grad**2 + v_hat = np.maximum(v_hat, v) + new_position = current_position - learning_rate * m / (np.sqrt(v_hat) + eps) + if step > max_steps: + break + if fast_norm(current_position - new_position) < tol: + break + current_position = new_position + return np.asarray(current_position) + def single_layer_resonance(self, layer_indx: int, eq_position: np.ndarray): """We can compute the equilibrium position of a single layer directly. :param layer_indx: the index of the layer to compute the equilibrium diff --git a/cmtj/utils/parallel.py b/cmtj/utils/parallel.py index 10e38a4..ffeeeae 100644 --- a/cmtj/utils/parallel.py +++ b/cmtj/utils/parallel.py @@ -8,7 +8,12 @@ __all__ = ["distribute"] -def distribute(simulation_fn: Callable, spaces: list[list[float]], n_cores: int = None, shuffle: bool = False): +def distribute( + simulation_fn: Callable, + spaces: list[list[float]], + n_cores: int = None, + shuffle: bool = False, +): """ Distribute a function over a list of parameters in parallel. :param simulation_fn: function to be distributed diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index 64a1451..a116ea0 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -56,7 +56,7 @@ def compute_gmr(Rp: float, Rap: float, m1: np.ndarray, m2: np.ndarray): :param Rap: antiparallel resistance :param m1: magnetisation of layer 1 :param m2: magnetisation of layer 2""" - return Rp + 0.5 * (Rap - Rp) * np.sum(m1 * m2, axis=0) + return Rp + 0.5 * (Rap - Rp) * (1 - np.sum(m1 * m2, axis=0)) def calculate_magnetoresistance(Rp: float, Rap: float, m: np.ndarray): @@ -98,8 +98,8 @@ def calculate_resistance_parallel( :param w: width of the layers """ SxAll, SyAll = compute_resistance(Rx0, Ry0, AMR, AHE, SMR, m, l, w) - Rx = 1 / np.sum(1.0 / SxAll, axis=0) - Ry = 1 / np.sum(1.0 / SyAll, axis=0) + Rx = 1.0 / np.sum(1.0 / SxAll, axis=0) + Ry = 1.0 / np.sum(1.0 / SyAll, axis=0) return Rx, Ry From 7e1323d2cfa264e1f0943f8e04ba6d1e349a9404 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 13 Oct 2024 23:32:30 +0200 Subject: [PATCH 30/44] adding dynamic layer VSD computation based on voltage and oersted field --- cmtj/models/general_sb.py | 145 ++++++++++++++++++++++++++++++++++++-- 1 file changed, 139 insertions(+), 6 deletions(-) diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index 30481ba..40ac086 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -264,6 +264,16 @@ def __eq__(self, __value: "LayerSB") -> bool: @dataclass class LayerDynamic(LayerSB): alpha: float = 0.01 + torque_par: float = 0 + torque_perp: float = 0 + + @staticmethod + def get_hoe_ex_symbol(): + return sym.Symbol(r"H_{oe}") + + @staticmethod + def get_Vp_symbol(): + return sym.Symbol(r"V_{p}") def rhs_llg( self, @@ -274,6 +284,7 @@ def rhs_llg( J2bottom: float, top_layer: "LayerSB", down_layer: "LayerSB", + osc: bool = False, ): """Returns the symbolic expression for the RHS of the spherical LLG equation. Coupling contribution comes only from the bottom layer (top-down crawl)""" @@ -287,14 +298,35 @@ def rhs_llg( down_layer=down_layer, ) # sum all components - prefac = gamma_rad / (1.0 + self.alpha) ** 2 + prefac = gamma_rad / (1.0 + self.alpha**2) inv_sin = 1.0 / (sym.sin(self.theta) + EPS) dUdtheta = sym.diff(U, self.theta) dUdphi = sym.diff(U, self.phi) - # TODO: check if dtheta, dphi terms with inv_sin have correct isgn - dtheta = -inv_sin * dUdphi - self.alpha * dUdtheta - dphi = inv_sin * dUdtheta - self.alpha * dUdphi * (inv_sin) ** 2 - return prefac * sym.ImmutableMatrix([dtheta, dphi]) / self.Ms + + # Hoe can be used only for excitation, unlike Vp which controls torquances + Hoe = LayerDynamic.get_hoe_ex_symbol() if osc else 0 + # TODO: check if dtheta, dphi terms with inv_sin have correct sign (other terms have correct sign) + # pubs are contradictory for this sign + dtheta = -inv_sin * dUdphi - self.alpha * dUdtheta + self.Ms * Hoe + dphi = ( + inv_sin * dUdtheta + - self.alpha * dUdphi * (inv_sin) ** 2 + + self.alpha * self.Ms * Hoe / (sym.sin(self.theta) + EPS) + ) + return prefac * (sym.Matrix([dtheta, dphi]) + self.torque(osc=osc)) / self.Ms + + def torque(self, osc: bool = True): + # cannot be 0 because you may want to use Hoe + torques + Vp = LayerDynamic.get_Vp_symbol() if osc else 1 + torque_ex_par = self.torque_par * Vp + torque_ex_perp = self.torque_perp * Vp + + return sym.ImmutableMatrix( + [ + sym.sin(self.theta) * (-torque_ex_par - self.alpha * torque_ex_perp), + -torque_ex_perp + self.alpha * torque_ex_par, + ] + ) def __eq__(self, __value: "LayerDynamic") -> bool: return super().__eq__(__value) and self.alpha == __value.alpha @@ -385,7 +417,11 @@ def compose_llg_jacobian(self, H: VectorObj): jac = sym.ImmutableMatrix(fns).jacobian(symbols) return jac, symbols - def create_energy(self, H: Union[VectorObj, sym.ImmutableMatrix] = None, volumetric: bool = False): + def create_energy( + self, + H: Union[VectorObj, sym.ImmutableMatrix, None] = None, + volumetric: bool = False, + ): """Creates the symbolic energy expression. Due to problematic nature of coupling, there is an issue of @@ -653,6 +689,9 @@ def solve( """ if self.H is None: raise ValueError("H must be set before solving the system numerically.") + assert len(init_position) == 2 * len( + self.layers + ), f"Incorrect initial position size. Given: {len(init_position)}, expected: {2 * len(self.layers)}" eq = self.adam_gradient_descent( init_position=init_position, max_steps=max_steps, @@ -807,3 +846,97 @@ def analytical_field_scan( roots = np.asarray(roots, dtype=np.float32) * gamma / 1e9 yield eq, roots, Hvalue current_position = eq + + @lru_cache(maxsize=1000) # noqa: B019 + def _freq_independent_expr( + self, + H: VectorObj, + Vdc_ex_variable: sym.Expr, + Vdc_ex_value: float, + zero_pos: list[float], + ): + """Avoid recomputing the same expression for the same system given fixed + parameters. + """ + n = len(self.layers) + A_matrix = sym.zeros(2 * n, 2 * n) + V_matrix = sym.zeros(2 * n, 1) + subs = { + Vdc_ex_variable: Vdc_ex_value, + } + dummy_vp = LayerDynamic.get_Vp_symbol() + dummy_hoe = LayerDynamic.get_hoe_ex_symbol() + # subs for dummy variables if one of the excitations is present + if dummy_vp not in subs: + subs[dummy_vp] = 0 + if dummy_hoe not in subs: + subs[dummy_hoe] = 0 + + H = sym.ImmutableMatrix(H.get_cartesian()) + omega = sym.Symbol(r"\omega") + for i, layer in enumerate(self.layers): + theta, phi = layer.get_coord_sym() + subs[theta] = zero_pos[2 * i] + subs[phi] = zero_pos[2 * i + 1] + + top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references(i, self.J1) + _, _, J2top, J2bottom = self.get_layer_references(i, self.J2) + rhs = layer.rhs_llg(H, Jtop, Jbottom, J2top, J2bottom, top_layer, bottom_layer, osc=True) + V_matrix[2 * i] = sym.diff(rhs[0], Vdc_ex_variable) + V_matrix[2 * i + 1] = sym.diff(rhs[1], Vdc_ex_variable) + alpha_factor = 1 + layer.alpha**2 + for j, layer_j in enumerate(self.layers): + theta_, phi_ = layer_j.get_coord_sym() + A_matrix[2 * i, 2 * j] = sym.diff(rhs[0], theta_) * alpha_factor + A_matrix[2 * i + 1, 2 * j + 1] = sym.diff(rhs[1], phi_) * alpha_factor + A_matrix[2 * i, 2 * j + 1] = sym.diff(rhs[0], phi_) + A_matrix[2 * i + 1, 2 * j] = sym.diff(rhs[1], theta_) + if i == j: + A_matrix[2 * i, 2 * j] += alpha_factor * omega * sym.I + A_matrix[2 * i + 1, 2 * j + 1] += alpha_factor * omega * sym.I + + A_matrix = A_matrix.subs(subs) + V_matrix = V_matrix.subs(subs) + A_inv = A_matrix.inv(method="LU") + return A_inv, V_matrix + + def linearised_N_spin_diode( + self, + H: Union[VectorObj, np.ndarray], + frequency: float, + Vdc_ex_variable: sym.Expr, + Vdc_ex_value: float, + zero_pos: np.ndarray, + phase_shift: float = 0, + ): + """ + Linearised N-spin diode. Use `LayerDynamic.get_Vp_symbol()` + or `LayerDynamic.get_hoe_ex_symbol()` for Vdc_ex_variable. + :param H: the external field. + :param frequency: the frequency of the external field. + :param Vdc_ex_variable: the variable to use for the excitation (Vp or Hoe). + :param Vdc_ex_value: the value of the excitation. + :param zero_pos: the equilibrium position of the system. + :param phase_shift: the phase shift of the external field. + :return: the N-spin diode angle variations. + """ + # allow only if the layers are LayerDynamic + if not isinstance(self.layers[0], LayerDynamic): + raise ValueError("Linearised N-spin diode only works with LayerDynamic.") + A_inv, V_matrix = self._freq_independent_expr( + VectorObj.from_cartesian(*H) if isinstance(H, np.ndarray) else H, + Vdc_ex_variable, + Vdc_ex_value, + tuple(zero_pos.tolist()), # for hashing & caching + ) + extra_subs = {sym.Symbol(r"\omega"): 2 * sym.pi * frequency} + A_inv = A_inv.subs(extra_subs) + V_matrix = V_matrix.subs(extra_subs) + fstep = A_inv * V_matrix * sym.exp(sym.I * phase_shift) + return np.real(np.complex64(fstep.evalf())) + + def __hash__(self): + return hash(str(self)) + + def __eq__(self, other): + return str(self) == str(other) From 157eb5fa6cf8a999cd7d516801c9ddc51c284858 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 21 Oct 2024 11:21:49 +0200 Subject: [PATCH 31/44] updates to the SD computation with efficient caching and inverse comp step --- cmtj/models/general_sb.py | 99 +++++++++++++++++++++++++++++---------- cmtj/utils/resistance.py | 33 +++++++++++++ 2 files changed, 107 insertions(+), 25 deletions(-) diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index 40ac086..0e501ef 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -847,22 +847,41 @@ def analytical_field_scan( yield eq, roots, Hvalue current_position = eq - @lru_cache(maxsize=1000) # noqa: B019 - def _freq_independent_expr( + def _independent_linearised_jacobian_expr( self, - H: VectorObj, Vdc_ex_variable: sym.Expr, Vdc_ex_value: float, zero_pos: list[float], + H: Union[VectorObj, None] = None, + frequency: float = None, ): """Avoid recomputing the same expression for the same system given fixed - parameters. + parameters. Computes a linearised Jacobian matrix and its inverse. + :param Vdc_ex_variable: the variable to use for the excitation (Vp or Hoe). + :param Vdc_ex_value: the value of the excitation. + :param zero_pos: the equilibrium position of the system. + :param H: the external field. If None, the H symbol is used. + :param frequency: the frequency of the external field. If None, the omega symbol is used. + :return: the inverse of the Jacobian matrix and the V matrix. """ n = len(self.layers) - A_matrix = sym.zeros(2 * n, 2 * n) - V_matrix = sym.zeros(2 * n, 1) + H = ( + sym.ImmutableMatrix(H.get_cartesian()) + if H is not None + else sym.ImmutableMatrix([sym.Symbol(r"H_{x}"), sym.Symbol(r"H_{y}"), sym.Symbol(r"H_{z}")]) + ) + A_matrix, V_matrix = self._compute_A_and_V_matrices( + n=n, + Vdc_ex_variable=Vdc_ex_variable, + H=H, + frequency=frequency, + ) subs = { Vdc_ex_variable: Vdc_ex_value, + sym.Symbol(r"\omega"): 2 * sym.pi * frequency, + sym.Symbol(r"H_{x}"): H[0], + sym.Symbol(r"H_{y}"): H[1], + sym.Symbol(r"H_{z}"): H[2], } dummy_vp = LayerDynamic.get_Vp_symbol() dummy_hoe = LayerDynamic.get_hoe_ex_symbol() @@ -872,13 +891,28 @@ def _freq_independent_expr( if dummy_hoe not in subs: subs[dummy_hoe] = 0 - H = sym.ImmutableMatrix(H.get_cartesian()) - omega = sym.Symbol(r"\omega") for i, layer in enumerate(self.layers): theta, phi = layer.get_coord_sym() subs[theta] = zero_pos[2 * i] subs[phi] = zero_pos[2 * i + 1] + A_matrix = sym.ImmutableMatrix(A_matrix) + A_matrix = A_matrix.subs(subs) + V_matrix = V_matrix.subs(subs) + return A_matrix, V_matrix + def _compute_numerical_inverse(self, A_matrix): + # Use NumPy for faster matrix inversion + A_np = np.asarray(A_matrix, dtype=np.complex128) + A_inv_np = np.linalg.inv(A_np) + return sym.Matrix(A_inv_np) + + @lru_cache(maxsize=1000) # noqa: B019 + def _compute_A_and_V_matrices(self, n, Vdc_ex_variable, H, frequency): + A_matrix = sym.zeros(2 * n, 2 * n) + V_matrix = sym.zeros(2 * n, 1) + + omega = sym.Symbol(r"\omega") if frequency is None else 2 * sym.pi * frequency + for i, layer in enumerate(self.layers): top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references(i, self.J1) _, _, J2top, J2bottom = self.get_layer_references(i, self.J2) rhs = layer.rhs_llg(H, Jtop, Jbottom, J2top, J2bottom, top_layer, bottom_layer, osc=True) @@ -889,16 +923,12 @@ def _freq_independent_expr( theta_, phi_ = layer_j.get_coord_sym() A_matrix[2 * i, 2 * j] = sym.diff(rhs[0], theta_) * alpha_factor A_matrix[2 * i + 1, 2 * j + 1] = sym.diff(rhs[1], phi_) * alpha_factor - A_matrix[2 * i, 2 * j + 1] = sym.diff(rhs[0], phi_) - A_matrix[2 * i + 1, 2 * j] = sym.diff(rhs[1], theta_) + A_matrix[2 * i, 2 * j + 1] = sym.diff(rhs[0], phi_) * alpha_factor + A_matrix[2 * i + 1, 2 * j] = sym.diff(rhs[1], theta_) * alpha_factor if i == j: A_matrix[2 * i, 2 * j] += alpha_factor * omega * sym.I A_matrix[2 * i + 1, 2 * j + 1] += alpha_factor * omega * sym.I - - A_matrix = A_matrix.subs(subs) - V_matrix = V_matrix.subs(subs) - A_inv = A_matrix.inv(method="LU") - return A_inv, V_matrix + return A_matrix, V_matrix def linearised_N_spin_diode( self, @@ -908,9 +938,9 @@ def linearised_N_spin_diode( Vdc_ex_value: float, zero_pos: np.ndarray, phase_shift: float = 0, + cache_var: str = "H", ): - """ - Linearised N-spin diode. Use `LayerDynamic.get_Vp_symbol()` + """Linearised N-spin diode. Use `LayerDynamic.get_Vp_symbol()` or `LayerDynamic.get_hoe_ex_symbol()` for Vdc_ex_variable. :param H: the external field. :param frequency: the frequency of the external field. @@ -921,17 +951,36 @@ def linearised_N_spin_diode( :return: the N-spin diode angle variations. """ # allow only if the layers are LayerDynamic - if not isinstance(self.layers[0], LayerDynamic): + if not all(isinstance(layer, LayerDynamic) for layer in self.layers): raise ValueError("Linearised N-spin diode only works with LayerDynamic.") - A_inv, V_matrix = self._freq_independent_expr( - VectorObj.from_cartesian(*H) if isinstance(H, np.ndarray) else H, - Vdc_ex_variable, - Vdc_ex_value, - tuple(zero_pos.tolist()), # for hashing & caching + H = VectorObj.from_cartesian(*H) if isinstance(H, np.ndarray) else H + + extra_args = {} + extra_subs = {} + if cache_var == "H": + extra_args["frequency"] = frequency + Hcart = H.get_cartesian() + extra_subs = { + sym.Symbol(r"H_{x}"): Hcart[0], + sym.Symbol(r"H_{y}"): Hcart[1], + sym.Symbol(r"H_{z}"): Hcart[2], + } + elif cache_var == "f": + extra_args["H"] = H + extra_subs = { + sym.Symbol(r"\omega"): 2 * sym.pi * frequency, + } + + A_matrix, V_matrix = self._independent_linearised_jacobian_expr( + Vdc_ex_variable=Vdc_ex_variable, + Vdc_ex_value=Vdc_ex_value, + zero_pos=tuple(zero_pos.tolist()), # for hashing & caching + **extra_args, ) - extra_subs = {sym.Symbol(r"\omega"): 2 * sym.pi * frequency} - A_inv = A_inv.subs(extra_subs) + A_matrix = A_matrix.subs(extra_subs) V_matrix = V_matrix.subs(extra_subs) + + A_inv = self._compute_numerical_inverse(A_matrix) fstep = A_inv * V_matrix * sym.exp(sym.I * phase_shift) return np.real(np.complex64(fstep.evalf())) diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index a116ea0..1cc9299 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -132,3 +132,36 @@ def calculate_resistance_series( Rx = np.sum(SxAll, axis=0) Ry = np.sum(SyAll, axis=0) return Rx, Ry + + +def angular_calculate_resistance_gmr( + Rp: float, + Rap: float, + theta_1: np.ndarray, + phi_1: np.ndarray, + theta_2: np.ndarray, + phi_2: np.ndarray, +): + """Computes the GMR using parallel and antiparallel resistance. + :param Rp: parallel resistance + :param Rap: antiparallel resistance + :param theta_1: angle of layer 1 + :param phi_1: angle of layer 1 + :param theta_2: angle of layer 2 + :param phi_2: angle of layer 2 + """ + m1 = np.array( + [ + np.cos(theta_1) * np.cos(phi_1), + np.cos(theta_1) * np.sin(phi_1), + np.sin(theta_1), + ] + ) + m2 = np.array( + [ + np.cos(theta_2) * np.cos(phi_2), + np.cos(theta_2) * np.sin(phi_2), + np.sin(theta_2), + ] + ) + return compute_gmr(Rp, Rap, m1, m2) From 7e0090e887367bfa8adcfcba52cb2d4bfd645c0b Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Wed, 23 Oct 2024 10:46:06 +0200 Subject: [PATCH 32/44] unifying energy and notation for dynamic layer models --- cmtj/models/general_sb.py | 147 +++++++++++++---------------------- docs/tutorials/SBModel.ipynb | 26 +++---- 2 files changed, 67 insertions(+), 106 deletions(-) diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index 0e501ef..a6b042b 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -11,8 +11,8 @@ from numba import njit from tqdm import tqdm -from ..utils import VectorObj, gamma, gamma_rad, mu0, perturb_position -from ..utils.solvers import RootFinder +from cmtj.utils import VectorObj, gamma, gamma_rad, mu0, perturb_position +from cmtj.utils.solvers import RootFinder EPS = np.finfo("float64").resolution @@ -197,7 +197,7 @@ def get_m_sym(self): return self.m @lru_cache(3) # noqa: B019 - def symbolic_layer_energy( + def total_symbolic_layer_energy( self, H: sym.ImmutableMatrix, J1top: float, @@ -211,22 +211,22 @@ def symbolic_layer_energy( Coupling contribution comes only from the bottom layer (top-down crawl)""" m = self.get_m_sym() - eng_non_interaction = self.no_iec_symbolic_layer_energy(H) + eng_non_interaction = self.no_interaction_symbolic_energy(H) * self.thickness top_iec_energy = 0 bottom_iec_energy = 0 if top_layer is not None: other_m = top_layer.get_m_sym() - top_iec_energy = -(J1top / self.thickness) * m.dot(other_m) - (J2top / self.thickness) * m.dot(other_m) ** 2 + mdot = m.dot(other_m) + top_iec_energy = -J1top * mdot - J2top * mdot**2 if down_layer is not None: other_m = down_layer.get_m_sym() - bottom_iec_energy = ( - -(J1bottom / self.thickness) * m.dot(other_m) - (J2bottom / self.thickness) * m.dot(other_m) ** 2 - ) + mdot = m.dot(other_m) + bottom_iec_energy = -J1bottom * mdot - J2bottom * mdot**2 return eng_non_interaction + top_iec_energy + bottom_iec_energy - def no_iec_symbolic_layer_energy(self, H: sym.ImmutableMatrix): + def no_interaction_symbolic_energy(self, H: sym.ImmutableMatrix): """Returns the symbolic expression for the energy of the layer. Coupling contribution comes only from the bottom layer (top-down crawl)""" m = self.get_m_sym() @@ -275,28 +275,17 @@ def get_hoe_ex_symbol(): def get_Vp_symbol(): return sym.Symbol(r"V_{p}") - def rhs_llg( + def rhs_spherical_llg( self, - H: sym.Matrix, - J1top: float, - J1bottom: float, - J2top: float, - J2bottom: float, - top_layer: "LayerSB", - down_layer: "LayerSB", + U: sym.Matrix, osc: bool = False, ): """Returns the symbolic expression for the RHS of the spherical LLG equation. - Coupling contribution comes only from the bottom layer (top-down crawl)""" - U = self.symbolic_layer_energy( - H, - J1top=J1top, - J1bottom=J1bottom, - J2top=J2top, - J2bottom=J2bottom, - top_layer=top_layer, - down_layer=down_layer, - ) + Coupling contribution comes only from the bottom layer (top-down crawl) + + :param H: external field + :param U: energy expression of the layer + """ # sum all components prefac = gamma_rad / (1.0 + self.alpha**2) inv_sin = 1.0 / (sym.sin(self.theta) + EPS) @@ -386,7 +375,7 @@ def __post_init__(self): if id_sets != ideal_set: raise ValueError("Layer ids must be 0, 1, 2, ... and unique." "Ids must start from 0.") - def get_layer_references(self, layer_indx, interaction_constant): + def get_layer_references(self, layer_indx: int, interaction_constant: list[float]): """Returns the references to the layers above and below the layer with index layer_indx.""" if len(self.layers) == 1: @@ -409,14 +398,14 @@ def compose_llg_jacobian(self, H: VectorObj): H = sym.ImmutableMatrix(H.get_cartesian()) symbols, fns = [], [] - for i, layer in enumerate(self.layers): + U = self.create_energy(H=H, volumetric=False) + for layer in self.layers: symbols.extend((layer.theta, layer.phi)) - top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references(i, self.J1) - _, _, J2top, J2bottom = self.get_layer_references(i, self.J2) - fns.append(layer.rhs_llg(H, Jtop, Jbottom, J2top, J2bottom, top_layer, bottom_layer)) + fns.append(layer.rhs_spherical_llg(U, osc=False)) jac = sym.ImmutableMatrix(fns).jacobian(symbols) return jac, symbols + @lru_cache(3) def create_energy( self, H: Union[VectorObj, sym.ImmutableMatrix, None] = None, @@ -434,63 +423,39 @@ def create_energy( if H is None: h = self.H.get_cartesian() H = sym.ImmutableMatrix(h) - energy = 0 - if volumetric: - # volumetric energy -- DO NOT USE IN GENERAL - for i, layer in enumerate(self.layers): - top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references(i, self.J1) - _, _, J2top, J2bottom = self.get_layer_references(i, self.J2) - ratio_top, ratio_bottom = 0, 0 - if top_layer: - ratio_top = top_layer.thickness / (top_layer.thickness + layer.thickness) - if bottom_layer: - ratio_bottom = bottom_layer.thickness / (layer.thickness + bottom_layer.thickness) - energy += layer.symbolic_layer_energy( - H, - Jtop * ratio_top, - Jbottom * ratio_bottom, - J2top, - J2bottom, - top_layer, - bottom_layer, + energy = sum(layer.no_interaction_symbolic_energy(H) * layer.thickness for layer in self.layers) + + for i in range(len(self.layers) - 1): + l1m = self.layers[i].get_m_sym() + l2m = self.layers[i + 1].get_m_sym() + + # IEC + ldot = l1m.dot(l2m) + energy -= self.J1[i] * ldot + energy -= self.J2[i] * (ldot) ** 2 + + # IDMI, sign is the same J1 + lcross = l1m.cross(l2m) + energy -= self.ilD[i].dot(lcross) + + # dipole fields + if self.dipoleMatrix is not None: + mat = self.dipoleMatrix[i] + # is positive, just like demag + energy += ( + (mu0 / 2.0) + * l1m.dot(mat * l2m) + * self.layers[i].Ms + * self.layers[i + 1].Ms + * self.layers[i].thickness + ) + energy += ( + (mu0 / 2.0) + * l2m.dot(mat * l1m) + * self.layers[i].Ms + * self.layers[i + 1].Ms + * self.layers[i + 1].thickness ) - else: - # surface energy for correct angular gradient - for layer in self.layers: - # to avoid dividing J by thickness - energy += layer.no_iec_symbolic_layer_energy(H) * layer.thickness - - for i in range(len(self.layers) - 1): - l1m = self.layers[i].get_m_sym() - l2m = self.layers[i + 1].get_m_sym() - - # IEC - ldot = l1m.dot(l2m) - energy -= self.J1[i] * ldot - energy -= self.J2[i] * (ldot) ** 2 - - # IDMI, sign is the same J1 - lcross = l1m.cross(l2m) - energy -= self.ilD[i].dot(lcross) - - # dipole fields - if self.dipoleMatrix is not None: - mat = self.dipoleMatrix[i] - # is positive, just like demag - energy += ( - (mu0 / 2.0) - * l1m.dot(mat * l2m) - * self.layers[i].Ms - * self.layers[i + 1].Ms - * self.layers[i].thickness - ) - energy += ( - (mu0 / 2.0) - * l2m.dot(mat * l1m) - * self.layers[i].Ms - * self.layers[i + 1].Ms - * self.layers[i + 1].thickness - ) return energy def create_energy_hessian(self, equilibrium_position: list[float]): @@ -910,12 +875,10 @@ def _compute_numerical_inverse(self, A_matrix): def _compute_A_and_V_matrices(self, n, Vdc_ex_variable, H, frequency): A_matrix = sym.zeros(2 * n, 2 * n) V_matrix = sym.zeros(2 * n, 1) - + U = self.create_energy(H=H, volumetric=False) omega = sym.Symbol(r"\omega") if frequency is None else 2 * sym.pi * frequency for i, layer in enumerate(self.layers): - top_layer, bottom_layer, Jtop, Jbottom = self.get_layer_references(i, self.J1) - _, _, J2top, J2bottom = self.get_layer_references(i, self.J2) - rhs = layer.rhs_llg(H, Jtop, Jbottom, J2top, J2bottom, top_layer, bottom_layer, osc=True) + rhs = layer.rhs_spherical_llg(U / layer.thickness, osc=True) V_matrix[2 * i] = sym.diff(rhs[0], Vdc_ex_variable) V_matrix[2 * i + 1] = sym.diff(rhs[1], Vdc_ex_variable) alpha_factor = 1 + layer.alpha**2 diff --git a/docs/tutorials/SBModel.ipynb b/docs/tutorials/SBModel.ipynb index 90e6d9e..6cb6ef1 100644 --- a/docs/tutorials/SBModel.ipynb +++ b/docs/tutorials/SBModel.ipynb @@ -15,14 +15,14 @@ }, { "cell_type": "code", - "execution_count": 1, + "execution_count": 45, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 100/100 [00:53<00:00, 1.85it/s]\n" + "100%|██████████| 50/50 [00:27<00:00, 1.80it/s]\n" ] } ], @@ -58,7 +58,7 @@ "# we indicate the \"guess\" of the initial position\n", "# it's generally good to align it with the field, but it's not necessary\n", "current_position = [np.deg2rad(89), np.deg2rad(0.1), np.deg2rad(180), np.deg2rad(0.1)]\n", - "Hspace = np.linspace(-400e3, 400e3, 100)\n", + "Hspace = np.linspace(-400e3, 400e3, 50)\n", "result_dictionary = defaultdict(list)\n", "# we perform a sweep over the field magnitude\n", "for Hmag in tqdm(Hspace):\n", @@ -88,19 +88,17 @@ }, { "cell_type": "code", - "execution_count": 2, + "execution_count": 43, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "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", "text/plain": [ "
" ] }, - "metadata": { - "needs_background": "light" - }, + "metadata": {}, "output_type": "display_data" } ], @@ -167,14 +165,14 @@ }, { "cell_type": "code", - "execution_count": 3, + "execution_count": 141, "metadata": {}, "outputs": [ { "name": "stderr", "output_type": "stream", "text": [ - "100%|██████████| 100/100 [00:25<00:00, 3.92it/s]\n" + "100%|██████████| 50/50 [00:14<00:00, 3.36it/s]\n" ] } ], @@ -213,7 +211,7 @@ "# we indicate the \"guess\" of the initial position\n", "# it's generally good to align it with the field, but it's not necessary\n", "current_position = [np.deg2rad(89), np.deg2rad(0.1), np.deg2rad(180), np.deg2rad(0.1)]\n", - "Hspace = np.linspace(-400e3, 400e3, 100)\n", + "Hspace = np.linspace(-400e3, 400e3, 50)\n", "result_dictionary_dynamic = defaultdict(list)\n", "# we perform a sweep over the field magnitude\n", "for Hmag in tqdm(Hspace):\n", @@ -251,12 +249,12 @@ }, { "cell_type": "code", - "execution_count": 4, + "execution_count": 142, "metadata": {}, "outputs": [ { "data": { - "image/png": "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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+UAAAH3CAYAAADdQv4zAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAB7CAAAewgFu0HU+AACZo0lEQVR4nOzdfXQb530n+i8oUA5JyQIpOyFai7VAOzWVNooAKvd0t8duJcDpOrX6IkAUG1bctBEnbnfblRITVruupHivZTCNtffuuY1Bpb66KqM3IE5fnKQJIKd2sye7FQGbTWI6qTlyKCVg64iCZYmKRYq4f4xnABDvwACDmfl+zuEhOc9g8GCAmcFvnpefJZVKpUBEREREREREDdeidQWIiIiIiIiIzIpBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERacSqdQX07ic/+Qm+/vWv4+6770ZbW5vW1SEioiZy48YNvPHGG/jIRz6CO+64Q+vqUJl4bSciomLUvr4zKK/R17/+dQwNDWldDSIiamITExP42Mc+pnU1qEy8thMRUTnUur4zKK/R3XffDUB6Q/r6+qrezvT0NIaGhmrejt63W89t62279dy23rZbz22zzvXfbj233ezblbcjXytIH8x6ba/ntlnn+m+3ntvW23bruW29bbee2zZzndW+vjMor1G+bm12ux12u72q7fX19cHpdNZaLd1vt57b1tt267ltvW23nttmneu/3Xpuu5m2m0gkkEgkspaxC7S+yO+XWp+rZvp8ar1t1rn+263ntvW23XpuW2/bree2zVxnta7vDMpVktnN7eDBgzh06JB2lSEiIs0Eg0EcPnxY62oQERGRTjAoV0lmF4hqWsntdjsOHjxYdQu7UbZbT3rcF3qsc73ocV/osc71ord9Uct2BUHAjh07AKS7t5E5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sXC/NxceX8bGoJwayufzKXfq8olEIjldZfx+P5LJJObn55FMJhEIBLK63qxcV87HKIoitm7dmnWSovrgfm8ePMaISAs89xgT93tz4PFlfAzKiYiIiIiIiDTCMeVEREREREREGmFQTkRERERERKQRBuVEREREREREGmFQTkRERERERKQRBuVEREREREREGmFQTkRERERERKQRBuVEREREREREGmFQTkRERERERKQRBuVEREREREREGmFQTkRERERERKQRBuVEREREREREGmFQTkRERERERKQRBuVEREREREREGmFQTkQAgHg8jng8rnU1AACiKKq2rXg8rur2iIiI9ILXdiJ9YFBOpAPJZBIWiwW9vb0F1wmHw7BYLBAEoeLtR6NRbN++HQ6HI2uZxWKp+GIuP66zs7PieshcLlfVj13JZrPB5XIhGo2qtk0iIqJa8dpePV7byWgYlBOZXDweh8fjQSgUgs1mq3l7wWAQNpsNyWQS4XC44seHw2Hs2rWr5nrIHA4Hjh07Bp/Px7vqRERkCry2E+kLg3Iik/P7/XC73XC73TVvS75YHzt2DIB0Ea9UMBisqkWgGK/XC4fDofp2iYiImhGv7UT6wqCcyMTi8Tii0Sj8fr8q2zt79iwA6ULpdrsRjUaRTCbLfrwoihBFEU6nU5X6ZDpw4ACi0WjTjK0jIiKqB17bifSHQTmRicl3u9W4ky5vz+v1AoBy53p8fLyix9frjrdcr2ru8BMREekFr+1E+sOgnMjEzp49W9FFWxRFdHZ2wuPx5C2Lx+PKhVfebiUXynA4jJGRkaxl4+Pj6OzshCiK8Pv96O3thcVigcfjUe6+ezweZQKaYi0DTqeTk8IQEZGh8dpOpD8Myol0RBRFWCyWvD8+n6+ibSWTSSSTybK7k4miCJfLBYfDgUgkklMeCARgs9mUC7b8tyiKZV0so9EonE5n3glpkskkPB4PkskkAoEARkZGEI1G4fP54PF44PP5EAwG4XA4MDY2VvAOvlyfSrrdERER1ROv7by2E1m1rgARlc9msyEUCuUti0QiGBsbK3tb8mylxVKxZK4rX7RjsVjedc6ePZszs6rP50M0GkUwGCx5175U9zan06ncmfd6vcoYslAopHRfc7vd6O3tRSQSybkrDwDr169XXk89xrYRERFVitd2XtuJGJQT6UhXV1fBC2Cld4jn5+eVbRYjiiL27t2LZDJZ8KIdDoeRTCbhcrmyUpP09/cr5cUkk0nE4/GiF/eBgYGs/x0OB0RRzHqMnIu10L6Q79TLr52IiEhrvLan8dpOZsXu60QmVe6F3ufzKRf3Qnfr5bvcgiCgt7dX+XG5XMo6xSaFOXv2rHJHvJCVXd/k/yvJvyq/DnZxIyIiI+K1nUifGJQTmVS5d5adTidmZmYwOjoKv9+fk3YkmUwiGo0iEAgglUrl/Mhj1IpNClPPmVkzya9VvutORERkJLy2E+kTg3Iikyr3zrI8zi0QCMDpdOZMOiPfJc83zguQxoI5HA7E4/Gs7m8y+YtAIy6m8mut5A48ERGRXvDaTqRPDMqJTEqeDGVmZqboepnj0kKhEERRzLrzLU/0UuxiKK+f7456o+6kA8D58+cB8G46EREZE6/tRPrEoJzIxCrN7elwOBAMBjE+Po5wOKzcIS914ZXvtOcbe3b27NmCd+LVVmrCGSIiIr3jtZ1IfyypVCqldSWISBt+vx9jY2O4cuWKJt2+wuEwIpFI0TFpahFFEb29vQgEAhgdHa378xEREWmB13Yi/WFLOZGJHThwAEDx2VPrqZHd2+TULY26c09ERKQFXtuJ9Ict5UQm5/f7MT4+jitXrjT0eeXcp6XGvamls7MTIyMjCAQCDXk+IiIirfDaTqQvDMqJCC6XC263u6EXNTkvaiO6mwmCgMnJScRisbo/FxERUTPgtZ1IP9h9nYhw7tw5RKNRpRtYI5w/f74h3c3C4TAmJydx7ty5uj8XERFRs+C1nUg/2FJOREREREREpBG2lBMRERERERFphEE5ERERERERkUYYlBMRERERERFphEE5ERERERERkUYYlBMRERERERFphEE5EREREdH/334dCwAAAAAM8rcexp6yCGAi5QAAADCRcgAAAJhIOQAAAEykHAAAACZSDgAAABMpBwAAgImUAwAAwETKAQAAYCLlAAAAMJFyAAAAmEg5AAAATKQcAAAAJgFK5+6iZb5EwgAAAABJRU5ErkJggg==", "text/plain": [ "
" ] @@ -441,7 +439,7 @@ "outputs": [ { "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+UAAAHyCAYAAACNj2+AAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAB7CAAAewgFu0HU+AACzpElEQVR4nOz9fXwb13knfP9AEbJJWTJEyRaYmLIFxo7pxKUFUG43aavGIp20TZWsReilYaxmG3Gadne7UmNC6m4r6d7dSGQTud12E4NKXd0KKlEC7SS92zxtADlh2rovImAxTsLEDiCZcgIqNilYtklZpIDnj8MBAXAAAuAMBpj5fW18AM0cYA4HgwHOnHNdx5JMJpMgIiIiIiIiorKr0bsCRERERERERGbFRjkRERERERGRTtgoJyIiIiIiItIJG+VEREREREREOmGjnIiIiIiIiEgnbJQTERERERER6YSNciIiIiIiIiKdsFFOREREREREpBM2yomIiIiIiIh0wkY5ERERERERkU7YKCciIiIiIiLSCRvlRERERERERDpho5yIiIiIiIhIJ2yUExEREREREemEjXIiIiIiIiIinbBRTkRERERERKQTNsqJiIiIiIiIdMJGOREREREREZFOavWuQLV7/fXX8Q//8A+45557UFdXp3d1iIiogkxPT+PSpUv48Ic/jLVr1+pdHSoQv9uJiCgftb/f2Shfon/4h39AV1eX3tUgIqIK5vP58IlPfELvalCB+N1ORESFUOv7nY3yJbrnnnsAAP/zf/5PbNiwAQCwdu1a3HHHHUW9zujoKLq6uuDz+dDS0qJa/artdbV87Wp7XS1fu9peV8vXZp21f10tX7sSX/e1117D66+/DgC4ePEi/uiP/ij1XUHVQX6/lnpcVeLxqddrs87av66Wr11tr6vla1fb62r52maus/w6an2/s1G+RPKwtj/6oz9KLTt48CAOHTpU0uu1tLTA6XSqUbWqfl0tX7vaXlfL166219XytVln7V9Xy9eupNc9dOgQDh8+nLGMQ6Cri/x+qXVcVdLxqfdrs87av66Wr11tr6vla1fb62r52maus1rf72yUqyT9aktjY6POtSEiIr1IkoStW7cCmL+STkRERJQLG+UqWerVlsbGRhw8eFD1Bn21va6WqnFfVGOdtVKN+6Ia66yVatsXS3ndxsbGqnpvSDuV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", + "image/png": "iVBORw0KGgoAAAANSUhEUgAAA+UAAAHyCAYAAACNj2+AAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMCwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy80BEi2AAAACXBIWXMAAB7CAAAewgFu0HU+AACh3ElEQVR4nOz9e3wb530n/n5AkbZJ3SDKiUnbki3QdgI3WVoA2TRNd5lYgJw0iZLGhChtWWtf3YgTZ8/Z85MSE1Z+5/eTtD0nMriJtGcvTUBlvarLtWQBcTeXXmxATuSmm9YiYLFpy9Q2IUWyDdqxSUi2SduEgPPHwwEBEnfMYAYzn/eLeAGYGx4OZp7Bd56bJZ1Op0FEREREREREddekdQKIiIiIiIiIzIpBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGGJQTERERERERaYRBOREREREREZFGmrVOQKN744038NRTT+H2229Ha2ur1skhIqIGMz8/j4sXL+K+++7DjTfeqHVyTIHXbiIiqoXS124G5TV66qmnMDg4qHUyiIiowY2NjeH3f//3tU6GKfDaTURESlDq2s2gvEa33347APGF2O32qrczOTmJwcHBmrfDbWu3XW6b29Zyu9x2425b3o58PSH18drN/I3bNt62GzHN3HbjblvpazeD8hrlq/bW2dmJzs7OqrZnt9vhcDhqTRa3reF2uW1uW8vtctuNse14PI54PJ4zjdWo60fe10odF3o7vrTediOmmds2zrYbMc3cduNuW6lrN4NyhWRXgzt48CAOHTqkXWKIiEjX/H4/Dh8+rHUyiIiISAcYlCskuwpENaXknZ2dOHjwYNUl7EbctloadX806rbV1Ij7pBHTrLZG3Se1bFuSJOzYsQPAUhU4ajx6Pb603LZaGnV/NOq21cTra/22raZG3Sd63d+WdDqd1joRjSwajcLpdCISiahWvcKsuG+Ngd+jMfB7VA/3bf1xn6uD+9U4+F0aA79H9Si9bzlOOREREREREZFGGJQT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S69vYVBO1EgYlCtkcHAQTqcTTqcTfr9f6+SQye3eDYyPA3v2LLWzbmsT78fHcztsU7wtdksLsGlTzZ27VXuzoJL/nUgrfr8/c83IHiubiKgqk8eAN/926f2vf6ZdWoioYqbtfV1pY2NjsNvtAKBaV/lElejuBk6cEEN/FRpGDFhqi/3YY6W3Wc+22PLNgnIC8+U3C8r934m0IkkSduzYAUDUtGJgTkRVm58Gzj+UO+3cg8Atn2O7cqIGwaBcIRzrlPSqqan00F/794thz4q13653W2wlbhaU878TaUHNsU6JyGSuTgLpa7nT0kkxnUE5UUNg2RER6bYtNjtuIyIiKmXVykmWZmCdvf5JIaKqMCgnIgD6bIut15sFREREunE5kPve0gz0foel5EQNhNXXiShDj22xd+8G7r5bDHsWCIg25m1tosr6vn0MyImIyMQW3gJifyJeN90A/M4TwMbfZEBO1GAYlBPRCnpri63HmwVERESau/CnQPIt8XrLHwC37tA2PURUFQblRNQw9HazgIiISDPpNPDif1t6f9e/0y4tRFQTljURERERETWa138KXPkn8foDvwNsYHsuokbFoJyIiIiIqNG8kFVKfidLyYkaGYNyIiIiIqJGMvcy8PL/Eq9v6AA2fUnT5BBRbdimXCGTk5OZ152dnejs7NQwNUREpGfxeBzxeBxA7vWDiKgsL/qB9DXx+o4hYNV12qaHiGrCoFwhg4ODmdcHDx7EoUOHtEsMERHpmt/vx+HDh7VOhunxhjo1pGvvAVOj4rWlGbhD0jY9RCah5g11BuUKGRsbg91uBwBe1ImIqChJkrBjhxi6aHJyMufGLtUPb6hTQ7oUBN59Xbze9CWg7WZt00NkEmreUGdQrhC73Q6Hw6F1MoiIqAGwVFYfeEOdGhKHQSPShJo31BmUExERkSnxhjo1nJko8MbPxWvrR4EP/Ett00NkImreUGfv60REREREjWD5MGgWi3ZpISLFMCgnIiIiItK7994EfvW4eN2yHrj997VNDxEphkE5EREREZHexf4HcO1d8dr2b4CWNZomh4iUw6CciIiIiEjPUteAF/546f2dX9UuLUSkOAblRERERER6Fv8r4J0L4nXnfcC6u7RNDxEpikE5EREREZGevfBfl17fyWHQiIyGQTkRERERkV699ZIoKQeA1bcDN/+upskhIuVx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", "text/plain": [ "
" ] From ae1d603af63d44a8947ef9f9a5b372c4e7cdba4a Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 3 Nov 2024 17:30:21 +0100 Subject: [PATCH 33/44] unblocking all energy terms for new layer dynamic objects --- cmtj/models/general_sb.py | 28 +++++++++------------------- 1 file changed, 9 insertions(+), 19 deletions(-) diff --git a/cmtj/models/general_sb.py b/cmtj/models/general_sb.py index a6b042b..50978d4 100644 --- a/cmtj/models/general_sb.py +++ b/cmtj/models/general_sb.py @@ -11,8 +11,8 @@ from numba import njit from tqdm import tqdm -from cmtj.utils import VectorObj, gamma, gamma_rad, mu0, perturb_position -from cmtj.utils.solvers import RootFinder +from ..utils import VectorObj, gamma, gamma_rad, mu0, perturb_position +from ..utils.solvers import RootFinder EPS = np.finfo("float64").resolution @@ -294,14 +294,8 @@ def rhs_spherical_llg( # Hoe can be used only for excitation, unlike Vp which controls torquances Hoe = LayerDynamic.get_hoe_ex_symbol() if osc else 0 - # TODO: check if dtheta, dphi terms with inv_sin have correct sign (other terms have correct sign) - # pubs are contradictory for this sign dtheta = -inv_sin * dUdphi - self.alpha * dUdtheta + self.Ms * Hoe - dphi = ( - inv_sin * dUdtheta - - self.alpha * dUdphi * (inv_sin) ** 2 - + self.alpha * self.Ms * Hoe / (sym.sin(self.theta) + EPS) - ) + dphi = inv_sin * dUdtheta - self.alpha * dUdphi * (inv_sin) ** 2 + self.alpha * self.Ms * Hoe * inv_sin return prefac * (sym.Matrix([dtheta, dphi]) + self.torque(osc=osc)) / self.Ms def torque(self, osc: bool = True): @@ -354,8 +348,6 @@ def __post_init__(self): if self.ilD is None: # this is optional, if not provided, we assume zero DMI self.ilD = [VectorObj(0, 0, 0) for _ in range(len(self.layers) - 1)] - elif isinstance(self.layers[0], LayerDynamic): - raise ValueError("interlayer DMI coupling is not yet supported for LayerDynamic.") if len(self.layers) != len(self.ilD) + 1: raise ValueError("Number of layers must be 1 more than ilD.") if not all(isinstance(d, VectorObj) for d in self.ilD): @@ -366,8 +358,6 @@ def __post_init__(self): if self.Ndipole is not None: if len(self.layers) != len(self.Ndipole) + 1: raise ValueError("Number of layers must be 1 more than number of tensors.") - if isinstance(self.layers[0], LayerDynamic): - raise ValueError("Dipole coupling is not yet supported for LayerDynamic.") self.dipoleMatrix = [sym.Matrix([d.get_cartesian() for d in dipole]) for dipole in self.Ndipole] id_sets = {layer._id for layer in self.layers} @@ -401,11 +391,11 @@ def compose_llg_jacobian(self, H: VectorObj): U = self.create_energy(H=H, volumetric=False) for layer in self.layers: symbols.extend((layer.theta, layer.phi)) - fns.append(layer.rhs_spherical_llg(U, osc=False)) + fns.append(layer.rhs_spherical_llg(U / layer.thickness, osc=False)) jac = sym.ImmutableMatrix(fns).jacobian(symbols) return jac, symbols - @lru_cache(3) + @lru_cache(3) # cache for 3 calls def create_energy( self, H: Union[VectorObj, sym.ImmutableMatrix, None] = None, @@ -884,10 +874,10 @@ def _compute_A_and_V_matrices(self, n, Vdc_ex_variable, H, frequency): alpha_factor = 1 + layer.alpha**2 for j, layer_j in enumerate(self.layers): theta_, phi_ = layer_j.get_coord_sym() - A_matrix[2 * i, 2 * j] = sym.diff(rhs[0], theta_) * alpha_factor - A_matrix[2 * i + 1, 2 * j + 1] = sym.diff(rhs[1], phi_) * alpha_factor - A_matrix[2 * i, 2 * j + 1] = sym.diff(rhs[0], phi_) * alpha_factor - A_matrix[2 * i + 1, 2 * j] = sym.diff(rhs[1], theta_) * alpha_factor + A_matrix[2 * i, 2 * j] = -sym.diff(rhs[0], theta_) * alpha_factor + A_matrix[2 * i + 1, 2 * j + 1] = -sym.diff(rhs[1], phi_) * alpha_factor + A_matrix[2 * i, 2 * j + 1] = -sym.diff(rhs[0], phi_) * alpha_factor + A_matrix[2 * i + 1, 2 * j] = -sym.diff(rhs[1], theta_) * alpha_factor if i == j: A_matrix[2 * i, 2 * j] += alpha_factor * omega * sym.I A_matrix[2 * i + 1, 2 * j + 1] += alpha_factor * omega * sym.I From 99d851c96a35399e56ab0af4ccd13f54a21ce51e Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 3 Nov 2024 19:58:18 +0100 Subject: [PATCH 34/44] test update --- tests/test_symbolic.py | 29 ++++++++++++----------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/tests/test_symbolic.py b/tests/test_symbolic.py index 4652725..1602e5f 100644 --- a/tests/test_symbolic.py +++ b/tests/test_symbolic.py @@ -6,11 +6,9 @@ def test_layer_energy(): # create a test layer - layer = LayerSB(_id=1, - thickness=1.0, - Kv=VectorObj(theta=0, phi=0.0, mag=1.0), - Ks=1.0, - Ms=1.0) + layer = LayerSB( + _id=1, thickness=1.0, Kv=VectorObj(theta=0, phi=0.0, mag=1.0), Ks=1.0, Ms=1.0 + ) # create test values for the input parameters H = sym.ImmutableMatrix([0, 0, 1]) @@ -22,8 +20,9 @@ def test_layer_energy(): down_layer = None # calculate the energy of the layer - energy = layer.symbolic_layer_energy(H, J1top, J1bottom, J2top, J2bottom, - top_layer, down_layer) + energy = layer.total_symbolic_layer_energy( + H, J1top, J1bottom, J2top, J2bottom, top_layer, down_layer + ) # check that the energy is a sympy expression assert isinstance(energy, sym.Expr) @@ -31,16 +30,12 @@ def test_layer_energy(): def test_solver_init(): # create test layers - layer1 = LayerSB(_id=0, - thickness=1.0, - Kv=VectorObj(theta=00, phi=0.0, mag=1.0), - Ks=1.0, - Ms=1.0) - layer2 = LayerSB(_id=1, - thickness=1.0, - Kv=VectorObj(theta=0, phi=0.0, mag=1.0), - Ks=1.0, - Ms=1.0) + layer1 = LayerSB( + _id=0, thickness=1.0, Kv=VectorObj(theta=00, phi=0.0, mag=1.0), Ks=1.0, Ms=1.0 + ) + layer2 = LayerSB( + _id=1, thickness=1.0, Kv=VectorObj(theta=0, phi=0.0, mag=1.0), Ks=1.0, Ms=1.0 + ) layers = [layer1, layer2] # create test values for J1 and J2 From aa13c6d46353d2745018a5c2b6d37b7e97e02748 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 4 Nov 2024 17:26:29 +0100 Subject: [PATCH 35/44] adding group interaction code --- core/junction.hpp | 25 +- core/reservoir.hpp | 153 ++++++++- core/stack.hpp | 2 +- python/cmtj.cpp | 780 +++++++++++++++++++++++---------------------- 4 files changed, 568 insertions(+), 392 deletions(-) diff --git a/core/junction.hpp b/core/junction.hpp index 0b61252..fb5d509 100644 --- a/core/junction.hpp +++ b/core/junction.hpp @@ -123,7 +123,7 @@ template class Layer { ScalarDriver IECQuadDriverBottom; AxialDriver IDMIDriverTop; AxialDriver IDMIDriverBottom; - + AxialDriver HreservedInteractionFieldDriver; // CMTJ Torque & Field drivers ScalarDriver currentDriver; ScalarDriver anisotropyDriver; @@ -461,6 +461,10 @@ template class Layer { this->HdmiDriver = driver; } + void setReservedInteractionField(const AxialDriver &driver) { + this->HreservedInteractionFieldDriver = driver; + } + /** * @brief Sets reference layer with a custom vector * Set reference layer parameter. This is for calculating the spin current @@ -524,6 +528,8 @@ template class Layer { this->Hidmi = calculateIDMI(time, stepMag, bottom, top); this->HAnis = calculateAnisotropy(stepMag, time); this->Hdmi = calculateHdmiField(time); + CVector HreservedInteractionField = + this->HreservedInteractionFieldDriver.getCurrentAxialDrivers(time); const CVector Heff = this->Hext // external + this->HAnis // anistotropy + this->HIEC // IEC @@ -534,7 +540,9 @@ template class Layer { // demag -- negative contribution - this->Hdemag // dipole -- negative contribution - - dipole; + - dipole + // reserved interaction field + + HreservedInteractionField; return Heff; } @@ -1137,6 +1145,10 @@ template class Junction { const ScalarDriver &driver) { scalarlayerSetter(layerID, &Layer::setFieldLikeTorqueDriver, driver); } + void setLayerReservedInteractionField(const std::string &layerID, + const AxialDriver &driver) { + axiallayerSetter(layerID, &Layer::setReservedInteractionField, driver); + } void setLayerHdmiDriver(const std::string &layerID, const AxialDriver &driver) { @@ -1520,7 +1532,8 @@ template class Junction { T Ry_acc = 0.0; for (unsigned int i = 0; i < this->layers.size(); i++) { - const T Rx = Rx0[i] + AMR_X[i] * (this->layers[i].mag.x * this->layers[i].mag.x) + + const T Rx = Rx0[i] + + AMR_X[i] * (this->layers[i].mag.x * this->layers[i].mag.x) + SMR_X[i] * (this->layers[i].mag.y * this->layers[i].mag.y); const T Ry = Ry0[i] + 0.5 * AHE[i] * this->layers[i].mag.z + @@ -1629,8 +1642,10 @@ template class Junction { throw std::runtime_error( "The time step cannot be larger than write frequency!"); } - const unsigned int totalIterations = static_cast(totalTime / timeStep); - const unsigned int writeEvery = static_cast(writeFrequency / timeStep); + const unsigned int totalIterations = + static_cast(totalTime / timeStep); + const unsigned int writeEvery = + static_cast(writeFrequency / timeStep); std::chrono::steady_clock::time_point begin = std::chrono::steady_clock::now(); // pick a solver based on drivers diff --git a/core/reservoir.hpp b/core/reservoir.hpp index 05134b0..c765d8d 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -2,6 +2,7 @@ #define RESERVOIR_H #include "cvector.hpp" +#include "drivers.hpp" #include "junction.hpp" #include #include @@ -28,6 +29,155 @@ void comb(int N, int K) { std::cout << std::endl; } while (std::prev_permutation(bitmask.begin(), bitmask.end())); } +template +using solverFn = void (Layer::*)(T t, T timeStep, const CVector &bottom, + const CVector &top); +template +using runnerFn = void (Junction::*)(solverFn &functor, T &t, T &timeStep); + +typedef std::function( + const CVector &, const CVector &, const Layer &, + const Layer &)> + interactionFunction; + +CVector computeDipoleInteraction(const CVector &r1, + const CVector &r2, + const Layer &layer1, + const Layer &layer2) { + const DVector rij = r1 - r2; // 1-2 distance vector + const double r3 = pow(rij.length(), 3); + const double r5 = pow(rij.length(), 5); + const Layer ref_magnetic_moment = layer2; + const DVector m1 = ref_magnetic_moment.mag; + const double V = + ref_magnetic_moment.thickness * ref_magnetic_moment.cellSurface; + const double prefactor = (ref_magnetic_moment.Ms / MAGNETIC_PERMEABILITY) * V; + + return prefactor * (3 * c_dot(m1, rij) * rij / r5 - m1 / r3); +} + +CVector computeDipoleInteractionNoumra(const CVector &r1, + const CVector &r2, + const Layer &layer1, + const Layer &layer2) { + return computeDipoleInteraction(r1, r2, layer1, layer2); +} + +class GroupInteraction { + std::string topId; // Id of the top junction + std::vector> coordinateMatrix; + std::vector> junctionList; + interactionFunction interactionFunc = computeDipoleInteraction; + unsigned int noElements; + + void stepFunctionalSolver(double time, double timeStep, + interactionFunction interaction, + runnerFn runner, solverFn solver) { + // collect all frozen states + // for each element, compute the extra field from all other elements + for (unsigned int i = 0; i < this->noElements; i++) { + CVector H_extra; + for (unsigned int j = 0; j < this->noElements; j++) { + if (i == j) + continue; + H_extra += + interaction(this->coordinateMatrix[i], this->coordinateMatrix[j], + this->junctionList[i].getLayer(this->topId), + this->junctionList[j].getLayer(this->topId)); + } + std::cout << "H_extra: " << H_extra << " " << H_extra.length() + << std::endl; + this->junctionList[i].setLayerReservedInteractionField( + this->topId, AxialDriver(H_extra)); + } + // step the solver with the extra field + for (unsigned int i = 0; i < this->noElements; i++) { + (this->junctionList[i].*runner)(solver, time, timeStep); + } + } + +public: + GroupInteraction(std::vector> coordinateMatrix, + std::vector> junctionList, + const std::string &topId = "free") + : topId(topId) { + if (coordinateMatrix.size() != junctionList.size()) { + throw std::runtime_error( + "Coordinate matrix and junction list must have the same size!"); + } + if (coordinateMatrix.empty() || junctionList.empty()) { + throw std::runtime_error( + "Coordinate matrix and junction list cannot be empty!"); + } + this->noElements = junctionList.size(); + this->coordinateMatrix = std::move(coordinateMatrix); + this->junctionList = std::move(junctionList); + } + + void setInteractionFunction(interactionFunction interactionFunc) { + this->interactionFunc = interactionFunc; + } + + void runSimulation(double totalTime, double timeStep = 1e-13, + double writeFrequency = 1e-13) { + const unsigned int writeEvery = (int)(writeFrequency / timeStep); + const unsigned int totalIterations = (int)(totalTime / timeStep); + + if (timeStep > writeFrequency) { + throw std::runtime_error( + "The time step cannot be larger than write frequency!"); + } + + // pick a solver based on drivers + std::vector modes; + auto localRunner = &Junction::runMultiLayerSolver; + for (auto &j : this->junctionList) { + // again, solver mode does not make a difference for legacy reasons + auto [runner, solver, mode] = j.getSolver(RK4, totalIterations); + modes.push_back(mode); + localRunner = runner; + // TODO: handle the rare case when the user mixes 1 layer with 2 layer + // junction in the same stack -- i.e. runner is runSingleLayerSolver and + // runMultiLayerSolver + } + auto solver = &Layer::rk4_step; + if (!std::equal(modes.begin() + 1, modes.end(), modes.begin())) { + throw std::runtime_error( + "Junctions have different solver modes!" + " Set the same solver mode for all junctions explicitly." + " Do not mix stochastic and deterministic solvers!"); + } + + for (unsigned int i = 0; i < totalIterations; i++) { + double t = i * timeStep; + stepFunctionalSolver(t, timeStep, this->interactionFunc, localRunner, + solver); + + if (!(i % writeEvery)) { + for (auto &jun : this->junctionList) + jun.logLayerParams(t, timeStep, false); + } + } + } + + void clearLogs() { + for (auto &j : this->junctionList) { + j.clearLog(); + } + } + + std::unordered_map> & + getLog(unsigned int id) { + if (id <= this->junctionList.size()) { + return this->junctionList[id].getLog(); + } + throw std::runtime_error("Asking for id of a non-existing junction!"); + } + + std::unordered_map> &getLog() { + throw std::runtime_error("Not implemented!"); + } +}; typedef std::array, 3> tensor; // typedef std::vector> tensorMatrix; @@ -35,7 +185,6 @@ typedef std::vector tensorList; class Reservoir { private: // log stuff - const std::string intendedKeys = {"m_"}; std::vector logKeys; std::unordered_map> reservoirLog; @@ -47,7 +196,7 @@ class Reservoir { std::vector>> layerMatrix; std::vector> computeReservoirDipoleMatrix( - std::vector>> coordinateMatrix) { + const std::vector>> &coordinateMatrix) { std::vector> localReservoirDipoleTensor; // reserve some place here localReservoirDipoleTensor.resize(this->noElements); diff --git a/core/stack.hpp b/core/stack.hpp index 4bd9f6c..f9a978c 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -232,7 +232,7 @@ template class Stack { // junction in the same stack -- i.e. runner is runSingleLayerSolver and // runMultiLayerSolver } - auto solver = &Layer::rk4_step; + auto solver = &Layer::rk4_step; // legacy, this actually doesn't matter if (!std::equal(modes.begin() + 1, modes.end(), modes.begin())) { throw std::runtime_error( "Junctions have different solver modes!" diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 036c0cd..7dd8538 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -32,409 +32,421 @@ using DLLGBJunction = LLGBJunction; #define USING_PY true PYBIND11_MODULE(cmtj, m) { - // helpers - m.def("c_dot", &c_dot); - m.doc() = "Python binding for C++ CMTJ Library."; + // helpers + m.def("c_dot", &c_dot); + m.doc() = "Python binding for C++ CMTJ Library."; - // driver aliases - m.def( - "constantDriver", - [](double value) { return DScalarDriver::getConstantDriver(value); }, - "value"_a); - m.def( - "pulseDriver", - [](double constantValue, double amplitude, double period, double cycle) { - return DScalarDriver::getPulseDriver(constantValue, amplitude, period, - cycle); - }, - "constantValue"_a, "amplitude"_a, "period"_a, "cycle"_a); - m.def( - "sineDriver", - [](double constantValue, double amplitude, double frequency, - double phase) { - return DScalarDriver::getSineDriver(constantValue, amplitude, frequency, - phase); - }, - "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); - m.def( - "posSineDriver", - [](double constantValue, double amplitude, double frequency, - double phase) { - return DScalarDriver::getPosSineDriver(constantValue, amplitude, - frequency, phase); - }, - "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); - m.def( - "stepDriver", - [](double constantValue, double amplitude, double timeStart, - double timeStop) { - return DScalarDriver::getStepDriver(constantValue, amplitude, timeStart, - timeStop); - }, - "constantValue"_a, "amplitude"_a, "timeStart"_a, "timeStop"_a); - m.def( - "trapezoidDriver", - [](double constantValue, double amplitude, double timeStart, - double edgeTime, double steadyTime) { - return DScalarDriver::getTrapezoidDriver( - constantValue, amplitude, timeStart, edgeTime, steadyTime); - }, - "constantValue"_a, "amplitude"_a, "timeStart"_a, "edgeTime"_a, - "steadyTime"_a); - m.def( - "gaussianImpulseDriver", - [](double constantValue, double amplitude, double t0, double sigma) { - return DScalarDriver::getGaussianImpulseDriver(constantValue, amplitude, - t0, sigma); - }, - "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); - m.def( - "gaussianStepDriver", - [](double constantValue, double amplitude, double t0, double sigma) { - return DScalarDriver::getGaussianStepDriver(constantValue, amplitude, - t0, sigma); - }, - "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); + // driver aliases + m.def( + "constantDriver", + [](double value) { return DScalarDriver::getConstantDriver(value); }, + "value"_a); + m.def( + "pulseDriver", + [](double constantValue, double amplitude, double period, double cycle) { + return DScalarDriver::getPulseDriver(constantValue, amplitude, period, + cycle); + }, + "constantValue"_a, "amplitude"_a, "period"_a, "cycle"_a); + m.def( + "sineDriver", + [](double constantValue, double amplitude, double frequency, + double phase) { + return DScalarDriver::getSineDriver(constantValue, amplitude, frequency, + phase); + }, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); + m.def( + "posSineDriver", + [](double constantValue, double amplitude, double frequency, + double phase) { + return DScalarDriver::getPosSineDriver(constantValue, amplitude, + frequency, phase); + }, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a); + m.def( + "stepDriver", + [](double constantValue, double amplitude, double timeStart, + double timeStop) { + return DScalarDriver::getStepDriver(constantValue, amplitude, timeStart, + timeStop); + }, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "timeStop"_a); + m.def( + "trapezoidDriver", + [](double constantValue, double amplitude, double timeStart, + double edgeTime, double steadyTime) { + return DScalarDriver::getTrapezoidDriver( + constantValue, amplitude, timeStart, edgeTime, steadyTime); + }, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "edgeTime"_a, + "steadyTime"_a); + m.def( + "gaussianImpulseDriver", + [](double constantValue, double amplitude, double t0, double sigma) { + return DScalarDriver::getGaussianImpulseDriver(constantValue, amplitude, + t0, sigma); + }, + "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); + m.def( + "gaussianStepDriver", + [](double constantValue, double amplitude, double t0, double sigma) { + return DScalarDriver::getGaussianStepDriver(constantValue, amplitude, + t0, sigma); + }, + "constantValue"_a, "amplitude"_a, "t0"_a, "sigma"_a); - // CVector - py::class_(m, "CVector") - .def(py::init()) - .def_readwrite("x", &DVector::x) - .def_readwrite("y", &DVector::y) - .def_readwrite("z", &DVector::z) - .def("length", [](const DVector &vec) { return vec.length(); }) - .def("normalize", &DVector::normalize) - .def("tolist", &DVector::tolist) - // operators - .def(py::self + py::self) - .def(py::self += py::self) - .def(py::self - py::self) - .def(py::self -= py::self) - .def(py::self *= double()) - .def(py::self == py::self) - .def(py::self != py::self) - .def(double() * py::self) - .def(py::self * double()) - .def("__getitem__", - [](const DVector &v, const int key) { return v[key]; }) - .def("__len__", [](const DVector &v) { return 3; }) - .def("__str__", py::overload_cast<>(&DVector::toString)) - .def("__repr__", py::overload_cast<>(&DVector::toString)); + // CVector + py::class_(m, "CVector") + .def(py::init()) + .def_readwrite("x", &DVector::x) + .def_readwrite("y", &DVector::y) + .def_readwrite("z", &DVector::z) + .def("length", [](const DVector& vec) { return vec.length(); }) + .def("normalize", &DVector::normalize) + .def("tolist", &DVector::tolist) + // operators + .def(py::self + py::self) + .def(py::self += py::self) + .def(py::self - py::self) + .def(py::self -= py::self) + .def(py::self *= double()) + .def(py::self == py::self) + .def(py::self != py::self) + .def(double() * py::self) + .def(py::self * double()) + .def("__getitem__", + [](const DVector& v, const int key) { return v[key]; }) + .def("__len__", [](const DVector& v) { return 3; }) + .def("__str__", py::overload_cast<>(&DVector::toString)) + .def("__repr__", py::overload_cast<>(&DVector::toString)); - py::implicitly_convertible, DVector>(); - py::implicitly_convertible, DVector>(); + py::implicitly_convertible, DVector>(); + py::implicitly_convertible, DVector>(); - py::enum_(m, "Axis") - .value("xaxis", xaxis) - .value("yaxis", yaxis) - .value("zaxis", zaxis) - .value("all", all) - .value("none", none) - .export_values(); + py::enum_(m, "Axis") + .value("xaxis", xaxis) + .value("yaxis", yaxis) + .value("zaxis", zaxis) + .value("all", all) + .value("none", none) + .export_values(); - py::enum_(m, "Reference") - .value("none", NONE) - .value("fixed", FIXED) - .value("top", TOP) - .value("bottom", BOTTOM) - .export_values(); + py::enum_(m, "Reference") + .value("none", NONE) + .value("fixed", FIXED) + .value("top", TOP) + .value("bottom", BOTTOM) + .export_values(); - py::enum_(m, "SolverMode") - .value("RK4", RK4) - .value("Heun", HEUN) - .value("EulerHeun", EULER_HEUN) - .value("DormandPrice", DORMAND_PRICE) - .export_values(); + py::enum_(m, "SolverMode") + .value("RK4", RK4) + .value("Heun", HEUN) + .value("EulerHeun", EULER_HEUN) + .value("DormandPrice", DORMAND_PRICE) + .export_values(); - // Driver Class - py::class_(m, "ScalarDriver") - .def(py::init<>()) - .def(py::self + double()) - .def(py::self += double()) - .def(py::self * double()) - .def(py::self *= double()) - .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, - "time"_a) - .def_static("getConstantDriver", &DScalarDriver::getConstantDriver, - "constantValue"_a) - .def_static("getPulseDriver", &DScalarDriver::getPulseDriver, - "constantValue"_a, "amplitude"_a, "period"_a, "cycle"_a) - .def_static("getSineDriver", &DScalarDriver::getSineDriver, - "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a) - .def_static("getPosSineDriver", &DScalarDriver::getPosSineDriver, - "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a) - .def_static("getStepDriver", &DScalarDriver::getStepDriver, - "constantValue"_a, "amplitude"_a, "timeStart"_a, "timeStop"_a) - .def_static("getTrapezoidDriver", &DScalarDriver::getTrapezoidDriver, - "constantValue"_a, "amplitude"_a, "timeStart"_a, "edgeTime"_a, - "steadyTime"_a) - .def_static("getGaussianImpulseDriver", - &DScalarDriver::getGaussianImpulseDriver, "constantValue"_a, - "amplitude"_a, "t0"_a, "sigma"_a) - .def_static("getGaussianStepDriver", - &DScalarDriver::getGaussianStepDriver, "constantValue"_a, - "amplitude"_a, "t0"_a, "sigma"_a); + // Driver Class + py::class_(m, "ScalarDriver") + .def(py::init<>()) + .def(py::self + double()) + .def(py::self += double()) + .def(py::self * double()) + .def(py::self *= double()) + .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, + "time"_a) + .def_static("getConstantDriver", &DScalarDriver::getConstantDriver, + "constantValue"_a) + .def_static("getPulseDriver", &DScalarDriver::getPulseDriver, + "constantValue"_a, "amplitude"_a, "period"_a, "cycle"_a) + .def_static("getSineDriver", &DScalarDriver::getSineDriver, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a) + .def_static("getPosSineDriver", &DScalarDriver::getPosSineDriver, + "constantValue"_a, "amplitude"_a, "frequency"_a, "phase"_a) + .def_static("getStepDriver", &DScalarDriver::getStepDriver, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "timeStop"_a) + .def_static("getTrapezoidDriver", &DScalarDriver::getTrapezoidDriver, + "constantValue"_a, "amplitude"_a, "timeStart"_a, "edgeTime"_a, + "steadyTime"_a) + .def_static("getGaussianImpulseDriver", + &DScalarDriver::getGaussianImpulseDriver, "constantValue"_a, + "amplitude"_a, "t0"_a, "sigma"_a) + .def_static("getGaussianStepDriver", + &DScalarDriver::getGaussianStepDriver, "constantValue"_a, + "amplitude"_a, "t0"_a, "sigma"_a); - py::class_(m, "NullDriver") - .def(py::init<>()) - .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, - "time"_a); + py::class_(m, "NullDriver") + .def(py::init<>()) + .def("getCurrentScalarValue", &DScalarDriver::getCurrentScalarValue, + "time"_a); - py::class_(m, "AxialDriver") - .def(py::init()) - .def(py::init>>()) - .def(py::init()) - .def(py::init()) - .def("getVectorAxialDriver", &DAxialDriver::getVectorAxialDriver) - .def("getCurrentAxialDrivers", &DAxialDriver::getCurrentAxialDrivers, - "time"_a) - .def("applyMask", - py::overload_cast(&DAxialDriver::applyMask)) - .def("applyMask", py::overload_cast &>( - &DAxialDriver::applyMask)); + py::class_(m, "AxialDriver") + .def(py::init()) + .def(py::init>>()) + .def(py::init()) + .def(py::init()) + .def("getVectorAxialDriver", &DAxialDriver::getVectorAxialDriver) + .def("getCurrentAxialDrivers", &DAxialDriver::getCurrentAxialDrivers, + "time"_a) + .def("applyMask", + py::overload_cast(&DAxialDriver::applyMask)) + .def("applyMask", py::overload_cast &>( + &DAxialDriver::applyMask)); - py::class_(m, "Layer") - .def(py::init, // demagTensor - double // damping - >(), - "id"_a, "mag"_a, "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, - "demagTensor"_a, "damping"_a = 0.011) - .def_static("createSOTLayer", &DLayer::LayerSOT, "id"_a, "mag"_a, - "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, - "demagTensor"_a, "damping"_a = 0.011, - "fieldLikeTorque"_a = 0.0, "dampingLikeTorque"_a = 0.0) - .def_static("createSTTLayer", &DLayer::LayerSTT, "id"_a, "mag"_a, - "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, - "demagTensor"_a, "damping"_a = 0.011, - "SlonczewskiSpacerLayerParameter"_a = 1.0, "beta"_a = 0.0, - "spinPolarisation"_a = 0.0) - .def("setMagnetisation", &DLayer::setMagnetisation) - .def("setAnisotropyDriver", &DLayer::setAnisotropyDriver) - .def("setExternalFieldDriver", &DLayer::setExternalFieldDriver) - .def("setOerstedFieldDriver", &DLayer::setOerstedFieldDriver) - .def("setHdmiDriver", &DLayer::setHdmiDriver) - // reference layers - .def("setReferenceLayer", - py::overload_cast(&DLayer::setReferenceLayer)) - .def("setReferenceLayer", - py::overload_cast(&DLayer::setReferenceLayer)) + py::class_(m, "Layer") + .def(py::init, // demagTensor + double // damping + >(), + "id"_a, "mag"_a, "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a = 0.011) + .def_static("createSOTLayer", &DLayer::LayerSOT, "id"_a, "mag"_a, + "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a = 0.011, + "fieldLikeTorque"_a = 0.0, "dampingLikeTorque"_a = 0.0) + .def_static("createSTTLayer", &DLayer::LayerSTT, "id"_a, "mag"_a, + "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a = 0.011, + "SlonczewskiSpacerLayerParameter"_a = 1.0, "beta"_a = 0.0, + "spinPolarisation"_a = 0.0) + .def("setMagnetisation", &DLayer::setMagnetisation) + .def("setAnisotropyDriver", &DLayer::setAnisotropyDriver) + .def("setExternalFieldDriver", &DLayer::setExternalFieldDriver) + .def("setOerstedFieldDriver", &DLayer::setOerstedFieldDriver) + .def("setHdmiDriver", &DLayer::setHdmiDriver) + // reference layers + .def("setReferenceLayer", + py::overload_cast(&DLayer::setReferenceLayer)) + .def("setReferenceLayer", + py::overload_cast(&DLayer::setReferenceLayer)) - .def("setFieldLikeTorqueDriver", &DLayer::setFieldLikeTorqueDriver) - .def("setDampingLikeTorqueDriver", &DLayer::setDampingLikeTorqueDriver) - .def("setTemperatureDriver", &DLayer::setTemperatureDriver) - .def("setTopDipoleTensor", &DLayer::setTopDipoleTensor) - .def("setBottomDipoleTensor", &DLayer::setBottomDipoleTensor) - .def("setKappa", &DLayer::setKappa) - .def("setAlternativeSTT", &DLayer::setAlternativeSTT) - // readonly props - .def_readonly("id", &DLayer::id) - .def_readonly("Ms", &DLayer::Ms) - .def_readonly("thickness", &DLayer::thickness) - .def_readonly("damping", &DLayer::damping) - .def_readonly("cellSurface", &DLayer::cellSurface) - .def_readonly("demagTensor", &DLayer::demagTensor) - // noise - .def("setAlphaNoise", &DLayer::setAlphaNoise) - .def("setOneFNoise", &DLayer::setOneFNoise) - // getters - .def("getId", &DLayer::getId) - .def("getOneFVector", &DLayer::getOneFVector) - .def("createBufferedAlphaNoise", &DLayer::createBufferedAlphaNoise); + .def("setFieldLikeTorqueDriver", &DLayer::setFieldLikeTorqueDriver) + .def("setDampingLikeTorqueDriver", &DLayer::setDampingLikeTorqueDriver) + .def("setTemperatureDriver", &DLayer::setTemperatureDriver) + .def("setTopDipoleTensor", &DLayer::setTopDipoleTensor) + .def("setBottomDipoleTensor", &DLayer::setBottomDipoleTensor) + .def("setKappa", &DLayer::setKappa) + .def("setAlternativeSTT", &DLayer::setAlternativeSTT) + // readonly props + .def_readonly("id", &DLayer::id) + .def_readonly("Ms", &DLayer::Ms) + .def_readonly("thickness", &DLayer::thickness) + .def_readonly("damping", &DLayer::damping) + .def_readonly("cellSurface", &DLayer::cellSurface) + .def_readonly("demagTensor", &DLayer::demagTensor) + // noise + .def("setAlphaNoise", &DLayer::setAlphaNoise) + .def("setOneFNoise", &DLayer::setOneFNoise) + // getters + .def("getId", &DLayer::getId) + .def("getOneFVector", &DLayer::getOneFVector) + .def("createBufferedAlphaNoise", &DLayer::createBufferedAlphaNoise); - py::class_(m, "Junction") - .def(py::init>(), "layers"_a) - .def(py::init, double, double>(), "layers"_a, - "Rp"_a = 100, "Rap"_a = 200) - .def(py::init, std::vector, - std::vector, std::vector, - std::vector, std::vector, - std::vector, std::vector>(), - "layers"_a, "Rx0"_a, "Ry0"_a, "AMR_X"_a, "AMR_Y"_a, "SMR_X"_a, - "SMR_Y"_a, "AHE"_a) - // log utils - .def("getLog", &DJunction::getLog) - .def("clearLog", &DJunction::clearLog) - .def("saveLog", &DJunction::saveLogs, "filename"_a) - // main run - .def("runSimulation", &DJunction::runSimulation, "totalTime"_a, - "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11, "log"_a = false, - "calculateEnergies"_a = false, "solverMode"_a = RK4) + py::class_(m, "Junction") + .def(py::init>(), "layers"_a) + .def(py::init, double, double>(), "layers"_a, + "Rp"_a = 100, "Rap"_a = 200) + .def(py::init, std::vector, + std::vector, std::vector, + std::vector, std::vector, + std::vector, std::vector>(), + "layers"_a, "Rx0"_a, "Ry0"_a, "AMR_X"_a, "AMR_Y"_a, "SMR_X"_a, + "SMR_Y"_a, "AHE"_a) + // log utils + .def("getLog", &DJunction::getLog) + .def("clearLog", &DJunction::clearLog) + .def("saveLog", &DJunction::saveLogs, "filename"_a) + // main run + .def("runSimulation", &DJunction::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11, "log"_a = false, + "calculateEnergies"_a = false, "solverMode"_a = RK4) - // driver setters - .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) - .def("setLayerExternalFieldDriver", - &DJunction::setLayerExternalFieldDriver) - .def("setLayerCurrentDriver", &DJunction::setLayerCurrentDriver) - .def("setLayerAnisotropyDriver", &DJunction::setLayerAnisotropyDriver) - .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) - .def("setLayerMagnetisation", &DJunction::setLayerMagnetisation) - .def("setLayerHdmiDriver", &DJunction::setLayerHdmiDriver) - // interaction setters - .def("setIECDriver", &DJunction::setIECDriver) - .def("setQuadIECDriver", &DJunction::setQuadIECDriver) - .def("setIDMIDriver", &DJunction::setIDMIDriver) - // noise - .def("setLayerTemperatureDriver", &DJunction::setLayerTemperatureDriver) - .def("setLayerNonStochasticLangevinDriver", - &DJunction::setLayerNonStochasticLangevinDriver) - .def("setLayerOneFNoise", &DJunction::setLayerOneFNoise) - // SOT setters - .def("setLayerFieldLikeTorqueDriver", - &DJunction::setLayerFieldLikeTorqueDriver) - .def("setLayerDampingLikeTorqueDriver", - &DJunction::setLayerDampingLikeTorqueDriver) - // Reference setters - .def("setLayerReferenceType", &DJunction::setLayerReferenceType) - .def("setLayerReferenceLayer", &DJunction::setLayerReferenceLayer) - // other setters - .def("setLayerAlternativeSTT", &DJunction::setLayerAlternativeSTT) - // junction calculations - .def("getLayerMagnetisation", &DJunction::getLayerMagnetisation) - .def("getMagnetoresistance", &DJunction::getMagnetoresistance) - // getters - .def("getLayerIds", &DJunction::getLayerIds) - .def("getLayer", &DJunction::getLayer, "layerId"_a, - py::return_value_policy::reference) - // readonly props - .def_readonly("layers", &DJunction::layers); + // driver setters + .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) + .def("setLayerExternalFieldDriver", + &DJunction::setLayerExternalFieldDriver) + .def("setLayerCurrentDriver", &DJunction::setLayerCurrentDriver) + .def("setLayerAnisotropyDriver", &DJunction::setLayerAnisotropyDriver) + .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) + .def("setLayerMagnetisation", &DJunction::setLayerMagnetisation) + .def("setLayerHdmiDriver", &DJunction::setLayerHdmiDriver) + // interaction setters + .def("setIECDriver", &DJunction::setIECDriver) + .def("setQuadIECDriver", &DJunction::setQuadIECDriver) + .def("setIDMIDriver", &DJunction::setIDMIDriver) + // noise + .def("setLayerTemperatureDriver", &DJunction::setLayerTemperatureDriver) + .def("setLayerNonStochasticLangevinDriver", + &DJunction::setLayerNonStochasticLangevinDriver) + .def("setLayerOneFNoise", &DJunction::setLayerOneFNoise) + // SOT setters + .def("setLayerFieldLikeTorqueDriver", + &DJunction::setLayerFieldLikeTorqueDriver) + .def("setLayerDampingLikeTorqueDriver", + &DJunction::setLayerDampingLikeTorqueDriver) + // Reference setters + .def("setLayerReferenceType", &DJunction::setLayerReferenceType) + .def("setLayerReferenceLayer", &DJunction::setLayerReferenceLayer) + // other setters + .def("setLayerAlternativeSTT", &DJunction::setLayerAlternativeSTT) + // junction calculations + .def("getLayerMagnetisation", &DJunction::getLayerMagnetisation) + .def("getMagnetoresistance", &DJunction::getMagnetoresistance) + // getters + .def("getLayerIds", &DJunction::getLayerIds) + .def("getLayer", &DJunction::getLayer, "layerId"_a, + py::return_value_policy::reference) + // readonly props + .def_readonly("layers", &DJunction::layers); - // stack module - py::module stack_module = - m.def_submodule("stack", "A stack submodule for joining MTJ junctions"); + // stack module + py::module stack_module = + m.def_submodule("stack", "A stack submodule for joining MTJ junctions"); - py::class_(stack_module, "SeriesStack") - .def(py::init, std::string, std::string, double>(), - "junctionList"_a, "topId_a"_a = "free", "bottomId"_a = "bottom", - "phaseOffset"_a = 0.0) - .def("runSimulation", &DSeriesStack::runSimulation, "totalTime"_a, - "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11) - .def("setMagnetisation", &DSeriesStack::setMagnetisation, "junction"_a, - "layerId"_a, "mag"_a) - .def("getMagnetisation", &DSeriesStack::getMagnetisation, "junction"_a, - "layerId"_a) - .def("setCoupledCurrentDriver", &DSeriesStack::setCoupledCurrentDriver, - "driver"_a) - .def("setExternalFieldDriver", &DSeriesStack::setExternalFieldDriver, - "driver"_a) - .def( - "setCouplingStrength", - py::overload_cast(&DSeriesStack::setCouplingStrength), - "coupling"_a) - .def("setCouplingStrength", - py::overload_cast &>( - &DSeriesStack::setCouplingStrength), - "coupling"_a) - .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) - .def("getJunction", &DParallelStack::getJunction, "junctionId"_a, - py::return_value_policy::reference) - .def("setJunctionAnisotropyDriver", - &DSeriesStack::setJunctionAnisotropyDriver, "junctionId"_a, - "layerId"_a, "k"_a) - // logging - .def("clearLogs", &DSeriesStack::clearLogs) - .def("getLog", py::overload_cast(&DSeriesStack::getLog)) - .def("getLog", py::overload_cast<>(&DSeriesStack::getLog)); + py::class_(stack_module, "SeriesStack") + .def(py::init, std::string, std::string, double>(), + "junctionList"_a, "topId_a"_a = "free", "bottomId"_a = "bottom", + "phaseOffset"_a = 0.0) + .def("runSimulation", &DSeriesStack::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11) + .def("setMagnetisation", &DSeriesStack::setMagnetisation, "junction"_a, + "layerId"_a, "mag"_a) + .def("getMagnetisation", &DSeriesStack::getMagnetisation, "junction"_a, + "layerId"_a) + .def("setCoupledCurrentDriver", &DSeriesStack::setCoupledCurrentDriver, + "driver"_a) + .def("setExternalFieldDriver", &DSeriesStack::setExternalFieldDriver, + "driver"_a) + .def( + "setCouplingStrength", + py::overload_cast(&DSeriesStack::setCouplingStrength), + "coupling"_a) + .def("setCouplingStrength", + py::overload_cast &>( + &DSeriesStack::setCouplingStrength), + "coupling"_a) + .def("setDelayed", &DSeriesStack::setDelayed, "delayed"_a) + .def("getJunction", &DParallelStack::getJunction, "junctionId"_a, + py::return_value_policy::reference) + .def("setJunctionAnisotropyDriver", + &DSeriesStack::setJunctionAnisotropyDriver, "junctionId"_a, + "layerId"_a, "k"_a) + // logging + .def("clearLogs", &DSeriesStack::clearLogs) + .def("getLog", py::overload_cast(&DSeriesStack::getLog)) + .def("getLog", py::overload_cast<>(&DSeriesStack::getLog)); - py::class_(stack_module, "ParallelStack") - .def(py::init, std::string, std::string, double>(), - "junctionList"_a, "topId_a"_a = "free", "bottomId"_a = "bottom", - "phaseOffset"_a = 0.0) - .def("runSimulation", &DParallelStack::runSimulation, "totalTime"_a, - "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11) - .def("setMagnetisation", &DParallelStack::setMagnetisation, "junction"_a, - "layerId"_a, "mag"_a) - .def("getMagnetisation", &DParallelStack::getMagnetisation, "junction"_a, - "layerId"_a) - .def("setCoupledCurrentDriver", &DParallelStack::setCoupledCurrentDriver, - "driver"_a) - .def("setExternalFieldDriver", &DParallelStack::setExternalFieldDriver, - "driver"_a) - .def("setCouplingStrength", - py::overload_cast( - &DParallelStack::setCouplingStrength), - "coupling"_a) - .def("setCouplingStrength", - py::overload_cast &>( - &DParallelStack::setCouplingStrength), - "coupling"_a) - .def("setDelayed", &DParallelStack::setDelayed, "delayed"_a) - .def("getJunction", &DParallelStack::getJunction, "junctionId"_a, - py::return_value_policy::reference) - .def("setJunctionAnisotropyDriver", - &DSeriesStack::setJunctionAnisotropyDriver, "junctionId"_a, - "layerId"_a, "k"_a) - // logging - .def("clearLogs", &ParallelStack::clearLogs) - .def("getLog", - py::overload_cast(&ParallelStack::getLog)) - .def("getLog", py::overload_cast<>(&ParallelStack::getLog)); + py::class_(stack_module, "ParallelStack") + .def(py::init, std::string, std::string, double>(), + "junctionList"_a, "topId_a"_a = "free", "bottomId"_a = "bottom", + "phaseOffset"_a = 0.0) + .def("runSimulation", &DParallelStack::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11) + .def("setMagnetisation", &DParallelStack::setMagnetisation, "junction"_a, + "layerId"_a, "mag"_a) + .def("getMagnetisation", &DParallelStack::getMagnetisation, "junction"_a, + "layerId"_a) + .def("setCoupledCurrentDriver", &DParallelStack::setCoupledCurrentDriver, + "driver"_a) + .def("setExternalFieldDriver", &DParallelStack::setExternalFieldDriver, + "driver"_a) + .def("setCouplingStrength", + py::overload_cast( + &DParallelStack::setCouplingStrength), + "coupling"_a) + .def("setCouplingStrength", + py::overload_cast &>( + &DParallelStack::setCouplingStrength), + "coupling"_a) + .def("setDelayed", &DParallelStack::setDelayed, "delayed"_a) + .def("getJunction", &DParallelStack::getJunction, "junctionId"_a, + py::return_value_policy::reference) + .def("setJunctionAnisotropyDriver", + &DSeriesStack::setJunctionAnisotropyDriver, "junctionId"_a, + "layerId"_a, "k"_a) + // logging + .def("clearLogs", &ParallelStack::clearLogs) + .def("getLog", + py::overload_cast(&ParallelStack::getLog)) + .def("getLog", py::overload_cast<>(&ParallelStack::getLog)); - // reservoir module - py::module reservoir_module = m.def_submodule( - "reservoir", "A reservoir submodule for joining MTJ junctions"); - py::class_(reservoir_module, "Reservoir") - .def(py::init(), "coordinateMatrix"_a, - "layerMatrix"_a) - .def("runSimulation", &Reservoir::runSimulation) - .def("clearLogs", &Reservoir::clearLogs) - .def("saveLogs", &Reservoir::saveLogs) - .def("getLayer", &Reservoir::getLayer) - .def("setAllExternalField", &Reservoir::setAllExternalField) - .def("setLayerAnisotropy", &Reservoir::setLayerAnisotropy) - .def("setLayerExternalField", &Reservoir::setLayerExternalField) - .def("getMagnetisation", &Reservoir::getMagnetisation); + // reservoir module + py::module reservoir_module = m.def_submodule( + "reservoir", "A reservoir submodule for joining MTJ junctions"); + py::class_(reservoir_module, "GroupInteraction") + .def(py::init, std::vector, std::string>(), + "coordinateMatrix"_a, "junctionList"_a, "topId"_a = "free") + .def("setInteractionFunction", &GroupInteraction::setInteractionFunction) + .def("runSimulation", &GroupInteraction::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11) + .def("clearLogs", &GroupInteraction::clearLogs) + .def("getLog", + py::overload_cast(&GroupInteraction::getLog)) + .def("getLog", py::overload_cast<>(&GroupInteraction::getLog)); - // generator module - py::module generator_module = - m.def_submodule("noise", "Submodule with noise generation functions"); - py::class_>(generator_module, "BufferedAlphaNoise") - .def(py::init(), "bufferSize"_a, - "alpha"_a, "std"_a, "scale"_a) - .def("fillBuffer", &BufferedAlphaNoise::fillBuffer) - .def("tick", &BufferedAlphaNoise::tick); - py::class_>(generator_module, "VectorAlphaNoise") - .def(py::init(), - "bufferSize"_a, "alpha"_a, "std"_a, "scale"_a, "axis"_a = Axis::all) - .def("tickVector", &VectorAlphaNoise::tickVector) - .def("tick", &VectorAlphaNoise::tick) - .def("getPrevSample", &VectorAlphaNoise::getPrevSample) - .def("getScale", &VectorAlphaNoise::getScale); + py::class_(reservoir_module, "Reservoir") + .def(py::init(), "coordinateMatrix"_a, + "layerMatrix"_a) + .def("runSimulation", &Reservoir::runSimulation) + .def("clearLogs", &Reservoir::clearLogs) + .def("saveLogs", &Reservoir::saveLogs) + .def("getLayer", &Reservoir::getLayer) + .def("setAllExternalField", &Reservoir::setAllExternalField) + .def("setLayerAnisotropy", &Reservoir::setLayerAnisotropy) + .def("setLayerExternalField", &Reservoir::setLayerExternalField) + .def("getMagnetisation", &Reservoir::getMagnetisation); - // LLGB module - auto llgb_module = m.def_submodule("llgb", "A submodule for LLGB junctions"); - llgb_module.def("MFAWeissCurie", &LLGB::MFAWeissCurie, "me"_a, "T"_a, - "J0"_a, "relax"_a = 0.2, "tolerance"_a = 1e-6, - "maxIter"_a = 1000); - llgb_module.def("langevin", &LLGB::langevin); - llgb_module.def("langevinDerivative", &LLGB::langevinDerivative); - py::class_(llgb_module, "LLGBLayer") - .def(py::init &, double, double, - double, double>(), - "id"_a, "mag"_a, "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, - "demagTensor"_a, "damping"_a, "Tc"_a, "susceptibility"_a, "me"_a) - // setters - .def("setTemperatureDriver", &DLLGBLayer::setTemperatureDriver) - .def("setExternalFieldDriver", &DLLGBLayer::setExternalFieldDriver) - .def("setAnisotropyDriver", &DLLGBLayer::setAnisotropyDriver); + // generator module + py::module generator_module = + m.def_submodule("noise", "Submodule with noise generation functions"); + py::class_>(generator_module, "BufferedAlphaNoise") + .def(py::init(), "bufferSize"_a, + "alpha"_a, "std"_a, "scale"_a) + .def("fillBuffer", &BufferedAlphaNoise::fillBuffer) + .def("tick", &BufferedAlphaNoise::tick); + py::class_>(generator_module, "VectorAlphaNoise") + .def(py::init(), + "bufferSize"_a, "alpha"_a, "std"_a, "scale"_a, "axis"_a = Axis::all) + .def("tickVector", &VectorAlphaNoise::tickVector) + .def("tick", &VectorAlphaNoise::tick) + .def("getPrevSample", &VectorAlphaNoise::getPrevSample) + .def("getScale", &VectorAlphaNoise::getScale); - py::class_(llgb_module, "LLGBJunction") - .def(py::init>(), "layers"_a) - .def("runSimulation", &DLLGBJunction::runSimulation, "totalTime"_a, - "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11, "log"_a = false, - "solverMode"_a = HEUN) - .def("setLayerTemperatureDriver", - &DLLGBJunction::setLayerTemperatureDriver) - .def("setLayerExternalFieldDriver", - &DLLGBJunction::setLayerExternalFieldDriver) - .def("saveLogs", &DLLGBJunction::saveLogs) - .def("getLog", &DLLGBJunction::getLog) - .def("clearLog", &DLLGBJunction::clearLog); + // LLGB module + auto llgb_module = m.def_submodule("llgb", "A submodule for LLGB junctions"); + llgb_module.def("MFAWeissCurie", &LLGB::MFAWeissCurie, "me"_a, "T"_a, + "J0"_a, "relax"_a = 0.2, "tolerance"_a = 1e-6, + "maxIter"_a = 1000); + llgb_module.def("langevin", &LLGB::langevin); + llgb_module.def("langevinDerivative", &LLGB::langevinDerivative); + + py::class_(llgb_module, "LLGBLayer") + .def(py::init &, double, double, + double, double>(), + "id"_a, "mag"_a, "anis"_a, "Ms"_a, "thickness"_a, "cellSurface"_a, + "demagTensor"_a, "damping"_a, "Tc"_a, "susceptibility"_a, "me"_a) + // setters + .def("setTemperatureDriver", &DLLGBLayer::setTemperatureDriver) + .def("setExternalFieldDriver", &DLLGBLayer::setExternalFieldDriver) + .def("setAnisotropyDriver", &DLLGBLayer::setAnisotropyDriver); + + py::class_(llgb_module, "LLGBJunction") + .def(py::init>(), "layers"_a) + .def("runSimulation", &DLLGBJunction::runSimulation, "totalTime"_a, + "timeStep"_a = 1e-13, "writeFrequency"_a = 1e-11, "log"_a = false, + "solverMode"_a = HEUN) + .def("setLayerTemperatureDriver", + &DLLGBJunction::setLayerTemperatureDriver) + .def("setLayerExternalFieldDriver", + &DLLGBJunction::setLayerExternalFieldDriver) + .def("saveLogs", &DLLGBJunction::saveLogs) + .def("getLog", &DLLGBJunction::getLog) + .def("clearLog", &DLLGBJunction::clearLog); } From 6a35567bf1a6c2cb797e401c9d4a5f276f845c8d Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Tue, 5 Nov 2024 17:22:23 +0100 Subject: [PATCH 36/44] linearised resistance added --- cmtj/utils/resistance.py | 135 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index 1cc9299..dd09fd9 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -1,6 +1,7 @@ from typing import Union import numpy as np +import sympy as sym from .filters import Filters @@ -165,3 +166,137 @@ def angular_calculate_resistance_gmr( ] ) return compute_gmr(Rp, Rap, m1, m2) + + +def calculate_linearised_resistance( + GMR: float, + AMR: list[float], + SMR: list[float], +): + """ + Compute the resistance of the two FM bilayer system from the linearised angles. + :param GMR: GMR + :param AMR: AMR + :param SMR: SMR + :param stationary_angles: stationary angles [t1, p1, t2, p2] + :param linearised_angles: linearised angles [dt1, dp1, dt2, dp2] + """ + theta1 = sym.Symbol(r"\theta_1") + phi1 = sym.Symbol(r"\phi_1") + theta2 = sym.Symbol(r"\theta_2") + phi2 = sym.Symbol(r"\phi_2") + m1 = sym.Matrix( + [ + sym.sin(theta1) * sym.cos(phi1), + sym.sin(theta1) * sym.sin(phi1), + sym.cos(theta1), + ] + ) + m2 = sym.Matrix( + [ + sym.sin(theta2) * sym.cos(phi2), + -sym.sin(theta2) * sym.sin(phi2), + sym.cos(theta2), + ] + ) + GMR_resistance = GMR * (1 - (m1.dot(m2))) / 2.0 + + Rxx1 = AMR[0] * m1[0] ** 2 + SMR[0] * m1[1] ** 2 + Rxx2 = AMR[1] * m2[0] ** 2 + SMR[1] * m2[1] ** 2 + return Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 + + +def calculate_linearised_resistance_parallel( + GMR: float, + AMR: list[float], + SMR: list[float], + stationary_angles: list[float], + linearised_angles: list[float], +): + """ + Compute the parallel resistance of the two FM bilayer system from the linearised angles. + :param GMR: GMR + :param AMR: AMR + :param SMR: SMR + :param stationary_angles: stationary angles [t1, p1, t2, p2] + :param linearised_angles: linearised angles [dt1, dp1, dt2, dp2] + """ + t01, p01 = stationary_angles[:2] + t02, p02 = stationary_angles[2:] + dt1, dp1 = linearised_angles[:2] + dt2, dp2 = linearised_angles[2:] + Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 = calculate_linearised_resistance(GMR, AMR, SMR) + Rparallel = GMR_resistance + if any(AMR) or any(SMR): + Rparallel += (Rxx1 * Rxx2) / (Rxx1 + Rxx2) + dRparallel = ( + sym.diff(Rparallel, theta1) * dt1 + + sym.diff(Rparallel, phi1) * dp1 + + sym.diff(Rparallel, theta2) * dt2 + + sym.diff(Rparallel, phi2) * dp2 + ) + + dRparallel = dRparallel.subs( + { + theta1: t01, + phi1: p01, + theta2: t02, + phi2: p02, + } + ).evalf() + Rparallel = Rparallel.subs( + { + theta1: t01, + phi1: p01, + theta2: t02, + phi2: p02, + } + ).evalf() + return dRparallel, Rparallel + + +def calculate_linearised_resistance_series( + GMR: float, + AMR: list[float], + SMR: list[float], + stationary_angles: list[float], + linearised_angles: list[float], +): + """ + Compute the resistance of the two FM bilayer system from the linearised angles. + :param GMR: GMR + :param AMR: AMR + :param SMR: SMR + :param stationary_angles: stationary angles [t1, p1, t2, p2] + :param linearised_angles: linearised angles [dt1, dp1, dt2, dp2] + """ + t01, p01 = stationary_angles[:2] + t02, p02 = stationary_angles[2:] + dt1, dp1 = linearised_angles[:2] + dt2, dp2 = linearised_angles[2:] + Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 = calculate_linearised_resistance(GMR, AMR, SMR) + Rseries = GMR_resistance + Rxx1 + Rxx2 + dRseries = ( + sym.diff(Rseries, theta1) * dt1 + + sym.diff(Rseries, phi1) * dp1 + + sym.diff(Rseries, theta2) * dt2 + + sym.diff(Rseries, phi2) * dp2 + ) + + dRseries = dRseries.subs( + { + theta1: t01, + phi1: p01, + theta2: t02, + phi2: p02, + } + ).evalf() + Rseries = Rseries.subs( + { + theta1: t01, + phi1: p01, + theta2: t02, + phi2: p02, + } + ).evalf() + return dRseries, Rseries From bdd579f5fc93da3c1abcd0aa149799928a689c41 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 11 Nov 2024 14:30:30 +0100 Subject: [PATCH 37/44] readme update and stubs --- README.md | 31 +++++++ cmtj/reservoir/__init__.pyi | 163 ++++++++++++++++++++++++++++++++++++ core/reservoir.hpp | 59 +++++++++++-- python/cmtj.cpp | 4 + 4 files changed, 251 insertions(+), 6 deletions(-) create mode 100644 cmtj/reservoir/__init__.pyi diff --git a/README.md b/README.md index c87648d..6303dcf 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - [CMTJ](#cmtj) - [Table of contents](#table-of-contents) - [Short description](#short-description) + - [What can you simulate?](#what-can-you-simulate) - [Web GUI](#web-gui) - [Quickstart](#quickstart) - [Installation :rocket:](#installation-rocket) @@ -34,6 +35,36 @@ The `cmtj` name may be misleading -- the MTJ (Magnetic Tunnel Junctions) are not The library allows for macromagnetic simulation of various multilayer spintronic structures. The package uses C++ implementation of (s)LLGS (stochastic Landau-Lifschitz-Gilbert-Slonczewski) equation with various field contributions included for instance: anisotropy, interlayer exchange coupling, demagnetisation, dipole fields etc. It is also possible to connect devices in parallel or in series to have electrically coupled arrays. +### What can you simulate? + +Below is a brief list of examples (it's not exhaustive! Check the docs for more). + +**Magnetic devices:** + +- Magnetic Tunnel Junctions + - Voltage-Driven Magnetic Tunnel Junctions + - Spin-Torque Oscillators + - VCMA sensors and devices + - Magnetic Tunnel Junction Arrays +- SOT devices + - Current-Driven SOT +- Reservoirs (diploe coupling) +- Electrically coupled MTJs +- Base equations + - Landau-Lifshitz-Gilbert-Slonczewski equation + - Stochastic Landau-Lifshitz-Gilbert-Slonczewski equation + - Landau-Lifshitz-Gilbert-Bloch equation +- Domain wall motion + +**Experimental methods:** + +Some of the experimental methods available: + +- PIMM +- Spin-Diode +- CIMS +- R(H), M(H) + ## Web GUI Check out the [streamlit hosted demo here](http://cmtj-simulations.streamlit.app/). You can simulate PIMM spectra and Spin-Diode spectra there. Let us know if you have any issues with the demo. diff --git a/cmtj/reservoir/__init__.pyi b/cmtj/reservoir/__init__.pyi new file mode 100644 index 0000000..a6b0913 --- /dev/null +++ b/cmtj/reservoir/__init__.pyi @@ -0,0 +1,163 @@ +from typing import Callable, overload + +import cmtj + +class GroupInteraction: + def __init__( + self, + coordinateMatrix: list[cmtj.CVector], + junctionList: list[cmtj.Junction], + topId: str = "free", + ) -> None: + """Initialize GroupInteraction for coupled junctions. + + :param coordinateMatrix: List of position vectors for each junction + :param junctionList: List of junctions to couple + :param topId: ID of the top layer to use for interactions (default: "free") + :raises RuntimeError: If coordinate and junction lists have different sizes or are empty + """ + ... + + def clearLogs(self) -> None: + """Clear the logs""" + ... + + @overload + def getLog(self, junctionIndex: int) -> dict[str, list[float]]: + """Get the logs for a specific junction. + + :param junctionIndex: Index of the junction + :raises RuntimeError: If junction index is out of bounds + :return: Dictionary containing log data + """ + ... + + @overload + def getLog(self) -> dict[str, list[float]]: + """Get the logs for all junctions. + + :return: Dictionary containing log data + """ + ... + + def runSimulation(self, totalTime: float, timeStep: float = 1e-13, writeFrequency: float = 1e-13) -> None: + """Run the coupled simulation. + + :param totalTime: Total simulation time + :param timeStep: Time step for integration + :param writeFrequency: How often to write data to logs + :raises RuntimeError: If timeStep > writeFrequency or junctions have incompatible solver modes + """ + ... + + def setInteractionFunction( + self, + function: Callable[[cmtj.CVector, cmtj.CVector, cmtj.Layer, cmtj.Layer], cmtj.CVector], + ) -> None: + """Set the interaction function for the coupled junctions. + + :param function: Interaction function. + Either `computeDipoleInteraction` or `computeDipoleInteractionNoumra` or `nullDipoleInteraction` + or provide your own custom function. + """ + ... + +class Reservoir: + def __init__( + self, + coordinateMatrix: list[list[cmtj.CVector]], + layerMatrix: list[list[cmtj.Layer]], + ) -> None: + """Initialize Reservoir simulation. + + :param coordinateMatrix: 2D matrix of position vectors + :param layerMatrix: 2D matrix of magnetic layers + """ + ... + + def clearLogs(self) -> None: ... + def getLayer(self, arg0: int) -> cmtj.Layer: + """Get layer at the specified index (using row-major ordering). + + :param arg0: Index of the layer + :return: Layer object + """ + ... + + def getMagnetisation(self, arg0: int) -> cmtj.CVector: + """Get magnetization vector for layer at specified index (using row-major ordering). + + :param arg0: Index of the layer + :return: Magnetization vector + """ + ... + + def runSimulation(self, totalTime: float, timeStep: float) -> None: + """Run reservoir simulation and log data. + + :param totalTime: Total simulation time + :param timeStep: Integration time step + """ + ... + + def saveLogs(self, filename: str) -> None: + """Save simulation logs to file. + + :param filename: Path to save the log file. Empty string will skip saving. + """ + ... + + def setAllExternalField(self, arg0: cmtj.AxialDriver) -> None: + """Set external field for all layers. + + :param arg0: External field driver + """ + ... + + def setLayerAnisotropy(self, arg0: int, arg1: cmtj.ScalarDriver) -> None: + """Set anisotropy for specific layer. + + :param arg0: Layer index + :param arg1: Anisotropy driver + """ + ... + + def setLayerExternalField(self, arg0: int, arg1: cmtj.AxialDriver) -> None: + """Set external field for specific layer. + + :param arg0: Layer index + :param arg1: External field driver + """ + ... + +def computeDipoleInteraction( + r1: cmtj.CVector, r2: cmtj.CVector, layer1: cmtj.Layer, layer2: cmtj.Layer +) -> cmtj.CVector: + """Compute dipole interaction between two junctions (Kanao et al. 2019 PRA). + + :param r1: Position vector of the first junction + :param r2: Position vector of the second junction + :param layer1: Magnetic layer of the first junction + :param layer2: Magnetic layer of the second junction + + :return: Dipole interaction vector + """ + ... + +def computeDipoleInteractionNoumra( + r1: cmtj.CVector, r2: cmtj.CVector, layer1: cmtj.Layer, layer2: cmtj.Layer +) -> cmtj.CVector: + """Compute dipole interaction between two junctions (Nomura et al. 2019 JJAP). + + :param r1: Position vector of the first junction + :param r2: Position vector of the second junction + :param layer1: Magnetic layer of the first junction + :param layer2: Magnetic layer of the second junction + + :return: Dipole interaction vector + """ + ... + +def nullDipoleInteraction(r1: cmtj.CVector, r2: cmtj.CVector, layer1: cmtj.Layer, layer2: cmtj.Layer) -> cmtj.CVector: + """Compute null dipole interaction between two junctions.""" + ... diff --git a/core/reservoir.hpp b/core/reservoir.hpp index c765d8d..5b14335 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -29,17 +29,54 @@ void comb(int N, int K) { std::cout << std::endl; } while (std::prev_permutation(bitmask.begin(), bitmask.end())); } + +typedef std::array, 3> tensor; +typedef std::vector tensorList; template using solverFn = void (Layer::*)(T t, T timeStep, const CVector &bottom, const CVector &top); template using runnerFn = void (Junction::*)(solverFn &functor, T &t, T &timeStep); +const tensor getDipoleTensorFromRelPositions(const CVector &r1, + const CVector &r2) { + const CVector rij = r1 - r2; // 1-2 distance vector + const double r_mag = pow(rij.length(), 2); + const double mult = 3 / (4 * M_PI * pow(rij.length(), 5)); + const tensor dipoleTensor = {CVector(pow(rij.x, 2) - (r_mag / 3), + rij.x * rij.y, rij.x * rij.z) * + mult, + CVector(rij.x * rij.y, + pow(rij.y, 2) - (r_mag / 3), + rij.y * rij.z) * + mult, + CVector(rij.x * rij.z, rij.y * rij.z, + pow(rij.z, 2) - (r_mag / 3)) * + mult}; + return dipoleTensor; +} + typedef std::function( const CVector &, const CVector &, const Layer &, const Layer &)> interactionFunction; +CVector nullDipoleInteraction(const CVector &, + const CVector &, + const Layer &, + const Layer &) { + return CVector(0, 0, 0); +} + +/** + * @brief Compute dipole interaction between two junctions. + * From: Kanao et al, Reservoir Computing on Spin-Torque Oscillator Array (2019) + * PRA + * @param r1 1st junction position + * @param r2 2nd junction position + * @param layer1 1st junction layer + * @param layer2 2nd junction layer + */ CVector computeDipoleInteraction(const CVector &r1, const CVector &r2, const Layer &layer1, @@ -56,11 +93,26 @@ CVector computeDipoleInteraction(const CVector &r1, return prefactor * (3 * c_dot(m1, rij) * rij / r5 - m1 / r3); } +/** + * @brief Compute dipole interaction between two junctions. + * From: Nomura et al, Reservoir computing with dipole-coupled nanomagnets + * (2019) JJAP + * @param r1 1st junction position + * @param r2 2nd junction position + * @param layer1 1st junction layer + * @param layer2 2nd junction layer + */ CVector computeDipoleInteractionNoumra(const CVector &r1, const CVector &r2, const Layer &layer1, const Layer &layer2) { - return computeDipoleInteraction(r1, r2, layer1, layer2); + const tensor dipoleTensor = getDipoleTensorFromRelPositions(r1, r2); + const double V = layer2.thickness * layer2.cellSurface; + const CVector dipoleVector = calculate_tensor_interaction( + layer2.mag, dipoleTensor, (layer2.Ms / MAGNETIC_PERMEABILITY) * V); + // in the paper they don't multiply explicitly by V, but it's necessary for + // units to match + return dipoleVector; } class GroupInteraction { @@ -85,8 +137,6 @@ class GroupInteraction { this->junctionList[i].getLayer(this->topId), this->junctionList[j].getLayer(this->topId)); } - std::cout << "H_extra: " << H_extra << " " << H_extra.length() - << std::endl; this->junctionList[i].setLayerReservedInteractionField( this->topId, AxialDriver(H_extra)); } @@ -179,9 +229,6 @@ class GroupInteraction { } }; -typedef std::array, 3> tensor; -// typedef std::vector> tensorMatrix; -typedef std::vector tensorList; class Reservoir { private: // log stuff diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 7dd8538..ca21bd7 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -378,6 +378,10 @@ PYBIND11_MODULE(cmtj, m) { // reservoir module py::module reservoir_module = m.def_submodule( "reservoir", "A reservoir submodule for joining MTJ junctions"); + reservoir_module.def("nullDipoleInteraction", &nullDipoleInteraction, "r1"_a, "r2"_a, "layer1"_a, "layer2"_a); + reservoir_module.def("computeDipoleInteraction", &computeDipoleInteraction, "r1"_a, "r2"_a, "layer1"_a, "layer2"_a); + reservoir_module.def("computeDipoleInteractionNoumra", &computeDipoleInteractionNoumra, "r1"_a, "r2"_a, "layer1"_a, "layer2"_a); + py::class_(reservoir_module, "GroupInteraction") .def(py::init, std::vector, std::string>(), "coordinateMatrix"_a, "junctionList"_a, "topId"_a = "free") From f274b361534b4fc209e63d5fe47693f2e919b316 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 11 Nov 2024 15:06:12 +0100 Subject: [PATCH 38/44] fixing nomura style dipole tensor --- CHANGELOG.md | 5 +++-- core/reservoir.hpp | 6 +++--- setup.py | 2 +- 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 130e5e7..13fb96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,14 @@ # Changelog -# 1.5.5 +# 1.6.0 - Extended the `Stack` models allowing for non-symmetric coupling between devices. - `Stack` current drivers can now be of any type and are adequately scaled. + `Stack` current drivers can now be of any type and are adequately scaled. - Custom definition of the `ScalarDriver` is now possible and documented. - Fixed a bug in the `Stack` class which inverted the connection order of in-series connections. - Exposed IDMI interaction to Layer and Junction classes. - Added `getLayer` method to the `Junction` class and `getJunction` method to the `Stack` class that return a reference to the object. +- Fixed and expanded the `reservoir` module. Now, `GroupInteraction` can use any dipole interaction function, with 3 provided as default: `computeDipoleInteraction`, `computeDipoleInteractionNoumra` and `nullDipoleInteraction` (0 dipole tensor). # 1.5.0-1.5.4 diff --git a/core/reservoir.hpp b/core/reservoir.hpp index 5b14335..39a2ae9 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -108,8 +108,9 @@ CVector computeDipoleInteractionNoumra(const CVector &r1, const Layer &layer2) { const tensor dipoleTensor = getDipoleTensorFromRelPositions(r1, r2); const double V = layer2.thickness * layer2.cellSurface; - const CVector dipoleVector = calculate_tensor_interaction( - layer2.mag, dipoleTensor, (layer2.Ms / MAGNETIC_PERMEABILITY) * V); + // remember that calculate_tensor_interaction normalises Ms by 1/mu0 + const CVector dipoleVector = + calculate_tensor_interaction(layer2.mag, dipoleTensor, layer2.Ms * V); // in the paper they don't multiply explicitly by V, but it's necessary for // units to match return dipoleVector; @@ -232,7 +233,6 @@ class GroupInteraction { class Reservoir { private: // log stuff - std::vector logKeys; std::unordered_map> reservoirLog; // reservoir matrices diff --git a/setup.py b/setup.py index 7652927..58e7dc2 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ from setuptools import Extension, find_namespace_packages, setup from setuptools.command.build_ext import build_ext -__version__ = "1.5.5" +__version__ = "1.6.0" """ As per https://github.com/pybind/python_example From e13c3d989dd390793ec7189c5f3a5f122e6ad3d3 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 11 Nov 2024 15:15:24 +0100 Subject: [PATCH 39/44] small PR fixes --- README.md | 3 ++- cmtj/utils/resistance.py | 4 +++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6303dcf..729235a 100644 --- a/README.md +++ b/README.md @@ -48,7 +48,8 @@ Below is a brief list of examples (it's not exhaustive! Check the docs for more) - Magnetic Tunnel Junction Arrays - SOT devices - Current-Driven SOT -- Reservoirs (diploe coupling) +- Advanced device coupling +- Reservoirs (dipole coupling) - Electrically coupled MTJs - Base equations - Landau-Lifshitz-Gilbert-Slonczewski equation diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index dd09fd9..32144cc 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -3,6 +3,8 @@ import numpy as np import sympy as sym +from new_sb import EPS + from .filters import Filters @@ -228,7 +230,7 @@ def calculate_linearised_resistance_parallel( Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 = calculate_linearised_resistance(GMR, AMR, SMR) Rparallel = GMR_resistance if any(AMR) or any(SMR): - Rparallel += (Rxx1 * Rxx2) / (Rxx1 + Rxx2) + Rparallel += (Rxx1 * Rxx2) / (Rxx1 + Rxx2 + EPS) dRparallel = ( sym.diff(Rparallel, theta1) * dt1 + sym.diff(Rparallel, phi1) * dp1 From f952c2f7fb77fc17fe10a9b21e638288317023df Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Mon, 11 Nov 2024 15:19:35 +0100 Subject: [PATCH 40/44] small PR fixes --- cmtj/utils/resistance.py | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/cmtj/utils/resistance.py b/cmtj/utils/resistance.py index 32144cc..94c3fff 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -3,12 +3,14 @@ import numpy as np import sympy as sym -from new_sb import EPS - from .filters import Filters +EPS = np.finfo("float64").resolution + -def compute_sd(dynamic_r: np.ndarray, dynamic_i: np.ndarray, integration_step: float) -> np.ndarray: +def compute_sd( + dynamic_r: np.ndarray, dynamic_i: np.ndarray, integration_step: float +) -> np.ndarray: """Computes the SD voltage. :param dynamic_r: magnetoresistance from log :param dynamic_i: excitation current @@ -49,7 +51,11 @@ def compute_resistance( for i in range(number_of_layers): w_l = w[i] / l[i] SxAll[i] = Rx0[i] + (AMR[i] * m[i, 0] ** 2 + SMR[i] * m[i, 1] ** 2) - SyAll[i] = Ry0[i] + 0.5 * AHE[i] * m[i, 2] + (w_l) * (SMR[i] - AMR[i]) * m[i, 0] * m[i, 1] + SyAll[i] = ( + Ry0[i] + + 0.5 * AHE[i] * m[i, 2] + + (w_l) * (SMR[i] - AMR[i]) * m[i, 0] * m[i, 1] + ) return SxAll, SyAll @@ -71,7 +77,10 @@ def calculate_magnetoresistance(Rp: float, Rap: float, m: np.ndarray): if not isinstance(m, np.ndarray): m = np.asarray(m) if m.shape[0] != 2: - raise ValueError("The magnetoresistance can only be computed for 2 layers" f". Current shape {m.shape}") + raise ValueError( + "The magnetoresistance can only be computed for 2 layers" + f". Current shape {m.shape}" + ) return Rp + 0.5 * (Rap - Rp) * np.sum(m[0] * m[1], axis=0) @@ -227,7 +236,9 @@ def calculate_linearised_resistance_parallel( t02, p02 = stationary_angles[2:] dt1, dp1 = linearised_angles[:2] dt2, dp2 = linearised_angles[2:] - Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 = calculate_linearised_resistance(GMR, AMR, SMR) + Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 = ( + calculate_linearised_resistance(GMR, AMR, SMR) + ) Rparallel = GMR_resistance if any(AMR) or any(SMR): Rparallel += (Rxx1 * Rxx2) / (Rxx1 + Rxx2 + EPS) @@ -276,7 +287,9 @@ def calculate_linearised_resistance_series( t02, p02 = stationary_angles[2:] dt1, dp1 = linearised_angles[:2] dt2, dp2 = linearised_angles[2:] - Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 = calculate_linearised_resistance(GMR, AMR, SMR) + Rxx1, Rxx2, GMR_resistance, theta1, phi1, theta2, phi2 = ( + calculate_linearised_resistance(GMR, AMR, SMR) + ) Rseries = GMR_resistance + Rxx1 + Rxx2 dRseries = ( sym.diff(Rseries, theta1) * dt1 From 212ad8bc8cfd5aec0eed535b9d0816c085208a22 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Sun, 24 Nov 2024 18:49:10 +0100 Subject: [PATCH 41/44] formatting and checking for uniqness of the coordinate vectors --- cmtj/reservoir/__init__.pyi | 12 ++++++------ core/reservoir.hpp | 9 +++++++++ 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/cmtj/reservoir/__init__.pyi b/cmtj/reservoir/__init__.pyi index a6b0913..5f75dd0 100644 --- a/cmtj/reservoir/__init__.pyi +++ b/cmtj/reservoir/__init__.pyi @@ -107,26 +107,26 @@ class Reservoir: """ ... - def setAllExternalField(self, arg0: cmtj.AxialDriver) -> None: + def setAllExternalField(self, driver: cmtj.AxialDriver) -> None: """Set external field for all layers. - :param arg0: External field driver + :param driver: External field driver """ ... - def setLayerAnisotropy(self, arg0: int, arg1: cmtj.ScalarDriver) -> None: + def setLayerAnisotropy(self, arg0: int, driver: cmtj.ScalarDriver) -> None: """Set anisotropy for specific layer. :param arg0: Layer index - :param arg1: Anisotropy driver + :param driver: Anisotropy driver """ ... - def setLayerExternalField(self, arg0: int, arg1: cmtj.AxialDriver) -> None: + def setLayerExternalField(self, arg0: int, driver: cmtj.AxialDriver) -> None: """Set external field for specific layer. :param arg0: Layer index - :param arg1: External field driver + :param driver: External field driver """ ... diff --git a/core/reservoir.hpp b/core/reservoir.hpp index 39a2ae9..cbb5082 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -163,6 +163,15 @@ class GroupInteraction { this->noElements = junctionList.size(); this->coordinateMatrix = std::move(coordinateMatrix); this->junctionList = std::move(junctionList); + + // check that all vectors in coordinateMatrix are unique + for (size_t i = 0; i < this->coordinateMatrix.size(); i++) { + for (size_t j = i + 1; j < this->coordinateMatrix.size(); j++) { + if (this->coordinateMatrix[i] == this->coordinateMatrix[j]) { + throw std::runtime_error("Coordinate vectors must be unique!"); + } + } + } } void setInteractionFunction(interactionFunction interactionFunc) { From dfe6816d3f62464cd072ab5483e5ac13e5db1689 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Fri, 6 Dec 2024 20:19:30 +0100 Subject: [PATCH 42/44] fixing some of the issues with the out of bounds references --- core/reservoir.hpp | 16 ++-- core/stack.hpp | 2 +- python/cmtj.cpp | 5 +- tests/test_drivers.py | 2 +- tests/test_group_interaction.py | 135 ++++++++++++++++++++++++++++++++ tests/test_stack.py | 91 +++++++++++++++++++++ 6 files changed, 240 insertions(+), 11 deletions(-) create mode 100644 tests/test_group_interaction.py diff --git a/core/reservoir.hpp b/core/reservoir.hpp index cbb5082..8154aef 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -41,6 +41,10 @@ using runnerFn = void (Junction::*)(solverFn &functor, T &t, T &timeStep); const tensor getDipoleTensorFromRelPositions(const CVector &r1, const CVector &r2) { const CVector rij = r1 - r2; // 1-2 distance vector + if (rij.length() < 1e-10) { + throw std::runtime_error( + "Points are too close for stable dipole calculation"); + } const double r_mag = pow(rij.length(), 2); const double mult = 3 / (4 * M_PI * pow(rij.length(), 5)); const tensor dipoleTensor = {CVector(pow(rij.x, 2) - (r_mag / 3), @@ -82,6 +86,10 @@ CVector computeDipoleInteraction(const CVector &r1, const Layer &layer1, const Layer &layer2) { const DVector rij = r1 - r2; // 1-2 distance vector + if (rij.length() < 1e-10) { + throw std::runtime_error( + "Points are too close for stable dipole calculation"); + } const double r3 = pow(rij.length(), 3); const double r5 = pow(rij.length(), 5); const Layer ref_magnetic_moment = layer2; @@ -228,7 +236,7 @@ class GroupInteraction { std::unordered_map> & getLog(unsigned int id) { - if (id <= this->junctionList.size()) { + if (id < this->junctionList.size()) { return this->junctionList[id].getLog(); } throw std::runtime_error("Asking for id of a non-existing junction!"); @@ -326,12 +334,6 @@ class Reservoir { CVector(rij.x * rij.z, rij.y * rij.z, pow(rij.z, 2) - (r_mag / 3)) * mult}; - // print dipole tensor - // std::cout << "Dipole tensor: " << std::endl; - // for (auto& row : dipoleTensor) - // { - // std::cout << row << " " << std::endl; - // } return dipoleTensor; } diff --git a/core/stack.hpp b/core/stack.hpp index f9a978c..19c65ea 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -180,7 +180,7 @@ template class Stack { return this->stackLog; } std::unordered_map> &getLog(unsigned int id) { - if (id <= this->junctionList.size()) { + if (id < this->junctionList.size()) { return this->junctionList[id].getLog(); } throw std::runtime_error("Asking for id of a non-existing junction!"); diff --git a/python/cmtj.cpp b/python/cmtj.cpp index ca21bd7..7b12417 100644 --- a/python/cmtj.cpp +++ b/python/cmtj.cpp @@ -383,7 +383,7 @@ PYBIND11_MODULE(cmtj, m) { reservoir_module.def("computeDipoleInteractionNoumra", &computeDipoleInteractionNoumra, "r1"_a, "r2"_a, "layer1"_a, "layer2"_a); py::class_(reservoir_module, "GroupInteraction") - .def(py::init, std::vector, std::string>(), + .def(py::init&, const std::vector&, const std::string&>(), "coordinateMatrix"_a, "junctionList"_a, "topId"_a = "free") .def("setInteractionFunction", &GroupInteraction::setInteractionFunction) .def("runSimulation", &GroupInteraction::runSimulation, "totalTime"_a, @@ -391,7 +391,8 @@ PYBIND11_MODULE(cmtj, m) { .def("clearLogs", &GroupInteraction::clearLogs) .def("getLog", py::overload_cast(&GroupInteraction::getLog)) - .def("getLog", py::overload_cast<>(&GroupInteraction::getLog)); + .def("getLog", py::overload_cast(&GroupInteraction::getLog), + py::return_value_policy::reference); py::class_(reservoir_module, "Reservoir") .def(py::init(), "coordinateMatrix"_a, diff --git a/tests/test_drivers.py b/tests/test_drivers.py index 30b7c8d..0e8510d 100644 --- a/tests/test_drivers.py +++ b/tests/test_drivers.py @@ -1,6 +1,6 @@ from cmtj import AxialDriver, CVector, Junction, Layer from cmtj import constantDriver, sineDriver - +import pytest def test_cvector_operators(): vec1 = (1.0, 2.0, 3.0) diff --git a/tests/test_group_interaction.py b/tests/test_group_interaction.py new file mode 100644 index 0000000..eae2bf1 --- /dev/null +++ b/tests/test_group_interaction.py @@ -0,0 +1,135 @@ +import numpy as np +import pytest +from cmtj import ( + CVector, + Layer, + Junction, + reservoir, +) + + +def create_test_junction(pos=(0, 0, 0), Ms=1e6): + """Helper function to create a test junction""" + mag = CVector(0, 0, 1) + anis = CVector(0, 0, 1) + demag = [CVector(0, 0, 0)] * 3 + + layer = Layer( + "free", + mag, + anis, + Ms=Ms, + thickness=2e-9, + cellSurface=np.pi * (20e-9) ** 2, + demagTensor=demag, + ) + return Junction([layer]), CVector(*pos) + + +def test_group_interaction_initialization(): + """Test basic initialization of GroupInteraction""" + j1, pos1 = create_test_junction((0, 0, 0)) + j2, pos2 = create_test_junction((100e-9, 0, 0)) + + # Should initialize successfully + group = reservoir.GroupInteraction([pos1, pos2], [j1, j2]) + + # Should fail with mismatched sizes + with pytest.raises(RuntimeError): + reservoir.GroupInteraction([pos1], [j1, j2]) + + # Should fail with empty lists + with pytest.raises(RuntimeError): + reservoir.GroupInteraction([], []) + + # Should fail with duplicate positions + with pytest.raises(RuntimeError): + reservoir.GroupInteraction([pos1, pos1], [j1, j2]) + + +def test_dipole_interactions(): + """Test different dipole interaction functions""" + j1, pos1 = create_test_junction((0, 0, 0)) + j2, pos2 = create_test_junction((100e-9, 0, 0)) + + # Test null interaction + h_null = reservoir.nullDipoleInteraction( + pos1, pos2, j1.getLayer("free"), j2.getLayer("free") + ) + assert h_null.x == 0 and h_null.y == 0 and h_null.z == 0 + + # Test regular dipole interaction + h_dipole = reservoir.computeDipoleInteraction( + pos1, pos2, j1.getLayer("free"), j2.getLayer("free") + ) + assert isinstance(h_dipole, CVector) + + # Test Noumra dipole interaction + h_noumra = reservoir.computeDipoleInteractionNoumra( + pos1, pos2, j1.getLayer("free"), j2.getLayer("free") + ) + assert isinstance(h_noumra, CVector) + + +def test_group_simulation(): + """Test running a simulation with group interaction""" + j1, pos1 = create_test_junction((0, 0, 0)) + j2, pos2 = create_test_junction((100e-9, 0, 0)) + + group = reservoir.GroupInteraction([pos1, pos2], [j1, j2]) + # Then test invalid indices + with pytest.raises( + (RuntimeError, IndexError, TypeError) + ): # Accept either exception type + group.getLog(-1) # Test negative index + + with pytest.raises((RuntimeError, IndexError)): # Accept either exception type + group.getLog(2) # Test out of bounds index + # Run a short simulation + total_time = 1e-9 + time_step = 1e-12 + group.runSimulation(total_time, time_step) + + # Check logs exist + log1 = group.getLog(0) + log2 = group.getLog(1) + assert len(log1["time"]) > 0 + assert len(log2["time"]) > 0 + + # Clear logs + group.clearLogs() + log1 = group.getLog(0) + log2 = group.getLog(1) + with pytest.raises(KeyError): + log1["time"] + with pytest.raises(KeyError): + log2["time"] + + +def test_interaction_functions(): + """Test setting different interaction functions""" + j1, pos1 = create_test_junction((0, 0, 0)) + j2, pos2 = create_test_junction((100e-9, 0, 0)) + + group = reservoir.GroupInteraction([pos1, pos2], [j1, j2]) + + # Test setting null interaction + group.setInteractionFunction(reservoir.nullDipoleInteraction) + + # Test setting Noumra interaction + group.setInteractionFunction(reservoir.computeDipoleInteractionNoumra) + + # Test setting regular dipole interaction + group.setInteractionFunction(reservoir.computeDipoleInteraction) + + +def test_invalid_log_access(): + """Test accessing invalid log indices""" + j1, pos1 = create_test_junction((0, 0, 0)) + j2, pos2 = create_test_junction((100e-9, 0, 0)) + + group = reservoir.GroupInteraction([pos1, pos2], [j1, j2]) + + # Should raise error for invalid index + with pytest.raises(RuntimeError): + group.getLog(2) diff --git a/tests/test_stack.py b/tests/test_stack.py index 10ca7df..de7de52 100644 --- a/tests/test_stack.py +++ b/tests/test_stack.py @@ -1,8 +1,27 @@ from cmtj.stack import ParallelStack, SeriesStack from cmtj.utils.procedures import ResistanceParameters +from cmtj import Layer, CVector from typing import Tuple from cmtj import Junction import pytest +import numpy as np + + +def test_invalid_stack_indices( + single_layer_mtj_fictious: Tuple[Junction, ResistanceParameters] +): + junction, _ = single_layer_mtj_fictious + with pytest.raises(RuntimeError, match="Asking for id of a non-existing junction!"): + ParallelStack([junction, junction]).getLog(2) + + with pytest.raises(RuntimeError, match="Asking for id of a non-existing junction!"): + SeriesStack([junction, junction]).getLog(10) + + with pytest.raises(TypeError): + ParallelStack([junction, junction]).getLog(-1) + + with pytest.raises(TypeError): + SeriesStack([junction, junction]).getLog(-10) def test_invalid_stack( @@ -41,3 +60,75 @@ def test_basic_series_stack(arg: Tuple[Junction, ResistanceParameters]): stack.runSimulation(5e-9, 1e-12, 1e-12) log = stack.getLog() assert "Resistance" in log.keys() + + +def test_stack_simulation_parameters(): + """Test stack behavior with invalid simulation parameters""" + junction = Junction( + [ + Layer( + "free", + CVector(0, 0, 1), + CVector(0, 0, 1), + Ms=1e6, + thickness=2e-9, + cellSurface=np.pi * (20e-9) ** 2, + demagTensor=[CVector(0, 0, 0)] * 3, + ) + ], + ) + with pytest.raises(RuntimeError, match="must have at least 2 junctions"): + ParallelStack([junction]) + with pytest.raises(RuntimeError, match="must have at least 2 junctions"): + SeriesStack([junction]) + + +@pytest.mark.parametrize( + "arg", ["single_layer_mtj_fictious", "two_layer_mtj"], indirect=True +) +def test_stack_log_consistency(arg: Tuple[Junction, ResistanceParameters]): + """Test that stack logs maintain consistency across simulations""" + junction, _ = arg + stack = ParallelStack([junction, junction]) + + # Run first simulation + stack.setCouplingStrength(0.5) + stack.runSimulation(5e-9, 1e-12, 1e-12) + log1 = stack.getLog() + + # Clear and run second simulation + stack.clearLogs() + stack.runSimulation(5e-9, 1e-12, 1e-12) + log2 = stack.getLog() + + # Verify log structure remains consistent + assert set(log1.keys()) == set(log2.keys()) + assert len(log1["time"]) == len(log2["time"]) + assert "Resistance" in log1 and "Resistance" in log2 + + +@pytest.mark.parametrize( + "arg", ["single_layer_mtj_fictious", "two_layer_mtj"], indirect=True +) +def test_stack_resistance_behavior(arg: Tuple[Junction, ResistanceParameters]): + """Test that resistance values follow expected patterns""" + junction, params = arg + parallel_stack = ParallelStack([junction, junction]) + series_stack = SeriesStack([junction, junction]) + + # Test with no coupling + parallel_stack.setCouplingStrength(0) + series_stack.setCouplingStrength(0) + + parallel_stack.runSimulation(5e-9, 1e-12, 1e-12) + series_stack.runSimulation(5e-9, 1e-12, 1e-12) + + p_log = parallel_stack.getLog() + s_log = series_stack.getLog() + + # Basic sanity checks for resistance values + assert all(r > 0 for r in p_log["Resistance"]) # Resistance should be positive + assert all(r > 0 for r in s_log["Resistance"]) + + # Series resistance should be larger than parallel + assert np.mean(s_log["Resistance"]) > np.mean(p_log["Resistance"]) From 6774b8b1ba8c0cf5be6221f29ff2e4e408629190 Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Fri, 6 Dec 2024 20:33:52 +0100 Subject: [PATCH 43/44] some basic tests --- tests/test_llgb.py | 88 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/test_llgb.py b/tests/test_llgb.py index fc319d2..852915c 100644 --- a/tests/test_llgb.py +++ b/tests/test_llgb.py @@ -31,3 +31,91 @@ def test_basic(): junction.runSimulation(sim_time, 1e-13, 1e-13) log = junction.getLog() assert "free_T" in log + + +def test_temperature_dependence(): + """Test magnetization response to temperature changes""" + Ms = 0.27 + Tc = 448 # Curie temperature + susceptibility = 0.04 + me = 0.9 + damping = 0.0275 + demag = [CVector(0, 0, 0), CVector(0, 0, 0), CVector(0, 0, 1)] + + layer = LLGBLayer( + "free", + CVector(1, 0, 0), + CVector(1, 0, 0), + Ms, + 2e-9, + 100e-9 * 100e-9, + demag, + damping, + Tc, + susceptibility, + me, + ) + junction = LLGBJunction([layer]) + + # Test at room temperature (300K) + junction.setLayerTemperatureDriver("all", ScalarDriver.getConstantDriver(300)) + junction.runSimulation(1e-9, 1e-13, 1e-13) + log_room = junction.getLog() + mx_room = log_room["free_mx"][-1] + + # Test near Curie temperature + junction.setLayerTemperatureDriver("all", ScalarDriver.getConstantDriver(Tc - 1)) + junction.runSimulation(1e-9, 1e-13, 1e-13) + log_hot = junction.getLog() + mx_hot = log_hot["free_mx"][-1] + + # Magnetization should be significantly reduced near Tc + assert abs(mx_hot) < abs(mx_room) + + +def test_multiple_layers(): + """Test LLGB with multiple layers""" + Ms = 0.27 + Tc = 448 + susceptibility = 0.04 + me = 0.9 + damping = 0.0275 + demag = [CVector(0, 0, 0), CVector(0, 0, 0), CVector(0, 0, 1)] + + layer1 = LLGBLayer( + "free1", + CVector(1, 0, 0), + CVector(1, 0, 0), + Ms, + 2e-9, + 100e-9 * 100e-9, + demag, + damping, + Tc, + susceptibility, + me, + ) + + layer2 = LLGBLayer( + "free2", + CVector(-1, 0, 0), # Opposite initial magnetization + CVector(1, 0, 0), + Ms, + 2e-9, + 100e-9 * 100e-9, + demag, + damping, + Tc, + susceptibility, + me, + ) + + junction = LLGBJunction([layer1, layer2]) + junction.setLayerTemperatureDriver("all", ScalarDriver.getConstantDriver(300)) + junction.runSimulation(5e-9, 1e-13, 1e-13) + + log = junction.getLog() + assert "free1_mx" in log + assert "free2_mx" in log + # Layers should maintain opposite magnetization + assert log["free1_mx"][-1] * log["free2_mx"][-1] < 0 From 51c8bd45b85a09e96350abd5e95dea2ede8bd1eb Mon Sep 17 00:00:00 2001 From: LemurPwned Date: Fri, 6 Dec 2024 20:46:37 +0100 Subject: [PATCH 44/44] update github workflow --- .github/workflows/main.yml | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index dd9d2a9..c76b620 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -2,14 +2,13 @@ name: Python Package Publication on: + pull_request: + types: [closed] + branches: [master] workflow_dispatch: inputs: release-version: required: true - dry-run: - required: true - default: true - type: boolean linux: type: boolean required: true @@ -37,7 +36,7 @@ jobs: with: python-versions: 'cp38-cp38 cp39-cp39 cp310-cp310 cp311-cp311' - name: upload wheel - if: ${{ !inputs.dry-run }} + if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' run: | python -m pip install --upgrade pip python -m pip install wheel setuptools twine @@ -68,13 +67,13 @@ jobs: python -m pip install wheel setuptools twine python setup.py bdist_wheel - name: upload wheel - if: ${{ !inputs.dry-run }} + if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' run: | twine upload dist/* continue-on-error: false release-build: - if: ${{ !inputs.dry-run }} + if: github.event.pull_request.merged == true || github.event_name == 'workflow_dispatch' needs: [ linux-build, other-os-build ] runs-on: ubuntu-latest steps: