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.
- udaan/__init__.py +33 -0
- udaan/__main__.py +5 -0
- udaan/cli/__init__.py +41 -0
- udaan/cli/demos.py +79 -0
- udaan/cli/generate.py +32 -0
- udaan/cli/run.py +199 -0
- udaan/control/__init__.py +66 -0
- udaan/control/quadrotor/__init__.py +15 -0
- udaan/control/quadrotor/geometric_attitude.py +80 -0
- udaan/control/quadrotor/geometric_l1_attitude.py +185 -0
- udaan/control/quadrotor/position_l1.py +107 -0
- udaan/control/quadrotor/position_pd.py +32 -0
- udaan/control/quadrotor/propeller_allocation.py +54 -0
- udaan/control/quadrotor_cspayload/__init__.py +7 -0
- udaan/control/quadrotor_cspayload/payload_controller.py +62 -0
- udaan/core/__init__.py +37 -0
- udaan/core/exceptions.py +75 -0
- udaan/core/types.py +73 -0
- udaan/manif/S2.py +40 -0
- udaan/manif/SO3.py +77 -0
- udaan/manif/__init__.py +16 -0
- udaan/manif/utils.py +33 -0
- udaan/models/__init__.py +24 -0
- udaan/models/assets/mjcf/multi_quad_pointmass.xml +68 -0
- udaan/models/assets/mjcf/multi_quad_rigidbody.xml +125 -0
- udaan/models/assets/mjcf/quad_cable_mj.xml +127 -0
- udaan/models/assets/mjcf/quad_flxcbl_mj.xml +265 -0
- udaan/models/assets/mjcf/quad_payload_mj.xml +67 -0
- udaan/models/assets/mjcf/quadrotor.xml +31 -0
- udaan/models/assets/mjcf/quadrotor_comparison.xml +60 -0
- udaan/models/assets/mjcf/quadrotor_l1_comparison.xml +62 -0
- udaan/models/assets/mjcf/quadrotor_mj.xml +52 -0
- udaan/models/assets/mjcf/quadrotor_payload.xml +44 -0
- udaan/models/assets/mjcf/quadrotor_quat.xml +43 -0
- udaan/models/assets/mjcf/quadrotor_x.xml +62 -0
- udaan/models/assets/mjcf/scene.xml +25 -0
- udaan/models/assets/mjcf/two_quad_linearpayload.xml +78 -0
- udaan/models/assets/mjcf/two_quad_masslesslink_payload.xml +76 -0
- udaan/models/base/__init__.py +72 -0
- udaan/models/base/floating_pointmass.py +55 -0
- udaan/models/base/multi_pointmass_suspended_payload.py +123 -0
- udaan/models/base/pointmass_suspended_payload.py +123 -0
- udaan/models/base/quadrotor.py +364 -0
- udaan/models/base/quadrotor_cspayload.py +290 -0
- udaan/models/base/s2_pendulum.py +70 -0
- udaan/models/bullet/__init__.py +1 -0
- udaan/models/mujoco/__init__.py +331 -0
- udaan/models/mujoco/multi_quad_cs_pointmass.py +236 -0
- udaan/models/mujoco/multi_quad_rigidbody.py +230 -0
- udaan/models/mujoco/quadrotor.py +136 -0
- udaan/models/mujoco/quadrotor_comparison.py +149 -0
- udaan/models/mujoco/quadrotor_cspayload.py +344 -0
- udaan/models/mujoco/quadrotor_fleet.py +242 -0
- udaan/py.typed +2 -0
- udaan/utils/__init__.py +32 -0
- udaan/utils/assets/__init__.py +11 -0
- udaan/utils/assets/mujoco_asset_creator.py +651 -0
- udaan/utils/assets/xml_model_generator.py +92 -0
- udaan/utils/flat2state.py +251 -0
- udaan/utils/geometry.py +12 -0
- udaan/utils/logging.py +113 -0
- udaan/utils/printout.py +89 -0
- udaan/utils/trajectory/__init__.py +33 -0
- udaan/utils/trajectory/trajectory.py +307 -0
- udaan/utils/vfx/__init__.py +141 -0
- udaan/utils/vfx/quadrotor_cspayload_vfx.py +34 -0
- udaan/utils/vfx/quadrotor_vfx.py +95 -0
- udaan-1.0.1.dist-info/METADATA +219 -0
- udaan-1.0.1.dist-info/RECORD +72 -0
- udaan-1.0.1.dist-info/WHEEL +4 -0
- udaan-1.0.1.dist-info/entry_points.txt +2 -0
- 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
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
|