Skip to content

Commit 87beab6

Browse files
authored
Merge pull request #109 from LemurPwned/feat/add-generalised-linearisation
adding generalised linearisation for cartesian systems at eq
2 parents 7606dee + 3a782f6 commit 87beab6

File tree

16 files changed

+1225
-128
lines changed

16 files changed

+1225
-128
lines changed

.github/workflows/python-test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ jobs:
4343
- name: Install dependencies
4444
run: |
4545
python -m pip install --upgrade pip
46-
python -m pip install flake8 pytest
46+
python -m pip install flake8 pytest pytest-xdist
4747
python -m pip install -e .[utils,test]
4848
4949
- name: Test with pytest
@@ -68,7 +68,7 @@ jobs:
6868
- name: Install dependencies
6969
run: |
7070
python -m pip install --upgrade pip
71-
python -m pip install flake8 pytest
71+
python -m pip install flake8 pytest pytest-xdist
7272
python -m pip install -e .[utils,test]
7373
7474
- name: Test with pytest

CHANGELOG.md

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,32 @@
11
# Changelog
22

3-
# 1.8.0
3+
## 1.9.0
4+
5+
- Added generalised linearisation options for frequency computing in linear models
6+
- Revamped SB modelling for better processing and analytical roots + code cleanup
7+
8+
## 1.8.0
49

510
- Fixing bugs in electrical coupling
611
- You can now have useKCL = True Stacks which will implicitly preserve Kirchoff's current law, and will simulate perfect voltage sources
712

8-
# 1.7.0
13+
## 1.7.0
914

1015
- Adaptive simulation is now available again with Dormand-Price method
1116
- Adding curated simulation examples
1217
- Memory cells are now supported (you can specify two polarisation vectors per Layer now)
1318
- Import fixes and other minor bugfixes
1419

15-
# 1.6.2-1.6.6
20+
## 1.6.2-1.6.6
1621

1722
- Fixed a bug in the `ScalarDriver` class which prevented the use of custom drivers with no arguments.
1823

19-
# 1.6.1
24+
## 1.6.1
2025

2126
- Small fixes to the noise interfaces and resistance functions.
2227
- Documentation updates: added a tutorial on custom dipole interactions + interface fixes and typing.
2328

24-
# 1.6.0
29+
## 1.6.0
2530

2631
- Extended the `Stack` models allowing for non-symmetric coupling between devices.
2732
`Stack` current drivers can now be of any type and are adequately scaled.
@@ -31,7 +36,7 @@
3136
- Added `getLayer` method to the `Junction` class and `getJunction` method to the `Stack` class that return a reference to the object.
3237
- 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).
3338

34-
# 1.5.0-1.5.4
39+
## 1.5.0-1.5.4
3540

3641
- Dipole interaction added to the `SB Model`
3742
- Kasdin 1/f noise generator added to the `noise` module and to the solvers
@@ -41,12 +46,12 @@
4146
- Added aliases for `ScalarDriver` -- for example, instead of calling `ScalarDriver.getConstantDriver`, you can now call `constantDriver` directly to create a constant driver.
4247
- Improve stub detection across editors and IDEs
4348

44-
# 1.4.1
49+
## 1.4.1
4550

4651
- Adding a basic optimisation script in the `optimization` module.
4752
- Streamlit optimization updates.
4853

49-
# 1.4.0
54+
## 1.4.0
5055

5156
- Adding new, dynamic symbolic model compatible with `Solver` class. It is now possible to use the `Solver` class with `LayerDynamic` to solve the LLG equation.
5257
- Added tests for procedures and operators.
@@ -57,11 +62,11 @@
5762
- Added some optimization utilities like `coordinate_descent`.
5863
- Added a `streamlit` service for an example PIMM simulation.
5964

60-
# 1.3.2
65+
## 1.3.2
6166

6267
- Added new `ScalarDrivers` -- Gaussian impulse and Gaussian step.
6368

64-
# 1.3.1
69+
## 1.3.1
6570

6671
- `CVector` got extra functionality in Python bindings. Operators are now supported.
6772
- Domain Wall dynamics is now also for 2 layer systems. Added edge potential.

