udaan 1.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. udaan/__init__.py +33 -0
  2. udaan/__main__.py +5 -0
  3. udaan/cli/__init__.py +41 -0
  4. udaan/cli/demos.py +79 -0
  5. udaan/cli/generate.py +32 -0
  6. udaan/cli/run.py +199 -0
  7. udaan/control/__init__.py +66 -0
  8. udaan/control/quadrotor/__init__.py +15 -0
  9. udaan/control/quadrotor/geometric_attitude.py +80 -0
  10. udaan/control/quadrotor/geometric_l1_attitude.py +185 -0
  11. udaan/control/quadrotor/position_l1.py +107 -0
  12. udaan/control/quadrotor/position_pd.py +32 -0
  13. udaan/control/quadrotor/propeller_allocation.py +54 -0
  14. udaan/control/quadrotor_cspayload/__init__.py +7 -0
  15. udaan/control/quadrotor_cspayload/payload_controller.py +62 -0
  16. udaan/core/__init__.py +37 -0
  17. udaan/core/exceptions.py +75 -0
  18. udaan/core/types.py +73 -0
  19. udaan/manif/S2.py +40 -0
  20. udaan/manif/SO3.py +77 -0
  21. udaan/manif/__init__.py +16 -0
  22. udaan/manif/utils.py +33 -0
  23. udaan/models/__init__.py +24 -0
  24. udaan/models/assets/mjcf/multi_quad_pointmass.xml +68 -0
  25. udaan/models/assets/mjcf/multi_quad_rigidbody.xml +125 -0
  26. udaan/models/assets/mjcf/quad_cable_mj.xml +127 -0
  27. udaan/models/assets/mjcf/quad_flxcbl_mj.xml +265 -0
  28. udaan/models/assets/mjcf/quad_payload_mj.xml +67 -0
  29. udaan/models/assets/mjcf/quadrotor.xml +31 -0
  30. udaan/models/assets/mjcf/quadrotor_comparison.xml +60 -0
  31. udaan/models/assets/mjcf/quadrotor_l1_comparison.xml +62 -0
  32. udaan/models/assets/mjcf/quadrotor_mj.xml +52 -0
  33. udaan/models/assets/mjcf/quadrotor_payload.xml +44 -0
  34. udaan/models/assets/mjcf/quadrotor_quat.xml +43 -0
  35. udaan/models/assets/mjcf/quadrotor_x.xml +62 -0
  36. udaan/models/assets/mjcf/scene.xml +25 -0
  37. udaan/models/assets/mjcf/two_quad_linearpayload.xml +78 -0
  38. udaan/models/assets/mjcf/two_quad_masslesslink_payload.xml +76 -0
  39. udaan/models/base/__init__.py +72 -0
  40. udaan/models/base/floating_pointmass.py +55 -0
  41. udaan/models/base/multi_pointmass_suspended_payload.py +123 -0
  42. udaan/models/base/pointmass_suspended_payload.py +123 -0
  43. udaan/models/base/quadrotor.py +364 -0
  44. udaan/models/base/quadrotor_cspayload.py +290 -0
  45. udaan/models/base/s2_pendulum.py +70 -0
  46. udaan/models/bullet/__init__.py +1 -0
  47. udaan/models/mujoco/__init__.py +331 -0
  48. udaan/models/mujoco/multi_quad_cs_pointmass.py +236 -0
  49. udaan/models/mujoco/multi_quad_rigidbody.py +230 -0
  50. udaan/models/mujoco/quadrotor.py +136 -0
  51. udaan/models/mujoco/quadrotor_comparison.py +149 -0
  52. udaan/models/mujoco/quadrotor_cspayload.py +344 -0
  53. udaan/models/mujoco/quadrotor_fleet.py +242 -0
  54. udaan/py.typed +2 -0
  55. udaan/utils/__init__.py +32 -0
  56. udaan/utils/assets/__init__.py +11 -0
  57. udaan/utils/assets/mujoco_asset_creator.py +651 -0
  58. udaan/utils/assets/xml_model_generator.py +92 -0
  59. udaan/utils/flat2state.py +251 -0
  60. udaan/utils/geometry.py +12 -0
  61. udaan/utils/logging.py +113 -0
  62. udaan/utils/printout.py +89 -0
  63. udaan/utils/trajectory/__init__.py +33 -0
  64. udaan/utils/trajectory/trajectory.py +307 -0
  65. udaan/utils/vfx/__init__.py +141 -0
  66. udaan/utils/vfx/quadrotor_cspayload_vfx.py +34 -0
  67. udaan/utils/vfx/quadrotor_vfx.py +95 -0
  68. udaan-1.0.1.dist-info/METADATA +219 -0
  69. udaan-1.0.1.dist-info/RECORD +72 -0
  70. udaan-1.0.1.dist-info/WHEEL +4 -0
  71. udaan-1.0.1.dist-info/entry_points.txt +2 -0
  72. udaan-1.0.1.dist-info/licenses/LICENSE +28 -0
