Metadata-Version: 2.5
Name: oshd
Version: 1.1.0
Summary: Download, synchronize and turn the OSHD multimodal robotics dataset (events, RGB-D, lidar, mocap) into pose-estimation, detection, segmentation and optical-flow training data.
Project-URL: Homepage, https://github.com/ricard-inho/osh-dataset
Project-URL: Issues, https://github.com/ricard-inho/osh-dataset/issues
Project-URL: Documentation, https://github.com/ricard-inho/osh-dataset#readme
Project-URL: Dataset, https://huggingface.co/datasets/r3m3c3/oshd
Author-email: Ricard Marsal <ricard.marsal@uni.lu>
License-Expression: MIT
License-File: LICENSE
Keywords: dataset,event-camera,huggingface,motion-capture,optical-flow,pose-estimation,robotics
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Classifier: Topic :: Scientific/Engineering :: Image Recognition
Requires-Python: >=3.10
Requires-Dist: huggingface-hub>=0.23
Requires-Dist: numpy>=1.23
Requires-Dist: pillow>=9.0
Requires-Dist: pyarrow>=14
Requires-Dist: pyyaml>=6.0
Requires-Dist: tqdm>=4.60
Provides-Extra: all
Requires-Dist: rerun-sdk>=0.38; extra == 'all'
Requires-Dist: torch>=2.0; extra == 'all'
Requires-Dist: trimesh>=4.0; extra == 'all'
Provides-Extra: calib
Requires-Dist: trimesh>=4.0; extra == 'calib'
Provides-Extra: mesh
Requires-Dist: trimesh>=4.0; extra == 'mesh'
Provides-Extra: torch
Requires-Dist: torch>=2.0; extra == 'torch'
Provides-Extra: viz
Requires-Dist: rerun-sdk>=0.38; extra == 'viz'
Description-Content-Type: text/markdown

# oshd

