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: diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e3d95f8..1bd61c9 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,30 +28,38 @@ 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 + - 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 + args: ["--fix"] + 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 hooks: - id: cppcheck - # - id: clang-format - # - id: oclint - # - id: uncrustify - - id: include-what-you-use + args: ["--check-level=exhaustive"] + files: ^(cmtj|core)/.*\.(cpp|hpp)$ + exclude: ^third_party/ | ^core/compute.hpp + - id: clang-format + args: [-i] + 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/CHANGELOG.md b/CHANGELOG.md index ff4115d..13fb96a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,24 @@ # Changelog -# 1.5.0 (WIP) +# 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. +- 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 - 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. +- 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 @@ -40,12 +51,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/README.md b/README.md index 2a4c38b..6fca613 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,37 @@ 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 +- Advanced device coupling +- Reservoirs (dipole 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](https://cmtj-app.streamlit.app/spectrum). @@ -151,13 +183,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: @@ -175,8 +210,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 66f5e1f..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,14 +28,13 @@ 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 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 @@ -46,13 +43,12 @@ 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. +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: - 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 @@ -61,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 @@ -72,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. @@ -84,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) @@ -113,18 +103,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, arg0: 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 + """ + ... + + @overload + 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 object with x, y, z components + """ + ... + @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 +180,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,23 +228,31 @@ 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: Parallel magnetoresistance + :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], - 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 @@ -220,16 +271,20 @@ class Junction: @overload def __init__(*args, **kwargs) -> Any: ... - def clearLog(self) -> Dict[str, Any]: + def clearLog(self) -> dict[str, Any]: """Reset current simulation state.""" ... - def getLayerMagnetisation(self, layer_id: str) -> CVector: ... - def getLog(self) -> Dict[str, List[float]]: + def getLayerMagnetisation(self, layerId: str) -> CVector: + """Get the magnetisation of a layer. + :param layerId: the layer id""" + ... + + 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, @@ -250,9 +305,7 @@ class Junction: """ ... - def setIECDriver( - self, bottom_layer: str, top_layer: 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. @@ -261,9 +314,7 @@ class Junction: """ ... - def setQuadIECDriver( - self, bottom_layer: str, top_layer: 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. @@ -272,48 +323,78 @@ 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, layerId: str, driver: ScalarDriver) -> None: + """Set a temperature driver for a layer. + :param layerId: the id of the layer. + :param driver: the temperature driver to be set. + """ + ... + + def setLayerAnisotropyDriver(self, layerId: str, driver: ScalarDriver) -> None: + """Set anisotropy driver for a layer. + :param layerId: the id of the layer. + :param driver: the anisotropy driver to be set. + """ + ... + + def setLayerCurrentDriver(self, layerId: str, driver: ScalarDriver) -> None: + """Set a current driver for a layer. + :param layerId: the layer id + :param driver: the driver + """ + ... + + def setLayerExternalFieldDriver(self, layerId: str, driver: 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 setLayerMagnetisation(self, layerId: str, mag: CVector) -> None: + """Set the magnetisation of a layer. + :param layerId: the layer id + :param mag: the magnetisation + """ + ... + @overload - def setLayerOerstedFieldDriver( - self, layer_id: str, driver: AxialDriver - ) -> None: ... - def setLayerDampingLikeTorqueDriver( - self, layer_id: str, driver: ScalarDriver - ) -> None: + def setLayerOerstedFieldDriver(self, layerId: str, driver: AxialDriver) -> None: + """Set an Oersted field driver for a layer. + :param layerId: the id of the layer. + :param driver: the field driver to be set. + """ + ... + + def setLayerDampingLikeTorqueDriver(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 - ) -> None: + def setLayerFieldLikeTorqueDriver(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 - ) -> None: + def setLayerOneFNoise(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 """ ... + 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, @@ -323,7 +404,7 @@ class Layer: Ms: float, thickness: float, cellSurface: float, - demagTensor: List[CVector], + demagTensor: list[CVector], temperature: float = ..., damping: float = ..., ) -> Layer: @@ -351,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, @@ -365,7 +446,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. """ ... @@ -378,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, @@ -410,25 +490,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 setTopDipoleTensor(self, tensor: List[CVector]) -> None: - """Set a dipole tensor from the top layer.""" + 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 setBottomDipoleTensor(self, tensor: List[CVector]) -> None: - """Set a dipole tensor from the bottom layer.""" + 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: + """Set a dipole tensor from the bottom layer. + :param tensor: the dipole tensor to be set. + """ ... def getId(self) -> str: @@ -438,6 +542,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 +550,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 """ ... @@ -458,8 +564,15 @@ 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": + 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) @@ -467,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. @@ -494,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) @@ -524,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: @@ -540,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: @@ -556,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 @@ -567,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 e06aea7..3033bf9 100644 --- a/cmtj/llgb/__init__.pyi +++ b/cmtj/llgb/__init__.pyi @@ -1,11 +1,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 +23,37 @@ class LLGBJunction: writeFrequency: float = ..., log: bool = ..., solverMode: cmtj.SolverMode = ..., - ) -> None: ... - def saveLogs(self, arg0: str) -> None: ... - def setLayerExternalFieldDriver( - self, arg0: str, arg1: cmtj.AxialDriver - ) -> None: ... - def setLayerTemperatureDriver(self, arg0: str, arg1: cmtj.ScalarDriver) -> 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, 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, @@ -29,15 +62,41 @@ class LLGBLayer: Ms: float, thickness: float, cellSurface: float, - demagTensor: List[cmtj.CVector], + demagTensor: list[cmtj.CVector], damping: float, 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 +105,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/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 e4d4998..50978d4 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, 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 @@ -157,6 +156,9 @@ 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]. + :param Ndemag: demagnetisation tensor diagonal. Defaults to [0, 0, 1] (thin film). + for sphere, use [1/3, 1/3, 1/3]. """ _id: int @@ -164,17 +166,27 @@ 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)) + Ndemag: VectorObj = VectorObj.from_cartesian(0, 0, 1) 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.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([ - 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.""" @@ -184,8 +196,8 @@ def get_m_sym(self): """Returns the magnetisation vector.""" return self.m - @lru_cache(3) - def symbolic_layer_energy( + @lru_cache(3) # noqa: B019 + def total_symbolic_layer_energy( self, H: sym.ImmutableMatrix, J1top: float, @@ -199,36 +211,38 @@ 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() - 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) - 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 = -mu0 * self.Ms * m.dot(self.Hdmi) + # 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) + 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") @@ -238,45 +252,64 @@ 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 class LayerDynamic(LayerSB): - alpha: float + alpha: float = 0.01 + torque_par: float = 0 + torque_perp: float = 0 - def rhs_llg( + @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_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 + 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 - 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 + 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 * inv_sin + 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 @@ -293,53 +326,54 @@ 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. """ - 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 - 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: 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: - 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("Number of layers must be 1 more than number of tensors.") + 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): + 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: 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], @@ -354,20 +388,19 @@ 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 / layer.thickness, osc=False)) jac = sym.ImmutableMatrix(fns).jacobian(symbols) return jac, symbols - def create_energy(self, - H: Union[VectorObj, sym.ImmutableMatrix] = None, - volumetric: bool = False): + @lru_cache(3) # cache for 3 calls + 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 @@ -380,56 +413,42 @@ def create_energy(self, 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() - ldot = l1m.dot(l2m) - energy -= self.J1[i] * ldot - energy -= self.J2[i] * (ldot)**2 - - # 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]): + 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) @@ -475,8 +494,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) @@ -512,12 +530,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: @@ -527,6 +543,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 @@ -542,8 +600,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 @@ -586,8 +643,10 @@ 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.") + 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, @@ -611,7 +670,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. @@ -625,10 +684,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: @@ -648,25 +704,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)): @@ -678,10 +735,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): @@ -690,14 +744,14 @@ def set_H(self, H: VectorObj): def analytical_field_scan( self, - Hrange: List[VectorObj], - init_position: List[float] = 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. @@ -721,11 +775,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) @@ -745,3 +801,144 @@ def analytical_field_scan( roots = np.asarray(roots, dtype=np.float32) * gamma / 1e9 yield eq, roots, Hvalue current_position = eq + + def _independent_linearised_jacobian_expr( + self, + 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. 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) + 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() + # 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 + + 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) + 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): + 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 + 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 + 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 + return A_matrix, 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, + cache_var: str = "H", + ): + """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 all(isinstance(layer, LayerDynamic) for layer in self.layers): + raise ValueError("Linearised N-spin diode only works with LayerDynamic.") + 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, + ) + 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())) + + def __hash__(self): + return hash(str(self)) + + def __eq__(self, other): + return str(self) == str(other) 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/reservoir/__init__.pyi b/cmtj/reservoir/__init__.pyi new file mode 100644 index 0000000..5f75dd0 --- /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, driver: cmtj.AxialDriver) -> None: + """Set external field for all layers. + + :param driver: External field driver + """ + ... + + def setLayerAnisotropy(self, arg0: int, driver: cmtj.ScalarDriver) -> None: + """Set anisotropy for specific layer. + + :param arg0: Layer index + :param driver: Anisotropy driver + """ + ... + + def setLayerExternalField(self, arg0: int, driver: cmtj.AxialDriver) -> None: + """Set external field for specific layer. + + :param arg0: Layer index + :param driver: 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/cmtj/stack/__init__.pyi b/cmtj/stack/__init__.pyi index 5763093..11aecab 100644 --- a/cmtj/stack/__init__.pyi +++ b/cmtj/stack/__init__.pyi @@ -1,37 +1,49 @@ -from typing import Dict, List, overload +from typing import 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. """ ... + 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]]: + def getLog(self, junctionId: int) -> dict[str, list[float]]: """ Get the logs of a specific junction -- integer id from the `junctionList`. :param junctionId: integer junction id as was passed in the init. """ ... + @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. @@ -39,6 +51,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 +60,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,15 +68,15 @@ 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 - ) -> 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. @@ -71,38 +85,60 @@ 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__(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. """ ... + 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]]: + def getLog(self, junctionId: int) -> dict[str, list[float]]: """ Get the logs of a specific junction -- integer id from the `junctionList`. :param junctionId: integer junction id as was passed in the init. """ ... + @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. @@ -110,6 +146,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 +155,8 @@ class SeriesStack: :param driver: the current driver to be set. """ ... + + @overload def setCouplingStrength(self, coupling: float) -> None: """ Coupling constant that represents the energy losses as the current @@ -125,15 +164,25 @@ class SeriesStack: :param coupling: the coupling strength (or the losses) """ ... + + @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. :param driver: the field driver to be set. """ ... - def setMagnetistation( - self, juncionId: 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. @@ -141,4 +190,15 @@ 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.""" + ... + + 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/cmtj/utils/__init__.py b/cmtj/utils/__init__.py index aa9f9bf..9ebd4f8 100644 --- a/cmtj/utils/__init__.py +++ b/cmtj/utils/__init__.py @@ -3,7 +3,14 @@ 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_gmr, + compute_resistance, + compute_sd, +) # constants OetoAm = 79.57747 @@ -14,15 +21,22 @@ 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", + "compute_gmr", ] 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 366336c..0f5c53f 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 @@ -87,8 +84,9 @@ def amplitude_scan( 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 @@ -105,8 +103,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 e733c6d..ffeeeae 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,24 +8,26 @@ __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 :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] 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] @@ -41,8 +43,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 8755e2d..2656ab4 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 @@ -12,14 +11,14 @@ 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) 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,35 +33,30 @@ 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() - 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) -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,36 +65,34 @@ 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() - 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)) @@ -124,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 @@ -142,13 +130,12 @@ 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) - 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 @@ -173,21 +160,20 @@ 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["right"].set_position( - ("axes", i / (ys.shape[1] - 1))) + 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 @@ -197,47 +183,44 @@ 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(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() 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(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. @@ -262,22 +245,17 @@ def create_stack(ax, 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)) - ax.text(offset_x - labelpad_left, - offset_y + height / 2, - label, - horizontalalignment='center', - verticalalignment='center', - fontsize=text_fontsize, - zorder=11) + 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, + 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 +263,15 @@ 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/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 b1e29e3..94c3fff 100644 --- a/cmtj/utils/resistance.py +++ b/cmtj/utils/resistance.py @@ -1,12 +1,16 @@ -from typing import List, Union +from typing import Union import numpy as np +import sympy as sym 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 @@ -18,65 +22,296 @@ 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 + 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) 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])) 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)) - 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 +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) * (1 - 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: raise ValueError( "The magnetoresistance can only be computed for 2 layers" - f". Current shape {m.shape}") + 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]): +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.0 / np.sum(1.0 / SxAll, axis=0) + Ry = 1.0 / 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) 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) + + +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 + EPS) + 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 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): 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 b7b5dd8..301a207 100644 --- a/core/cvector.hpp +++ b/core/cvector.hpp @@ -1,282 +1,217 @@ #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(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) { + 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; + }; + + 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/drivers.hpp b/core/drivers.hpp index b9b3783..b6d9ec0 100644 --- a/core/drivers.hpp +++ b/core/drivers.hpp @@ -5,496 +5,437 @@ #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 - -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; + // 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; + 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 -{ -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 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"); - } - } +template class ScalarDriver : public Driver { - /** - * 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); - } +private: + T edgeTime = 0; + T steadyTime = 0; - /** - * 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); +protected: + T stepUpdate(T amplitude, T time, T timeStart, T timeStop) { + if (time >= timeStart && time <= timeStop) { + return amplitude; + } else { + return 0.0; } - - /** - * 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); + } + 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; } + } - /** - * 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); + T trapezoidalUpdate(T amplitude, T time, T timeStart, T edgeTime, + T steadyTime) { + if (time < timeStart) { + return 0; } - - static ScalarDriver getHalfSineDriver(T constantValue, T amplitude, T frequency, T phase) - { - return ScalarDriver( - halfsine, - constantValue, - amplitude, - frequency, phase); + // growth + else if (time <= timeStart + edgeTime) { + return (amplitude / edgeTime) * (time - timeStart); } - /** - * 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); + // steady + else if (time <= timeStart + edgeTime + steadyTime) { + return amplitude; } - - /** - * 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); + // decay + else if (time <= timeStart + 2 * edgeTime + steadyTime) { + return amplitude - (amplitude / edgeTime) * + (time - (timeStart + edgeTime + steadyTime)); } + return 0; + } - /** - * @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); +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"); } - - 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; + } + + /** + * 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!"); } - void setConstantValue(const T& val) - { - this->constantValue = val; + 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; + } + + 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, + 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(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(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)) - { + 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!"); + } } - - 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); + } + + 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!"); } - - 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); + 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; - CVector getCurrentAxialDrivers([[maybe_unused]] T time) - { - return CVector(0., 0., 0.); - } - CVector getConstantValues() - { - return CVector(0., 0., 0.); - } + NullAxialDriver() = default; }; #endif diff --git a/core/junction.hpp b/core/junction.hpp index 44367b2..fb5d509 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,1628 +39,1637 @@ 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; - - 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; + AxialDriver HreservedInteractionFieldDriver; + // 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; - - 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; - } - - /** - * @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); - const CVector Heff = this->Hext // external - + this->HAnis // anistotropy - + this->HIEC // IEC - + this->Hoe // Oersted field - + 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 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, + Hidmi; + + 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; + } + + 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 + * 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->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 + + this->Hidmi // IDMI + + this->Hoe // Oersted field + + this->Hdmi // regular DMI + + Hfluctuation // fluctuations + // demag -- negative contribution + - this->Hdemag + // dipole -- negative contribution + - dipole + // reserved interaction field + + HreservedInteractionField; + 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 &Dvector, + const CVector &stepMag, + const CVector &coupledMag) { + // 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, + 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!"); + 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 setLayerReservedInteractionField(const std::string &layerID, + const AxialDriver &driver) { + axiallayerSetter(layerID, &Layer::setReservedInteractionField, 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->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 (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] * (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; + 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; } - if (!found) - { - throw std::runtime_error("Failed to find a layer with a given id: " + layerID + "!"); + } + 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; } - } - 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 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(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; - } - } - 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); - } - } - 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) - { - 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 = + 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 + 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/core/llgb.hpp b/core/llgb.hpp index 57e58d0..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(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) - { - 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/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 0c68015..8154aef 100644 --- a/core/reservoir.hpp +++ b/core/reservoir.hpp @@ -1,14 +1,14 @@ #ifndef RESERVOIR_H #define RESERVOIR_H -#include -#include -#include -#include -#include #include "cvector.hpp" +#include "drivers.hpp" #include "junction.hpp" - +#include +#include +#include +#include +#include /** * @brief Computes the combinations @@ -16,273 +16,474 @@ * @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 -{ -private: - // 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; - - 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 - - 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())); - - return localReservoirDipoleTensor; - } +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); - 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 = 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), + 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, + 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; + 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); +} + +/** + * @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) { + const tensor dipoleTensor = getDipoleTensorFromRelPositions(r1, r2); + const double V = layer2.thickness * layer2.cellSurface; + // 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; +} + +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)); + } + 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); + } + } - 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; +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); - 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]); + // 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!"); } - return HdipoleEff * volumeNormaliser; + } } + } -public: - unsigned int rows, cols; - unsigned int noElements; + void setInteractionFunction(interactionFunction interactionFunc) { + this->interactionFunc = interactionFunc; + } - 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); + 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!"); } - 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; + // 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!"); } - 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); - } + 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 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 clearLogs() { + for (auto &j : this->junctionList) { + j.clearLog(); } + } - 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]; + 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!"); + } - void setAllExternalField(const AxialDriver& hdriver) - { - for (auto& r : this->layerMatrix) - { - for (auto& l : r) - { - l.setExternalFieldDriver(hdriver); - } - } + std::unordered_map> &getLog() { + throw std::runtime_error("Not implemented!"); + } +}; + +class Reservoir { +private: + // log stuff + std::unordered_map> reservoirLog; + + // reservoir matrices + std::vector> coordinateMatrix; + std::vector frozenMMatrix; + std::vector MsMatrix, volumeMatrix; + std::vector> reservoirDipoleTensor; + std::vector>> layerMatrix; + + std::vector> computeReservoirDipoleMatrix( + const 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 + + 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())); + + return localReservoirDipoleTensor; + } + + 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}; + 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; + } - void setLayerExternalField(unsigned int index, const AxialDriver& hDriver) - { - this->getLayer(index).setExternalFieldDriver(hDriver); +public: + 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); + } - void setLayerAnisotropy(unsigned int index, const ScalarDriver& anisotropyDriver) - { - this->getLayer(index).setAnisotropyDriver(anisotropyDriver); + 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; + } - 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 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 - saveLogs(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 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 clearLogs() - { - this->reservoirLog.clear(); + } + + 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 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 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); + } + + 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 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); } + } }; #endif diff --git a/core/stack.hpp b/core/stack.hpp index 0f305a9..19c65ea 100644 --- a/core/stack.hpp +++ b/core/stack.hpp @@ -1,350 +1,369 @@ #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: - std::string topId, bottomId; // Ids of the top and bottom junctions - T couplingStrength = 0; - bool delayed = true; - virtual T calculateStackResistance(std::vector resistances) = 0; - virtual T computeCouplingCurrentDensity(T currentDensity, - CVector m1, CVector m2, CVector p) = 0; + 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; + std::vector> junctionList; - 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; + 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 setMagnetisation(unsigned int junctionId, const std::string& layerId, CVector mag) - { - this->junctionList[junctionId].setLayerMagnetisation(layerId, mag); + 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(); + } - const CVector getMagnetisation(unsigned int junctionId, const std::string& layerId) - { - return this->junctionList[junctionId].getLayerMagnetisation(layerId); + T getCoupling(const unsigned int &order) const { + if (this->couplingStrength.empty()) { + throw std::runtime_error("Coupling strength is not set!"); } - - void setOerstedFieldDriver(const AxialDriver& oDriver) - { - for (auto& j : this->junctionList) - { - j.setLayerOerstedFieldDriver("all", oDriver); - } + if (this->couplingStrength.size() == 1) { + return this->couplingStrength[0]; } + return this->couplingStrength[order]; + } - void setExternalFieldDriver(const AxialDriver& fDriver) - { - for (auto& j : this->junctionList) - { - j.setLayerExternalFieldDriver("all", fDriver); - } + 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; + } + + 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); + } + + 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); } + } - 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; + } - 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 setCoupledCurrentDriver(const ScalarDriver &cDriver) { + this->currentDriver = cDriver; + for (auto &j : this->junctionList) { + j.setLayerCurrentDriver("all", this->currentDriver); } - void - saveLogs(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(); + 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(T coupling) - { - 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; // 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!" + " 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) - { - 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 - 1], + frozenMags[j], frozenPols[j - 1])); + + } else { + effectiveCoupling *= + (1 + this->getEffectiveCouplingStrength( + j - 1, + junctionList[j - 1].getLayerMagnetisation(this->topId), + junctionList[j].getLayerMagnetisation(this->topId), + junctionList[j - 1].getLayerMagnetisation( + this->bottomId))); + } } - - T tCurrent; - 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(); - } - } - const T plainCurrent = this->currentDriver.getCurrentScalarValue(t); - T coupledCurrent = plainCurrent; - for (std::size_t j = 0; j < junctionList.size(); ++j) - { - // 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]); - } - else { - coupledCurrent = coupledCurrent + this->computeCouplingCurrentDensity( - // j -> k, j-1 -> k' - coupledCurrent, junctionList[j].getLayerMagnetisation(this->topId), - junctionList[j - 1].getLayerMagnetisation(this->topId), - junctionList[j].getLayerMagnetisation(this->bottomId)); - } - tCurrent = coupledCurrent; - } - else { - tCurrent = plainCurrent; - } - - // set the current -- same for all layers - junctionList[j].setLayerCurrentDriver("all", ScalarDriver::getConstantDriver( - tCurrent)); - (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; - } - 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 computeCouplingCurrentDensity(T currentDensity, 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; - } +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") : Stack(jL, topId, bottomId) {} + 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 computeCouplingCurrentDensity(T currentDensity, 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; - } +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") : Stack(jL, topId, bottomId) {} + 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/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/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/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/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..c87648d 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 @@ -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/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": "iVBORw0KGgoAAAANSUhEUgAAA+UAAAHxCAYAAAALGx0uAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjcuMSwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy/bCgiHAAAACXBIWXMAAB7CAAAewgFu0HU+AACnbElEQVR4nOz9f3Bb530n+r9Bgo4lxxFIyWtyV+KaYJxbur2RBYi6+Wnd2kCy39mtOhsTohjB0tzZiKfZmTsdSzFhdaZXUnduJLC1tHs70xhUm1ElWJQFxk23+925DSA3trPurUhAYru7vJuGkJdyA2YjUYhiS4kJ8tw/jp6Dg58EwAMc4Jz3a0YjAjg8fPSA4uHnPM/n87HJsiyDiIiIiIiIiBquzegBEBEREREREVkVg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIigzAoJyIiIiIiIjIIg3IiIiIiIiIig9iNHgARERFZRzqdxsmTJ5FOpwEAyWQSXq8XY2NjRY8fHx/H9PQ0urq6AAButxujo6PrPpaIiKhZ2GRZlo0eBBEREVmD1+tFKBSC0+kEoATpfX192LlzJ6LRaMGxTqcToVBIfc7n86GrqyvnuWqPJSIiaiYMyomIiKghEokE3G43IpEIhoaG1Od9Ph+mpqYQj8fhcrkAALFYDF6vF3fu3IHD4VCPTafT6OzsrPlYIiKiZsPt6+t069Yt/OVf/iWeeOIJbNiwwejhEBFRE7l//z7ee+89fPnLX8aWLVuMHo7hHA4HHA4HlpaWcp4X2821z0ciEfX4YucIhULqCng1x1aC13YiIipH7+s7V8rX6bXXXoPf7zd6GERE1MTC4TD2799v9DCaVn9/PwBgfn4+5zmHw4F4PF70eO1r1RxbCV7biYioEnpd37lSvk5PPPEEAODf/Jt/g76+PgDAli1b8Nhjj1V1nrm5Ofj9foTDYQwMDOg2vlY7bz3P3Wrnree5W+289Tw3x1z/89bz3M143p/+9Ke4desWAODGjRv43d/9XfVaQbmSySQCgQAcDgeuXLlS8FqpLecOhwPJZLKmYytRyfs1OjoKSZLKHtOM359GnZtjrv9563nuVjtvPc/dauet57nNPuZQKISJiYmyx+h1fWdQvk5iW9vv/u7vqs8dO3YMx48fr+l8AwMDdcl7a7Xz1vPcrXbeep671c5bz3NzzPU/bz3P3UznPX78OE6cOJHzHLdA59JWYE8mkxgeHq7pHPU4Fsi+X+V+aevp6UFPT09F52um70+jz80x1/+89Tx3q523nudutfPW89xmHfPx48dL3nwVwb1e13cG5TrRXrgrvUgTEZH5SJKEPXv2AMhetCmXw+FAMBhUH3u9Xpw8eRLxeFytyl5OPQNyrXr+oklERM2tmpuv68WgXCfrvXD39PTg2LFjur/xrXbeemrFuWjFMddLK85FK465XlptLtZz3kZexM0iEAjA6/VCkiS1LVq54HxpaSnn9WqObaRm/P408tz10mrzzPcvqxXnotXOW098/3Qk07rE43EZgByPx40eimlwTlsX37vWxvdPf5zTXENDQ7LT6Sx4fn5+XgaQ89rQ0JDscDiKngeAPDo6WtOxleD7pj/OaWvj+9fa+P7pT+85bTPkTgARERFZTiKRwNLSUsGWclGITbvjbHh4GOl0uuBY8djn89V0LBERUbNhUE5EREQNEQgEMDo6WtBPXFRgP3v2rPrc0NAQPB4PAoFAzrGHDh2Cx+OBx+Op6VgiIqJmw5xyajpNm+tBa+J719r4/lG9jY6OIhaL5VSzTSaT8Hg8OHr0aEGwHo1GEQgE4PP54HQ6kUwmMTg4iLGxsYJzV3MsNR5/vrQ2vn+tje9f87PJsiwbPYhWlkgk4Ha7EY/HWaGViIhy8BrRmvi+ERFROXpfJ7h9nYiIiIiIiMgg3L5OREREVMTc3Jz6MVvdERFZWyqVQiqVApB7fdADg3IiIiKiIvx+v/rxsWPHcPz4ceMGQ0REhgqFQjhx4kRdzs2gXCfrvZu+ugrcvw9s2AC0MamAiMhQ6/mZXM876dRY4XAYAwMDAMBVciIii5MkCXv27AGgXN+1N27Xi+GfTvx+P9xuN9xuN0KhUMWfNzsLHDwIPPoo8PGPK38fPKg8T0REjaXHz+RQKKReD/S8YFPjDQwMwOVyweVyMSgnIrK4np4e9ZogbtjqhSvlOqnlbvrkJHDgAJDJAHYsYysWsXivG+fPd+DiReD8eWBkpJ6jJiIiQa+fyfW8k06tZXHxA8zN/RQDA4+hu/vjOY8BlHytmmOb7TwcO8feCl+TY+fYxeNmwaBcJ+JueqVmZ7O//D2Dt3AJ+9CDRaTQjX24hLczu3HgAPDUU8D27XUcOBER6fozmQXBCAD+r//rb3D48F9iZUVGe7sN/+yffRL/9//9I6ysyLDZAJvNhtVV5bUvf7kff/mX8+qx2sf5x37pS/343vfWfk157MT3vpcseqzX60Q0uvZr7e02eDxOxGLFj9W+ttaxzz3XhytXbhR97dln+/DmmzfU82gf5x/767/eh7/6q1KvPYG/+qv31PNoH+cf+7/+r0/g+9/PHrt79z/FW2/996LHlnvtmWf+Kd5++7+r59E+zj/2i1/sxTvvLBR97Qtf6MUPfrCgnkf7WDlWSa1pb7fh85/vxX/6T9ljtY9tNuV7UJaVYz/3uW14992bRV/77Ge34q//+n31PNrH+cd+5jNb8f/8P8Vf+1/+l3+Cv/mbf1DPs2vXP8HVq/9Q9Nhdu/4x/uZvfozVVRltbcrjq1eVx9pj29psGBz8x5ieLv7azp3/GDMz2fPs3NmDmZlU0WPd7h7E48Vfc7l6kEik1PNoHxce241EYlE9dseObly7tlj02B07Hse1az8p+trTTz+O69d/op5n+/bHMTtb/Nhyr33604/jb/82e55Pf/of4W//9n+UODb72lrH/tqv/SP85/+89mvK48fwn//zT0scW/q1X/3Vx/Bf/stP1fNoHxceuwX/5b/cUo996qkt+K//9VbRY8u9NjCwBXNzymt2uw3f+ta/wNe+1hxtL9mnfJ1q7VF38KCy6mLHMhbQix4sqq+l0I1eLCCDDhw8CJw7V4eBExGRql4/k9nvujWt931bXPwAW7eexsoKf8UiImpWdnsbbt58saYVc/YpN4HVVWBqSvm4G4s5v/wBQA8W0f3guXAYuHat0SMkIrKOa9eAixeVj9f6mRyJKD/DicqZm/spA3IioiaXyaxibu6nRg8DAINyQ9y/D9y7p3y8iG6k0J3zegqPw45l2LGMlRVg1y4l15GIiPQ1OQkMDmbzyO1YLvIzuRuLD567d0/5GU5UzsDAY7Db+SsWEVEzs9vb1Lxzo/GKYYANG4CNG5WPM+jAPlxSfwm8jS48hGXcQD8W0Itn8BYyGSXXkRXZiYj0I/LIV1aUPPIF9OIG+vEQPsJtdAGAmlOeQQcA5Wf3hg1GjppaQXf3x/Gtb/1zNTC329tw4MCn1cdtbTa0t9uKvlbNsc12Ho6dY2+Fr8mxc+zi8be+9c+bptgbc8rXab055YJS6fcm3sXnS+YyHjgA/Omf6jl6IiLrOnAAuHChVB754/gc3sX72KYG5ACYU24Rer1vzVBZuJWrInPszf01OXaO3Qxjr5Xe13cG5etU6xsyOwvs3KlsmRS24iZuorfg2G1YwPvYBgB44QXgyBFWZCciqtXsLPDKK0pADlT2sxcA7HZgZqa6n78MylsT3zciIiqHhd5MYvt2ZaW8vT37XPH88m7cwmZsxU3YsYwLF5RgnjnmRETVm5xUfoaKFfKtuIlb2Fw2jxxQAvLz53lD1Grm5uaQSCSQSCSQSqWMHg4RERkolUqp14S5uTldz82g3EAjI8D0tPLLHlCYX55CN07iZSTRj5vozckxf+EF5pgTEVUjvxf5AnpxE71Ioh8n8XLOz15tHnl7O3D1qvIzm6zF7/fD7XbD7XYjFAoZPRwiIjJQKBRSrwl+v1/Xc9t1PRtVbccO4KtfzeaXv43d6MUCurGIW9iMJPrVPMceLOIS9ik55isdeP554Dvf4coNEdFaZmeBr3wlW2X9Evbl/Gw9ilNwYh5bcBuL6M7JI/f7lZ/VZD3hcBgDAwMAgJ6eHoNHQ0RERpIkCXv27AGg7KTSMzBnUK4T7RaGnp6eqi7ehw8rPXJFfnkGHXgf27AVN0v2y30f2zA/r2zDPH+eKzhERKVMTiq7i1ZWlMelepFvwe2cHHJA2cn04ovVfb1UKqVuddZ7exs11sDAAHPKiYgIQPUxXjW4fV0n69niJvLL7Xm3SNbqYQ6A7dKIiMrQtj0DKutFLtSaR17P7W1ERERkPgzKdRIOhxGPxxGPxyFJUtWfPzKiVPU9cCD7XCU9zAElMD99Wpd/BhGRqbzySnYXUqW9yAHlZ/HMTG27kCRJUq8H4XBYj38GERERmRi3r+tEjy1u27crfchlOduqR+SY5/cwz8kvRwfOn1c+j+3SiIgK257l55FvxhJSeBx9mC/oRX7ggPKzuFb13N5GRERE5sOV8iZ05EjuVvYMOpBBR9EcyK24yXZpREQaxdqeFa/R8RP156tgtyt1PoiIiIgahUF5E6q0h/ltdOFdfL6gXRpzzInIqkq1PXsXn1O3qwvsRU5ERETNgEF5kxoZAeJxoL9feVzYw/xxACjYzm7HMnPMiciyRA55YduznwBAyV7k/f2155ATERERrQdzypvY9u1KH/KdO5VfMrU9zO0PCr5padulMceciKwkP4e8WNuzzVhCH+aRQUdOL/L2duVnLX9WEhERkRFME5Sn02mcPHkS6XQaAJBMJuH1ejE2Nlb0+PHxcUxPT6OrS9nO6Ha7MTo62qjhVkxsZRfbMUUPc9HSR/tLZwrduIXN2IqbWEQ3LlzowOQk+5gTkblNTmZ/RtqxjG4s4hY2F/0ZmV/UjVvWqRxtn3kW8CMisrZUKoVUKgUg9/qgB9ME5T6fD6FQCE6nE4ASpPf19SEajSIajeYc6/V64XQ6EYlEcj4/Ho9X3WO8EUZGgKeeUraknz+vPCe2s4vtmSl04yReRhL96uN9uIS3M7tx4IDy+fylk4jMJj+HPP9n4lGcyvmZmF9l/fBh/myk0rR95o8dO4bjx48bNxgiIjJUKBTCiRMn6nJuUwTliUQCsVgMiURCDcodDgc8Hg+mpqaQSCTUdmWxWAyxWAx37tzJOcfZs2fR2dkJSZLW3dqsHsq1SxOrQiIgB/JapmU6cOYMcO6cceMnIqqH06dL5ZAv4ihOwYl5bMHtnO3qwPrbnpE1hMNhDAwMAABXyYmILE6SJOzZsweAslKuvXG7XqYo9OZwOOBwOLC0tJTzvNiarn0+Eomoxxc7RzOulGsVa5f2PrZhC24XbZnW/eC5cBi4dq2RIyUiqq9r14CLF5WPi+WQ92ARW3C76JZ1tj2jSgwMDMDlcsHlcjEoJyKyuJ6eHvWaIG7Y6sUUK+VOp7Ng5RtQVsWdTic8Hk/Bc8V0dXVhZmampjGUyyvQMw8tP8dcEC3TcvMnH4cdy0pF9pUO7NrF/HIiMofJSeCFF4CVFag/54rlkC/mtZKsRw65Nscsn945Z0RERGQ+pgjK8yWTSQQCATgcDly5cqXgtVLb0x0OB5LJZE1fs9z2Bb3z0CrJMb+NLjz0oEI788uJyExEHvnKSm4e+W104Ta6sBlLDc0hr2eOGREREZmfqYJybQX2ZDKJ4eHhms5RC23eWb56bHkrl2O+FTfxLj5fMr/89GnmUhJR6yrVi1wJxh9HH+YLtqzXM4dcm2OWT++cMyIiIjIfUwXlDocDwWBQfez1enHy5EnE4/GSW9a1ag3IgWzeWaMdOaJs4xRb2TPoQAYdJfPL2cOciFpVJb3Ie/AT9eegUO8ccrbKIiIiovUwRaG3UgKBANLpNCRJUp8rF5wvLS1VFLw3E5Fj3t6efU7kl2tpe5jbsYwLF4CdO5WAnoio2U1OKj+zLlxQVsi34qbai1wrP4+cfciJiIio2ZkiKPf5fOjv7y94XgTY2jxxl8tVMm88nU7nFIVrFSMjwPR0tiq7yC8Xv6xqe5jfRC8W0Itn8BYyGWVL5+ysgYMnIlpDfi/yBfTiJnqRRD9O4uWcn3XaPPL2duDqVRa3JCIiouZmiqA8kUhgaWmpYPu5CL6128qHh4eRTqcLjhWPfT5fPYdaNzt2AF/9avaxyC/fhgU4MY+jOFWQY27HMjIZ4MwZgwZNRFSBSnqRb8MCerGAt7Fb/Ty/X/nZSERERNTMTBGUBwIBjI6OFvQeFxXYz549qz43NDQEj8eDQCCQc+yhQ4fg8XhacqVcOHyYPcyJyFzW04v8xRcbOVIiIiKi2pii0Nvo6ChisVhO7ngymYTH48HRo0cLgvVoNIpAIACfzwen04lkMonBwUGMjY01eOT6Yg9zIjKTZupFTtak7TPPgn5ERNaWSqWQSqUA5F4f9GCKoBxA1avc2irtZsIe5kRkBs3Wi5ysSdvO7tixYzh+/LhxgyEiIkOFQiGcOHGiLuc2TVBOWexhTkStrtl6kZM1hcNhDAwMAABXyYmILE6SJOzZsweAslKuvXG7XgzKTYw9zImo1TRrL3KypoGBgZxisUREZF31TGMyRaG3ZjA3N4dEIoFEIqHmGhiNPcyJqJWYpRd5KpVSrwd655wRERGR+TAo14nf74fb7Ybb7UYoFDJ6OCr2MCeiVmCmXuShUEi9Hui5tY2IiIjMidvXddLMeWeih7ko/Cbyy7uxiFvYjCT6mWNORIYqlUOu7UW+BbexiO6cbevN2Iu8njlnREREZD4MynXS7Hlnhw8rvX61+eXvYxu24iZzzInIMJXlkGd7kWs1ay9yts5aWyAQQDKZRDKZBKDcyBgdHS167Pj4OKanp9HV1QUAcLvduhxLRETULBiUW0R1PcyzOeaL6MaFCx2YnGQfcyLS1+Rk9meSHcvq7h32Ijc3r9eLYDCo3siOxWLwer2IRqOIRCIFxzqdzpznfT4f4vF4QapYNccSERE1E+aUW8jICDAzo/wSLDDHnIiMUGsOOaB83swMbxK2ovHxcUiSlLOzzOPxYGxsDFNTU5iamlKfj8ViiMViCAaDOec4e/YsJiYmkEgkajqWiIio2TAotxjRw/yFF7LPiRzzbViAE/M4ilMFOeZ2LCOTAc6cMWjgRGQqp0+vnUO+DQvoxQLexm7180Qvcq6Qt6ZoNAqfz4d0Op3z/PDwsPq6EIlE4HA44HA4co4Vz2lXv6s5loiIqNlw+7pFFethXkmOeTgM/PZvN19hJSJqHdeuKTUugOpzyNmLvLW5XC7MzMwUPC+CaZFjDiir306ns+h5urq6cs5TzbHVKNfSjrUDiIjMLZVKlWx1rXfLUwblFlVdjvnjsGNZWS1f6cCuXcwvJ6LaTE4qO3VWVqD+XGEOuXUEg8GCLeaAElQDSl64kEwmSxZQdTgcOQF8NcdWo1zl/GPHjuH48eM1nZeIiJpfKBTCiRMnGvK1GJRb2MgI8NRTyjZS0S5N5JiL7aS30YWHsIwb6FdzO9/O7MaBA8rn8hdkIqqUyCNfWVHyyLU/Z26jC5uxVDKH/PBh/rwxs2AwCKfTibGxsYo/J38LvF7HamnbnebjKjkRkblpW5zm07vlKYNyixM55rKcbUkkcsy34ibexefZw5yIdFGqF7kSjD+OPszjfWwrCMj5c8bcfD4fHA4Hrly5UvHnNCIgB5q/3SkREdVPI9OUWOiNACg55nbNLZoMOpBBR8n8ciC7/Z0V2YmoHLFCXr4X+U/UnzsCc8jNz+fzAQDi8XhBkbZSOeIAsLS0lPN6NccSERE1GwblOpmbm0MikUAikShZEKCZiRzz9vbscyK/XEvbw9yOZVy4AOzcqeSJEhHlm5xUfkZcuKCskG/FTbUXuVZ+Hnkr55CnUin1eqB3IRgz8fl88Hq9OX3FRW45oBSFK5ULnk6n4fF4ajqWiIio2TAo14nf74fb7Ybb7W7Z1isjI8D0dHbFnD3MiWg9au1F3t4OXL3ausUkQ6GQej3QM9/MTHw+H44ePYrR0VH1uXQ6nROgDw8PI51OF2w/F4/FKnu1xxIRETUbmyzLstGDaGWJRAJutzunGEyrt0k5eDBb+A1QVre6sYhb2Iwk+guqJPdiARl0MPeTiHKILet2LGMBvQU/O5yYxxbcxiK6c7atHzwInDtnwIB1om2hIgrBxONx5iY/4Ha7ARRuOU8mkxgeHs4p9ub1euF0OnNudos+59qe5tUeuxZxbef7RkRExeh9nWChN52YqRjM4cNKD+Fqe5ifP68UjDtypDW3nBKRPmZnlaJu5XPIS/cif/HFRo20Plr9xmw9+Xw+JBIJAFD/1spvlxaNRhEIBODz+eB0OpFMJjE4OFi0Sns1xxIRETUTBuVUoLoe5tkc80V048KFDkxOso85kVVNTmZ/dmh32bAXOQHI2Z5eqWJ9zfU4loiIqFkwp5yKGhkBZmaUX64F5pgTUTm15pADyufNzPBmHhEREVkPV8qppHI9zIvlmOf3MT9zprXzQomoOqdPF+9D3oNFHMWpkjnkrEdBzUpbPZ9pCURE1pZfM0ZPXCmnNRXrYf4+tmELbpftYx4OA9euNXKkRGSUa9eUWhTA2jnk7EVOrcIMnVWIiEgf9eyuwqCc1iRyzO15+yqK9zF/HHYsw45lrKwAu3axhzmR2U1OAoOD2VVyO5bX7EMOMIecml84HEY8Hkc8HockSUYPh4iIDCRJknpNCIfDup6bQTlVpJIc89vowkNYxg30M7+cyCJEHvnKSjaP/Ab68RA+wm10AWAOObUu0VnF5XJx6zoRkcX19PSo1wTRClsvDMqpYiLH/IUXss+JHPM+zOMjPITNWAKQzS+3YxmZjJJrSkTm88orxfPIN2MJH6EDfZhHLxbwNnarnyNyyLlCTkRERMSgnGpQLMc8g46y+eWixRpXzInMQayQl+9F/hP154PAHHIiIiKiXAzKqWoix7y9Pftc8fzybA9zO5Zx4QKwcydzzIla3eSk8n/5wgVlhXwrbqq9yLXy88iZQ05ERERUiEG5Tubm5pBIJJBIJNRS+WY2MgJMT2dXzNnDnMgaau1F3t4OXL1qjRzyVCqlXg/0bplCRERE5sOgXCdWbJuyYwfw1a9mH4v88m1YgBPzOIpTBT3MRY75mTMGDZqI1qWSXuTbsFCQR+73Kz8zrKCeLVOIiIjIfBiU68SqbVMOH2YPcyKrWE8v8hdfbORIjVXPlilERERkPva1D6FKiLYpViPyy8V2VkHkmGt/adf2MM+sdGDXLuVzrbCdlajVTU4qnRdWVnJ7kef+H2cvckBpmcL2WURERFQprpTTurGHOZG5sRc5ERERUf1wpZx0IXqYy3K2RZLIMd+Km3gXny/IL+/FAjKZDpw+rXwuETWncr3IU3gcfZgv2LIuepETtTJtoT7ugCAisrZUKqUW9Na7kCtXyklX7GFOZB7sRU5WZ8UirkREVFw9C7kyKCddsYc5kTmwFzmRdYu4EhFRoXoWcmVQTrpjD3Oi1sZe5EQKUcTV5XJx6zoRkcX19PSo14SBgQFdz82gnOpiPT3MT582aNBEBKB0Djl7kRMRERHpj0E51U2tPcyZY05kjMpyyNmLnIiIiEhPDMqpbkR+uT2vxj9zzImaT6055ADzyImIiIjWg0G5Tubm5pBIJJBIJNRS+VRZD3PmmBMZq9YccoC9yItJpVLq9UDvlilERERkPuxTrhNtWfxjx47h+PHjxg2myZTrYd6NRdzCZiTRX7KP+ZkzwLlzxo2fyOxOn147h3wLbmMR3exFXoFQKIQTJ04YPQwiIiJqEVwp1wnbpqytWA/zSnLMw2Hg2rVGjpTIOq5dAy5eVD6uNoecvciLq2fLFCIiIjIfrpTrRLRNodJEjrnYJiuIHHNtMJDC47BjWanIvtKBXbuUz+UWWSL9TE4CL7wArKxA/f9W+H+ROeTV6unpYfssIiIiqhhXyqmhKskxv40uPIRl3EA/88uJ6kTkka+sZPPIb6AfD+Ej3EYXAOaQE7FeDBERCfWsGcOgnBpO5Ji/8EL2OZFj3od5fISHsBlLANjDnKheSvUi34wlfIQO9GG+oA+5yCHnCjlZhd/vh9vthtvtRigUMno4RERkoFAopF4TtPXE9MDt601idRW4fx/YsAFos8itkiNHlO2zYit7Bh3IoKNkfvn72Ibz55WCcUeOMDAgqsXsrBKQl+9F/hP1/6NgtRxyK/5MpkLhcBgDAwMAwJQEIiKLkyQJe/bsAaDspNIzMLfkrxrJZLKq5+tpdhY4eBB49FHg4x9X/j540BrbtEWOeXt79jn2MCeqn1p7kVsph9zKP5OpkKgX43K5GJQTEVlcT0+Pek0QN2z1YqqgPBAIwOfzqdsKJiYmih4nSRJsNhvcbje8Xi/cbjc6OzsbvjVN/IJ8/jzw0T3lF+SP7i3j/HnrBJ0jI8D0dLYqO3uYE9VHrb3I29uBq1etkUPOn8lERERkBNME5V6vF8PDw4hEIojH4wgGg5AkCT6fr+jxTqcTiUQCMzMz6OrqQiQSQTAYbNh4S/2CbMWgc8cO4KtfzT4W+eXbsAAn5nEUpwp6mIsc8zNnDBo0UYuppBf5NiwU5JH7/cr/UbPjz2QiIiIyiimC8vHxcUiSlNOSzOPxYGxsDFNTU5iamir4nPn5eciyjDt37iAajcLj8TRyyGV/QbZi0Hn4MHuYE9XLenqRv/hiI0dqHP5MJiIiIqOYIiiPRqPw+XxIp9M5zw8PD6uvN5PVVUDcJyj1C7LVgk6RX27PKz1YPMc828N8ZQXYtYvbSolKmZwEBgezAafoRa5l9V7kldy0ED+TIxHlZzgRERGRXkxRfd3lcmFmZqbgeYfDAaB0AbepqSkkk0k4nU54PB71+FqU61XX09OTUyDm/n3g3j3lYxF0an8J1AadmZUO7Nql/HJs9pzOkRHgqaeUFavz55XnRI65WLnS9jAXua9vZ3bjwAHlc60QQBBVKr8Xufb/0W10YTOWSvYiP3zYGv+fJieV9owrK7k3LXJ/JmdvWty7p/wMf+SR7DlSqVTJHtZ69zElIiIi8zHFSnkwGMSdO3cKgupYLAZAyTfPFwgE4HQ6MTY2BofDUbYwXCW0vUzz/+QXkNuwAdi4Ufk4v7CZNui0Yi4je5gT6Ye9yMvLv2mxgF7cQD8ewke4jS4AhYXvNm5UfoZrafuW5v/Ru48pERERmY8pVspLCQaDauCtFQqF4HQ61ccejwfBYBA+nw87d+7MyU2vlLaXab78NiptbcDQUHY1WASdW3ET7+LzBbmMvVhAJtOB06eVX5atgD3MiWrHXuSVKXfTIoXH0Yf5gjx7n6+wb7m2b2k+vfuYEhERkfmYNij3+XxwOBy4cuVKwWvagFwQhd5CoVBNrdFEL9NKHT6s5DAy6CxO5JiLbaVAqa3+ubmwFy4owbwVtvsTFTM5ma0iLlTyf8dKOeTruWlRrPBdfooSERERUTVMsX09n2iDFo/HC7a0j4+Pw+12l/zcUvnnehNBZ3t79rniRc26cQubsRU3YccyLlywTr/cSnqY78MlAFDnB4CltvsTaWnbegHKCvBW3ASAov93rNyL/MKF7PzcwuY1i99Z6aYFZc3NzSGRSCCRSJSsG0BERNaQSqXUa4LeNWNMF5T7fD54vV5EIhH1OZFbDiiV2POrtAPA0pKSp1zL1vVaVRJ0nsTLSKLfsv1yy/Uw78UCABT0EwbAHHOyJLEdGyjstQ0g5/8Oe5Fn5yeJfpzEy7xpQQW09WJq2UVHRETmoa0ho3dqmqm2r/t8Phw9ejQnsE6n04hEIur2dK/XWzTwFr3MJUlqzGAfEEFnfn55NxZxC5uRRH/JHPMzZ4Bz5xo6XEMU2+r/PraV7CfciwVk0GGp7f5kbfnbscv933gf23I+l73Ilfk5ilNwYh5bcBuL6M7Ztm6VmxZUSFsvhikKRETWpq0ho3fNGNME5WJL+smTJ3OeTyaTar9yABgbG4PX64XT6VRzyxOJBE6ePFlQAK5RSgWdW3GzbI55OAz89m+b/5dFsdU/P0+2XD9hEXgwx5zMrlgOeSX/NwBrbcmupBf5Fty29E0LKlRtvRgiIjKvetaQMUVQ7vP5kEgkAED9WysYDOY8jkajCAQCSKfTWFpaQjqdxpUrVwy78JYKOtcqzrSyAkv3MC81PyIHX6x2ie3+7GNOZlMsh1zsslmrsJtVe5EDlRW+A6x104KIiIiMY4qc8kgkAlmWS/4RW9e1gsEgQqEQIpEIotGo4XfCR0aAmRnlF2WhWI75foTRjUVLFjXL72FeaQ4+oMzTmTNGjZyoPsR2bKC6HGmr9iIHsjcu9iNccn4A5XNmZsx/w5OIiIiMZ4qg3Czyg04gt7DZfoTxGvyWL2p25Ei2OJ52fpyYx1GcKsijFTcwIhFgddWoURPpa3UVeFAKo2yOdH5hN6v2Igdyb1y8Bj/2I1y08J2VbloQERGR8Uyxfd1sjhxRtltqc8wX0Y2r2MWiZijc7l9pDv69e0oF5c98xqCBE+no6lXg3j3l40pzpK20HbuS4nevwa/+DBWsdtPCKOl0Gj6fDz6fD6OjoyWPGx8fx/T0NLq6ugAo9WNKHV/NsURERM2EQblOtL3q1lsEQASd2hxIFjXLVXmO+eOwYxl2LCODDnzxi9aYHzI3kSMNQP3+Zg55Vq3F7/S6aZFKpdSe1nr3MW11kiSpLUhjsRi8Xm/JY0VRVm2LU5/Ph3g8XtCerJpjiYiImo5M6xKPx2UAOX+OHTumy7kTCVm222UZkGU7PpJ/jG7lwYM/P0a3/DA+lLdiQbbjI/Ulu12Wr1/XZQgt4YUXstPyDL6vztMtdMm30KXO1TP4viXnh8zl+vXsz4VKvt8BWT5wwOhRN452fsTPzq1YkB/Gh0V/hoqfne3tys9cPRw7dqzguhCPx/U5uUncuXNHBiAHg8Gir0ejURmAfOfOnaKfp53Pao6tlLi2830jIqJi9L5OMKdcJ+FwGPF4HPF4XLde56KHOVB9UTOr55j3YR4f4SFshrIio80vt9r8kLmIHOn87dibsYSP0IE+zDOHvIbid3r2IpckSb0ehMNhfU5qMZFIBA6HAw6HI+d58Zx29buaY4mIiJoRt6/rpF69TLU9zEXAKVoeJdHPHHMUbvfPoAMZdJTdqmql+SFzyM+RLr4d+yfq9z/AHPJSxe+24LbaMhHQvxd5PfuYWkUsFoPT6Sz6WldXF2ZmZmo6tlrl0g/4PhMRmZs2HS2f3ulpDMqbXK1FzQDr5Zj39QGf/azyuJIe5hcudFhmfqi1aXOkq+lF/s471ihsWE0OuZWL37WSZDJZ8ka3w+FAMpms6dhq+f3+kq8dO3YMx48fr/ncRETU3EKhEE6cONGQr8WgvAVUXtQsN+jMoEPtY/7UU+b/pXPXLmDjRqUitdjuL1bKtNv9xeN9uIS3M7stMz/UmkSf7UxG2Y6d/z0t2gDmb8feuFH5P2F22vkBqrtpYaXid2aTTqfrcmy+cDiMgYGBoq9xlZyIyNwkScKePXuKvjY3N1f2xm21GJS3CNHDXJaVFfCKg07sRiYDnDkDnDtn9L+ivtragKGh7I2Lirf7Zzpw+rQyv0TNplQOebnt2ADg8yn/J8zu9OncHPJKb1qIXuTUehoVkAP1S00jIqLm18g0JQv8ymYuxYqabcMCnJhXf/kEcgubAUAkAqyuGjXqxjl8ODs/QHa7/xbcLrndH8imCMzONnK0RKWJFeDyOeTZ7dj5vbb1zJFuVqurwNSU8nG5mxbbsGDp4netqFSOOAAsLS3lvF7NsURERM2IK+UtptYc83v3gKtXzZ9fmj8/AnPMqZXUmkMOWCtH+upVJV0FYA652bhcLsRisaKvpdNp7N27t6ZjG+kv/uK/4Td/81LB8zabLe9x6XPodWz+a+XOWc159BpPuTHU+lo1X7Pe4xHPr3fs1Zyn3LH5x6z1efnP6/0113usXnNZ6ddc67X81xvxPVyvr5mvHufJP3Y9PxOr+Vn3r/6VC7t2/ZPSX6zBGJS3oMpzzB+HHctKGzB04ItftEbAWWx+mGNOraLWHHLAWjnSk5NKxwUA6s855pCbx/DwMKamppBOp3NanYnt6D6fr6ZjG02Wiz1X5MnSZ9BtLERElLV79xMMymn91soxv40uPIRl3EC/JQPO/PkBqssxt0IOPjUnkSNdbQ65lXKkxY2LlZXcGxe30YXb6MJmLDGHvMktLS0BAG7fvl309aGhIXg8HgQCgZw+44cOHYLH44HH46np2EZyOB4u+IUvPyAvF5+XO3atwL7csXp9zXqcZ62voX293GtGfM1Sr4nnqx17/ueX+5rrPbbcWPOfX+vfSUS1scnV3bKlPIlEAm63G/F43JBiMLOzwM6duZWHt+Im3sXnC1aMRA9zK/1ymj8/wlbcxE30Fhy/DQt4H9vQ3g5MTwM7djRooEQArl1TKqZnMmt/j2rZ7cDMjPlvtgki196OZSygt2CH0Ofwbk6evZHzY/Q1otkEAgEkk0kkEgkkk0k4HA7s3LkTDocDZ8+ezVnp1h7vdDqRTCYxODiIsbGxsueu5Ni18H0jWr9KbkzodUMh/9hqz1Pq2Gpfq2Z8zX4TKp9eYy88b+X/zkq/5lrHAsC2bZ9AZ+eG8oMrQ+/rBFfKW5zIoX7hBWXVKIMOZNBRNr/8/Hnlm/PIEfP/El9djrlmu/9KB3btssZ2f2oOYjv2ykrl27EBa+VIz84q1ejLF7/7ifpzELDW/LSCYDBYt+OrPTcR1VdhbniZZGEii2P1dZ3Mzc0hkUggkUgglUo19GuPjAA/+EH2sQg4tbRFzexYxoULygry5GRDh2qIkRFllezAgexzYru/mCftdv8F9OIZvKX2eGdFdqq3/O3YC+jFDfTjIXyE2+gCgJI55DMz1rhxNDmp/MwSK+RbcVMtfqeVf+PinXcaPz+pVEq9HszNzTX2ixMREVHLYVCuE7/fD7fbDbfbnZPT1ii7dgEbNyof5wec2qJmN9FryaBT5JiLwlBANse8D/P4CA9hM5QcR207uUxGyfElqqdSvcg3YwkfoQN9mM9p6QVkc6StsAKcX/xuAb24iV4k0Y+TeDnnZ532xsXGjcrPxkYLhULq9cDv9zd+AKQbI2+4ExFRc6nnTXduX9dJOBzGwMAAADSsybxWWxswNJStNs6iZsUdOaKsuImt7NzuT0aqZTs2YL0+27UWv/P5lJ+NjSZJEvbs2QNACeoYmLcu7Xt37NgxHD9+3LjBEBGRoUKhEE6cOFGXc7PQ2zo1UzEYFjWrjDZ3FyhVLCpbGE8QualW2CpM9aftRS5U8r1ote/DVi9+10zXCKqceN/yb7gbcdOdiIiaQyqVUndNiZvuel3fuX3dRERRM3ve/ofiOebZomYrK8ovvVbILweUYGZ6OjtPxbb778MlAFBz8AFYars/1Zd2OzaQzZEGUPR7UQTk7e3A1avWCcgnJ4HBwewquSh+p2X14ndUXwMDA3C5XHC5XAzIiYgsrqenR70miBu2emFQbjIsalaZHTuAr341+1hs99+GBfRiAQDUvFUxR4ASHJw5Y8SIyUzEdmwgN0d64cEqsPZ7UZtH7vdbZ0cLi98RERGRVTAoNyEWNavM4cO5uwoy6FC3wObnrYo5AoBwWNlSS1SLa9eAixeVj4vlSF/CPgDI6bMNKN+rL77Y8OEahsXviIiIyCoYlJvYkSOFQWe5omZAtqe3FVbMS233L15sKztHVtvuT/rRbscG1v5eE6y0HVuskLP4HREREVkFg3ITE0Fne3v2OfYwz1Vsu/9aOfgA88upetrt2EDlOdJW2o5day9yK920ICIiIvNhUG5ylRQ1Yw/z3O3+leTgA8wvp+oUyyNfK0faStuxa+1FbrXid0RERGQ+DMotoFxRMyfmcRSniuZPWy3HXLvdv5IcfAC4fBlYXTVqxNQqVleBqSnl40pzpK22HbtUDrm2F7nVi98RERGROTEot4hSRc224DZzzB/I3+5fSQ7+/fvKCrsV5odqMzurBI737imPK8mRttJ27MpyyBexBbctX/yOiIiIzIlBuU7m5uaQSCSQSCTUpvLNpLoe5tbOMddu969kfi5etM78UHVEjvTkZOU50lbajl1rDjnQ3DcuUqmUej2Ym5szejhERETU5BiU68Tv98PtdsPtdiMUChk9nKIq6WHOHPPc7f6cH6pVrTnSVtmOXev8AM1f/C4UCqnXA7/fb/RwaB2a/YY7ERE1Tj1vuttkWZZ1PaPFJBIJuN1uhMNhDAwMAAB6enrQ09Nj8MjK024XBZRVqm4s4hY2I4n+nO2jKXSjFwvIoAMHDwLnzjV+vI02O6us4InCXJXOjyjMRST+j9mxjAX0FnzPODGPLbiNRXTnbFufmWnO1V+9HTz4YPdOFfMDoCX+j6VSKTWAm5ubg9/vRzweh8vlMnhkVClxbdc6duwYjh8/bsyAiIjIcMePH8eJEydyntPr+m5f+xCqxMDAQEv9wnXkiLJ1VASdIsd8K26WzKF+H9sQDgO//dvmX8kT2/3FSl6l83P+PCDLyvxaIbCiQrOzStGySnOkhWbejq23a9eAixeVjyudH6B1it+1wo1Zqkz+DXciIrIuSZKwZ88eANmb7nrh9nWLqjbHXORzrqwAu3ZZI39abPfXVq5nDj6VU2uO9P79zb0dW0+Tk8DgYPaG4Fo/cwQr3bSg5iFuuLtcLgblREQW19PTo14TxA1bvTAot7BKc8z3I4xuLKptwKyUP719uxJgbdyoPGaOOZVSa470hg3WCTbFHK2sKI9FWsh+hFs6h5yIiIhoPRiUW9z27Up+5gsvZJ/T9jHfjzBegz8n4ASUwOPMGYMG3WBtbcDQUPZxNX3erTJHBJw+XVuf7b17le8xKxBzBOTeuHgNfuxHuOj8iBxyK9y0ICIiImuyyK+CtJYjRwr7mC+iG6/BXzTgBIDLl4HVVSNG23i19nkPh5X8WTK3anKkrdpne3UVmJpSPi524+I1+AuKurVKDjkRERHRejAoJwDZHPP29uxzpYILEXDev6+ssFthizZz8KkU5kivbXZWafV2757yeK2fLYC15oeIiIisjUE5qUZGgOnpbOBZSVGzixetU9SMOfiUjznSaxPF7yYnKy9+194OXL1qjfkhIiIiYlBOOXbsyFYbZ1GzQszBJy3mSJdXa/E7v9/8bReJiIiIBAblVECbP11NUbPTpw0cdIMxB5+YI722V16pvvidlfLsiYiIiAAG5bqZm5tDIpFAIpFAKpUyejjrkp8/XWlRs/PnrbVizhx862KOdHlihfzCBeVxpcXvzDJHqVRKvR7Mzc0ZPRxaBzNd24mIzCSRSGBiYqKhX7Oe13cG5Trx+/1wu91wu90IhUJGD2fdRP602MoOVJZjfuGCtXLMmYNvPcyRLk/Mz4ULlc8PAOzfb548+1AopF4P/H6/0cOhdTDbtZ2IyCxisRh27tzZ0K9Zz+s7g3KdhMNhxONxxONxSJJk9HB0sX278ov1xo3KY+aYF2IOvrUwR7q8WudnwwZzrJALkiSp14NwOGz0cGgdzHhtJyIyg+npabhcroZ+zXpe3+1rH0KVGBgYaPg3RiO0tQFDQ8ovzEA2x7wbi7iFzUiivyCHuhcLyGQ6cOYMcO6ccWNvlMOHlR7VmQznx+xEYbdyOdJbcDsnl9xKOdK1zA8A7N2r/Kwxi56eHvT09DT86969exfJZBJLS0tIp9NwOp1wOBx44oknGj4WszDrtZ2IiKpXz+u7iX4NonrRFn4DKs8xD4eBa9caOVJj1JqDb5X5MYtr15SbL4D1cqQrUcv8ANa6aVEP169fx9e//nU8+eST6OzshNvthtfrhc/ng9vtRn9/P9rb2/HlL38Zr7zyCu7evWv0kImIiNYlmUxicHDQ6GHoikE5rSk/6BSK51A/DjuWYccyVlaAXbuskT9deQ6+Neen1U1OAoOD2VVgO5YtlSO9llrmB7DWTQu9vffee/jyl7+s5jpv2rQJL730Ek6dOoVXX30Vly9fxquvvopTp07hK1/5Cubn5/HSSy+hs7MTv/M7v2P08ImIiKqSTqcRCAQgSRJ8Ph+mp6chSRKmRCucFsft61SRkRHgqaeU7aliK7vIoRbbVG+jCw9hGTfQr+aMvp3ZjQMHlM81+y/eIgf/u99VqnJzfsxB5EmvrCh50tr38za6sBlLJXOkzbQlu5Ra5gdQPufwYX7f1+LNN9/E0NAQnE4nLl++jOeff76iz7tx4wYikQhOnTqFWCyGK1eu4NFHH63zaImIqF5+88Jv4taHt4weRllbHtmCP3/hz9d1jomJCQSDQUQiEbhcLvh8PkQiEQBKnnc0Gm35YpwMyqli27cDf/qngCxnWx2JHOqtuIl38fmS+dOnTyufa3alcvA5P62rVK9tJdh8HH2YL9iSbbYc6XJqmZ8DB/j9XqsbN25gaGgIZ8+erTgYF/r6+jA2NoaxsTFIkoRnn30W09PTdRopERHV260Pb2Hxg8W1D2xhExMTCAQCuHHjBhwOR8HrwWAQnZ2dkCQppwZIIpHAyZMnMTg4iLGxsQaOuDamCsoDgQCSySSSySQA5c7J6Oho0WPHx8cxPT2Nrq4uAIDb7S55LOU6ckTZrprJKI8z6EAGHSXzp9/HNpw/rwTzR46Yf2VMW/gN4Py0qtlZJeAs32v7J+r7K1glR3o983P4cCNHai7pdBrxeBx9fX3rOk8oFMJ3vvMdnUZFRERG2PLIFqOHsKb1jDGZTEKSJEQiETUgTyaTcDqd6jHi+VgspgblkiTB7XYjkUi0TO65aYJyr9eLYDCovhmxWAxerxfRaFTd3qA91ul05jzv8/kQj8dbfutDI4gc8xdeULasAtn8ae0v5fk5pBcuKMH8+fPmzrPl/LS+yclsay+hkvfQKjnSnB/j7NCxt161K+1ERNRc1rstvNmJuGxoaEh9TsR4QjqdBoCcVXTxea0U15lig+X4+HjBlgWPx4OxsTFMTU3lFACIxWKIxWIIBoM55zh79iwmJiaQSCQaNu5WNjICTE/nVhzP79G9D5cAAFtxE3YsK8dZpEc356d1aXttA8q27K24CQBF30OxCtzeDly9av4bKpyf5jQ4OIg333yz5Ot3797F0aNH8fWvfx3Xr19v3MCIiIhqJNp7akWjUXg8HvXxxMQEAGDv3r0NHZveTBGUR6NR+Hw+9U6JMDw8rL4uiO0P+TkJ4rlWuqNitB07cquNi/zpbVhALxYAAAvoxU30YgG9eAZvAVB+mT9zxogRNxbnpzWJXtuAUrhM+x4ByHkP38Zu9fP8fuU9NzvOT3Oan58v+/rQ0BCCwSBef/11PPfcc3jvvfcaM7AWNzc3h0QigUQigVQqZfRwiIgsxe12Y2lpqeTryWQSgUAA0Wi0aL653lKplHpNmJub0/XcpgjKXS5X0TdCm3sgxGKxgjsuQldXF2ZmZuoxRNMq1cMcQE7RJ1HYTKwIRyLA6mrDh9twtc7P5cvWmJ9ms7oKiI01+YXLxHsEwLK9tjk/zcvj8SASiWBwcBCDg4P4kz/5E/W1a9euIRaLYWJiAktLS+jr68P4+LiBo20dfr8fbrdbbT1HRESNMzo6CqfTqV6ztPnkYht7JBLJWTmvp1AopF4T/H6/ruc2RU55MBgs2I4OKG8WgJy8g2QymbPNXcvhcOQE8NUod7ekp6cHPT09NZ232Yn86fz80uJFn7KFze7dU7ayfuYzDR5wg9U6P/fvKznpY2PMv22U2VkgGFTa2QFrv0eClfKkr17l/BSTSqVKrqLqfSe9lMHBQQQCAfUXk0OHDmF+fh7f/OY3MTMzA5vNpm7tGx4eVrf7UXnhcBgDAwMAYNrrOBFRM4vH4xgfH8/ZFS1JEvr7+9fcJaY3SZKwZ88eAMr1XdfAXDYxp9MpO53OnOcAyC6Xq+jxLpdLrnZK4vG4DKDsn2PHjtX6T2gZ16/L8oEDsqzUEJdlOz6Sf4zu7BOA/GM8Lj+BedmOj5Rj7LJ88aLRI2+MyuanW34YH8pbsWDJOTLSxYvKXIv3ZisW5IfxYdH3SLw3gPKeXr9u9Ogb4+JFWW5vz87RE5jn/Dxw7NixNa8D8Xi8rmPo7++Xf+u3fkt9HIlE5La2NlmWZXl8fFz9WJZlORaL5TymQuLaXu/3jYiIKjc6OirfuXOn4uNdLpccDAbrMha9rxOmWCkvxufzweFw4MqVKxV/Tn5OejW0d9PzWeHuen4Pc1HYTGxvvY0uPIRl3EC/WgDq7cxuHDgAPPWUuVfRgLXnJ4VunMTLSKJffWy1OTKKtnDZM3ir4D05ilM574nYlm2lXttijlZWcufoNrpwG10PepJbd360d87z6X4nvYRkMgmfz6c+9nq9kGW5ZO54I3LviIiI9LS0tGTa65cpg3Lxi0k8Hi94rVQ+OaC80eVeL2dgYKDktngr0fYwF4XNtuIm3sXnC3JPe7GATKYDp09b55f3YvPTjUXcwmY1IAcK5+jMGeDcOWPHblaicFmxHOmjOAUn5rEFt7GIbjXgtFqv7VdeKT5HSjD+OPown5NHbrX5aYYUJZfLhampKTz77LMAgMuXL8Nms+GJJ57A7du3c46NRqM1X+uIiIiMkE6n0dXVVdGxgUAA6XQayWQSoVAI8/PzcLvdGB0drfMoa2eKQm9aPp9PTfoXRG45oPziUipvPJ1ON6xQgFmJHOr2duVxBh3IoKNk7imQzbm2QhswMT/aVmnvYxu24HbZOQqHgWvXGj1a87t2Dbh4Ufm4VI70FtwuCDjNniMtiBXyCxeUx8Xn6Cfq/3PAWvPTTE6dOoVXX30VTz75JJ588klIkoRNmzbh61//upo//sorr+C9997DxMSE2p2EiIioFczMzOTUCSsnGAwiFArhzp07mJ+fRygUauqAHGhQUH737l1cv34db775Jt544w1cv369Lu1YfD4fjh49mjPp6XQ6J0AfHh5GOp0u2KouHmu3/1FtRkaAH/wg+3gR3WrvYiGFbtzCZrVH94ULwM6dyiqy2Y2MADMzue3Sis/R47BjGXYsY2UF2LXLGvPTKJOTwOBgdgXYjuWi36eLmuf271feOyv02p6cVP5PXriQ7UV+C5vXnKN33rHG/DQbj8eDmZkZPPvss9ixYwcikQjOnj0LWZZx9OhRvPTSS3jppZfQ39+PzZs34xvf+IbRQyYiIqqYx+PB0NCQ0cOoG5ssy3I9Tnz9+nWEQiHEYrGyFc09Hg++9KUv4dChQ/jEJz5R89dzu90ACrenJ5NJDA8PY2xsTH3O6/XC6XTmtDcRFf20Pc0rkUgk4Ha7EY/HuX1dY3UVePTRbKXmSnJ138Zu2O1K0GOFVbZyc3QbyvYcba6u1eannmZnlYAzP4+81LwDwIYNwAcfAG2m219UqNT8lPu/CwAbNwI//7k15qhSzXSNuHbtGpLJJJ5//nlDx1FP4+PjmJ6eVrc41rpdsZneNyIiaj56Xyd0zyl/7733IEkSYrEYZFmGy+XCSy+9hM2bN8PhcKCrqwtLS0tIp9O4evUqrl27hpdeegljY2MIBAL45je/WfXX9Pl8SCQSAKD+rZXfLi0ajSIQCMDn88HpdCKZTGJwcDAncKf1aWsDhoaUbaxAdfnTVskxLzVHzMGvv2pzpAFg717rBJul5qdcnj0A+HzWmaNmdffuXfVmuFgN/+M//mPs3bsXO3bswI4dOwweYf2IG+7a3XE+nw/xeJw9xomIqLnpUsP9gStXrsidnZ2y2+2Wp6amKv68ZDIpB4NBubOzUx4cHJTv3r2r57Dqim1TSrt+PdtmSvtnKxYKnwTkrVhQH77wgjVaKRWbI85P/Vy/rsxdNXMtWtNZYb5rnR8rzVG1GnmN2Lt3r9zW1ib39/fntDxzu93yH/zBH9T96xspGo3KAApa5dy5c6em+dfrffsfH/wP+d3//q78Pz74HwWPy71WzbHNdh6O3TpjJ7Kypm2JduPGDQwNDeHs2bNVb43r6+vD2NgYxsbGIEkSnn32WUxPT+s1NDKIKGom2k0JIn9aWzAqPy/1wgUlp/X8eXPnp4o5euEFpd0UwPmpl8nJ2r4XrVK4rNb5AawzR83s5ZdfRjQaxczMDDZt2oQnn3xSfW3v3r24dOkSjhw5YuAI6ysSicDhcBS0yhHPhUKhhq+W/7v/9O/wh3/9h5AhwwYbnF1OJJeSkKFkDdpgK/paNcc223k49uYce39XP+aX5gtfs9nQ39mP+TvzkOXCx9pj22xt+J+2/E/4b7f+G1blVdjb7Pg9z+9h+NMsGkmkB91yyq9duwaHw4G+vr51n+s73/lOy+S8Me9sbbOzStspsU0bKMxT3YdLeBefQzcWC1pPWSGH+to1pZCbCIg4P/rS5kgDyrZsMZefw7sFcy1ypNvbgelpwMQ7fgHUPj+AEsgfPszvwVIadY345Cc/iZdffhlf+9rXcOPGDXzyk5/EyoM7fVeuXMGXvvQl9bEZ9ff3w+FwFG2FWu61UsT7Fg6HMTAwUPSYcq3wfvrhT/G5Vz+HVXm14q9J1GrsbXb8QPoBHnvkMaOHQlQXqVQKqVSq6Gtzc3Pw+/26Xd91y/7bsWOHLgE5gJYJyKky27crOdAvvJB9TuRPb8MCerEAAFhAL26iFwvoxTN4C4ASJJw5Y8SoG2vHjtxq7JwffYle5IByw0M7lwBy5lobcPr95g/Igdrn58AB5f82A3LjLS0tYfPmzUVfSyaTpu9LXq6grMPhKPt6OX6/H263u+ifcivvP7r9IwbkZHqZ1Qx+dPtHRg+DqG5CoVDJa4Df79f1a9Wt+rpw/fp1PP3000Vf+9nPfoZ4PI5nn322nkOoq2J308vdPbey/NU4wY5lLKC3YItsLxaQQYdlKjrXOj9WqgpeC22V+7XmUssquxA4P/rT3lnX+056KT6fD++99x6mp6cLVsp37tyJ/v5+vP7663X7+kaz2WxwuVxFV8PdbjcSiQSq+XVHj5XyL4S+gMxqpujrRGbAlXIyu5ZcKS/F5XJh3759RV+rpgl8s9PeTWeV1+JE/rQ9r5JBNxZzggBAqfLc/eC5e/eAq1cbNUrj1Do/9+8ruxBmZxs10tYxO6usdou2c2vNpWClHOmrVzk/etPeWdf7Tnop4+PjmJ+fx6c+9SlMTEwAAN588018+ctfxrVr1wq6kFhJOp2u+XMHBgbgcrmK/il38/2xRx7D73l+D/Y25Qe6vc2Of/nUv1Qft9na0GZrK/paNcc223k4dnOP/Vf/0a9CaLO14fc8v8eAnEytp6en5DWg1A3bmulSLq4Mm80m22w2+cknn5Tfe++9nNdisVhOhdhWJCrvhcNhOR6Py/F4XP7xj39s9LCa2vXrsnzggKZqMz6Sf4zunFLOP8bj8hOYl+34SK3sfPGi0SNvjMrmp1t+GB/KW7FgyTmqxMWL2cr2dnwkb8WC/DA+LDqXYg4BZe6tUkX84kVZbm/PztETmOf86ODHP/6xej0Ih8MNq74+Pz8vezwe9bprs9nkzs5OOZFI1P1rG83pdMoul6voaw6HQ3Y6nVWdj9XXOXaOvfDYb898W3b+vlN2/r5TvpC4UOl/AyJT0rv6ekOC8kAgIDscDrmtrU1+44031NfMFJSzJVr1tK2XnsH31WDgFrrkW+hSA4Jn8H1LtlwqNT8/Rrf8v+Pf5jy26hyVom01V+nciYDTKkrNUan/f1abH70YcY1Ip9NyLBazRDAuDA0NyQ6Ho+hrAOTR0dGqzsdrO1Ghi9cvqkH563/7utHDIQuKRCLy0NCQPDo6KgeDQUPHovd1oiFZqPv27UM8HsfTTz+NoaEh/Ot//a8b8WWpyR05kt2qLQqb9WEeH+EhbMYSAGXr7CXsgx3LyGSUglRWUWx+tmEBTszjKE6p24zz54iF37KFy+xYViuHA8pcHcUpODFfULjMbleqiFvFK68Un6PNWMJH6EAf5i09P61s06ZNeO6557DDClUKHxgeHkY6nS7Yqi4e+3y+xg+KyGQetj+sfvyL5V8YOBKyovHxcYRCIUQiEbXNZSKRMHpYumlYaSin04l4PI6vfe1rePXVV7Fr1y7cuHGjUV+empDIoW5vVx5n0IEMOsrmtIq+51bIn87PMc+gA+9jG7bgdtk5CoeVFmtWde0acPGi8nGpHOktuI33sS2ntZxVcqRnZ5X/QxcuKI+Lz9FP1P+PgLXmh1rT0NAQPB4PAoFAzvOHDh2Cx+OBx+MxaGRE5pETlGcYlFPjxGIxBAIBRCIR9TmPx2OqOl4Nr9ccCoXw6quvYmZmBpIkNfrLU5MZGQF+8IPs40V0I4XunGNS6Mai5rkLF5Qq5ZOTjRqlcUZGlArX2nZpa83RyorS89wK85NvchIYHMxWsK/k+2n/fmWOR0YaOVJjTE4q/3dEQA5UNkfvvGON+WkVbW1taG9vr+rPrl27jB523UWjUTgcDvh8PgQCAfh8PgwODiIajRo9NCJT0Ablv8z80sCRkNX4fD6MjY3B4XDkPD8zM2PMgOrAvvYh+hsdHYXH44HX68V7771nxBCoiezaBWzcqFSAzqAD+3BJ3U6bQjf24RIAYCtuYhHdygpeRlnte+op86/ebd+uBFHf/W7pOdqPMLqxaMn5EcQK8IMuULBjGd1YxH6E8Rr8Od9PYgV4wwZlBdgK7eTE/IgbFmJ+Fh/MSf7/OTFHGzcq/0epeTz//POw2WwFz09NTcHlcqGrq0t9LplMIplMwu12N3KIhrFylXmievtYx8fUj7lSTo0yMTGBdDpdsJi7tLS0ru4azabuQfn8/Dz6+voKnnc6nZifn8fZs2frPQRqcm1twNCQEhwB2fxpETB8Du+qvZNFwPA2dqv50+fOGTr8hig3R0/ihwVBp9XmB8jmkQPAM3ir4KbF3+NT6k0LYe9eawTkQPn52YdLOf/ntHPk81lnjlqFdvue8Pu///sAgMuXLxe8tnPnTuZUE9G6cft689m5cwKLix8YPYyyurs/jpmZ0Zo/PxQKwel0wul05jyfSCQKVs5bWd2D8mIBudahQ4fqPQRqAYcPK3nAImgQ+dPFCnVdwj70YgEZdCASAb79bWsEDcXmaBHduIpdJefn8mVrzM/qKjA1pXxc7HvmNfjVORHsduDFF40YbeOtNT/ie+Z9bMv5PCvNUau7fPkyjh49WvQ1SZIQDAbx7LPPNnhUrW9ubk79uKenp2xvciKzY1DefBYXP8A//MPPjR5G3SQSCSQSCYyNjRW8lkwmMTQ01NDxpFIppFIpALnXBz0Ysn2dKJ8oaqbdXguULtTVjUW8j224dw+4ehX4zGcaPGADiDl64YXsFu215uf+feX4sTHzbmOfnQWCQWVrP7D2nADWK1x29Wp18wNYb45aXTweL1s81Ux5d43k9/vVj48dO4bjx48bNxgigzEobz7d3R83eghrWs8YY7GY+rfX61WfX1pSujQNDg4WfE4ikcDJkycxODhYNJhfj1AohBMnTuh6TkHXoPzrX/961Z9js9nwR3/0R3oOg1rUyIiSA336dHabtihCpQ0iUngcdiwrLcDQgS9+UTneCoWoRkaAX/kVJcc3kyk1P924hc1qDv7Fi8qKuRnnaHIyeyNH5EjfwuaicyIKl7W3K0GqVbpFTU4qN2YAqP9vys0PoMzp4cMMyFvJjh078M1vfhOjo6N49NFHc14LBoM5eeZUuXA4jIGBAQDgKjlZHgu9NZ/1bAtvBdPT0wCUG89agUAAiUQCo6O5/35JkuB2u5FIJIoG7OslSRL27NkDQFkp1964XS9dg/JSZeltNhtkWS75mhmCcm5x08f27cCf/ikgy0pxs/yiZrfRhYewjBvoz+ZPZ3ZbqqjZjh1KNfbz54sXfTuJl5FEf26OuQnnSFu4LD9H+iReVnu55xcu8/utE5Bri99p5+g2unAbXdiMpYL5OXBA+T9Itavn9rZSjh49ir179+KJJ56AJElq3RZRIKdYHjqtbWBgAC6Xy+hhEDWFj9lZ6I0aK51OF+SSA0ph09HR0YKcchGL1qtVWj1jPF2D8mIXfVmWsXfvXoyNjdXljkWz4BY3fR05oqzwZTLZomZbcRPv4vPF86czHTh92jrBhDa/XFv07RY2qwE5UDhHZir8JgqXFcuRPopTcGIeW3A7p3CZ1XKkX3ml+Bwpwfjj6MN8Qb/2w4eNHLE51HN7WylDQ0O4fPkyAoEATp06pT7vcDhw+fJlfOUrX2noeIjIfLh9nYyQH5THYjEkk0kEAgGDRlQfugblzz//fMnXvvSlL5m6yAy3uOkrP386gw5k0FE2F/b8eWWF/cgR86wGl5Kfgy8K423FzbJzZJbCeNrCZaVypLfgtmVzpGdnlYBc9CMvPkc/Uf9fAdaan3qr5/a2coaGhjA0NIQbN24gmUzC6XSuWWyViKhSDMqp0ZxOJ5LJZM5zgUAAY2NjRVfQW1mL/2rePMQWN5fLxaBcJyMjwA9+kH0s8qe18nNhL1wAdu5UVtnNbmQEmJlRtrILa82RKIzX6rSFyyr5vti/X5krs+XUFzM5qfwfEAE5UNkcvfOONeanEXp6etTrgbhZ20h9fX147rnnGJATka7a29rR0abcyGVOOTWCJEk5hUolSUJXVxeCwaCBo6oPBuXU1HbtAjZuVD4W+dMiuBC5sACwFTdhx7JyXEZZQZ6dNWTIDbV9uxJ8lZuj/QijG4vq/Hzxi61902JyEvjCF5SPRXG3/QgXfF+IFeANG6yzAqzNsweU+dmKmwBQ9P+OmKONG5X/a9T8rl+/jrt37+pyrjfeeEOX8xCRdYi8cq6UUyO4XC4Eg0FIkgRJktDf349oNGr0sOqCQTk1tbY2QNuCUORPb8MCerEAAFhAL26iFwvoxTN4C4ASlJw5Y8SIG6/cHO1HGK/BnzM/rXzTIr9wmXjvX4Mf+xFWvy/exm71c/bubf3t+pUSefZA7vwsoBcAcv7vaOfI57POHLU6WZbR19eHv/qrv1rXeV5++WWcPHlSp1ERkVWILey/WGZQTo0xOjqKUCiEUCike4uzZsJfw6jpHT6s5LsKIn8aQEGBr0vYp64IX76s5B5bQbE5WkQ3XoO/6PxkMkoA12pKFS7rwSJegz+nqBtgrcJu2jz7YvNzCfsAIKewG2CtOTKDHTt24PXXX8dzzz2Hf/bP/llVwfndu3fxB3/wB9i8eTOuXLmi9n8lIqqUGpRzpZxIV7oWeivHZrM16kuRyeQXNRNKFfgSRc3u31cKxY2NmX/rcn5hPGDt+WmlwniVFS7L/tsAaxUum50FgsFsnn0l8wNYa47MxOPxYGZmBoFAAM899xxsNhs8Hg9cLhf6+/vVnuRLS0tIp9OYn59Xq9XKsoyxsbGcCu1ERJUSQTlzyqkZBQIBpNNpJJNJhEIhzM/Pw+12F/Qzb0a6BuVPPvlk0edtNhuGhobUXxTyX/vhD3+o5zDIhEZGlB7bp08rQQSQLV6lDT5S6MYtbMZW3MQiunHxYgcuX1Y+x+xFrEZGgL4+4LOfVR6Xmp/8wniTk809P5OThTdkKvm3vfMO8JnPNHKkxtDOj8ixv4XNa87PgQPKDgsG5K3J5XIhGo0ikUggFAohEonk5NnZbDbIspxz/EsvvYSjR49i06ZNRgyZiEzgYx3MKafmJQrA1atPeT3pGpTPz8+XfO3OnTu4c+dOwfNcQadKbd+u9CGXZSWYFEXNxDbdFLpxEi+rfbpFMau3M7tx4IAS1Js9ABGF8e7dKz4/2sJ4Yqu3yDFvxvkpVrisG4tYfPBvyf+3Wa1wmXZ+nsFbBf8XjuJU0fk5cED5v0Stz+Vyqbl2P/vZz5BMJtUVcofDga6uLuzYscPoYbasubk59eOenh52VyHLEyvlK/IKlleW0dHescZnEJlHKpVCKpUCkHt90IOuQXmxoJtIb0eOKKuDmUy2qJlYHRQBOZDNo+3FAjKZDpw5A5w7Z+zY600UfRO7CbTzs4hufA7vYgG9uTctsFstjNds85NfuCw/CNf+27R50lYpXCbmp1gO+VGcghPz2ILbOfNjtysr5GQ+mzZtYgCuM22P+WPHjuH48ePGDYaoCeT3KmdQTlYSCoVw4sSJupxb119bN23aVNMfomqI/GlR2EwUftuC2yXzaAEgHAauXWv0aBvPLIXxWLisvGvXgIsXlY9L5ZBvwe2c+WEOOVF1wuEw4vE44vE4JEkyejhEhtMG5cwrJ6uRJEm9JoTDYV3PbYG1pMaYm5tDIpFAIpFQtzVQ/YyMADMzwFe/mn1O5BlrafNoV1aULc2t3KO7Evk3LYRyxb8AqIXxmqFV2uws4PdXVrhMyypB5+QkMDiY3UWw1vc+AOzfr/yfadbaAWaSSqXU64He29uosQYGBuByueByubh1nQjZPuUA88rJenp6etRrwsDAgK7n1j0ov3v3bsnX3njjjYI/ZuH3++F2u+F2u1uyuEAr2r5dyS3fuFF5LHKoRXCSQjf2I4xuLKqrwa3co7sa4qbFgQPZ50oFbqIwnh3LuHgR2LnT2BsXk5PZMdixjK24qRYu0ypWuMwKQae2VzuQzbPfj3DO9742h3zDBmvcrGgWoVBIvR5otz8TEbW6/O3rRKQPXYPyK1euoLOzE3/wB39Q9PWhoSH4fD74fD714z/7sz/TcwiG4RY3Y4gcakHkUG/DAvYjjNfgx030YgG9eAZvAYCaP212ojDeCy8oj4vdtBCF8bRzZOSNi/zCZQvoxU30Iol+nMTLJYNOUbjMCkFnfp69mKPX4Md+hLENC+jFAt7GbvVz9u61Ro59s6jn9jYiIiNtsG9QP2ZQTqQfXQu9hUIhOBwOfOMb3yh5zEsvvYTBwUHIsoxTp07h0qVL+Jf/8l/qOQxDiC1u1HiHDyu5tSJQyaADi+jGVewqXvQNSpu0b3/bGoFKKxXGY+Gy8tbKs38NfvV7XLBKjn0zYZVuIjIrrpQT1YeuIUkikcDevXvLHvOlL30Jzz//PIaGhuDxeJBIJPQcAlmQyKFub88+10r50/VWa2G8SKSxhd+0AScLlxWqJc/eSvNDRET1J/qUAyz0RqQnXYPyZDKJ/v7+io/v7+9HMpnUcwhkUSMjwPR0NvCspPBVM+RPN0othfHu3QOuXm3cGK9ezQacLFyWS5tnL6w1R+3typxaYX4o6+7du3jjjTdy0sj++I//uGy9FyKiSnGlnKg+dA3KHQ4HHA5HyddXV1fx7LPPqo/T6bSeX54sbseObNBZLH96Hy4BgFrUDLBO4TegtsJ4X/xiY25aTE4CX/iC8jELl+XS5tkD2eJ3AIp+j4s58vuV/xNkHcPDw+js7MTY2BgCgYD6/KuvvoqzZ88aODIiMoucoHyZQTkZJ5FIYGJiwuhh6EbXoNzpdCIWi1V8fDQaZR426Urbo1tb9K0XCwCgFsWyYuE3oPrCeI24aaGtJs7CZYVKFXZbQC8A5HyPizliHrn1vPzyy4hGo5iZmcH3vve9nNf27t2LS5cuGTQyIjITrpRTs4jFYti5c6fRw9CNrr/Wjo6OIhKJVFRR/cqVK4jFYhgeHtZzCGRxpfKnARQUxbqEfeqK8OXLjc2fNpL2xgWQLYz3GvxF5yeTUQLDennlldLF3V6DP6eoG2CtgHOtwm6XsA8ALJtnT1lTU1MYHx/Hjh07YLPZcl5zu92s31Kjubk5ted8KpUyejhEhtP2KWdOORlpenq64Yu7qVRKvSbMzc3pem7dg/Knn34aQ0NDZQPzN954A1/60pfgdrvLVmonqkWx/GkWfsuqpTDe+fP6r5iLFfILFyobA2CtgLOWwm5WyrOnXEtLS9i8eXPR15LJJJxOZ4NHZA5+v1/tOR8KhYweDpHhtCvl9zP3DRwJ1cNPP/wp/nrhr/HTD39q9FCaUigUUq8Jfr9f13Pr2hINACKRCNxuN4aGhtDf34/R0VH1l4FkMonXX38diUQCmzZtQiQS0fvLEwHI5k9/97tKUCOKYmmDmhS6cQubsRU3sYhuXLyotEo7f978Qc3ICNDXB3z2s8rjUvOjLax24YKS+63H/ExO5uZJVzqGd94BPvOZ9X3tVqCdH5Fjfwuby86PyLO3yrZ+yvXcc8/hm9/8ZtEWo6FQiKliNQqHwxgYGAAAtrkjArevm9nrf/s6/o/Y/4HMagb2Njt+z/N7GP50c+5oTiaTGBwcbPjXlSQJe/bsAaDspNIzMNc9KHc6nXjvvffwta99Dd/5zndyis0AgCzLGBoawtmzZ7Fp0ya9vzyRSuRPnz+fLWomtv+m0I2TeFnt0y2KZL2d2Y0DB4CnnjL/auyuXUrRt3v3is+PtjCe2EIucszXMz/FCpd1YxGLD75m/hjEtuyNG5Uxm512fp7BWwXfs0dxquj8WCnPngqNj4/D7XbjU5/6FJ5//nkAwJtvvolgMIhr165hSuRBUFUGBgZ4Q4NIg0F5c/nNC7+JWx/eWvd5VlZX8NN72dXxzGoGv/O938GZH5xBe1t7mc9c25ZHtuDPX/jz9Q4R6XQaJ0+eRDqdxszMDJxOJyRJgtfrxZC2YFId9fT01O0Gre5BOQB1FfzatWt4/fXX1bZnTqcTw8PD2MGSwNQghw8rrc8ymWxRM7HqKAJyIJuf24sFZDIdOHMGOHfO2LHXm/amBZA7P4voxufwLhbQm3vTArvVwni1zk9+4bL8IFw7Bm0uuc9njaBTzE+xHPKjOAUn5rEFt3Pmx0p59lRcX18fZmZmIEkSgsEgAMDj8cDhcGBmZgZPPPGEsQMkIlPQBuW/XGZOudFufXgLix8srn1gjbSBupEmJiYQDAYRiUTgcrng8/nUHdeSJCEajbZ8ilFdgnJhx44dlgnAtcn+9byLQtUR+dNi5VEUftuKmyXzc9/HNoTDwG//tvlbSmlvWgDZ+SlVVKwXC8igA5EI8O1vVx8kV1K4rBcLanE+wSpB57VryvsBlM4h34LbOfNjpTz7VpFKpdSiYHoXginH6XQiGo3iZz/7GWZmZtDV1WWZazARNYa20BtXyo235ZEtupwnf6VceGzjY7qslK/HxMQEAoEAbty4UbT1djAYRGdnJyRJUnc2TU1N4fXXXwegbHUfHh7G2NjYusZRb3UNyq1Em1Nw7NgxHD9+3LjBUI6REWW79fh4NuBZK395ZUXZKm32/PL8mxZCuaJi72Mb7t0Drl6tPr/76tXKCpdZMeicnFSKDa6sKI8rybHfvx946SXzz02rCYVCOHHihGFff9OmTXjuuecM+/pEZF7cvt5c9NgWLjRjTnkymYQkSYhEImpAnl+8VDwfi8XgcrkwNTWF6elpdSU9nU6jr68P8/PzTb2arltQfv36dTidTnziE59Y97neeOMNfOUrX9FhVI3DYjDNLb/wW7Ec6v0I52yb1iN/uhWImxanT2e3slcSEH7xi9XdtBBBp1DJ1zhwQFnNN/P8A7m92oFsnr3oHV8sh5yF3ZpXPQvBAMr1thZPP/20ruMgIuthUG5ew58exrP9z+JHt3+ET27+JB575DGjh6QG0dqc8VgsBq/Xqz5Op9MAssG5WCEXHA4Hjh49ikAgYI2gXJZl9PX1YWpqCr/+679e83lefvllXLlypeWCchaDaX7lcqifxA8Lgh898qdbxfbtwJ/+KSDLys0LvW9a1BJ0HjigjMkKyuXZ70cYf49PFeTYs7Bb86p3CpPL5SroRV6OLMuw2WxYEf8BiYhq9HCHJqecfcpN57FHHmuKYFxIp9MFLT2j0WhOB6+JiQkAwN69ewEoN8ZFoC4U2/bebHQLynfs2IHXX38dzz33HL70pS8hEAhUHJzfvXsXExMTOHnyJJxOJ2KxmF7DIspRLId6Ed24il0l86cvX64tf7oVHTmirGjnF8Zb702LaoNOu115r6xgrTz71+BXvxcFq+TYU3FsJ0pERuFKOTWS2+3G5cuXS76eTCYRCAQQjUbVwNvj8RQcFwqFij7fTHTNKfd4PJiZmUEgEMBzzz0Hm80Gj8cDl8uF/v5+dHV1AQCWlpaQTqcxPz+PWCyGZDIJWZYxNjaGU6dO6Tkkohwih1qbu7tWbvP9+8rxY2Pm30adPz963LSoNui0Sg45oOwgCAary7O30vxQcaLlGRFRo7HQGzXS6OgoQqEQxsfHMTY2lpNPHovF1HzzcgG3aM/d7De0dS/05nK5EI1GkUgkEAqFEIlEEI1G1ddtNhtkWc45/qWXXsLRo0fZt5waYmQE+JVfUQq5ZTKV5TZfvAhcvmz+wm+A8u/r6wM++1nl8XpuWtQSdL7zTvUF5FrR5GRhgb21vhfb25VieSyoTfkGBwcRDAbx7LPPFn397t27an9XSZKYX14hdlYhytXR1oE2WxtW5VUG5dQQ8Xgc4+Pj8Pl86rZ0SZLQ39+P+fn5sp87Pj6OZDKJeDyuy1jq2l1FboB0Oi0nEgk5FovJU1NTciwWkxOJRCO+dIH5+fmqnl9LPB6XAcjxeHw9wyIDHDggy0oWtSw/g+/LP0a3LAPyj9EtP4Pvy3Z8JG/FgmzHR+pxdrssX79u9Mjrb2VFljdufPBvxkfq3Ig/P0Z3zryIubl4MXuOixeV53KOWeNcGzcqX9vsrl/PnRvt91qx70Vx3MGDRo+cqtWoa0RnZ6d85cqVkq97vV7ZZrPJnZ2dcldXl3zjxo26jqfVifdN++fYsWNGD4uoKfzav/012fn7TvnL3/6y0UMhixkdHZXv3LlT0bFjY2NyMBhUH4dCoXV//WPHjhVcG/S6vjckS3bTpk3YsWMHnnvuOTz//PN47rnn6tI7NZ1Ow+v1qgn/xUiSBJvNBrfbDa/XC7fbjc7Ozqauxkf1cfiwshUYyOZPb8MCerEAAFhAL26iFwvoxTN4CwDUHGqzE0XxgGzRt9SD1VqRUw4AW3ETdiwrxz0o/DY7my3sJlaB7VjGVtwEgKLnElvXfT5r5O7n59hrv9cA5Hwvvo3dAJhHTuV5PB5EIhEMDg5icHAQf/Inf6K+du3aNcRiMUxMTGBpaQl9fX0YHx83cLStIxwOIx6PIx6PQ5Iko4dD1BREXjlXyqnRlpaWKiraJoq9iRZpU1NTumxflyRJvSaEw+F1n0/LFH3KJUnC0tISgMIy+cU4nU4kEgk4HA7s3LkTwWCw6ZP/SX/5Pboz6MD72FY071mbQx2JWKPwm7Yonrbo2yK68Tm8iwX0liz8JsulC7vtw6Wcc2lzya0QdK6VYy++16zYq51qNzg4iEAgoF7LDh06hPn5eXzzm9/EzMwMbDabWpl2eHi47M1rymJnFaJCIq+cQTk1UjqdVuuTlSNJknqN017r9Ij16pnGZIqwQuSunz17tqLj5+fnIcsy7ty5g2g0yoDcwkZGgJkZ4KtfzT5XLu8ZUPKjr15t5CiNIW5aiN0E4qYFgKKBpFgxf/31tYNOAHgf2yxZ3O3q1cpy7IX9+5XvUbPXMqD1CYVCkCQJ3/ve9/C9730Ply9fRjAYBJDt4fqJT3wCgFLLJZlMGjVUImpxXCknI8zMzKy58Aoo10NZlgv+aGucNSNTBOVE67F9u9Kbe+NG5bEotqWVX/jti19UCnWZXS03LX7xCwadpUxOAl/4QvbxWt9rGzZY52YFrU8ymYTP51Mfe71eyLKM9957r+jxrdCzlYiakwjK2aecGsnj8WBI5FaakCm2r9diampKLavv8XjW/QtKuQp8rNja/EQO9fnz2Rzq/F7a2u3WIn/6qafMHzCJmxbf/a4SbJeqEH4Lm7EVN3O2pK9VTVwEnWZPBQCyefaiFZ8dy+jGIvYjXNADXszf3r3WmJtWp63Gmk/36qwliLw5UX398uXLsNlseOKJJ3D79u2cY6PRqNpShoioWiIo/2jlI6ysrqC9rd3gERG1Pkv+uhcIBOB0OjE2NgaHwwG3273u/Dq/3w+32130D4vItYZShd9E0GTVom/A2oXfTuJlJNFfOEclisRZMegsVdztNfixH2EWdmthoVCo5M9/v9/fkDGcOnUKr776Kp588kk8+eSTkCQJmzZtwte//nX1+vbKK6/gvffew8TEBIaHhxsyLiIyH22vcq6WE+nDJsuapuHrdPfuXQDZvLVGS6fT6OzsRDAYxNjYWNFjtE3nhampKfh8PsTj8aoLuiQSCbjdboTDYQwMDBQ9hivlrWNyUum5rV3NFAXNhBS61aJvGzYAH3xgjcBydhbYuTO3ono3FnELm5FEf8k50h6bX9htZsb8Ow0Apbjbo48qOw3W+p4Csjn2VtjSbwZrrZT7/f6ari/VSiQSCIVCuHPnjhp0R6NR9Pf34/bt2xgfH4fNZoPT6cTf//3f13Usa0mn0/D5fPD5fBgdHS153Pj4OKanp9XiPm63u+Tx1Ry7FnFtb8T7RtRq/tV3/hW+f+P7AIDpfz2Nro1rF98iMhu9rxO6bl93u92QJAnf+MY39Dytropt2ROF3kKhUM2r2qzQag4jI0BfH/DZzyqPy+VEv49tuH9fCeLHxswfXJaqVr8VN8vOEZBbJA6wVmG32VkgGKwsz17M0TvvAJ/5TKNHSrVqlhuvLper4Br2/PPPqx8PDw8jmUzmPNdo1XRL8Xq9cDqdOW1sxA30/H9nNccS0fqI7esAi70R6UXX9b35+fmCoHfz5s24fv26nl+mZuPj43C73SVfZzVaAoBdu6or+nbxorKCbNXCb5XM0cMPrt8bNwIHD1qrsFv+98Za87Vxo/I9SKS3HTt2GBqQA5V3S4nFYojFYmoFeeHs2bOYmJhAIpGo6VgiWr+HO7JBObevE+lD16Dc5XJhZmYm57k7d+7o+SXWJRqNqq1htMRde650E7B2/vQ+XAIAbMVNtQ2YKPw2O2vIkBsqv1p9sTkShfHE/CwvK73df/5z4Nw566yQi10FgLJtfStuAkDZPHufzxrpEETlRCIROByOgiKs4jnt6nc1xxLR+mlzyrlSTqQPXbevv/zyy9i7dy/i8XjOinkgEChZ3dxms+HSpUt6DqMkr9dbNPCeetBUWZKkhoyDmt/hw8oKeCaTLfomcqI/h3fVnGARUL2N3Wrht3PnjB59/Wmr1QO5c/QkflhQTfztld0YHQVcLmsE5EBhYTdtNf99uJTzPaXNJWdxN6rFz372M+zduxczMzNFbz7bbDZkxDdkC4jFYiUrxHd1deUsAFRzbLXYWYWoELevk1U0sruKrkH50NAQLl++jFOnTqkN2m02W9lm7XoG5WLFO7/9izA2NqbmnYkLeCKRwMmTJxEKhdgihlSl8qftWFaDK0DJBb6EfWqRrsuXlRVhK6x0am9cAMocLaIbV7Gr+PxkOixz02J1FXhwr6/s94xV8+xJfz6fTw1O3W53y/chTyaTJXevORyOnHSzao6tVrnq+ceOHcPx48drPjdRq2JQTlYRCoVw4sSJhnwt3fuUDw0N5TR2b2trQyKRwNNPP633l1IFAgEkk0k1b0zkkDkcDpw9ezbnl5NoNIpAIIB0Oo2lpSWk02lcuXKFW9epwMiI0od8fFwJPgEWftMSNy601erXmp9IxPw3LWop7LZ/P/DSS+b/nqH6mZmZgSRJ+Na3vmX0UBqi2G4APY7Nt1ZnFSIrYlBORpmamsLrr7+Orq4u9Pf3l+y2pRdJkrBnz56ir4nuKnrRPSjPFwwG674CnV/cRe/jybpE/vR3v6sEWaJIV347q/zCb5cvW6OlVX61+rXm59494P594JFHjBht/U1O5uaRA2vPyYYNyveKmW9UUP11dXWVrWRuJo0KyAF2ViEqhn3KyQjj4+OIRqPqDuz+/n54PJ66/oxuZJpS3X8NfOmllwzrW06kBxZ+K09brb7U/Iic6Y0blSDUjGot7LZ3LwNyWr/nn3++bKqYHrxeL2w2W8V/Ojs7a/5a5W7mLy0t5bxezbFEtH5cKadGi8ViCAQCOW0vPR6PqQp58ldBogocPqzk/ALZombbsIBeLAAAFtCLm+jFAnrxDN4CALXwm9lpb1oAhfPzNnarr5m5snh+YTft9wSAonPCwm6kl9/6rd9CNBrFvn378MYbb+DNN98s+LNe0WgUsixX/Gc93VdcLlfJXPB0Og2Px1PTsUS0fgzKzWtx8QP81V/dwOLiB0YPJYfP58PY2FhBvZT1FPJsNnXfvm4V2gp8rMhqPrUWfrNCDjVQvOibtogZYO4AlIXdSEtbrVXv6qyluN1upNNpJJPJnJUEAJBlGTabDSui+EMLGB4extTUFNLpdM4vYWI7us/nq+lYIlo/BuXm9Md/nMDXv/7/RyazCru9Dd/61j/H175mfPrOxMQE0ul0QZcsURvMLBiU60Sb6M+KrOZUS+G3e/eAq1eBz3zGgAE3UP5Ni3xmD0CvXmVhN8pqZLVWodVqpazVLWVoaAgejweBQCBne+KhQ4fg8XhyVr+rOZaI1k8blP9ymTnlRtq5c0KXVe2VlVUsLn6oPs5kVnHo0F/gd3/3TbS3r29lqbv745iZGa3580WHrPxUJFHU2ywYlOtEW6GVq+TmVUvhty9+0TpF3556StmyH4ko87Nxo7Jl/cUXzRuATk4qFegFFnYjbbVWvauzlnLo0KG6fw091NItxefzwel0IplMYnBwsGi13WqOJaL10RZ640q5sRYXP8A//MPP63j+D9c+qI4SiQQSiUTRn+XJZDKn41erY1CuE1ZotQ6RQ33+fLawmdiunEI39iOMbixiEd3IoEMt+vbUU+YNTIXt25U+5N/+tlJlfcMGcweforib2BVsxzK6sYj9COM1+NXvCRZ2sxamMJVWz24prbZbgKhVcft68+ju/rgu58lfKc+e/xFdVsprFYvF1L+1HUbEbqvBwcGc40XLNEAJ2oeHh1vm5iyDcqIaaHOoRWGzbiziSfywIBh7G7vVom/nzhk98sZoazNv2zOt/OJu+Tdn/h6fUm/OAObOq6fmcPfu3ZJFz55++unGDoaITOnhDgblzWI928LzNWNO+fT0NAAgHo/nPB8IBJBIJDA6mv33T01NYXp6Wq2rkk6n0dfXh/n5+Zao0s6gnKgGIof6hReUVdIMOrCIblzFrpJF3y5ftkbRN6tYq7jba/Cr7z1g/rx6Mp4oeFaMy+VSf7mhyrGIK1EhrpSb09e+5sK/+BefwtzcTzEw8Jhuq/DrkU6ni7a1nJqawujoaE7Kk1ghFxwOB44ePVpQb2Q96lnIleEBUY1GRoAf/CD7uFyBL0DZzv3CC9boXW52s7OA319ZcTfhnXfMX1eAjPPyyy8jEong0KFDOHnyJGRZxksvvYRvfOMbkGW5oGotVcbv98PtdsPtdrfESgtRI2hzyn+ZYaE3M+nu/jh+/df7miIgF/KD8lgshmQyiUAgkPO8JEkYHh7OeU7vQnChUEi9JuhdL4Yr5UTrsGuXUsys0qJvFy8Cly9bo/CbWU1OFlaZX+u937hR+V4hqpepqSmMj4/jG9/4BgClgNq+ffvw9NNPw2azYX5+3uARtiYWcSUqxJVyahRRuFMrEAhgbGysIFgv1mkjFArp2oGjnoVcuVJOtA6i6BuQLfqWehCIiZxyANiKm7BjWTnuQeE3rpi3HlHYTQTkdixjK24CQNH3Xmxd9/mYtkD1lUwmc4qNan+R8Xq9Jbe1U3miiKvL5WJQTvTABvsG9WMG5VRPkiRhZmYm53FXV1dFhT3FSrrIMddDT0+Pek0QN2z1wl8Tidbp8GElXxjIFn3bhgX0YgEAsIBe3EQvFtCLZ/AWAKiF36i15Bd20763AHLe+7exGwCLu1FjOJ1OXLt2TX3scrkQjUYBKC1lShV/IyKqFlfKqVFcLheCwSAkSYIkSejv71evbeWMj48jmUwiHo+3TC9zbl8nWidR9E2soGbQgfexrWjxL23ht0iEhd9ayVqF3cR7+z62qZ/D4m7UKM8//zwuXbqEI0eOAAD27t2LnTt3wuFwIBQKFS2UQ0RUC+aUUyNpK6xXIhAIYPPmzeoK+cTERNXnMALDASIdjIwAMzPAV7+afW6t4l/37inF36g13L9fXWG3/fuV7wnWDqBG+J3f+R28/PLL6mOXy4VDhw6pW/z03L5HRNZms9nUwPz+Mn+RoeYhSRLS6TRcLhempqYwNTXVMtc/rpQT6WT7duDCBeC7362s8Ft7O/DDHwI7dhg0YKrKD3+orHxnMmu/txs2KCvk3AVBjbJp0yY8//zzOc+FQiGMj49j06ZNBo2KiMzqYfvD+GXml9y+Tk1DkiRMTEwAgPo3ULwAXDPir4w6mZubQyKRQCKRUPvXkfWsVfhtP8LoxiLsWMbKilKRe3LSwAFTRSYnlfcqk1G2rndjEfsRLlnYbe9eBuRWlkql1OuB3n1Mq8WAnIjqQeSVc/s6NYtQKARZlgv+VJKD3gz4a6NO2MuUhFKF3/YjjNfgzyn6xkrszU9bcV1b3O01+LEfYRZ2owL17GNKRNQMxPZ1rpQT6YNBuU7C4TDi8Tji8TgkSTJ6OGQgUfhNBOYZdGAR3XgN/oLCYHYssxJ7kxMV14sVd3sNfiyiW10hZ2E3ApQtdOJ6EA6HjR4OEZHuxEo5g3IifTCnXCeilykRoBT3+pVfyW55LlcY7H1sw+XLrMTejLQV19d6D9vbgatXWSOAlD6m7GlNRGamDcplWYbNZjN4REStjUE5UZ186lPZntZrFQa7fx944QVgbIyrrM1idhYIBrMV19d6D1dWlPeciMxDWxOAN1uIsrS9yj9a+SinTRqRWaVSKbV2mN41Y7guR1QnGzYAGzcqHxcr+qYtDAYAFy8CO3ey8FszmJwsfC/Weg83blTecyIyD9aLISpOG4RzCztZRT1rxnClnKhORCX28+eVx6LoWzcW1TxkUclbPBaF3556iivmRtEWdgOQ8x4Vew8Fn4/pB0RmEw6HMTAwAABcJSfS0K6U/yLzC2wCOz2Q+UmShD179gBQVsr1DMz5KyRRHWkrsQPKauv72IYMOnIqeYtq7ABY+M1gorAbgKLvkfY9FFhxncicRL0Yl8vFoJxIIz8oJ7KCnp4e9ZogbtjqhUE5UR3lV2IXilXyFtXYAeDyZaXIGDWWtrDbWu+RwIrrRERkNTlB+TKDcqL1YlBOVGcjI8DMDPDVr2afK1fJG8gWfmP/8saZnQX8/mxht7XeIwDYv195b0dGGjlSIiIiY2mD8l9mfmngSIjMgUE5UQNs3w5cuJAt/CYqeWtpK3kDLPzWSMUKu631Hm3YwBVyIiKyJhZ6I9IXg3KiBhGF34DSlbwBYCtuqlukReE3rpjXT7HCbltxEwDKVlvfu5eF3YiIyJqYU06kL/5KqZO5uTkkEgkkEgm1fx1RPm3hN1HJexsW0IsFAGDhNwOUK+wGIOc9ehu7AbCwG5WXSqXU64HefUyJiJrBwx0Myon0xKBcJ+xlSpXIL/wmKnkDKFtULBJh4bd6qKSwG4Ccauss7EZrqWcfUyKiZsCVciJ9MSjXSTgcRjweRzwehyRJRg+Hmlgthd/u3VOKv5G+7t9nYTfSnyRJ6vUgHA4bPRwiIt1pc8pZ6I1o/exrH0KVEL1MiSohCr9997tKUCiKimmDQm1RsfZ24Ic/BHbsMGjAJvXDHyor35nM2u+BKOzGPHJaS09PD3tam4Q2/YDvK1EWV8rJilKplJqmrHd6Gn+9JDJIJYXfxJbplRVg1y5WYtfT5KQypyKffK33gIXdiKyHqWlExTEoJyuqZ3oaV8qJDHT4sNL6LJPJFn7rxqK6OrsVN7GIbmTQoVZif+op5jOvV7GK691YxLv4XM57oM0jZ2E3IusJh8MYGBgAAK6SE2kwKCcrkiQJe/bsAaCslOsZmHPdh8hApQq/fQ7vshJ7HZWruP45vMvCbkQEIJua5nK5GJQTaTCnnKyop6dHvSaIG7Z6YVBOZLCREeDq1WxgXqoKOCux66OSiutirtvblfeGhd2IiIiyuFJOpC8G5URN4FOfyq7cshJ7fVVTcX1lRXlviIiIKItBOZG+GJQTNYENG4CNG5WPRRVwLW0VcAD4+teVvGiqzuysMnfCWnO9caPy3hAREVFWTlC+zKCcaL0YlBM1gUoqsQNK4Tc7lnHhArBzJ6uxV2NyUpmzCxeUbetbcRMAylZc9/lYcZ2IiCifNihnTjnR+vHXTaImcfhwNq9cVGLfhgX0YgEACgq/iWrsXDFfm7baen5hNwA5c/02dgNgxXUiIqJStIXeuH2daP3YEk0n2gbyPT09rNJKVROV2EXwKCqxlypG1osFZDIdOHMGOHfO2LE3O1Ftvdxcvo9t6vGsuE7rkUqlkEqlAOReG4iIzOLhjuxK+d1f3gUA/PTDn+JHt3+ET27+JB575LGcxwBKvlbNsc12Ho699cfeLBiU60Tbp+7YsWM4fvy4cYOhljUyovQhP31aCQqB8sXI3sc2RCLAt7/NbdalaKutrzWXgHJT5PBhBuRUu1AohBMnThg9DCKiuvmLub9QP575hxl84z9+A3/x//4FMqsZ2Nvs+I1f+Q31cZtN+QVlVV4teK2aY5vtPBx764/99zy/h+FPD+vyf2K9bLIsy0YPopUlEgm43W6Ew2G1Xx1Xymm9PvwQ+PjHlY/tWMYCenOCyRS6lZXyB7nPH3wAPPKIESNtfpxLarT8lXK/3494PA6Xy2XwyKhSelzbr8xfgfRnUr2GaAibzWb0EAiADbW9D/nvn/Y8xd5bm80GG2xot7Wjva0d9jY7Oto78Pnez+Pf/7//HpnVTE3jIGoW9jY7fiD9oOIV83pe37lSrpOBgQH+wkW6EdXY793LFn4T267zi5EBSkXxI0e4uptvdhZ45ZXs47XmktXWSQ+8MWse690FJ8Nc6x5cx2lxOr19b/zXN/Q5EZHBMqsZ/Oj2jyoOyuu5E45BOVETEtXYxRZ2UfitG4tYRDcy6IAdy+rjCxc6MDmpHD8yYuzYm8XkZDY/XztXxeZSYLV1ItLKXymvxic+9gl8uvvT9RgWWch6boSUuymU85qc+5wsy5Ahq3+vrq5iRV7B7Xu31fzxdls7VuSVmsdG1AzsbXY177wSkiRhz549ALIr5bqNRbczEZGuDh8GLl5UgkogW/gNUCqI56/2vp3ZjQMHlJx0q6+Y51dbL5gr7M4p7Aaw2joRFVrPLrjBrYP4M/+f6TwiIuP84V//If7tf/q3AID9T+/HxdmLRXN8mz0nuRlzmzl243LKqyn2Vs+dcMwpXyeRd8Z8QaoH7WqvsFZe9MGDrMZ+8KCya6CSHHIgW22duwxIb7xGtCa+b0SFzk6fxam3TgEA/vA3/hCDWwebrpJ2K1cB59hbq/q63tcJBuXrxAs31dvsbG419q24iZsP+mtrbXvQ1mvjRuDnP7fuNuzVVeDRR5V8/LXmCmC1daovXiNaE983okIXrl3A8SvHAQC////7fXzlV79i7ICIDKT3dcJUv7an02l4vV5MTEyUPW58fBw+nw+SJEGSpDWPJzLS9u3AH/1R9vEiupFCd84xKXRj8cFz9+4B9+83coTN5f59ZQ6AtecKUOaWATkREVF5D9uzvcl/kfmFgSMhMh9T5JRLkoSlpSUAQCwWg9frLXms1+uF0+lEJBJRn/P5fIjH4wiFQnUfK1EtqqnG3t4O/PCHwI4dBg/aID/8obIdPZNhtXUiIiK9fMz+MfXjX2Z+aeBIiMzHFEG5CKbT6TSmpqZKHheLxRCLxXDnzp2c58+ePYvOzk5IksRtatSUKqnGLqysALt2WTNHulgOPqutEzWXQCCAZDKJZDIJQLmxPjo6WvTY8fFxTE9Po6urCwDgdrt1OZaIqseVcqL6MUVQXqlIJAKHwwGHw5HzvHguFApxtZyaVrlq7NqWXxl0IJOB5SqxayuuA4VzwmrrRMbzer0IBoPqDXCxuy0ajebsYBPHVrqzjbvgiOovJyhfZlBOpCdLBeWxWAxOp7Poa11dXZiZman53HNzcyVfq2f5fLKO7duV1e/8leBSLb8yGeDMGetUYj99OjsvpeZEENXWrXLDguorlUohlUoVfa3ctcFqxsfHC3akeTwejI2NYXx8HFNTUxgaGgJQ3c427oIjagyulBPVj6U2boqtcsU4HI6yr6/F7/fD7XYX/cO79KSXkRHg6lUlqASU1WARfAJADxZxCftgxzIAIBJRqpGb3eoqIDJX1pqT9nZlDq22tZ/qJxQKlfz57/f7jR5e04hGo/D5fEin0znPDw8Pq68Llexsq+VYIqqdNihnTjmRviy1Ur6W/F8UqhEOhzEwMFD0Na6Sk54+9ansinA3FnN6cANKENqNRbyPbWol9kceMWCgDaStuL7WnKysKHNIpBdJkrBnz56ir83NzTEwf8DlchXdkSaCae2N8Wp2tnEXHFFjaAu9caWcrKCRO+EYlD+wnoAcAAYGBrg9jhpCW4ldtPzSBqH5Lb++/nXgyBHzbtWenQVeeSX7eK05YcV10huDs8oEg0EEg8GC52OxGADkdE5JJpMlr6n5O9uqObZa5W6oHDt2DMePH6/53ESt5uEObl8nawmFQjhx4kRDvpalgvJSd9IBYGlpqezrRM1CW4l9rZZfAHDhglKV3IzV2ItVW19rTlhxnai5BINBOJ1OjI2NVfw51dxI5y44In0wp5ysppE74SwVlLtcLvWOfL50Oo29e/c2eEREtdFWYi/W8ssK1djLVVsv1QaNFdeJmovP54PD4cCVK1cq/pxGBeQAd8ERaTEoJ6tp5E44S60XDQ8PI51OF1ykxWOfz9f4QRHVQFRiFwXfRMuvDDrwDN7CAnpxE71YQC+ewVvKMQ+qsZtFfrX1/H+zdk4AVlwnWi+v1wubzVbxn87OzrLnE9fceDxeUKStmp1t3AVH1Bgs9EZUP6YKypeWlgAAt2/fLvr60NAQPB4PAoFAzvOHDh2Cx+OBx+Op+xiJ9DIyAszMKKvFglWqsVdTbR1Q5mhmxnzb94kaKRqNQpbliv/ktyjT8vl88Hq9OX3FtTvZXC5XyVzwdDqdc72u5lgiqt1D7Q/BBhsArpQT6c0UQXkgEFAv8AAwMTEBr9dbtPVKNBqFw+GAz+dTP29wcDCnFQtRq9i+HfijP8o+Lld5HIBajb3VVVptXfijP+IKOVGz8Pl8OHr0KEZHR9Xn0ul0ToBezc427oIjagybzaZWYGdQTqQvU+SUF6vmqufxRM2smmrsZqk8bsV/M5EZuN1uAMDJkydznk8mk2q/ciB3Z5u2z3ixnW3VHEtE6/Ow/WH8IvMLBuVEOjNFUE5kZdVUY+/pAf7u71p/1fjv/g7o7gaSSVZbJ2oVPp8PiUQCANS/tfJvmEejUXVHm9PpRDKZxODgYNEq7dUcS0S1E3nlzCkn0heDciITKFeNHQC24iYW0Y35+Q7s3Nna7dG0bdBExfV38TlWWydqctrt6ZWqZmcbd8ER1R+3rxPVB9ePiEygVDX2z+HdwqrkD9qjzc4aO+ZaaNug5Vdc/xzeZbV1IiKiOhIr5QzKifTFlXKdzM3NqR83sqcdkTAyovQhf/55YH6+dFXyXiwgk+nAmTPAuXPGjrlaog1a2X8bOtDfD3znOwzIyRipVAqpVApA7rWBWg+v7US5tNvXZVmGzWYzeEREjVPP6ztXynXi9/vhdrvhdrtzCs0QNdL//D8DD35WrFmVvNXao2nboK31b0ullLkgMkIoFFKvB36/3+jh0Drw2k6Ui73KycrqeX3nSrlOwuEwBgYGAIB30skw2lZha1UlF+3RHnnEiJFWz8z/NjIXSZKwZ88eAMqddAbmrYvXdqJcIqccULawP9zxcJmjicylntd3BuU6GRgYgMvlMnoYZHHaVmFrVSXfsKG1WoVV829jGzQyErc5mwev7US5tEE488rJaup5fef2dSITEe3RBFGJfRsW0IsFvI3d6mvLy8D/9r+1RsG32VllrL/U7JQr929jGzQiIiL9abevMygn0g9/bSUymcOHs1XYgWwldrUqOZaxFTeBzDLOnwd27lTajDWryUmobdxsK8rY7VgGUPhvA9gGjYiIqF6YU05UHwzKiUwmvz2aVn4bsWZvkVauBdozeKvgeLZBIyIiqp/8nHIi0geDciITGhkBZmaAgweB9nbluVJtxOxYRiYDnDlj4IBLWKsFmlgxt9uVf+vMjPJvJyIiIv1x+zpRfTAoJzKp7duBb38b+NiDm9qt1iKtmhZoHR3Kv5Ur5ERERPXDoJyoPhiUE5lYsTZiWsXaiDWLasZ+/35zjZ2IiMiMGJQT1QeDciITE23EgGwbMRHcNnuLtGrGzhZoRERE9afNKWehNyL9MCgnMrFWbZHGFmhERETNhyvlRPVRpD4z1WJubk79uJ6N5YmqdfgwcPGiUjANyLYRy5fJKJXLL15U/jaqYNrkZLbier5iY2cLNGo2qVQKqVQKQO61gVoPr+1EuTZ0ZLel/WKZQTlZSz2v71xb0onf74fb7Ybb7UYoFDJ6OESqci3SgGzfcrX3t4Et0rQt0IqNLR9boFEzCoVC6vXA7/cbPRxaB17biXJxpZysrJ7XdwblOgmHw4jH44jH45AkyejhEOUo1iINKN3726gWaaIFWrmxAWyBRs1NkiT1ehAOh40eDq0Dr+1EuZhTTlZWz+s7t6/rZGBgAC6Xy+hhEJUkWqRFIkpV81K9v3uxgAw6EIkoxzcqV1vbAm2tsYkWaMwjp2bEbc7mwWs7US6ulJOV1fP6zl9piSxE22Zsrd7fjW6RVs3Y2AKNiIio8RiUE9UHg3IiC9G2GVur93ej24w189iIiIiIQTlRvTAoJ7IQbYu0tXp/N7rNWDOPjYiIiBiUE9ULc8qJLEbbIk30/u7GIhbRrQa9AHD7tlINvVGVzWdngaWl7ONSY2MLNCIiImNoC72xJRqRfrjWRGQx+S3SRO/vDDpyWpD9h/8A7Nyp9A2vt8lJ5Wv9h/+Q2wZNOzaALdCIiIiMxJVyovpgUE5kQaJF2m/8Rva5Yi3IGtGzXNubvFwbtN/4DbZAIyIiMhKDcqL6YFBOZFHbtwOdncrHpVqQ2bFc957lojd5uTEAQFcXV8iJiIiM9HBHNihnn3Ii/TAoJ7IobV/wtVqQRSLK8WYcAxFRKXNzc0gkEkgkEkilUkYPh8hwbbY2PNT+EACulJP1pFIp9ZowNzen67lZ6E0n2jemno3lifSi7QsuWpBpg2JtCzLRs/yRR8w3BiK9pVIpNYDT+6JNjeX3+9WPjx07huPHjxs3GKIm8TH7x/DRykcMyslyQqEQTpw4UZdzc6VcJ36/H263G263G6FQyOjhEK1J2xd8rRZk9eoL3gxjINJbKBRSrwfaoI5aTzgcRjweRzwehyRJRg+HqCmIvHIG5WQ1kiSp14RwOKzrublSrpNwOIyBgQEA4Co5tQTRF/z8eeVxufZoQ0P16Qve1gY8/zxw4cLaY2BvcmoVkiRhz549AJSVcgbmrWtgYAAul8voYRA1FRGUM6ecrKaeu6EZlOuEF25qRdqe5UC2PVq+y5ezx+tVbG12VinyFonkPl9sDOxNTq2EKUxEZGZcKSfSH9ediCwsv2d5Kb/4hXKcXn3LRV/y8+eVc5fD3uRERETN42P2jwFQgnJZlg0eDZE5MCgnsjjRs/zgQeDhh3Nfs2MZW3FTbUumR99ybV/yYl9DePhhZUzsTU5ERNQ8xEr5qryK5dXlNY4mokowKCcibN8OnDun5G0Lz+AtLKAXN9GLBfTiGbwFAOvuWy76kpf7GgCwd68yJq6QExERNQ8RlAPMKyfSC4NyIgKg9AD/zneUj+1YxiXsU9uT9WARl7BPXc2utWe4ti/5Wl9jaop9yYmIiJqNNihnXjmRPhiUExGA3J7h3VjM6RcOKEFz94PnRM/wZvwaREREVD8ipxwAfrHMoJxIDwzKiQhAbs/wRXSr/cKFFLqx+OC5WnuGN+JrEBERUf1wpZxIfwzKiQhAtm85oLQl24dLatCcQjf24ZLaN7zWnuGN+BpERHqZm5tDIpFAIpFAKpUyejhETYFBOVlVKpVSrwlzc3O6npu/8hKR6vDhbHu0t7EbvVjANiygFwt4G7sBAO3t6+sZXsnXYF9yImoGfr8fbrcbbrcboVDI6OEQNQUWeiOrCoVC6jXB7/freu41uhNTpbR3S3p6etDT02PgaIhqI/qWi5ZlGXTgfWzLOcZuVyqoHz5cfWX02Vnlc7Ur4Plfg33JqdWlUil1VVXvO+nUWOFwGAMDAwDA6zrRAx/r0OSUc6WcLESSJOzZsweAcn3XMzDnSrlOeDe9Nj6fDzabzehh6CIQCMBms2FiYsLooayLtm/5Qw8Vvv7LXypB886dwORk5eednFQ+5/x54KOPCl//2MfYl5zMoZ530qmxBgYG4HK54HK5GJQTPcDt62RVPT096jVB3LDVC1fKddJMd9NXV5Wq1Rs2MCeXarN9u7J9/LXXss/ZsYxuLGIR3cigA5mMsqL+1FNrr2rPzmZX34udC1Bee/FFrpBT66vnnXQiIqMxKCfSH0M2nTTD3fTZWWWl8dFHgY9/XPn74EHleaq/YDAIWZYxOjpq9FB0cfp0Noh+Bm9hAb24iV4soBfP4C0AyutnzuhzrpWVys5F1OzqeSediMhoDMqJ9Meg3CS0W4NFH+h792rbZky0ugpMTSkf27GMS9in9hTvwSIuYR/sWAYARCLK8Y04FxERERlL26echd6I9MGg3ATytwbnE9uMuWJOlbp/P3tzpxuLahAt9GAR3Q+eu3dPOb4R5yIiIiJjcaWcSH8Myk1AuzW4lEq3GdfT1NQU3G43bDYb3G43xsfHC44ZHx+HzWZDMpkseE2SJHR2dgIAJiYm0NnZiWQyiUAggP7+fnR2dsLr9SKdTud8XjqdVo+x2Wzo7+9HIBAoOH+xc9psNni9XiSTSSSTSXi9XthsNnR2dhacY2pqCjabDYlEouDrS5Kknq+zsxOSJBWMs5ls2ABs3Kh8vIhutZe4kEI3Fh88t3GjcnwjzkVErU38PJYkCZIkwev1Fr0WCOPj4/D5fOrx5QppVnMsEdWOQTmR/iwZlBcL+Mo938y0W4PXYuTW4KmpKfh8PiSTSQSDQRw9ehTT09OYyhu8yMcOBoMF55iYmMjJ106n02oQHgwGsXfvXsRiMfh8vpzPi8ViiMVikCQJ0WgUkiSpv7zlyz/n6Oioek6v1wufz4dQKASn04nx8fE1f+lLJpPo6+vD5cuXMTQ0hFAopJ4zFotVPH+N1tYGDA0pH2fQgX24pAbTKXRjHy6pBdp8vvIFBfU8FxG1NhE0h0IhhEIhRCIRnDx5El6vt+BYr9eL+fl5RCIR9XjxM3w9xxLR+jAoJ6oD2YI8Ho8MQHa5XLLH45FdLpfscDjksbGxqs8Vj8dlAHI8Hq/DSNf2wQeyDFT+54MPDBmm7HQ6ZYfDId+5cyfneZfLJed/G46OjsoAco4NhUIyAHl+fj7n8dDQUNGvs5ZyX6PYOQHIkUhEfW5+fr7g2EgkUvC9IL638v/dreD6dVm227PfO3Z8JG/FgmzHR9nn7MpxjTwXUSsx+hrRTMRcaH+WyrIsDw0NFcxRNBot+Bkty7J8586ddR1b7Vj5vhEVmr45LTt/3yk7f98p/59v/p9GD4fIEHpfJyy7JuV0OpFIJDAzM4Ouri5EIpGiq7PNTrs1eC1GbQ0WW79HR0fhcDhyXuvq6io4XmwL165Ch0IheDweOJ3OnGOHh4dzHjudzoq2hff396tjy1fsnADg8XgKniv3tdLpNBKJRNF/dyvYvl0pFGh/0Dgxgw68j23qqrbdrrxeSQszPc9FRK3J4XDA4XBgaWkp53lxHdA+H4lE1OOLnSMUCtV0LBGtn7bQG1fKifRh2T7l8/PzRg9BF2Jr8Pnzax9r1NZgEfiKQHgtTqcTHo8HoVAIY2NjSCQSSCQSiEajBccW+yWsmEQigddffx2JREK9SVBKqXNWG1jPzMwAqPzf3YxGRpQ+5GfOKOkP9+4pN3d8PuC3fxv41KeUlIi1vq9WV4E9e4CrV4F/9+8Kz8X+5ETm53Q6cefOnYLnY7GY+nM//7liurq61J+v1R5brbm5uZKv9fT0GNYClchI3L5OVpFKpZBKpYq+Vu76UAvLrpSbyeHD2RXIUux2JfAxgvhlqZobIcFgEMlkErFYDK+//nrBL2zVkCQJbrdbLbgWiUQwNjZW07mqUcu/uxlt3w6cOwf8/OfABx8AP/iBsuH8C18APv5x4NFHgYMHi1f3n51VXnv0UeXYL3xB+dwf/EA5189/rpybATmR9SSTSfh8PjgcDsTj8YLXSnE4HDmvV3Nstfx+P9xud9E/XIEnq2JQTlYRCoVKXgP8fr+uX8uyK+WAUnwsmUyqAV8rbjEGsluDS7VFM3prsNPphMPhwMTEREGKQKlfllwuF1wuFyKRCGKxWNFq6ZVIp9OYmJjA2NhYztd+/fXXazpfNZxOJ1wuFyYmJnD06NGC7690Ot1S33NtbcC///eF32f37infXxcvKn+PjCjPT05WfiwRWUc6ncbJkyeRTqeRTCYLUoYqPUc9js0XDocxMDBQ9DWukpNVPdyRDcrZp5zMTJIk7Nmzp+hrc3Nzugbmlg3KA4EAhoeHMTQ0hFgsBrfbjUAgkFPduxpGb3Ert824GbYGnz17Fj6fL6cdWSgUKruCcfToURw6dAjpdLrm90XkFE5MTGDz5s1wuVyIRqNlW/DoKRKJwO12o6+vTw3M5+fnMTU1BUmSGrJir5fZ2dwg245ldGMRi+hGBh3IZJTXn3pKeb3SY43+3iRar0ZubzMDh8ORc5PU6/Xi5MmTiMfjJbehazUqIAeAgYEBuFyudZ2DyGy4Uk5W0cg0JUtuXw+FQggGg+qF1uPxIBgMQpKkgh7TlWqGLW7524ybaWvw0NAQIpEIAOWGiCisNzo6WvIXnqEHfbRqDciFK1euoKurS+2NCyjfA434RcvpdOLGjRtqjrwkSZiamsLQ0NC6/12Ndvp0Nsh+Bm9hAb24iV4soBfP4C0AyutnzlR3LFGra+T2NiN5vV7YbLaK/3R2dlZ03kAgoKYXCeWC86WlpZzXqzmWiNaPhd6I9GeTZVk2ehDNIJ1Oo7OzE6Ojo1UF0YlEAm63e80tbtzmVpvOzs6KV0+oflZXlbzwe/eUVe8F9KIHi+rrKXSjFwvIoAMbNgA2W2XHbtyo3Dxib3JqZWutlPv9fsTjccuvuPp8PiQSiYI6G8lkEv39/XA6neprPp8PsVisaGE4m82Wc62u5thKiWs73zeiQrIs41OnP4VVeRW/9viv4c9f+HOjh0TUcHpfJyy3fX18fByvv/56QVEZodaCMNzipr+pqSk4nU4G5E3g/n0lyAaAbizmBNkA0INFdGMR72Mb7t/PPr/WsffuKed+5JF6/wuI6oc3XiuTSCSwtLRUUE9DXHe119Dh4WFMTU0VHCu2o/t8vpqOJaL1s9lseNj+MO4t32NOOZFOLLc+FY1Gi+aYif6oDKyNlU6nEYvFkEgkcOjQoZbsHW9GGzYoNQoAYBHdSKE75/UUurH44Llqjt24UTmeiMxP1G3JL3AZCATgcDhw9uxZ9bmhoSF4PJ6CIp+HDh2Cx+PJ6cZRzbFEpA+RV87t60T6sNxKudfrLRp4T01NAUBOThs13tLSErxeLwBgbGyMv0w1ibY2YGhIqZieQQf24RIuYR96sIgUurEPl5BBBwBg716l7Vklx/p83LpOZBWjo6OIxWI519lkMgmPx1O0Q0U0GkUgEIDP54PT6UQymcTg4GDRApnVHEtE6yfyyhmUE+nDckH52NgYvF5vzrboRCKBkydPIhQKcau0wZxOJ1jmoDkdPqy0MstkgLexG71YyKmoDijt9158UTm+mmOJyBqqXbmuZrcUd1YRNc6GDmWbG4NyIn1YLigHsnfU0+m0mt925coVbl0nKmP7dmX1W7Q6y6AD72Ob+rrdrrwuqv1XcywRERG1DrF9nTnlRPqwZFAO8I46US1GRpTe4mfOAJGIUvxt40ZlG/qLL+YG2dUcS0TUjLR95lnQjyhLbF//aOUjrKyuoL2t3eAREdWfttuK9vqgB8sG5URUm+3bgXPngG9/W6mcvmGDkhe+ugp8+GH2cbljiYhagbbP/LFjx3D8+HHjBkPURMRKOaBsYX/kIbZRIfMLhUI4ceJEXc7NX4+JqCZtbUors7/7O+DgQaWP+cc/rvx98CAwO1t4LANyImol4XAY8Xgc8XichWCJNPKDciIrkCRJvSaEw2Fdz82VciKq2eRkNm9cuHdPyRe/eFH5e2TEuPEREa3HwMAA680QFaENyplXTlZRzzQmBuU6Yd4ZWc3sbG5AbsdyToX1TEZ5/amnmD9O1lLPnDMiomYgcsoBrpQT6YGbSXXi9/vhdrvhdrsRCoWMHg5R3Z0+nQ3In8FbWEAvbqIXC+jFM3gLgPL6mTMGDpLIAKFQSL0eaHOSiYjMgtvXifTFlXKdhMNhDAwMAABXycn0VleBqSnlYzuWcQn70INFAEAPFnEJ+9CLBWTQgUhEKfTGfHKyCkmSsGfPHgDKSjkDcyIyGwblRPpiUK4T5p2Rldy/r+SOA0A3FtWAXOjBIrqxiPexDffuKcc/wsKsZBFMYSIis2NQTqQvrl1R05iamoLNZkMikWj41w4EArDZbJiYmKjL+Y38t9XDhg1Kz3EAWEQ3UujOeT2Fbiw+eG7jRuV4IiIiMoecQm/LLPRGtF4Myomoam1twNCQ8nEGHdiHS2pgnkI39uESMugAAPh83LpORERkJiz0RqQv/qpMBCAYDEKWZYyOjho9lJZx+DBgf5AA8zZ2oxcL2IYF9GIBb2M3AOX1F180cJBERESkO25fJ9IXg3IzWl4Gbt5U/iaqk+3blT7kIjDPoAPvY5u6Qm63K6+zHRoREZG5PNzBoJxITwzKzeatt4De3uyft94yekRkYiMjwMwMcPBgNsd840bl8cyM8joRUauam5tDIpFAIpFQe88TEVfKyZpSqZR6TZibm9P13AzKzWR5Gdi3D1h8UAl7cVF53CQr5ul0GpIkob+/HzabDZ2dnZAkCel0uuznBAIB9XP6+/sRCAQKjkskEvB6vbDZbEWPW+v1UoXY1hpzpeMzs+3bgXPngJ//HPjgA+Xvc+e4Qk5Erc/v96s950OhkNHDIWoa2pzyX2ZY6I2sIRQKqdcEvdudsiWamSwuZgPy/Oe2bTNmTA8kk0m43W4AwOjoKPr7+zE/P4+pqSnEYjEMiapheWKxGGKxGCRJgsvlQiKRQCAQQDKZRCQSUY9zu93weDyIRqNIp9NIJpOIRqMVv17rmCsdnxW0tbHtGRGZSzgcxsDAAACwzR2RBlfKyYokScKePXsAKDup9AzMGZSbSXe38kcbmIvnDObz+QAAN27cgMPhUJ8PBoNlP29oaCgnYPd4PJifn8fExATS6TQcDgdisRgApa2Zx+NRjx0bGwOANV9fz5grGR8REbWmgYEBuFwuo4dB1HQYlJMV9fT01O0GLbev66Qp8s46OoBLl7JBeHe38rijw5jxPJBOp5FIJDA6OqpLkNrf3w9AWckGgJ07dwJQgmhJkjA1NZWzJX6t1/Uec/74iMha6plzRkTUDBiUE+mLQblOmibvbPduYGEh+2f3buPG8sDMzAyAbLBaLbEl3Ov1Fs3ZdjgciMfjcDqdmJiYgM/nQ2dnJ8bHxyt6fb1jXmt8RGQt9cw5IyJqBtqgnDnlROvHoFwn4XAY8Xgc8XgckiQZO5iODiWH3OAVcsHpdAIA5ufnq/5cSZLgdrvVgmuRSKTotnOXy4V4PI47d+4gEonA5XIhEAiohdvWer3WMVc6PqtZXQU+/FD5m8hqJElSrwfhcNjo4RAR6U5b6I0r5UTrx6BcJyLvzOVysRhMHqfTCZfLpeZZ5yu1lTydTmNiYgJjY2MIhUIYGhpaM7fP4XBgaGhILbKWv4V8rderGXMt4zO72VmlHdqjjwIf/7jy98GDyvNEVtHT06NeD0SRMCIiM+H2dSJ9sdAbNUQkEoHb7UZfXx+OHj0Kh8OhVjKXJKno6rLD4YDD4cDExAQ2b94Ml8uFaDRasO08FovB5/Nh7969cLvd6OrqUlMIPB7Pmq+vZ8yVjM8qJieBAweATCb73L17wPnzwMWLyt/sW05ERNT6coLyZQblROvFlXJqCKfTiRs3bsDj8SAUCqkF14aGhjA6Olry865cuYKuri4EAgE1LSAUCuWsSO/cuROjo6OYmZlBIBCAz+fD0tISotEoHA7Hmq+vZ8yVjM8KZmdzA3I7lrEVN2HHMgDl+QMHuGJORERkBswpJ9KXTZZl2ehBtLJEIgG32414PG65QIxIOHhQWQkHgGfwFi5hH3qwiBS6sQ+X8DZ2q8edO2fcOIkajdeI1sT3jag8WZbxyVc+CQDYtmkbRnc9WGApE1XIZV5stnCk3FjJHL7wT7+Avq6+mj9f7+sEt68T0bqsrgJTU8rHdiyrATkA9GARl7APvVhABh2IRIBvfxto4x4dImoB2pZ29exPS9RqbDYbPmb/GH6Z+SVu/uwmfjf6u0YPiagqZ/75maqD8lQqpba+1rvlKX81JqJ1uX9fyR0HgG4sqgG50INFdD947t495XgiolbQNO1OiZqQ6x9zFwlZSz1bnnKlnIjWZcMGYONGJeBeRDdS6M4JzFPoxiK6ASjHbdhg1EiJiKoTDofVCvpcJSfK9a3f/Ba+f+P7+OXyg5xym/jLVvJzbLbSrxUcW+Y8za6afycZY0fPjqo/R5Ik7NmzB4CyUq5nYM6gnIjWpa0NGBpScsoz6MA+XCrIKc+gAwDg83HrOhG1DtHulIgKPfqxR/Ebv/IbRg+DqGHqmcbEX4+JaN0OHwbsD27xvY3d6MUCtmEBvVhQi7zZ7cCLLxo4SCIiIiKiJsSgnIjWbft2ZaVcBOYZdOB9bFNXyO125fXt2w0cJBERERFRE+L2dZ2wQitZ3cgI8NRTwJkzQCSi5Jhv3KhsWX/xRQbkZB31rM5KRERE5sOgXCfaRP9jx47h+PHjxg2GyCDbtyt9yL/9baXK+oYNzCEn6wmFQjhx4oTRwyAiIqIWwaBcJ6zQSpTV1gY88ojRoyAyRj2rsxIREZH5MCjXCSu0EhERwBQmIiIiqg43lhIREREREREZhEE5NZ1UKoXjx4+rhZKodfC9a218/4hyzc3NIZFIIJFI8P/FOvHnS2vj+9fa+P7pI5VKqdcEvQu5MiinppNKpXDixAn+4GhBfO9aG98/olx+vx9utxtutxuhUMjo4bQ0/nxpbXz/WhvfP32EQiH1mqB3vRjmlBMREREVwSKuREQk1LOQK4NyIiIioiJYxJWIiIR6FnLl9vUmUa9cj1Y7bz214ly04pjrpRXnohXHXC+tNhetOMfUfFrx+7MVv/dbbZ75/mW14ly02nnrie+ffhiUN4l65Xq02nnrqRXnohXHXC+tOBetOOZ6abW5aMU5pubTit+frfi932rzzPcvqxXnotXOW098//TDoJyIiIiIiIjIIMwpX6f79+8DAP7jf/yPamn8LVu24LHHHqvqPOJz9S6v32rnree5W+289Tx3q523nufmmOt/3nqeuxnP+9Of/hS3bt0CANy4cQNA9lpBrUG8X+v9vmrG70+jzs0x1/+89Tx3q523nudutfPW89xWHrP4fL2u7zZZlmVdzmRRr732mu4l8YmIyFzC4TD2799v9DCoQry2ExFRJfS6vjMoX6dbt27h0qVL2LhxIz72sY8BqG2lnIiIzEG7Uv7L/6+9u8tN5NgCAHyQ8pI37GQBUXsHkKxgQNmAO84Ggndg5CXgHeDZgWEDET0rsM0O3MoCItNPifLEfRg1dzxgj5mxXfx8n4QUN91FU0rVmVNUVf/3X/zzzz/x+++/x48//pj4zniuv//+O/7888/46aef4vvvv099OwBsmH///Tf++uuv+PXXX18kvkvKAQAAIBEbvQEAAEAiknIAAABIRFIOAAAAiUjKAQAAIBFJOQAAACQiKQcAAIBEJOUAAACQiKScjVaW5VrHgfVoY0AK+h54PdrX9mnM5/N56ptg//T7/SjLctE5nJ6eRq/XWzqv2+1GURTRarXi8PAw7u/voyzL6PV6MRgMls6/uLiI6+vrODw8jIiIdru9slxelnrfPNoYkIK+Z7eo982ife2wObyxTqczv729Xfw9mUzmETE/Pj5eeW6WZfOImDebzXmn05lPJpNHy+31eg+OHR8fLx3jZan3zaONASnoe3aLet8s2tduk5TzpgaDwXw0Gi0dPzs7m0fE0nudTudZ5dYd02w2e3B8NpvNI+JBJ8bLUe+bRxsDUtD37Bb1vlm0r91nTTlvajKZRJ7nUVXVg+MnJyeL97/GaDSKZrMZzWbzwfH62HA4/KpyeZp63zzaGJCCvme3qPfNon3tvu9S3wD7pdVqxc3NzdLxujN4bAOK8XgcZVlGlmXR6XSWOo+iKCLLspXXHh4ervxMvp163zzaGJCCvme3qPfNon3tPr+U86YGg0HMZrOVnULEx40pPtfv9yPLsjg7O4tmsxntdjsuLy8fnPPUbpLNZtNuk69EvW8ebQxIQd+zW9T7ZtG+9kDq+fMwn8/nWZbNsyxbOn53d7d0bDQaLa1ziYh5q9VaWXar1Zr7X/11qPftoY0BKeh7tpN63w7a1+7wSznJ5XkezWYzbm9vl95bNaWm0+lERDx7ncvn6294G+p9c2hjQAr6nt2k3jeD9rVbJOWspdvtRqPRePbr4ODgyfLyPI+IiNvb26UpORcXF9Futx+99tMpNY+th4mIuL+/f/J9vp5633zaGPAc4jufUu+bTfvaPZJy1jKZTGL+8VF6z3rNZrNHy8rzPLrdboxGo8Wxem1M/VmrRunu7+8j4uOmF7VWq/Xoupeqqhajg7ws9b7ZtDHgucR3PqXeN5f2tZsk5SSR53mcn59Hr9dbHKuq6kEH0+12V06xGY/HERFxenq6OHZychJVVS11QvXf9YgiL0u9by5tDEhB37Mb1Ptm0r52V2M+n89T3wT7pZ5S8/mUmLIs4+TkJM7OzhbH6o6lPnc6nca7d+9iMBg86JDqc7Mse9AR1c90/NrnN/Jl6n3zaGNACvqe3aLeN4v2tdsk5bypPM8XI3WrTCaTpaky/X4/qqqK+/v7qKoqBoPBg6k3n59bP4+xLMv45ZdfHnRSvA71vjm0MSAFfc9uUu+bQfvafZJyAAAASMSacgAAAEhEUg4AAACJSMoBAAAgEUk5AAAAJCIpBwAAgEQk5QAAAJCIpBwAAAASkZQDAABAIpJyAAAASERSDgAAAIlIygEAACARSTkAAAAkIikHAACARCTlQERETKfTmE6nqW8jIiLKsnyxsqbT6YuWBwDbQmyH7SAphy1QVVU0Go04Ojp69JzxeByNRiNOT0/XLr8oinj37l1kWfbgWKPRWDuY19cdHBysfR+1drv91dd+rtlsRrvdjqIoXqxMAPhWYvvXE9vZNZJy2HPT6TS63W6MRqNoNpvfXN5wOIxmsxlVVcV4PF77+vF4HL/99ts330cty7J4//595HluVB2AvSC2w3aRlMOe6/f70el0otPpfHNZdbB+//59RHwM4usaDodf9YvAU46PjyPLshcvFwA2kdgO20VSDntsOp1GURTR7/dfpLyrq6uI+BgoO51OFEURVVU9+/qyLKMsy2i1Wi9yP586Pz+Poig2Zm0dALwGsR22j6Qc9lg92v0SI+l1ecfHxxERi5Hry8vLta5/rRHv+r6+ZoQfALaF2A7bR1IOe+zq6mqtoF2WZRwcHES321353nQ6XQTeutx1AuV4PI5er/fg2OXlZRwcHERZltHv9+Po6CgajUZ0u93F6Hu3211sQPPULwOtVsumMADsNLEdto+kHLZIWZbRaDRWvvI8X6usqqqiqqpnTycryzLa7XZkWRaTyWTp/cFgEM1mcxGw6/8uy/JZwbIoimi1Wis3pKmqKrrdblRVFYPBIHq9XhRFEXmeR7fbjTzPYzgcRpZlcXFx8egIfn0/60y7A4DXJLaL7fBd6hsAnq/ZbMZoNFr53mQyiYuLi2eXVe9W+tSjWD49tw7at7e3K8+5urpa2lk1z/MoiiKGw+EXR+2/NL2t1WotRuaPj48Xa8hGo9Fi+lqn04mjo6OYTCZLo/IRET/88MPi+7zG2jYAWJfYLraDpBy2yOHh4aMBcN0R4vv7+0WZTynLMv7444+oqurRoD0ej6Oqqmi32w8eTfLzzz8v3n9KVVUxnU6fDO4nJycP/s6yLMqyfHBN/SzWx+qiHqmvvzsApCa2/5/Yzr4yfR321HMDfZ7ni+D+2Gh9Pcp9enoaR0dHi1e73V6c89SmMFdXV4sR8cd8PvWt/nud56/W38MUNwB2kdgO20lSDnvquSPLrVYr7u7u4uzsLPr9/tJjR6qqiqIoYjAYxHw+X3rVa9Se2hTmNXdm/VT9XetRdwDYJWI7bCdJOeyp544s1+vcBoNBtFqtpU1n6lHyVeu8Ij6uBcuyLKbT6YPpb7X6HwJvEUzr77rOCDwAbAuxHbaTpBz2VL0Zyt3d3ZPnfboubTQaRVmWD0a+641engqG9fmrRtTfaiQ9IuL6+joijKYDsJvEdthOknLYY+s+2zPLshgOh3F5eRnj8XgxQv6lwFuPtK9ae3Z1dfXoSPxL+9KGMwCw7cR22D6N+Xw+T30TQBr9fj8uLi5iNpslmfY1Ho9jMpk8uSbtpZRlGUdHRzEYDOLs7OzVPw8AUhDbYfv4pRz22Pn5eUQ8vXvqa3rL6W31o1veauQeAFIQ22H7+KUc9ly/34/Ly8uYzWZv+rn1s0+/tO7tpRwcHESv14vBYPAmnwcAqYjtsF0k5UC02+3odDpvGtTq56K+xXSz09PTuLm5idvb21f/LADYBGI7bA/T14H48OFDFEWxmAb2Fq6vr99kutl4PI6bm5v48OHDq38WAGwKsR22h1/KAQAAIBG/lAMAAEAiknIAAABIRFIOAAAAiUjKAQAAIBFJOQAAACQiKQcAAIBEJOUAAACQiKQcAAAAEpGUAwAAQCKScgAAAEhEUg4AAACJSMoBAAAgEUk5AAAAJCIpBwAAgEQk5QAAAJCIpBwAAAASkZQDAABAIpJyAAAASOR/K+PfscRAg48AAAAASUVORK5CYII=", + "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/+upskhIuVxnHKFTE5OZl6rOYYdERE1vng8jng8DiD3+kFEtEJOW/IHgaZV2qWFiFTBoFwhg4ODmdcHDx7EoUOHtEsMERHpmt/vx+HDh7VOBhHpXfId0es6AKy6Aej6t9qmh4hUwaBcIWNjY7Db7QDAUnIiIipKkiTs2LEDgCgpz76xS0SUcfFxYCEhXt+2C7h+o6bJISJ1MChXiN1uh8Ph0DoZRETUANjMiYhKSqeBF/7b0vu7/h/apYWIVMWgnIiIiBrCyMgIzp07h/b2dgCA0+nE0NCQxqkiUsmv/wZITIjXGz8GtDu1TQ8RqYZBOREREeme2+2GzWZDIBDITPN4PIhEIvD7/RqmjEglL7KUnMgsGJQTERGRroXDYYTDYczOzuZMP378ODZs2ABJktiEjIxlPg5cCorX138A2OzRNj1EpCqOU05E9bWwAFy+LJ6JiMoQCARgtVphtVpzpsvTNCspn58GXvuJeFZj+XqtY+Z06fV/n/w2kE6K13fsBVZdX/5nEVHDYUk5EdXP2bPArl3A9DTQ0QGcOgX09WmdKiLSuXA4DJvNlndee3s7xsfH65wiAH/7h0DsBIA0AAvQ7gDW5E8jAODtGDATLX/5fOtscABrtpRY5wIwW8E6K5bfWuZnPF/jOveUka7zucuvvj1rgfTKdd65CMxOZK3TDay+LWuVZeu88ysg8fdLy1s/CrRtLvAZi6/fuQxc+YelddbfDbTekrVsevFzst7PvQK89eLSOqtvF72op69lPVJLr99PAO+/ufTRLesL7yciMgQG5URUHwsLSwE5IJ537QIuXQJaWrRNGxHpWiwWK1g93Wq1IhaLVbXdycnJgvOK9pA/Pw3E/gQ5gddMRDzKUunyi+vMRsRDtXXSIkCfjVb4GdWs8/xikK7G8vI65xcD+zKXT/z9YpBewWdc+UfxqGSddy6IR7km/k9gywNAa0cFn0NEtYrH44jH43nnFbt+VINBORHVx/T0UkC+fNqmTdqkiYgMIZFIVLVesfHhDx48iEOHDuWfeXUSQKqqzyQSLIBl1coHUsDC1dxF00lxzDEoJ6orv9+Pw4cP1+WzGJQTUX10dIhHdmAuTyMiqlK1ATkAjI2NwW63551XdBz5dXbA0rzU5hcQ77f/LXDDB1cu/+7rwNO/Vf7ySq9z398VTtdTHyt/+aLrPFdind/Ms865IunqXbn8p8eXLW/JXeevnHnWieauY7EsLf+XW1cu/5nngRs6Vi4vr/MX/2LlOp/9R+CGmxaXlR+L686/Bvz4QyvX+eLl/EH2/DTwvzatXH5d/uOUiNQjSRJ27NiRd97k5GTRG7uVYlCukOwqDEWrvBGZVUuLaEO+vE05q66TCWVXiVO6CpwRFWpPDgAzMzNF5xdjt9ur67W9tQPo/Q5w7kERPFmaxfuNBcaRXr2psuWVXqe9wP/YdmtlyxddZ2uRdW4psM49BZa/Of/yG7oLf0ah72TDR/Mvf8MH8y9v/Ujhz7h+Y/511t1VeJ21tvzrFCr1LvR/sJScqO7qGdNZ0unlvV5QJaLRKJzO3Atk0SpvRGa3sLAUlDMgJ5M6dOjQiipxkUiEw3oV4PF48g6JBgAWiwVDQ0MV9cAuX7tr3ufz06Ja8Tp7eUFTpcvXax0zp8tI/zsR1Y1i15FFLClXSHYVOJaSExXR0sI25GR62VXilK4CZ0QDAwMIBoNIJBI5w6LJVdc9Ho3GcG7tqCxgqnT5eq1j5nQZ6X8noobFoFwhVVeBIyIi02Ezp8r09/fD5XLB6/XmlIjv3bsXLpcLLpdLw9QRERHVpknrBBARERGVEgqFYLVa4fF44PV64fF40Nvbi1AopHXSiIiIasKSciIiImoIPp9P6yQQEREpjiXlRERERERERBoxbUn5yMgIzp07h/b2dgCA0+nE0NCQxqkiIiIiIiIiMzFlSbnb7cbU1BQCgQD8fj/8fj9CoRAkSdI6aZQlHo/j0KFDmbF8qTHxezQGfo9EVArzCePgd2kM/B4bh+mC8nA4jHA4vKJd2vHjxzE6OopoNKpRymi5eDyOw4cPMyNpcPwejYHfIxGVwnzCOPhdGgO/x8ZhuqA8EAjAarXmjHMKIDMte6gVIiIiIiIiIjWZLigPh8Ow2Wx557W3t2N8fLzOKSIiIiIiIiKzMl1QHovFCs6zWq1F5xczOTmJaDSa91FOlRE123w06rbV0qj7o1G3raZG3CeNmGa1Neo+qWTb8Xi84DVicnJS8bRRfejl+NLTttXSqPujUbetJl5f67dtNTXqPtHt/k6bDIC0w+HIO8/hcKQr3SWRSCQNoOjj4MGDZW8nEolU9PmVpJHbVne73Da3reV2uW19b/vgwYMlrxVqpJHyU+q40MvxpZdtN2KauW3jbLsR08xtN+62lU6jaYdEyyeRSFS97tjYGOx2e955nZ2dVW+XiIganyRJ2LFjR955k5OTGBwcrHOKiIiISC9MF5QXak8OADMzM0Xn5zM/P19ymXg8XrKKhFx9UY1qjNx2fbbLbXPbWm6X2278bZdzPSFlyPu61u+ukY6vemy7EdPMbRtn242YZm67cbctr6/YtVuR8vYG0t/fn7ZarXnnAUgPDQ1VtL2xsbGSVRL54IMPPvjgo9RjbGxMicsclYHXbj744IMPPpR4KHXttqTT6TRMJBgMwuPxYHZ2NmdYtEQigQ0bNiAUCsHlcpW9vTfeeANPPfUUbr/9drS2tqqQYiIiMrL5+XlcvHgR9913H2688Uatk2MKvHYTEVEtlL52my4oBwC32w2bzZYzJrnH40EikUAoFNIwZURERERERGQmpgzKAcDr9SIWi8FmsyEWi6G3txfDw8NaJ4uIiIiIiIhMxLRBOREREREREZHWmrROABEREREREZFZMSgnIiIiIiIi0giDciIiIiIiIiKNMCgnIiIiIiIi0giDcmoIsVisoulEVDued0RUC+YhRPXFc65xsfd10pQ8NJ2cWUiShKGhoRXLud1uhMNhOBwOtLe3Y2ZmBrFYDENDQ/D5fCuWHxkZwblz59De3g4AcDqdebdL6uD+1zeed0RUC+YhxsT9r18850wgTaQRl8uVjkQimfehUCgNIN3f3593WZvNlgaQtlqtaZfLlQ6FQgW3OzQ0lDOtv79/xTRSB/e/vvG8I6JaMA8xJu5//eI5Zw4MykkTPp8vHQgEVkwfHh5OA1gxz+VylbVdOaOanZ3NmT47O5sGkJOpkfK4//WN5x0R1YJ5iDFx/+sXzznzYJty0kQoFILH40EikciZPjAwkJlfjUAgAKvVCqvVmjNdnub3+6vaLpWH+1/feN4RUS2YhxgT979+8Zwzj2atE0Dm5HA4MD4+vmK6nDkU6pAiGAwiFovBZrPB5XKtyEzC4TBsNlveddvb2/N+JimH+1/feN4RUS2YhxgT979+8ZwzD5aUkyZ8Ph9mZ2fzZhKA6KhiOa/XC5vNhuHhYVitVjidToyOjuYsU6x3SavVyt4nVcb9r28874ioFsxDjIn7X794zpmI1vXnibLZbLa0zWZbMX1qamrFtEAgsKLdC4C0w+HIu22Hw5HmIa8u7v/GxPOOiGrBPKSxcf83Hp5zxsOSctINj8cDq9WKSCSyYl6+KjYulwsAym73srw9DtUX978+8bwjolowDzE27n/94TlnTAzKqSputxsWi6Xsx4YNG4puz+PxAAAikciKKjojIyNwOp0F182uYlOofQwAzMzMFJ1PteP+byw874jMhdduyof7v3HwnDMuBuVUlVAohLQYUq+sx+zsbMFteTweuN1uBAKBzDS5rYz8Wfnu2s3MzAAQnWDIHA5HwXYwiUQic7eQ1MH93zh43hGZD6/dlA/3f2PgOWdsDMpJUx6PBwcOHMDQ0FBmWiKRyMlw3G533io3wWAQACBJUmbawMAAEonEikxJfi/fYSR1cP83Bp53RFQL5iHGwv2vfzznjM+STqfTWieCzEmuYrO8ikwsFsPAwACGh4cz0+SMRl42Go1i27Zt8Pl8ORmUvKzNZsvJmOQxHqsdz5HKx/2vbzzviKgWzEOMiftfv3jOmQODctKEx+PJ3LnLJxQKrag64/V6kUgkMDMzg0QiAZ/Pl1MVZ/my8viMsVgMvb29OZkWqYv7X5943hFRLZiHGBv3v/7wnDMPBuVEREREREREGmGbciIiIiIiIiKNMCgnIiIiIiIi0giDciIiIiIiIiKNMCgnIiIiIiIi0giDciIiIiIiIiKNMCgnIiIiIiIi0giDciIiIiIiIiKNMCgnIiIiIiIi0giDciIiIiIiIiKNMCgnIiIiIiIi0giDciIiIiIiIiKNMCgnIiIiIiL6/7d3t8dpc1sYQDcztwBIOhAdQDoIdABJBbE7MOMScAfgDkAdIFdgWx1YJcTqwO+PjHSDP7BxABnPWjPMxEY6iPx5vLfOOYKGKMoBAACgIYpy4Fl5nkee501fRkREFEWxs7HyPN/peADwUchuOE6KcjhCZVlGq9WKbrf74jFpmkar1YrT09Otx8+yLL5//x5Jkqz9rtVqbR321XmdTmfr66j0+/13n/tYu92Ofr8fWZbtbEwAeI3sfj/ZzWenKAfW5Hkew+EwlstltNvtfx5vNptFu92OsiwjTdOtz0/TNH78+PHP11FJkiQuLy9jPB7rugPwKchuOG6KcmDNZDKJwWAQg8Hgn8eqwvzy8jIi/oT8tmaz2bvuGGwyGo0iSZKdjwsATZDdcNwU5UAtz/PIsiwmk8lOxlssFhHxJ0gHg0FkWRZlWb75/KIooiiK6PV6O7mev52fn0eWZR9m7R0AvIfshuOnKAdqVTd8F532arzRaBQRUXe25/P5VufvqyNeXdd77gAAwEchu+H4KcqB2mKx2CrUi6KITqcTw+Hw2ffyPK+DuRp3myBN0zROTk7Wfjefz6PT6URRFDGZTKLb7Uar1YrhcFh354fDYb1BzaY7B71ez6YxABw12Q3HT1EOR6woimi1Ws++xuPxVmOVZRllWb55ullRFNHv9yNJklitVk/en06n0W6360Cv/l0UxZvCNMuy6PV6z25YU5ZlDIfDKMsyptNpnJycRJZlMR6PYzgcxng8jtlsFkmSxMXFxYsd/up6tpmWBwD/QnbLbnjsf01fAPB+7XY7lsvls++tVqu4uLh481jVbqabHtXy97FVqN/e3j57zGKxeLLz6ng8jizLYjabvdrVf236W6/Xqzv3o9GoXmO2XC7r6W2DwSC63W6sVqsnXfuIiK9fv9bfZx9r3wDgMdktu+ExRTkcsS9fvrwYkNt2kH///l2PuUlRFPHr168oy/LFUE/TNMqyjH6/v/bokm/fvtXvb1KWZeR5vjH8f/78ufZzkiRRFMXaOdWzWl/6v6g6+dV3B4B9k93/J7vhD9PXgYh4+x8C4/G4Dv+XuvlVF/z09DS63W796vf79TGbNo1ZLBZ1x/wlj6fGVT9v83zW6nuYAgfAMZLd8DkoyoGIeHvnudfrxd3dXZydncVkMnnyWJKyLCPLsphOp/Hw8PDkVa1h27RpzD53bv1b9V2rrjwAHBPZDZ+DohyIiLd3nqt1cNPpNHq93pNNaaou+nPrwCL+rBVLkiTyPF+bHlep/lA4RNhW33WbDj0AfBSyGz4HRTkQEVFvlnJ3d7fxuL/XrS2XyyiKYq0zXm0Esyksq+Of67gfqtMeEXF9fR0Ruu0AHCfZDZ+DohyobfvszyRJYjabxXw+jzRN6w76a8FcdeKfW5u2WCxe7NTv2msb0gDARye74fi1Hh4eHpq+COBjmEwmcXFxEff3941MC0vTNFar1cY1a7tSFEV0u92YTqdxdna2988DgH2Q3XD83CkHaufn5xGxeXfVfTrk9Lfq0S6H6uwDwD7Ibjh+7pQDayaTSczn87i/vz/o51bPRn1tXdyudDqdODk5iel0epDPA4B9kd1w3BTlwBP9fj8Gg8FBQ696buohpqOdnp7Gzc1N3N7e7v2zAOAQZDccL9PXgSeurq4iy7J6mtghXF9fH2Q6WpqmcXNzE1dXV3v/LAA4FNkNx8udcgAAAGiIO+UAAADQEEU5AAAANERRDgAAAA1RlAMAAEBDFOUAAADQEEU5AAAANERRDgAAAA1RlAMAAEBDFOUAAADQEEU5AAAANERRDgAAAA1RlAMAAEBDFOUAAADQEEU5AAAANERRDgAAAA1RlAMAAEBDFOUAAADQEEU5AAAANERRDgAAAA35D1F8hAxOs7ohAAAAAElFTkSuQmCC", "text/plain": [ "
" ] 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; } diff --git a/mkdocs.yml b/mkdocs.yml index a73a88b..20f4540 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 diff --git a/python/cmtj.cpp b/python/cmtj.cpp index 4a78964..7b12417 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,438 +31,427 @@ 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."; +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); + // 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< - 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)); + 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_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<>()); + 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) - .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< - 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) - // 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, - 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("setIECDriver", &DJunction::setIECDriver) - .def("setQuadIECDriver", &DJunction::setQuadIECDriver) - .def("setLayerOerstedFieldDriver", &DJunction::setLayerOerstedFieldDriver) - .def("setLayerMagnetisation", &DJunction::setLayerMagnetisation) - // 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); + // 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>(), - "junctionList"_a, - "topId_a"_a = "free", - "bottomId"_a = "bottom") - .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", &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)); + 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>(), - "junctionList"_a, - "topId_a"_a = "free", - "bottomId"_a = "bottom") - .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", &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)); + // 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&, const std::vector&, const 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), + py::return_value_policy::reference); - // 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_(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); + // 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/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" 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): diff --git a/setup.py b/setup.py index d6e7a77..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.4" +__version__ = "1.6.0" """ As per https://github.com/pybind/python_example 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) diff --git a/tests/test_drivers.py b/tests/test_drivers.py index 285abe9..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) @@ -43,6 +43,24 @@ 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(): + 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(): 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_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 diff --git a/tests/test_models.py b/tests/test_models.py index 8d5b0c5..148a93c 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -15,10 +15,10 @@ def test_sb_dynamic(two_layer_symbolic_dyn: Tuple[LayerDynamic]): ) # 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 + init_position=pos, perturbation=0, max_steps=1e6, 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() @@ -45,7 +45,7 @@ def test_sb_classic_dipole(two_layer_symbolic_classic: Tuple[LayerSB]): ) # 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 + init_position=pos, perturbation=0, max_steps=1e6, force_sb=True ) f_sb.sort() pos = eq_sb 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"]) 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