Metadata-Version: 2.4
Name: morphic-hub
Version: 0.3.1
Summary: Morphic - the embodied hub. Pull Triad repos (Brain + Body + World) from a Morphic hub, simulate them locally, deploy brains to hardware.
License: Apache-2.0
Project-URL: Hub, https://hub.ruliax.com
Project-URL: Source, https://github.com/Ruliax-AI/Morphic
Keywords: robotics,mujoco,simulation,physical-ai,sim2real
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: Apache Software License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering
Requires-Python: >=3.10
Description-Content-Type: text/markdown
Requires-Dist: numpy>=1.24
Requires-Dist: pyyaml>=6
Requires-Dist: httpx>=0.27
Provides-Extra: sim
Requires-Dist: mujoco<3.13,>=3.12; extra == "sim"
Provides-Extra: serial
Requires-Dist: pyserial>=3.5; extra == "serial"
Provides-Extra: dev
Requires-Dist: pytest>=8; extra == "dev"
Requires-Dist: mujoco<3.13,>=3.12; extra == "dev"

# `morphic` — the Python client, driver layer and CLI

Pull a Triad (Brain + Body + World) from a Morphic hub, simulate it locally, deploy the brain
to real actuators through one driver layer, and send the telemetry back for a Sim-Gap score.

Licence: Apache-2.0. Dependencies are all free/OSS: numpy (BSD-3), PyYAML (MIT),
httpx (BSD-3), MuJoCo (Apache-2.0, optional), pyserial (BSD-3, optional), pytest (MIT, dev).

## Quickstart

```bash
pip install "morphic-hub[sim]"       # client + CLI + MuJoCo (drop [sim] for the pure HTTP client)

morphic ls                              # what's on the hub (https://hub.ruliax.com by default)
morphic info morphic/go2-trot           # manifest, joints, actuators, BOM
morphic pull morphic/go2-trot           # -> ~/.morphic/cache/morphic/go2-trot   ($MORPHIC_HOME overrides ~/.morphic)
morphic sim  morphic/go2-trot --world ice --seconds 5 --out run.json
```

The distribution is `morphic-hub` (the name `morphic` on PyPI belongs to an unrelated project);
the import and the command are both `morphic`. Every command talks to the public hub unless you
pass `--hub URL` or set `$MORPHIC_HUB` (e.g. `http://127.0.0.1:8000` for a local `python -m morphic_hub`).

`<repo>` is resolved in this order: an existing directory → `hub/registry/<repo>`
(relative to the cwd or `$MORPHIC_REGISTRY`) → the pull cache → `morphic pull` from the hub.

```python
from morphic import Hub
hub = Hub()                                           # or Hub("http://127.0.0.1:8000")
triad = hub.pull("morphic/go2-trot")
run = triad.simulate(world="ice", seconds=5)          # local MuJoCo
report = hub.push_telemetry("morphic/go2-trot", "hw.json")
```

## Advanced: the driver layer — one policy output, any actuator brand

A policy emits a control vector in *model units* (radians for position servos,
N·m for torque motors — whatever the MJCF actuators expect). A `Driver` turns that
into wire packets; a `JointMap` does the per-actuator unit conversion. Swapping
hardware is swapping the driver:

```python
from morphic import Triad, deploy
from morphic.drivers import FeetechDriver, DynamixelDriver, JointMap

triad = Triad("~/.morphic/cache/morphic/so-arm100-pick")
jm = JointMap.from_manifest(triad.manifest)                 # ids / signs / offsets from morphic.yaml

arm  = FeetechDriver("COM4", joint_map=jm)                  # STS3215 bus (SO-ARM100)
deploy.run(triad, arm, seconds=5, record="so100_feetech.json")

arm  = DynamixelDriver("COM5", joint_map=jm)                # same brain, XL330 bus instead
deploy.run(triad, arm, seconds=5, record="so100_dynamixel.json")
```

Nothing about the brain changed. The deploy loop keeps the nominal simulation as a
digital twin: when the driver reports encoder state, the twin is corrected to it
(it is the state estimator); when the device is write-only, the twin runs open loop.

| driver      | class             | wire format                                                     | feedback |
|-------------|-------------------|-----------------------------------------------------------------|----------|
| `mock`      | `MockDriver`      | none — perturbed MuJoCo twin with command latency + encoder noise | yes |
| `serial`    | `SerialPWMDriver` | `'M' n {id u8, pwm_us u16 LE}… xor` to an Arduino/ESP32/STM32 running `morphic drivers arduino-sketch` | no |
| `feetech`   | `FeetechDriver`   | Feetech STS protocol, SYNC WRITE goal position (0x2A), 4096 ticks/rev | present position + speed (0x38) |
| `dynamixel` | `DynamixelDriver` | DYNAMIXEL Protocol 2.0, SYNC WRITE goal position (116), CRC-16   | present velocity + position (128..135) |
| `ros2`      | `ROS2Driver`      | `std_msgs/Float64MultiArray` on `/morphic/joint_commands`          | `sensor_msgs/JointState` |

