Skip to content

Commit 5ff0975

Browse files
committed
fixed parse_rotation for simpler interface
It can now do + and cross-products so xy is around the xy-plane normal vector. This might be a bit unintuitive, but it makes other things more consistent.
1 parent 5be4413 commit 5ff0975

File tree

5 files changed

+77
-52
lines changed

5 files changed

+77
-52
lines changed

src/sisl/_core/_ufuncs_geometry.py

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1176,14 +1176,12 @@ def rotate(
11761176
11771177
Parameters
11781178
----------
1179-
angle :
1180-
the angle in degrees to rotate the geometry. Set the ``rad``
1181-
argument to use radians.
1182-
v :
1183-
the normal vector to the rotated plane, i.e.
1184-
``[1, 0, 0]`` will rotate around the :math:`yz` plane.
1185-
If a str it refers to the Cartesian direction (xyz), or the
1186-
lattice vectors (abc). Providing several is the combined direction.
1179+
rotation :
1180+
the rotation method used for rotating the geometry.
1181+
It is parsed through `sisl.utils.parse_rotation` and allows
1182+
a consecutive rotation method.
1183+
1184+
See examples for details.
11871185
origin :
11881186
the origin of rotation. Anything but ``[0, 0, 0]`` is equivalent
11891187
to a ``geometry.translate(-origin).rotate(...).translate(origin)``.
@@ -1203,11 +1201,15 @@ def rotate(
12031201
Examples
12041202
--------
12051203
rotate coordinates around the :math:`x`-axis
1206-
>>> geom_x45 = geom.rotate(45, [1, 0, 0])
1204+
>>> geom_x45 = geom.rotate([45, [1, 0, 0]])
12071205
12081206
rotate around the ``(1, 1, 0)`` direction but project the rotation onto the :math:`x`
12091207
axis
1210-
>>> geom_xy_x = geom.rotate(45, "xy", what='x')
1208+
>>> geom_xy_x = geom.rotate([45, "xy"], what='x')
1209+
1210+
Currently, there is a compatibility layer with the older API (pre 0.16.3)
1211+
which will be removed at some point. Please update your code to use the new-style
1212+
rotation arguments which is much more versatile; allowing consecutive rotations.
12111213
12121214
See Also
12131215
--------

src/sisl/_core/geometry.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4223,7 +4223,7 @@ def __call__(self, parser, ns, values, option_string=None):
42234223
# Convert value[0] to the direction
42244224
# The rotate function expects degree
42254225
ang = angle(values[0], rad=False, in_rad=False)
4226-
ns._geometry = ns._geometry.rotate(ang, values[1], what="abc+xyz")
4226+
ns._geometry = ns._geometry.rotate([ang, values[1]], what="abc+xyz")
42274227

42284228
p.add_argument(
42294229
*opts("--rotate", "-R"),
@@ -4239,7 +4239,7 @@ class RotationX(argparse.Action):
42394239
def __call__(self, parser, ns, value, option_string=None):
42404240
# The rotate function expects degree
42414241
ang = angle(value, rad=False, in_rad=False)
4242-
ns._geometry = ns._geometry.rotate(ang, "x", what="abc+xyz")
4242+
ns._geometry = ns._geometry.rotate([ang, "x"], what="abc+xyz")
42434243

42444244
p.add_argument(
42454245
*opts("--rotate-x", "-Rx"),
@@ -4252,7 +4252,7 @@ class RotationY(argparse.Action):
42524252
def __call__(self, parser, ns, value, option_string=None):
42534253
# The rotate function expects degree
42544254
ang = angle(value, rad=False, in_rad=False)
4255-
ns._geometry = ns._geometry.rotate(ang, "y", what="abc+xyz")
4255+
ns._geometry = ns._geometry.rotate([ang, "y"], what="abc+xyz")
42564256

42574257
p.add_argument(
42584258
*opts("--rotate-y", "-Ry"),
@@ -4265,7 +4265,7 @@ class RotationZ(argparse.Action):
42654265
def __call__(self, parser, ns, value, option_string=None):
42664266
# The rotate function expects degree
42674267
ang = angle(value, rad=False, in_rad=False)
4268-
ns._geometry = ns._geometry.rotate(ang, "z", what="abc+xyz")
4268+
ns._geometry = ns._geometry.rotate([ang, "z"], what="abc+xyz")
42694269

42704270
p.add_argument(
42714271
*opts("--rotate-z", "-Rz"),

src/sisl/_core/tests/test_geometry.py

Lines changed: 27 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -408,67 +408,76 @@ def test_nsc2(self, setup):
408408
assert len(lattice.sc_off) == np.prod(lattice.nsc)
409409

410410
def test_rotation1(self, setup):
411-
rot = setup.g.rotate(180, [0, 0, 1], what="xyz+abc")
411+
rot = setup.g.rotate([180, [0, 0, 1]], what="xyz+abc")
412412
rot.lattice.cell[2, 2] *= -1
413413
assert np.allclose(-rot.lattice.cell, setup.g.lattice.cell)
414414
assert np.allclose(-rot.xyz, setup.g.xyz)
415415

416-
rot = setup.g.rotate(np.pi, [0, 0, 1], rad=True, what="xyz+abc")
416+
rot = setup.g.rotate([np.pi, [0, 0, 1]], rad=True, what="xyz+abc")
417417
rot.lattice.cell[2, 2] *= -1
418418
assert np.allclose(-rot.lattice.cell, setup.g.lattice.cell)
419419
assert np.allclose(-rot.xyz, setup.g.xyz)
420420

421-
rot = rot.rotate(180, "z", what="xyz+abc")
421+
rot = rot.rotate([180, "z"], what="xyz+abc")
422422
rot.lattice.cell[2, 2] *= -1
423423
assert np.allclose(rot.lattice.cell, setup.g.lattice.cell)
424424
assert np.allclose(rot.xyz, setup.g.xyz)
425425

426+
# Just check it runs,
427+
# xy-plane == z-direction
428+
rot = setup.g.rotate([180, "xy"], what="xyz+abc")
429+
rot.lattice.cell[2, 2] *= -1
430+
assert np.allclose(-rot.lattice.cell, setup.g.lattice.cell)
431+
assert np.allclose(-rot.xyz, setup.g.xyz)
432+
433+
rot.rotate([180, "x+y"], what="xyz+abc")
434+
426435
def test_rotation2(self, setup):
427-
rot = setup.g.rotate(180, "z", what="abc")
436+
rot = setup.g.rotate([180, "z"], what="abc")
428437
rot.lattice.cell[2, 2] *= -1
429438
assert np.allclose(-rot.lattice.cell, setup.g.lattice.cell)
430439
assert np.allclose(rot.xyz, setup.g.xyz)
431440

432-
rot = setup.g.rotate(np.pi, [0, 0, 1], rad=True, what="abc")
441+
rot = setup.g.rotate([np.pi, [0, 0, 1]], rad=True, what="abc")
433442
rot.lattice.cell[2, 2] *= -1
434443
assert np.allclose(-rot.lattice.cell, setup.g.lattice.cell)
435444
assert np.allclose(rot.xyz, setup.g.xyz)
436445

437-
rot = rot.rotate(180, [0, 0, 1], what="abc")
446+
rot = rot.rotate([180, [0, 0, 1]], what="abc")
438447
rot.lattice.cell[2, 2] *= -1
439448
assert np.allclose(rot.lattice.cell, setup.g.lattice.cell)
440449
assert np.allclose(rot.xyz, setup.g.xyz)
441450

442451
def test_rotation3(self, setup):
443-
rot = setup.g.rotate(180, [0, 0, 1], what="xyz")
452+
rot = setup.g.rotate([180, [0, 0, 1]], what="xyz")
444453
assert np.allclose(rot.lattice.cell, setup.g.lattice.cell)
445454
assert np.allclose(-rot.xyz, setup.g.xyz)
446455

447-
rot = setup.g.rotate(np.pi, [0, 0, 1], rad=True, what="xyz")
456+
rot = setup.g.rotate([np.pi, [0, 0, 1]], rad=True, what="xyz")
448457
assert np.allclose(rot.lattice.cell, setup.g.lattice.cell)
449458
assert np.allclose(-rot.xyz, setup.g.xyz)
450459

451-
rot = rot.rotate(180, "z", what="xyz")
460+
rot = rot.rotate([180, "z"], what="xyz")
452461
assert np.allclose(rot.lattice.cell, setup.g.lattice.cell)
453462
assert np.allclose(rot.xyz, setup.g.xyz)
454463

455464
def test_rotation4(self, setup):
456465
ref = setup.g.tile(2, 0).tile(2, 1)
457466

458-
rot = ref.rotate(10, "z", atoms=1)
467+
rot = ref.rotate([10, "z"], atoms=1)
459468
assert not np.allclose(ref.xyz[1], rot.xyz[1])
460469

461-
rot = ref.rotate(10, "z", atoms=[1, 2])
470+
rot = ref.rotate([10, "z"], atoms=[1, 2])
462471
assert not np.allclose(ref.xyz[1], rot.xyz[1])
463472
assert not np.allclose(ref.xyz[2], rot.xyz[2])
464473
assert np.allclose(ref.xyz[3], rot.xyz[3])
465474

466-
rot = ref.rotate(10, "z", atoms=[1, 2], what="y")
475+
rot = ref.rotate([10, "z"], atoms=[1, 2], what="y")
467476
assert ref.xyz[1, 0] == rot.xyz[1, 0]
468477
assert ref.xyz[1, 1] != rot.xyz[1, 1]
469478
assert ref.xyz[1, 2] == rot.xyz[1, 2]
470479

471-
rot = ref.rotate(10, "z", atoms=[1, 2], what="xy", origin=ref.xyz[2])
480+
rot = ref.rotate([10, "z"], atoms=[1, 2], what="xy", origin=ref.xyz[2])
472481
assert ref.xyz[1, 0] != rot.xyz[1, 0]
473482
assert ref.xyz[1, 1] != rot.xyz[1, 1]
474483
assert ref.xyz[1, 2] == rot.xyz[1, 2]
@@ -669,8 +678,8 @@ def test___add1__(self, setup):
669678
assert np.allclose(double.xyz, d.xyz)
670679

671680
def test___add2__(self, setup):
672-
g1 = setup.g.rotate(15, setup.g.cell[2, :])
673-
g2 = setup.g.rotate(30, setup.g.cell[2, :])
681+
g1 = setup.g.rotate([15, setup.g.cell[2, :]])
682+
g2 = setup.g.rotate([30, setup.g.cell[2, :]])
674683

675684
assert g1 != g2
676685
assert g1 + g2 == g1.add(g2)
@@ -1559,11 +1568,11 @@ def test_geometry_dispatch(self):
15591568
gr = sisl_geom.graphene()
15601569
to_ase = gr.to.ase()
15611570

1562-
ase_rotate = si.rotate(to_ase, 30, [0, 0, 1])
1571+
ase_rotate = si.rotate(to_ase, [30, [0, 0, 1]])
15631572
assert isinstance(ase_rotate, type(to_ase))
1564-
ase_sisl_rotate = si.rotate(to_ase, 30, [0, 0, 1], ret_sisl=True)
1573+
ase_sisl_rotate = si.rotate(to_ase, [30, [0, 0, 1]], ret_sisl=True)
15651574
assert isinstance(ase_sisl_rotate, Geometry)
1566-
geom_rotate = si.rotate(gr, 30, [0, 0, 1])
1575+
geom_rotate = si.rotate(gr, [30, [0, 0, 1]])
15671576

15681577
assert geom_rotate.equal(ase_sisl_rotate, R=False)
15691578

src/sisl/_core/tests/test_lattice.py

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -126,28 +126,28 @@ def test_add1(self, setup):
126126
assert np.allclose(s.cell, sc.cell + np.diag([R] * 3))
127127

128128
def test_rotation1(self, setup):
129-
rot = setup.lattice.rotate(180, [0, 0, 1])
129+
rot = setup.lattice.rotate([180, [0, 0, 1]])
130130
rot.cell[2, 2] *= -1
131131
assert np.allclose(-rot.cell, setup.lattice.cell)
132132

133-
rot = setup.lattice.rotate(m.pi, [0, 0, 1], rad=True)
133+
rot = setup.lattice.rotate([m.pi, [0, 0, 1]], rad=True)
134134
rot.cell[2, 2] *= -1
135135
assert np.allclose(-rot.cell, setup.lattice.cell)
136136

137-
rot = rot.rotate(180, [0, 0, 1])
137+
rot = rot.rotate([180, [0, 0, 1]])
138138
rot.cell[2, 2] *= -1
139139
assert np.allclose(rot.cell, setup.lattice.cell)
140140

141141
def test_rotation2(self, setup):
142-
rot = setup.lattice.rotate(180, setup.lattice.cell[2, :])
142+
rot = setup.lattice.rotate([180, setup.lattice.cell[2, :]])
143143
rot.cell[2, 2] *= -1
144144
assert np.allclose(-rot.cell, setup.lattice.cell)
145145

146-
rot = setup.lattice.rotate(m.pi, setup.lattice.cell[2, :], rad=True)
146+
rot = setup.lattice.rotate([m.pi, setup.lattice.cell[2, :]], rad=True)
147147
rot.cell[2, 2] *= -1
148148
assert np.allclose(-rot.cell, setup.lattice.cell)
149149

150-
rot = rot.rotate(180, setup.lattice.cell[2, :])
150+
rot = rot.rotate([180, setup.lattice.cell[2]])
151151
rot.cell[2, 2] *= -1
152152
assert np.allclose(rot.cell, setup.lattice.cell)
153153

@@ -317,9 +317,9 @@ def test_creation_rotate(self, setup):
317317
assert np.allclose(parama, np.array(lattice.parameters(True)))
318318
for ang in range(0, 91, 5):
319319
s = (
320-
lattice.rotate(ang, lattice.cell[0, :])
321-
.rotate(ang, lattice.cell[1, :])
322-
.rotate(ang, lattice.cell[2, :])
320+
lattice.rotate([ang, lattice.cell[0]])
321+
.rotate([ang, lattice.cell[1]])
322+
.rotate([ang, lattice.cell[2]])
323323
)
324324
assert np.allclose(param, np.array(s.parameters()))
325325
assert np.allclose(parama, np.array(s.parameters(True)))
@@ -388,7 +388,7 @@ def test_parallel1(self, setup):
388388
gbig = g.repeat(40, 0).repeat(40, 1)
389389
assert g.lattice.parallel(gbig.lattice)
390390
assert gbig.lattice.parallel(g.lattice)
391-
g = g.rotate(90, g.cell[0, :], what="abc")
391+
g = g.rotate([90, g.cell[0]], what="abc")
392392
assert not g.lattice.parallel(gbig.lattice)
393393

394394
def test_tile_multiply_orthogonal(self):
@@ -432,7 +432,7 @@ def test_angle2(self, setup):
432432

433433
def test_cell2length(self):
434434
gr = graphene(orthogonal=True)
435-
lattice = (gr * (40, 40, 1)).rotate(24, gr.cell[2, :]).lattice
435+
lattice = (gr * (40, 40, 1)).rotate([24, gr.cell[2]]).lattice
436436
assert np.allclose(
437437
lattice.length, (lattice.cell2length(lattice.length) ** 2).sum(1) ** 0.5
438438
)

src/sisl/utils/mathematics.py

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
from sisl import _array as _a
3131
from sisl._core.quaternion import Quaternion
3232
from sisl._indices import indices_le
33+
from sisl._internal import set_module
3334
from sisl.typing import RotationType
3435

3536
from .misc import direction
@@ -356,6 +357,7 @@ def intersect_and_diff_sets(a, b):
356357
return int1d, aover, bover, aonly, bonly
357358

358359

360+
@set_module("sisl.utils")
359361
def parse_rotation(
360362
rotation: Union[RotationType, Sequence[RotationType]],
361363
rad: bool,
@@ -383,6 +385,12 @@ def parse_rotation(
383385
2nd index of the abc argument
384386
>>> q = parse_rotation([45, "b"], rad=False, abc=[[1, 0, 0], [0, 1, 0], [0, 0, 1]])
385387
388+
A rotation of 45 degrees around the :math:`xy` plane:
389+
>>> q = parse_rotation([45, "xy"], rad=False)
390+
391+
A rotation of 45 degrees around the vector along :math:`x+y`:
392+
>>> q = parse_rotation([45, "x+y"], rad=False)
393+
386394
Explicitly specify the angle and the vector of rotation (around :math:`xy` vector)
387395
>>> q = parse_rotation([np.pi/4, [0.4, 0.4, 0]])
388396
@@ -428,21 +436,27 @@ def parse_rotation(
428436
return q
429437

430438
if lrot == 2 and (isinstance(rotation[0], str) or isinstance(rotation[1], str)):
439+
# this is:
440+
# angle, direction
441+
# or
442+
# direction, angle
431443

432444
if isinstance(rotation[0], str):
433-
dirs, angles = rotation
445+
dirs, angle = rotation
434446
elif isinstance(rotation[1], str):
435-
angles, dirs = rotation
436-
437-
if not isinstance(angles, (Sequence, np.ndarray)):
438-
angles = [angles]
447+
angle, dirs = rotation
439448

440449
xyz = np.identity(3)
441450

442-
for angle, dir in zip_longest(angles, dirs):
443-
# Convert to q
444-
dir = direction(dir, abc=abc, xyz=xyz)
445-
q = Quaternion(angle, dir / fnorm(dir), rad=rad) * q
451+
dir = 0.0
452+
for outer_dir in dirs.split("+"):
453+
tmp_dir = direction(outer_dir[0], abc=abc, xyz=xyz)
454+
for inner_dir in outer_dir[1:]:
455+
tmp_dir = np.cross(tmp_dir, direction(inner_dir, abc=abc, xyz=xyz))
456+
457+
dir = dir + tmp_dir
458+
459+
q = Quaternion(angle, dir / fnorm(dir), rad=rad)
446460

447461
return q
448462

0 commit comments

Comments
 (0)