Download, synchronize and turn the OSHD multimodal robotics dataset
([`r3m3c3/oshd`](https://huggingface.co/datasets/r3m3c3/oshd) on the Hugging Face Hub)
into training data for **6D pose estimation, object detection, segmentation and optical
flow**.

The dataset contains trajectories of a PPO-controlled floating-platform robot recorded with:
a Prophesee EVK4 event camera, a FLIR Firefly RGB camera, an Intel RealSense D455 (colour +
depth), a Livox Mid-360 lidar + IMU, OptiTrack motion capture (`SensorStack`, `cubesat`,
`floatingplatform`), and the robot's control / observation topics. All streams in a trajectory
share one clock.

The same repo also holds **simulated** trajectories recorded in
[Space Robotics Bench](https://github.com/snt-spacer/space_robotics_bench): orbital inspection
(`sim_3d`: ISS, Gateway, CubeSat, ... ) and a twin of the Zero-G lab (`sim_zerog`), each with
RGB, depth, semantic labels, emulated events, lidar, IMU, ground-truth poses and the policy's
steps. See [Simulated data](#simulated-data).

## Install

```bash
pip install oshd            # core: download, read, sync, labels, exports
pip install "oshd[torch]"   # + PyTorch Dataset wrappers
pip install "oshd[mesh]"    # + loading CAD meshes (.ply/.obj/.stl)
pip install "oshd[viz]"     # + the Rerun playback dashboard (oshd viz)
pip install "oshd[calib]"   # + calibration tools (oshd align-markers, oshd calibrate)
pip install "oshd[all]"     # everything above
pip install "oshd[torch,viz]"   # or any combination

# with uv
uv add "oshd[all]"          # in a uv project
uv pip install "oshd[all]"  # into the active environment
uv tool install "oshd[viz]" # just the `oshd` CLI, isolated, on your PATH
```

Python ≥ 3.10. No OpenCV or ROS needed.

## Download

Data is large (≈ 11 GB per trajectory, mostly Firefly frames), so download only what you
need. Downloads resume, and a rerun only fetches files that are new on the Hub.

```bash
oshd download                                  # all real data
oshd download -m mocap -m realsense_color -m realsense_depth
oshd download -t trajectory_007 -m events
oshd download --domain sim_3d -m rgb -m mocap  # simulated: synthetic, sim_3d, sim_zerog; all = both
oshd download --domain sim_zerog --task docking --split test   # one simulated task, eval runs only
oshd download --benchmark v1 --domain sim_zerog --task docking --split test  # same, frozen paper benchmark
oshd download --revision <commit-sha>          # pin a dataset version for a paper
oshd info                                      # what is local / remote, splits
oshd splits                                    # train / val / test counts per domain and task
oshd check                                     # which calibration / CAD inputs exist
```

Without `-t`, `--domain` picks what is downloaded (default `real`), and `--split` / `--task`
narrow it down. With `-t`, the named trajectories are downloaded whatever their domain.

The default location is `~/.cache/oshd/<repo>`. Override it with `--root` or
`$OSHD_DATA_DIR`, and point at another repo with `--repo` / `$OSHD_REPO_ID`.

## Inspect trajectories: is it good or bad?

**Health report**: runs in seconds on mocap + messages only.

```bash
oshd download -m mocap -m robot_messages -m imu
oshd quality                     # table + warnings; --json report.json for the full numbers
```

```
trajectory          dur s final err cm yaw err°  path m mocap drops label cov  status
trajectory_007       40.9          6.0      0.4    1.52         229       89%  CHECK
trajectory_009       40.9          2.6      0.5    2.32         231        -   CHECK
  trajectory_007: realsense_color at 7.3 Hz (< 10)
  trajectory_007: mocap/floatingplatform: 72 dropouts > 50 ms (max 130 ms)
  ...
```

It reports the final position and heading error against the goal on
`/observation_formater_input`, path length, per-sensor rate and gaps, mocap dropouts, the
share of camera frames that can be labelled, and colour/depth sync. Thresholds are
arguments (`--goal-tol-cm`, `--body`).

**Playback dashboard**: synchronized, scrubbable playback in [Rerun](https://rerun.io):

```bash
oshd viz -t trajectory_007 --download        # fetches everything but Firefly, then opens the viewer
oshd viz -t trajectory_007 -t trajectory_009 # several trajectories: switch in the recordings panel
oshd viz -m mocap -m robot_messages          # all local trajectories, trajectory + actions only
oshd viz -t trajectory_008 --start 10 --end 20 -m events -m realsense_color
oshd viz -t trajectory_050 --web             # viewer in the browser (e.g. over SSH port-forward)
oshd viz --save out/                         # write out/<trajectory>.rrd; open with `rerun out/*.rrd`
```

The browser viewer holds at most ~4 GB and drops the **oldest** data beyond that: the start of
the timeline goes empty (bodies stop moving until near the end). One real trajectory with every
modality is ~0.4 GB with `--web` (masks on every 10th frame, `--mask-stride`); open one or two
at a time, switch between them in the recordings panel (left), or use `--save` + the native
viewer for many.

Layout: 3D mocap world (bodies, full paths, goal and its heading) · x / y / yaw of every
body with the goal as a reference line · distance to goal · Firefly, RealSense colour +
depth and event frames (blue = ON, red = OFF) · Livox point cloud · thruster, reaction-wheel
and IMU plots · a text panel with the quality warnings. All panels share one timeline (seconds
from trajectory start).

Firefly frames are large (≈ 300 kB each), so they're logged every 2nd frame by default
(`--firefly-stride`). Events are rendered at `--events-fps 20` and `--events-scale 0.5`.
One trajectory without Firefly is ≈ 270 MB in the viewer and takes ≈ 20 s to log.

## Calibrate: object alignment + camera extrinsics

Masks and image-space poses need two transforms:

1. **Object → its OptiTrack body** (`T_body_model` in `models/objects.yaml`), from the
   Motive rigid-body export and the marker positions in the CAD.
2. **Camera → the `SensorStack` body** (`calibration/rig.yaml`), from cube corners you
   click in about 10 frames. The cube is static while the rig moves, so no calibration board
   or extra recording is needed.

The current dataset already has both for `cubesat` and `realsense_color`. The steps below
are for a new object, a re-built rigid body, or another camera.

```bash
pip install "oshd[calib]"
oshd download -m realsense_color -m mocap

# 1) object: Motive export + the marker centres in the CAD (here Onshape mm, Z-up part
#    exported Y-up, hence --cad-axes x,-z,y)
oshd align-markers --motive models/CubeSat_U1.motive --cad-axes x,-z,y \
  --cad-marker 46.077 -46.03447 -117.99807 --cad-marker -47.35423 -47.40068 -121.5 \
  --cad-marker -47.35423 47.39831 -121.5 --cad-marker 47.44476 47.39831 -121.5

# 2) camera: click the cube corners
oshd calibrate --camera realsense_color --box-keypoints -0.05 0 -0.05 0.05 0.1 0.05
```

`align-markers` matches your CAD markers to Motive's (subset and order are found
automatically) and refuses a fit worse than 3 mm. Motive stores bodies Y-up and the ROS
stream is Z-up; the standard conversion is the default (`--motive-to-body x,-z,y`).

`--box-keypoints` gives the cube body's 8 corners in CAD coordinates, excluding the marker
holder. When `T_body_model` comes from `align-markers`, `oshd calibrate` only solves the
camera (`--refine-model` overrides this). Solving both from clicks is unreliable: the
camera position and the object position trade off.

`oshd calibrate` prints a local URL (default http://localhost:8765) and opens it in your
browser. On a remote machine, run `ssh -L 8765:localhost:8765 <host>` on your laptop and open
the URL there.

**What to click:** the visible corners of the cube body, and nothing else. Skip the marker
holder, hidden corners and the stand; see [docs/calibration_example.png](docs/calibration_example.png).

On the page: **click** a corner · **right-click / u** undo · **wheel** zoom · **drag** pan ·
**f** fit · **n / Enter** next frame · **s** skip a frame where the cube is cut off or
over-exposed · **q** finish. Clicks are saved after every frame; `--add --frames N` clicks
N more later. After the first frames, yellow crosses show where the current solution
predicts the corners. A result with RMS > 10 px, or with implausible offsets, is flagged and
not written unless you pass `--force`.

Outputs, in `oshd_calibration/` (git-ignored), laid out like the Hub repo:

- `calibration/rig.yaml`: `T_body_cam` of the camera (`--also realsense_depth` gives the
  aligned depth camera the same transform)
- `models/objects.yaml`: `T_body_model` from `align-markers`, plus the keypoints
- `calibration/clicks_<camera>.json`: your clicks. Re-running re-solves from them.
- `overlays_<camera>/*.png`: rendered mask outline (green) and clicks (red), for checking

The results are also copied into your local dataset right away. Check them with
`oshd viz -m <camera> -m mocap`, which draws the masks on the images. Then share them:

```bash
hf upload r3m3c3/oshd oshd_calibration . --repo-type dataset --exclude 'overlays_*/*'
```

## Real targets

`ds.objects` holds the CAD of every real target, read by the label tasks and exports:

| object | trajectories | mocap body | CAD |
|---|---|---|---|
| `cubesat` | 007-009 | `cubesat` | `models/CubeSat_U1.obj` (`models/objects.yaml`) |
| `antena` (mockup, two solar panels) | 010-039 | `target` | `models/real/antena/` |
| `antena_1_solar` (mockup, one panel) | 040-098 | `target` | `models/real/antena_1_solar/` |

The mockups come from `models/real/mockups.json` (trajectory -> mockup), written by the
recording pipeline: `model.obj` + textures in metres, with the origin at the pivot of their
Motive rigid body. Where the mockup's own tracking drops out, its `static_pose` in that file
(mean of its mocap while it stood still) is used. An entry of the same name in
`models/objects.yaml` overrides a mockup's `T_body_model`.

Since trajectory_010 every camera is calibrated relative to the RealSense colour camera
(`T_realsense_color_cam` in `calibration/<trajectory>.yaml`, from the rig calibration). The
Firefly and the event camera get `T_body_cam = T_body_cam(realsense_color) @
T_realsense_color_cam`, with the RealSense's `T_body_cam` from `calibration/rig.yaml`.

## Read and synchronize

```python
from oshd import OshDataset
from oshd.labels import nearest_indices

ds = OshDataset.download(modalities=["mocap", "realsense_color", "realsense_depth", "events"])
traj = ds["trajectory_007"]

rgb   = traj.frame("realsense_color", 100)        # (480, 640, 3) uint8
depth = traj.depth(200)                           # (480, 640) float32 metres
ts    = traj.timestamps("realsense_color")        # int64 ns, host clock

ev = traj.events(ts[100] - 25_000_000, ts[100] + 25_000_000)   # reads only overlapping row groups
T_world_cube, ok = traj.body_pose("cubesat", ts)               # interpolated (slerp) mocap poses
idx, ok = nearest_indices(traj.timestamps("realsense_depth"), ts, tolerance_ns=20_000_000)

scan = traj.lidar_scan(10)                        # dict of x, y, z, intensity, ... arrays
t, obs = traj.message_array("/pingu_low_level")   # (N, D) policy observations
t, act = traj.message_array("/minimal_thruster_command_to_px4")
```

Event representations: `oshd.events.event_histogram`, `voxel_grid`, `time_surface`.

## Training data

Every task splits by **trajectory** (`splits.json` on the Hub) and returns dicts of numpy
arrays. Use `oshd.torch` for PyTorch.

```python
from oshd.tasks import PoseEstimation, ObjectDetection, Segmentation, OpticalFlow

pose = PoseEstimation(ds, "train", camera="firefly", undistort=True, image_size=(1536, 1024))
s = pose[0]            # image, K, poses (N, 4, 4) T_cam_model, object_ids, valid

det = ObjectDetection(ds, "train", camera="realsense_color", stride=2)
s = det[0]             # image, boxes (xyxy of the visible part), labels, masks, poses

seg = Segmentation(ds, "val", camera="realsense_color")    # semantic + instance maps

flow = OpticalFlow(ds, "train", camera="realsense_color")  # image0, image1, flow, valid
eflow = OpticalFlow(ds, "train", camera="events", window_ns=50_000_000)  # voxel grid, flow
```

```python
import torch
from oshd.torch import TorchDataset, detection_collate

loader = torch.utils.data.DataLoader(TorchDataset(det), batch_size=8,
                                     collate_fn=detection_collate, num_workers=4)
```

How labels are made: object pose = mocap pose of the object (plus marker-to-CAD transform)
seen from the camera (SensorStack mocap pose plus camera extrinsics). Masks and boxes are
rendered from the CAD mesh at that pose, with occlusion between tracked objects. Optical
flow comes from RealSense depth moved by the camera and object motion measured by mocap.

> **Labels need calibration.** They work today for the `cubesat` in the RealSense colour
> camera (trajectories 007–009). Other cameras raise `MissingCalibrationError` until they are
> calibrated (see "Calibrate" above). Run `oshd check` for the current status and see
> [DATA_REQUIREMENTS.md](https://github.com/ricard-inho/osh-dataset/blob/main/DATA_REQUIREMENTS.md).

## Simulated data

Simulated trajectories are named `trajectory_<tag>_<n>` and stored on the Hub as
`sim_3d_<modality>/<trajectory>/` and `sim_zerog_<modality>/<trajectory>/`. The library hides
the prefix: they are read like the real ones, with their own modality names.

| modality | content |
|---|---|
| `rgb`, `depth`, `semantic` | one pinhole camera (320 x 240, 20 Hz); depth in metres via `traj.depth(i)`; semantic class ids, names in `traj.semantic_classes` (`target` = 2) |
| `events` | emulated IMX636 events, pixel-aligned with the camera |
| `lidar` | scans in the sensor frame (`x`, `y`, `z`, `distance`, `channel`) |
| `mocap` | ground-truth poses: `robot`, `target`, `goal`, `camera` (+ `arm_end_effector` in the lab) |
| `imu` | robot IMU (linear acceleration in m/s^2, not g) |
| `joint_states` | lab: floating platform and arm joints (`traj.joint_states("arm_wall")`) |
| `steps` | policy steps at 10 Hz: action, observation, rewards, goal errors (`traj.steps()`) |

```python
from oshd import OshDataset
from oshd.labels import camera_pose

ds = OshDataset.download(domain="sim_3d", modalities=["rgb", "depth", "semantic", "mocap"])
ds.scenarios()                         # {"orbital/iss/earth": [...], ...}
ds.split("train", domain="sim_3d")     # train / test by recording run (splits.json)
ds.split("test", domain="sim_zerog", task="docking")
ds.tasks()                             # {"sim_3d/approach": [...], "sim_3d/orbit": [...],
                                       #  "sim_zerog/docking": [...], "sim_zerog/inspection": [...]}

traj = ds[ds.scenarios()["orbital/iss/earth"][0]]
traj.info                              # group, scenario, task, run, duration_s, goal errors, counts
traj.metadata                          # scenario parameters, rates, frames and units, term names
rgb, sem = traj.frame("rgb", 0), traj.frame("semantic", 0)
target_mask = sem == traj.semantic_classes["target"]
T_world_cam, ok = camera_pose(traj, "rgb", traj.timestamps("rgb")[0])
```

**Lab twins of the real mockups.** 200 zero-G lab trajectories (100 per mockup, `benchmark_v3`)
put the real mockups' CAD (`real_antena`, `real_antena_1_solar`, in the dataset's CAD frame of
`models/real/<mockup>/`) on a static mount at one of the stand poses measured in the real
recordings (17 for `antena`, 32 for `antena_1_solar`; world frame of the lab, which the twin
shares), and fly the same inspection and docking tasks as the other lab runs. Scenario
`zerog/real_<mockup>/static/<task>`; `ds.split("train" | "test", domain="twin")` gives them.
With `ds.split(..., domain="real")` they make the real <-> twin transfer pairs.

**Lidar frame.** Simulated scans are in the lidar sensor frame. Scans recorded before the twins
(Hub revisions up to `benchmark_v2`, `df3530b`) were stored about the robot's mount link, off by
the mount offset (`T_base_sensor`: 0.21, 0, 0.76 m in the lab); the Hub data from `benchmark_v3`
on is corrected (calibration `lidar.points_origin: sensor`), and oshd >= 1.1 corrects the older
files when it reads them, so `traj.lidar_scan(i)` is right at every revision.

**Target CAD.** Every simulated trajectory observes one target, and its CAD is on the Hub. The
satellite mockup, ISS, Gateway and Venus Express come from SRB's assets; a `cubesat<seed>` is
SimForge's procedural CubeSat, generated from the scenario seed. `oshd download` fetches the
models of the trajectories it downloads (`models/sim/<model>/`: `model.obj` + `.mtl` + textures,
and the simulator's `model.usdz`):

```python
traj.target                  # {"model": "cubesat1034538678", "kind": "cubesat", "scale": 2.48}
mesh = traj.target_mesh()    # trimesh, metres, in the frame of the mocap body "target", scaled
traj.target_model_path       # models/sim/<model>/model.obj (unscaled)
```

Calibration comes from the simulation (`calibration/<trajectory>.yaml`): intrinsics, and each
sensor's mount on the robot base, read as extrinsics on the `robot` mocap body (cameras in
their optical frame). So `oshd.labels.camera_pose`, `traj.body_pose("target", t)` and
`OpticalFlow(ds, ..., camera="rgb" | "events")` work as for the real data. The object tasks
(`PoseEstimation`, `ObjectDetection`, `Segmentation`) need a CAD model of a real target
(see [Real targets](#real-targets)) and are real-data only; in simulation, the `semantic` images give the
masks and the `target` mocap body the pose.

Domains in `ds.split(..., domain=...)`: `real`, `synthetic` (all simulated), `sim_3d`,
`sim_zerog`, and `twin`: the zero-G lab twins of the real mockup recordings (part of
`sim_zerog`, from `benchmark_v3` on). Tasks in `ds.split(..., task=...)`: `approach` and `orbit` (sim_3d), `docking` and
`inspection` (sim_zerog).

### Splits

`splits.json` on the Hub lists the train and test (eval) trajectories; there is no simulated
`val`. It is updated by the sim publisher every time new runs are uploaded (or by hand with
`oshd splits update --push`), append-only: a trajectory never changes split. All episodes of
one recording run (same scenario, same seed) are in the same split, and about 20% of the runs
of each scenario config (target, background or arm, task) are in `test`, so every task has
eval runs from every day of recording.

Real trajectories are split by **lighting setting**: the target (mockup or CubeSat), the light's
colour temperature and its intensity. Every trajectory of a setting is in the same split, so the
test lighting is never seen in training. About 20% of each target's trajectories are in `test`,
spread over the colour temperatures. Only real trajectories with a target model are in a split
(`trajectory_007` to `009` keep their `benchmark_v1` split); the early recordings without one
are in none. Pin `--revision` to cite one version of the file.

### Benchmarks (for papers)

The split above grows every day. Paper numbers use a **frozen benchmark** instead: the data of
one Hub revision with a fixed train / test split. It ships with the package
(`oshd/benchmarks/v1.json`), and the Hub's `splits.json` carries a copy.

| benchmark | Hub revision | trajectories | train / test |
|---|---|---|---|
| `v1` (`benchmark_v1`, frozen 2026-10-20) | `8205a11` | 2195 sim + 3 real | sim_3d/approach 378 / 93, sim_3d/orbit 800 / 201, sim_zerog/docking 300 / 72, sim_zerog/inspection 282 / 69, real 2 / 1 |
| `v2` (`benchmark_v2`, frozen 2026-10-31, oshd 1.0.0) | `df3530b` | 6413 sim + 92 real | sim_3d/approach 1089 / 186, sim_3d/orbit 2528 / 522, sim_zerog/docking 921 / 162, sim_zerog/inspection 861 / 144, real/antena 24 / 6, real/antena_1_solar 49 / 10, real/cubesat 2 / 1 |
| **`v3`** (`benchmark_v3`, frozen 2026-11-02, oshd 1.1.0) | `c8e5ca0` | 6613 sim + 92 real | v2 + the lab twins: twin/antena 80 / 20, twin/antena_1_solar 80 / 20 (in sim_zerog/docking 999 / 182, sim_zerog/inspection 943 / 164); simulated lidar about the sensor origin |

`v2` is the final data collection: every simulated campaign and every real recording with a
target model (the CubeSat and both mockups, with their CAD, rig calibration and real lighting
-setting split). It contains `v1`, and every `v1` trajectory keeps its split.

`v3` adds the zero-G lab twins of the real mockup recordings (see [Simulated data](#simulated-data)),
for real <-> twin transfer: train on `domain="real"` and test on `domain="twin"`, or the other
way round. Its Hub revision also has the simulated lidar re-expressed about the sensor. It
contains `v2`, and every `v2` trajectory keeps its split. **Use `v3` for new work.**

```bash
oshd download --benchmark v3 --domain sim_zerog --task docking --split test -m rgb -m depth -m mocap
oshd download --benchmark v3 --domain twin --split train -m rgb -m events -m mocap
oshd download --benchmark v3 --domain real --split test -m realsense_color -m realsense_depth -m mocap
oshd splits --benchmark v3
```

```python
ds = OshDataset.download(benchmark="v3", domain="sim_3d", split="train", modalities=["rgb", "mocap"])
ds.split("test", domain="sim_zerog", task="docking", benchmark="v3")
ds.split("test", domain="twin", benchmark="v3")       # 40 twin trajectories
```

`--benchmark` pins the download to the benchmark's revision, so later uploads and repairs never
change its data. For `v1` the target CAD was uploaded after the freeze, so it comes from its
`models_revision` (`22a8431`); `v2` and `v3` have everything at their own revision. Its simulated trajectories keep their split in the growing `splits.json` too.
Cite the benchmark name and revision in the paper. `ds.hub_splits` gives the splits shown by the Hub viewer (simulation scenarios and
groups of real trajectories); they organise the data and are not train / test splits.

## Export to standard formats

```bash
oshd export bop  --camera firefly         --split train --out out/bop     # undistorted, mm
oshd export coco --camera realsense_color --split val   --out out/coco --stride 2
oshd export yolo --camera firefly --size 1536 1024 --split train --out out/yolo
oshd extract-frames -m firefly --stride 10 --out out/frames               # raw JPEGs + timestamps.csv
```

The same exporters are available from Python in `oshd.export`. Frames that are not
resized or undistorted are copied byte-for-byte, with no re-encoding.

## Development

```bash
uv sync
uv run pytest        # tests run on a synthetic dataset with analytically known labels
uv run ruff check
```

Release: bump `__version__` in `src/oshd/__init__.py`, tag `vX.Y.Z` and push the tag.
The `publish` GitHub workflow builds the package and uploads it to PyPI via trusted publishing.

Code: MIT. Data: CC-BY-4.0.