udaan/__init__.py ADDED
@@ -0,0 +1,33 @@
1
+ """Udaan: Quadrotor dynamics and control for cable-suspended payload systems.
2
+
3
+ Udaan provides geometric control on SE(3) × S² manifolds for quadrotor systems
4
+ with cable-suspended payloads, supporting multiple physics backends.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import os
10
+ from pathlib import Path
11
+
12
+ # Package path for asset loading
13
+ _FOLDER_PATH, _FILE_PATH = os.path.split(os.path.abspath(os.path.dirname(__file__)))
14
+ PATH = _FOLDER_PATH
15
+ PACKAGE_DIR = Path(__file__).parent
16
+
17
+ # Core types and exceptions
18
+ # Submodules
19
+ from . import control, core, manif, models, utils
20
+
21
+ # Version
22
+ __version__ = "1.0.1"
23
+
24
+ __all__ = [
25
+ "core",
26
+ "models",
27
+ "control",
28
+ "utils",
29
+ "manif",
30
+ "PATH",
31
+ "PACKAGE_DIR",
32
+ "__version__",
33
+ ]
udaan/__main__.py ADDED
@@ -0,0 +1,5 @@
1
+ """Allow running udaan as: python -m udaan"""
2
+
3
+ from udaan.cli import app
4
+
5
+ app()
udaan/cli/__init__.py ADDED
@@ -0,0 +1,41 @@
1
+ """Udaan command-line interface."""
2
+
3
+ import typer
4
+
5
+ _ctx = {"help_option_names": ["-h", "--help"]}
6
+
7
+ app = typer.Typer(
8
+ name="udaan",
9
+ help="Simulation and geometric control of aerial manipulation systems.",
10
+ no_args_is_help=True,
11
+ context_settings=_ctx,
12
+ )
13
+
14
+ # --- Subcommand groups ---
15
+ from .generate import generate_app # noqa: E402
16
+ from .run import run_app # noqa: E402
17
+
18
+ app.add_typer(run_app, name="run")
19
+ app.add_typer(generate_app, name="generate-xml")
20
+
21
+
22
+ # --- Top-level commands ---
23
+ @app.command("version")
24
+ def version():
25
+ """Show udaan version."""
26
+ from udaan import __version__
27
+
28
+ typer.echo(f"udaan {__version__}")
29
+
30
+
31
+ def parse_vec(s: str | None, default):
32
+ """Parse 'x,y,z' string to numpy array, or return default."""
33
+ import numpy as np
34
+
35
+ if s is None:
36
+ return default
37
+ try:
38
+ return np.array([float(x) for x in s.split(",")])
39
+ except ValueError:
40
+ typer.echo(f"Invalid position format: '{s}'. Expected 'x,y,z'.", err=True)
41
+ raise typer.Exit(1) from None
udaan/cli/demos.py ADDED
@@ -0,0 +1,79 @@
1
+ """Fleet demo presets.
2
+
3
+ Each demo has:
4
+ - config(): returns {"num_quadrotors": N, "disturbances": {...}}
5
+ - configure(fleet): sets up controllers on the fleet
6
+ """
7
+
8
+ import numpy as np
9
+
10
+
11
+ def _l1_comparison_config():
12
+ return {
13
+ "num_quadrotors": 3,
14
+ "disturbances": {
15
+ 1: {"mass": 0.25, "offset": [0.2, 0.2, -0.05]},
16
+ 2: {"mass": 0.25, "offset": [0.2, 0.2, -0.05]},
17
+ },
18
+ }
19
+
20
+
21
+ def _l1_comparison_configure(fleet):
22
+ """Nominal PD vs full L1 (position + attitude) with disturbance."""
23
+ from udaan.control.quadrotor import PositionL1Controller
24
+ from udaan.control.quadrotor.geometric_l1_attitude import GeometricL1AttitudeController
25
+
26
+ traj = lambda t: (np.array([0.0, 0.0, 1.0]), np.zeros(3), np.zeros(3))
27
+
28
+ # quad 0 (red): PD + GeomAtt, NO disturbance — reference baseline
29
+ fleet[0].position_controller.setpoint = traj
30
+ fleet._labels[0] = "PD+Geom (clean)"
31
+
32
+ # quad 1 (blue): PD + GeomAtt, disturbed — shows drift
33
+ fleet[1].position_controller.setpoint = traj
34
+ fleet._labels[1] = "PD+Geom (disturbed)"
35
+
36
+ # quad 2 (green): PD_L1 + GeomL1Att, disturbed — full L1
37
+ m = fleet[2].mass
38
+ fleet[2].position_controller = PositionL1Controller(mass=m, setpoint=traj)
39
+ fleet[2].attitude_controller = GeometricL1AttitudeController(mass=m, inertia=fleet[2].inertia)
40
+ fleet._labels[2] = "L1+GeomL1 (disturbed)"
41
+
42
+
43
+ def _gain_sweep_config():
44
+ return {
45
+ "num_quadrotors": 4,
46
+ "disturbances": {},
47
+ }
48
+
49
+
50
+ def _gain_sweep_configure(fleet):
51
+ """PD with increasing gains (soft -> aggressive)."""
52
+ from udaan.control.quadrotor import PositionPDController
53
+
54
+ traj = lambda t: (np.array([0.0, 0.0, 2.0]), np.zeros(3), np.zeros(3))
55
+ gain_scales = [0.25, 0.5, 1.0, 2.0]
56
+ labels = ["very soft", "soft", "nominal", "aggressive"]
57
+
58
+ for i, (scale, label) in enumerate(zip(gain_scales, labels, strict=True)):
59
+ fleet[i].position_controller = PositionPDController(
60
+ mass=fleet[i].mass,
61
+ setpoint=traj,
62
+ kp=scale * np.array([4.0, 4.0, 8.0]),
63
+ kd=scale * np.array([3.0, 3.0, 6.0]),
64
+ )
65
+ fleet._labels[i] = f"PD ({label})"
66
+
67
+
68
+ DEMOS = {
69
+ "l1-comparison": {
70
+ "config": _l1_comparison_config,
71
+ "configure": _l1_comparison_configure,
72
+ "description": "PD (clean) vs PD (disturbed) vs L1 (disturbed)",
73
+ },
74
+ "gain-sweep": {
75
+ "config": _gain_sweep_config,
76
+ "configure": _gain_sweep_configure,
77
+ "description": "PD with increasing gains (soft -> aggressive)",
78
+ },
79
+ }
udaan/cli/generate.py ADDED
@@ -0,0 +1,32 @@
1
+ """MuJoCo XML generation commands."""
2
+
3
+ import typer
4
+
5
+ from . import _ctx
6
+
7
+ generate_app = typer.Typer(help="Generate MuJoCo XML model files.", context_settings=_ctx)
8
+
9
+
10
+ @generate_app.command("multi-quad")
11
+ def multi_quad(
12
+ num_quads: int = typer.Option(3, "--num-quads", "-n", help="Number of quadrotors."),
13
+ output: str = typer.Option("multi_quad_pointmass.xml", "--output", "-o", help="Output file."),
14
+ verbose: bool = typer.Option(False, "--verbose", "-v"),
15
+ ):
16
+ """Generate MJCF XML for multi-quadrotor pointmass payload."""
17
+ import udaan as U
18
+
19
+ U.utils.xml_model_generator.multi_quad_pointmass(nQ=num_quads, filename=output, verbose=verbose)
20
+ typer.echo(f"Written to {output}")
21
+
22
+
23
+ @generate_app.command("comparison")
24
+ def comparison(
25
+ output: str = typer.Option("quadrotor_comparison.xml", "--output", "-o", help="Output file."),
26
+ verbose: bool = typer.Option(False, "--verbose", "-v"),
27
+ ):
28
+ """Generate MJCF XML for quadrotor comparison model."""
29
+ import udaan as U
30
+
31
+ U.utils.xml_model_generator.quadrotor_comparison(filename=output, verbose=verbose)
32
+ typer.echo(f"Written to {output}")
udaan/cli/run.py ADDED
@@ -0,0 +1,199 @@
1
+ """Simulation run commands."""
2
+
3
+ import typer
4
+
5
+ from . import _ctx
6
+
7
+ run_app = typer.Typer(help="Run a simulation.", context_settings=_ctx)
8
+
9
+ # Common option for recording
10
+ _record_option = typer.Option(None, "--record", "-r", help="Save recording to file (.gif or .mp4).")
11
+
12
+
13
+ def _hold_viewer(mdl):
14
+ """Keep MuJoCo viewer open after simulation if it has one."""
15
+ if hasattr(mdl, "_mjMdl") and mdl._mjMdl is not None:
16
+ mdl._mjMdl.wait_for_close()
17
+
18
+
19
+ def _setup_recording(mdl, record_path):
20
+ """Enable recording on a model's viewer."""
21
+ if record_path is None:
22
+ return
23
+ mj = mdl._mjMdl if hasattr(mdl, "_mjMdl") else None
24
+ if mj and mj._viewer:
25
+ mj._viewer._record_path = record_path
26
+ mj._viewer._frames = []
27
+
28
+
29
+ @run_app.command("quadrotor")
30
+ def quadrotor(
31
+ time: float = typer.Option(10.0, "--time", "-t", help="Simulation duration in seconds."),
32
+ render: bool = typer.Option(True, help="Enable visualization."),
33
+ backend: str = typer.Option(
34
+ "mujoco", "--backend", "-b", help="Physics backend: mujoco or base."
35
+ ),
36
+ record: str | None = _record_option,
37
+ position: str | None = typer.Option(
38
+ None, "--position", "-p", help="Initial position as 'x,y,z'."
39
+ ),
40
+ ):
41
+ """Simulate a quadrotor with geometric SE(3) control."""
42
+ import numpy as np
43
+
44
+ import udaan as U
45
+
46
+ from . import parse_vec
47
+
48
+ x0 = parse_vec(position, default=np.array([1.0, 1.0, 0.0]))
49
+ mdl_module = getattr(U.models, backend)
50
+ mdl = mdl_module.Quadrotor(render=render)
51
+ _setup_recording(mdl, record)
52
+ typer.echo(f"Running quadrotor ({backend}) for {time}s ...")
53
+ mdl.simulate(tf=time, position=x0)
54
+ _hold_viewer(mdl)
55
+
56
+
57
+ @run_app.command("quad-payload")
58
+ def quad_payload(
59
+ time: float = typer.Option(10.0, "--time", "-t", help="Simulation duration in seconds."),
60
+ render: bool = typer.Option(True, help="Enable visualization."),
61
+ backend: str = typer.Option(
62
+ "mujoco", "--backend", "-b", help="Physics backend: mujoco or base."
63
+ ),
64
+ model_type: str = typer.Option(
65
+ "tendon", "--model-type", "-m", help="Cable model: tendon, links, or cable."
66
+ ),
67
+ record: str | None = _record_option,
68
+ position: str | None = typer.Option(
69
+ None, "--position", "-p", help="Initial payload position as 'x,y,z'."
70
+ ),
71
+ ):
72
+ """Simulate a quadrotor with cable-suspended payload."""
73
+ import numpy as np
74
+
75
+ import udaan as U
76
+
77
+ from . import parse_vec
78
+
79
+ x0 = parse_vec(position, default=np.array([-1.0, 2.0, 0.5]))
80
+ mdl_module = getattr(U.models, backend)
81
+ if backend == "mujoco":
82
+ mdl = mdl_module.QuadrotorCSPayload(render=render, model=model_type)
83
+ else:
84
+ mdl = mdl_module.QuadrotorCSPayload(render=render)
85
+ _setup_recording(mdl, record)
86
+ typer.echo(f"Running quad-payload ({backend}, {model_type}) for {time}s ...")
87
+ mdl.simulate(tf=time, payload_position=x0)
88
+ _hold_viewer(mdl)
89
+
90
+
91
+ @run_app.command("multi-quad")
92
+ def multi_quad(
93
+ time: float = typer.Option(10.0, "--time", "-t", help="Simulation duration in seconds."),
94
+ render: bool = typer.Option(True, help="Enable visualization."),
95
+ num_quads: int = typer.Option(3, "--num-quads", "-n", help="Number of quadrotors."),
96
+ record: str | None = _record_option,
97
+ position: str | None = typer.Option(
98
+ None, "--position", "-p", help="Initial payload position as 'x,y,z'."
99
+ ),
100
+ ):
101
+ """Simulate multiple quadrotors with cable-suspended pointmass payload."""
102
+ import numpy as np
103
+
104
+ import udaan as U
105
+
106
+ from . import parse_vec
107
+
108
+ x0 = parse_vec(position, default=np.array([-1.0, 2.0, 0.5]))
109
+ mdl = U.models.mujoco.MultiQuadrotorCSPointmass(render=render, num_quadrotors=num_quads)
110
+ _setup_recording(mdl, record)
111
+ typer.echo(f"Running multi-quad ({num_quads} quads) for {time}s ...")
112
+ mdl.simulate(tf=time, xL=x0)
113
+ _hold_viewer(mdl)
114
+
115
+
116
+ @run_app.command("multi-quad-rigid")
117
+ def multi_quad_rigid(
118
+ time: float = typer.Option(10.0, "--time", "-t", help="Simulation duration in seconds."),
119
+ render: bool = typer.Option(True, help="Enable visualization."),
120
+ record: str | None = _record_option,
121
+ position: str | None = typer.Option(
122
+ None, "--position", "-p", help="Initial payload position as 'x,y,z'."
123
+ ),
124
+ ):
125
+ """Simulate multiple quadrotors with cable-suspended rigid-body payload."""
126
+ import numpy as np
127
+
128
+ import udaan as U
129
+
130
+ from . import parse_vec
131
+
132
+ x0 = parse_vec(position, default=np.array([0.0, 0.0, 0.5]))
133
+ mdl = U.models.mujoco.MultiQuadRigidbody(render=render)
134
+ _setup_recording(mdl, record)
135
+ typer.echo(f"Running multi-quad rigid ({mdl.nQ} quads) for {time}s ...")
136
+ mdl.simulate(tf=time, xL=x0)
137
+ _hold_viewer(mdl)
138
+
139
+
140
+ @run_app.command("fleet")
141
+ def fleet(
142
+ time: float = typer.Option(10.0, "--time", "-t", help="Simulation duration in seconds."),
143
+ render: bool = typer.Option(True, help="Enable visualization."),
144
+ num_quads: int = typer.Option(3, "--num-quads", "-n", help="Number of quadrotors."),
145
+ demo: str | None = typer.Option(None, "--demo", "-d", help="Run a preset demo."),
146
+ verbose: bool = typer.Option(False, "--verbose", "-v", help="Log quad states during sim."),
147
+ record: str | None = _record_option,
148
+ position: str | None = typer.Option(
149
+ None, "--position", "-p", help="Initial position as 'x,y,z'."
150
+ ),
151
+ ):
152
+ """Compare N quadrotors with independent controllers side-by-side.
153
+
154
+ Available demos: l1-comparison, gain-sweep
155
+ """
156
+ import logging
157
+
158
+ import numpy as np
159
+
160
+ import udaan as U
161
+
162
+ if verbose:
163
+ logging.basicConfig(level=logging.DEBUG, format="%(message)s")
164
+
165
+ from . import parse_vec
166
+ from .demos import DEMOS
167
+
168
+ x0 = parse_vec(position, default=np.array([0.0, 0.0, 1.0]))
169
+
170
+ if demo is not None:
171
+ if demo not in DEMOS:
172
+ typer.echo(f"Unknown demo: '{demo}'. Available: {', '.join(DEMOS.keys())}", err=True)
173
+ raise typer.Exit(1)
174
+
175
+ entry = DEMOS[demo]
176
+ params = entry["config"]()
177
+ nQ = params["num_quadrotors"]
178
+
179
+ f = U.models.mujoco.QuadrotorFleet(
180
+ num_quadrotors=nQ, render=render, disturbances=params["disturbances"]
181
+ )
182
+ entry["configure"](f)
183
+ if record and f._mjMdl._viewer:
184
+ f._mjMdl._viewer._record_path = record
185
+ f._mjMdl._viewer._frames = []
186
+ typer.echo(f"Running demo '{demo}' ({nQ} quads) for {time}s ...")
187
+ f.simulate(tf=time, position=x0)
188
+ else:
189
+ f = U.models.mujoco.QuadrotorFleet(num_quadrotors=num_quads, render=render)
190
+
191
+ traj = lambda t: (np.array([0.0, 0.0, 2.0]), np.zeros(3), np.zeros(3))
192
+ for q in f.quadrotors:
193
+ q.position_controller.setpoint = traj
194
+
195
+ if record and f._mjMdl._viewer:
196
+ f._mjMdl._viewer._record_path = record
197
+ f._mjMdl._viewer._frames = []
198
+ typer.echo(f"Running fleet ({num_quads} quads) for {time}s ...")
199
+ f.simulate(tf=time, position=x0)
@@ -0,0 +1,66 @@
1
+ import numpy as np
2
+
3
+
4
+ class Gains:
5
+ def __init__(self, kp=np.zeros(3), kd=np.zeros(3), ki=np.zeros(3)):
6
+ self.kp = kp
7
+ self.kd = kd
8
+ self.ki = ki
9
+ return
10
+
11
+
12
+ class Controller:
13
+ def __init__(self):
14
+ self.freq = 500.0
15
+ self._g = 9.81
16
+ self._ge3 = np.array([0.0, 0.0, self._g])
17
+ self._e1 = np.array([1.0, 0.0, 0.0])
18
+ self._e2 = np.array([0.0, 1.0, 0.0])
19
+ self._e3 = np.array([0.0, 0.0, 1.0])
20
+ self._gravity = np.array([0.0, 0.0, -self._g])
21
+ return
22
+
23
+ def compute(self, *args):
24
+ raise NotImplementedError
25
+
26
+
27
+ class PDController(Controller):
28
+ def __init__(self, **kwargs):
29
+ super().__init__()
30
+ self._gains = Gains()
31
+
32
+ if "kp" in kwargs:
33
+ self._gains.kp = kwargs["kp"]
34
+ if "kd" in kwargs:
35
+ self._gains.kd = kwargs["kd"]
36
+
37
+ if "setpoint" in kwargs:
38
+ self.setpoint = kwargs["setpoint"]
39
+ else:
40
+ self.setpoint = lambda t: (
41
+ np.array([0.0, 0.0, 1.0]),
42
+ np.zeros(3),
43
+ np.zeros(3),
44
+ )
45
+ return
46
+
47
+ def compute(self, *args):
48
+ t = args[0]
49
+ s, ds = args[1][0], args[1][1]
50
+ sd, dsd, d2sd = self.setpoint(t)
51
+ e = s - sd
52
+ de = ds - dsd
53
+ u = -self._gains.kp * e - self._gains.kd * de + d2sd
54
+ return u
55
+
56
+
57
+ from . import quadrotor as quadrotor # noqa: E402
58
+ from . import quadrotor_cspayload as quadrotor_cspayload # noqa: E402
59
+
60
+ __all__ = [
61
+ "Gains",
62
+ "Controller",
63
+ "PDController",
64
+ "quadrotor",
65
+ "quadrotor_cspayload",
66
+ ]
@@ -0,0 +1,15 @@
1
+ """Quadrotor controllers."""
2
+
3
+ from .geometric_attitude import GeometricAttitudeController as GeometricAttitudeController
4
+ from .geometric_l1_attitude import GeometricL1AttitudeController as GeometricL1AttitudeController
5
+ from .position_l1 import PositionL1Controller as PositionL1Controller
6
+ from .position_pd import PositionPDController as PositionPDController
7
+ from .propeller_allocation import DirectPropellerForceController as DirectPropellerForceController
8
+
9
+ __all__ = [
10
+ "PositionPDController",
11
+ "GeometricAttitudeController",
12
+ "DirectPropellerForceController",
13
+ "PositionL1Controller",
14
+ "GeometricL1AttitudeController",
15
+ ]
@@ -0,0 +1,80 @@
1
+ """Geometric attitude controller on SE(3) for quadrotor.
2
+
3
+ Lee, Leok, McClamroch (2010) https://ieeexplore.ieee.org/document/5717652
4
+ """
5
+
6
+ import numpy as np
7
+
8
+ from ...control import Controller, Gains
9
+ from ...utils import hat, printc_fail
10
+
11
+
12
+ class GeometricAttitudeController(Controller):
13
+ """Geometric tracking control of a quadrotor UAV on SE(3)."""
14
+
15
+ def __init__(self, **kwargs):
16
+ self._gains = Gains(kp=np.array([2.4, 2.4, 1.35]), kd=np.array([0.35, 0.35, 0.225]))
17
+
18
+ self._inertia = np.eye(3)
19
+ self._inertia_inv = np.eye(3)
20
+ self.mass = 1.0
21
+
22
+ if "inertia" in kwargs:
23
+ self.inertia = kwargs["inertia"]
24
+ else:
25
+ printc_fail("Inertia not provided")
26
+ if "mass" in kwargs:
27
+ self.mass = kwargs["mass"]
28
+
29
+ @property
30
+ def inertia(self):
31
+ return self._inertia
32
+
33
+ @inertia.setter
34
+ def inertia(self, inertia):
35
+ self._inertia = inertia
36
+ self._inertia_inv = np.linalg.inv(inertia)
37
+
38
+ def _cmd_accel_to_cmd_att(self, accel):
39
+ """Convert desired acceleration to desired attitude.
40
+
41
+ Returns identity for near-zero acceleration to avoid singularity.
42
+ """
43
+ MIN_ACCEL_NORM = 0.001
44
+
45
+ norm_accel = np.linalg.norm(accel)
46
+ if norm_accel < MIN_ACCEL_NORM:
47
+ return np.eye(3)
48
+
49
+ b3 = accel / norm_accel
50
+
51
+ b1d = np.array([1.0, 0.0, 0.0])
52
+ b3_b1d = np.cross(b3, b1d)
53
+ norm_b3_b1d = np.linalg.norm(b3_b1d)
54
+
55
+ if norm_b3_b1d < 1e-6:
56
+ b1d = np.array([0.0, 1.0, 0.0])
57
+ b3_b1d = np.cross(b3, b1d)
58
+ norm_b3_b1d = np.linalg.norm(b3_b1d)
59
+
60
+ b1 = (-1 / norm_b3_b1d) * np.cross(b3, b3_b1d)
61
+ b2 = np.cross(b3, b1)
62
+
63
+ Rd = np.column_stack([b1, b2, b3])
64
+ return Rd
65
+
66
+ def compute(self, *args):
67
+ t = args[0]
68
+ R, Omega = args[1][0], args[1][1]
69
+ thrust_force = args[2]
70
+ Rd = self._cmd_accel_to_cmd_att(thrust_force)
71
+ Omegad = np.zeros(3)
72
+ dOmegad = np.zeros(3)
73
+
74
+ tmp = 0.5 * (Rd.T @ R - R.T @ Rd)
75
+ eR = np.array([tmp[2, 1], tmp[0, 2], tmp[1, 0]]) # vee-map
76
+ eOmega = Omega - R.T @ Rd @ Omegad
77
+ M = -self._gains.kp * eR - self._gains.kd * eOmega + np.cross(Omega, self.inertia @ Omega)
78
+ M += -1 * self.inertia @ (hat(Omega) @ R.T @ Rd @ Omegad - R.T @ Rd @ dOmegad)
79
+ f = thrust_force.dot(R[:, 2])
80
+ return f, M