Every driver honours `dry_run=True` (and the serial/ROS drivers fall back to it when
pyserial/rclpy are absent): packets are encoded exactly as they would be sent and kept
in `driver.log` as hex, so `morphic deploy … --dry-run` shows "what went over the wire"
with nothing plugged in. The drivers have been verified at the protocol level and against
the emulator, not on physical buses.

Feedback contract: `read()` returns `{"qpos": (nq,), "qvel": (nv,)}` in model units with
`NaN` for anything the hardware does not measure, or `None` for write-only devices.

### The `MockDriver` (hardware emulator)

A second MuJoCo instance compiled with deliberately different parameters — by default
`{damping ×1.6, frictionloss ×2.0, mass ×1.08, actuator gain ×0.85, floor friction ×0.7}` —
plus a 2-tick command FIFO and 3 mrad Gaussian encoder noise. Deploying against it
yields telemetry with a realistic sim-to-real gap, which is what the Sim-Gap scorer
is for.

## `deploy:` section of `morphic.yaml`

Optional. Tells `morphic deploy` how the model's actuators map onto physical devices.

```yaml
deploy:
  driver: feetech               # mock | serial | feetech | dynamixel | ros2   (default for --driver)
  port: COM4                    # serial port; --port overrides
  baud: 1000000                 # driver default if omitted (feetech/dynamixel 1 000 000, serial 115200)
  topic: /morphic/joint_commands   # ros2 only
  map:                          # per actuator (MJCF actuator name); missing actuators get identity entries
    shoulder_pan:  {id: 1, sign: 1,  offset: 2048}
    shoulder_lift: {id: 2, sign: -1, offset: 2048, min: 300, max: 3800}
    elbow_flex:    3            # shorthand: just the bus id
```

Entry keys (all optional except that each actuator needs an `id`):

| key      | meaning                                                   | default            |
|----------|-----------------------------------------------------------|--------------------|
| `id`     | bus / channel id                                          | 1..n in actuator order |
| `sign`   | `1` or `-1` — flips direction                             | `1`                |
| `scale`  | device units per model unit                               | driver: ticks/rad (`4096/2π`) for servo buses, µs/rad (`1000/π`) for PWM, `1` for ROS 2 |
| `offset` | device units at model zero                                | driver: `2048` ticks, `1500` µs, `0` |
| `min`, `max` | clamp in device units                                 | driver: `0..4095` ticks, `1000..2000` µs |
| `unit`   | model unit of the actuator: `rad`, `m`, `N`, `Nm`, `norm`  | `rad`              |

Conversion: `device = clamp(sign · value · scale + offset, min, max)`; readings come back
through the inverse. Manifest entries override driver defaults, which override identity.

## Telemetry

`morphic deploy --record run.json` writes `morphic-telemetry/1` (schema in
`docs/TRIAD.md`): joint positions/velocities of the hinge/slide joints, the commanded
`ctrl`, the floating-base pose when there is one, and `meta` describing the driver
(for the emulator: the perturbation it used). `morphic gap <repo> run.json` uploads it
and prints the Sim-Gap report; `--local` scores with `morphic.gap` instead.

```python
from morphic.telemetry import TelemetryRecorder, load_telemetry, validate_telemetry
rec = TelemetryRecorder("owner/name", "flat", joints, actuators, dt=0.02, source="hardware", driver="feetech")
rec.add(t, qpos, qvel, ctrl)
rec.save("run.json")
```

## CLI reference

```
morphic [--hub URL] [--registry DIR] <command>
  ls                               list repos (falls back to the local registry when the hub is down)
  info <repo>                      manifest, joints, actuators, BOM, deploy section
  pull <repo> [--dest DIR]         download a Triad
  worlds <repo>                    world variants
  sim <repo> [--world W] [--seconds S] [--out run.json] [--local|--hub]
  deploy <repo> --driver mock|serial|feetech|dynamixel|ros2 [--port COM3] [--baud N]
         [--world W] [--seconds S] [--record run.json] [--dry-run] [--latency T] [--noise STD] [-v]
  gap <repo> run.json [--local] [--out report.json]
  push <repo> run.json
  drivers [arduino-sketch]
```

## Tests

```bash
cd python && python -m pytest -q
```

`tests/` builds a one-joint Triad on the fly, so the suite runs without the registry
and without hardware. Packet encoders are checked against the vendors' documented
example packets (ROBOTIS e-manual for DYNAMIXEL Protocol 2.0).