cmtj/models/analytical_utils.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
import contextlib
2+
from typing import Union
3+
4+
import numpy as np
5+
import sympy as sym
6+
from numba import njit
7+
8+
from ..utils import VectorObj
9+
from ..utils.solvers import RootFinder
10+
11+
EPS = np.finfo("float64").resolution
12+
OMEGA = sym.Symbol(r"\omega", complex=True)
13+
14+
15+
def real_deocrator(fn):
16+
"""Using numpy real cast is way faster than sympy."""
17+
18+
def wrap_fn(*args):
19+
return np.real(fn(*args))
20+
21+
return wrap_fn
22+
23+
24+
def _lu_decomposition_det(matrix: sym.Matrix):
25+
_, U, perm = matrix.LUdecomposition()
26+
# sgn = Permutation(perm).signature()
27+
sgn = 1
28+
return sgn * U.det()
29+
30+
31+
def _berkowitz_det(matrix: sym.Matrix):
32+
return matrix.det(method="berkowitz")
33+
34+
35+
def _root_solver_numerical(
36+
root_expr,
37+
n_layers: int = None,
38+
*args,
39+
normalise_roots_by_2pi: bool = False,
40+
ftol: float = 0.01e9,
41+
max_freq: float = 80e9,
42+
):
43+
xtol = 1e-4
44+
if n_layers is None or n_layers <= 3:
45+
# makes it faster for small systems, otherise jit cost too high
46+
y = real_deocrator(njit(sym.lambdify(OMEGA, root_expr, "math")))
47+
else:
48+
y = real_deocrator(sym.lambdify(OMEGA, root_expr, "math"))
49+
mfreq = max_freq * 2 * np.pi if normalise_roots_by_2pi else max_freq
50+
r = RootFinder(xtol, mfreq, step=ftol, xtol=xtol, root_dtype="float16")
51+
roots = r.find(y)
52+
# convert to GHz
53+
# reduce unique solutions to 2 decimal places
54+
roots = np.unique(np.around(roots / 1e9, 2))
55+
return roots / (2 * np.pi) if normalise_roots_by_2pi else roots
56+
57+
58+
# we add n_layers and args to make the signature compatible with numerical solver
59+
def _root_solver_analytical(
60+
root_expr,
61+
n_layers: int = None,
62+
normalise_roots_by_2pi: bool = False,
63+
solve_direct: bool = False,
64+
**kwargs,
65+
):
66+
# Try multiple approaches to find roots
67+
all_solutions = []
68+
69+
# # Approach 1: Direct solving
70+
if solve_direct:
71+
with contextlib.suppress(Exception):
72+
direct_solutions = sym.solve(root_expr, OMEGA)
73+
all_solutions.extend(direct_solutions)
74+
75+
# Approach 2: Factorized solving
76+
with contextlib.suppress(Exception):
77+
factorised = sym.factor(root_expr)
78+
factored_solutions = sym.solve(factorised, OMEGA)
79+
all_solutions.extend(factored_solutions)
80+
81+
# Remove duplicates
82+
all_solutions = list(set(all_solutions))
83+
84+
# More robust real check - evaluate numerically and check if imaginary part is negligible
85+
real_solutions = []
86+
for sol in all_solutions:
87+
try:
88+
numeric_val = complex(sol.evalf())
89+
if abs(numeric_val.imag) < 1e-10: # More lenient than is_real
90+
real_solutions.append(numeric_val.real)
91+
except Exception:
92+
continue
93+
94+
# Convert to numpy and filter
95+
if not real_solutions:
96+
return np.array([])
97+
98+
all_roots = np.asarray(real_solutions) / 1e9
99+
100+
# Filter: positive and within frequency range
101+
all_roots = all_roots[(all_roots > 0)]
102+
103+
# Remove duplicates with tolerance
104+
if len(all_roots) > 1:
105+
all_roots = np.unique(np.around(all_roots, 6)) # Higher precision than numerical
106+
107+
return all_roots / (2 * np.pi) if normalise_roots_by_2pi else all_roots
108+
109+
110+
def _default_matrix_conversion(
111+
vector_or_matrix: Union[VectorObj, list[float], sym.Matrix],
112+
) -> sym.ImmutableMatrix:
113+
if isinstance(vector_or_matrix, VectorObj):
114+
vector_or_matrix = vector_or_matrix.get_cartesian()
115+
elif isinstance(vector_or_matrix, list):
116+
vector_or_matrix = sym.ImmutableMatrix(vector_or_matrix)
117+
return sym.ImmutableMatrix(vector_or_matrix)

0 commit comments

Comments
 (0)