Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"python.testing.pytestArgs": [
"tests"
],
"python.testing.unittestEnabled": false,
"python.testing.pytestEnabled": true
}
2 changes: 2 additions & 0 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,8 @@
"numpy",
"scipy",
"matplotlib",
"vispy",
"pandas",
]
hoverxref_domains = ["py"]
hoverxref_role_types = {
Expand Down
30,722 changes: 30,722 additions & 0 deletions docs/source/examples/ROS_ST_K020_OSPCLAM_U_V1.OBJ

Large diffs are not rendered by default.

Binary file modified docs/source/examples/hohmann-bielliptic-transfers.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified docs/source/examples/hohmann.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions docs/source/examples/render-dsk-kernels.myst.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
---
jupytext:
text_representation:
extension: .md
format_name: myst
format_version: 0.13
jupytext_version: 1.14.0
kernelspec:
display_name: Python 3 (ipykernel)
language: python
name: python3
---

# 3D Asteroid and Comet Visualization

## Overview
This enhancement adds a **3D visualization feature** to **Hapsira**, rendering precise models of asteroids and comets using **SPICE Digital Shape Kernel (DSK) files**. The visualization leverages the **Vispy** library for rendering, offering interactive exploration of shape and topography data. Additionally, a **bash script** automates the conversion of `.bds` files into `.obj` files using `dskexp` on Linux systems.


### Bash Script for BDS to OBJ Conversion

If your data file downloaded from **NAIF kernel** dsk is .bds format, you might have to convert the file into text format that is .obj. Link for [NAIF Kernels](https://naif.jpl.nasa.gov/pub/naif/generic_kernels/) is provided.

To use the .sh toolkit for conversion, cd into the directory where the setup_dskexp.sh file is located and run the following command:

```bash
source setup_dskexp.sh
```
```bash
dskexp -dsk filename.bds -text filename.obj -format obj -prec 10
```

### Key Features

The module includes the following steps:
* Load the 3D model data from a file.
* Create a MainWindow instance.
* Set the 3D model in the MainWindow.
* Run the Vispy application.

```{code-cell} ipython3
from hapsira.render.data_loader import load_data
from hapsira.render.scene import MainWindow
from vispy import app
```

```{code-cell} ipython3
vertices, faces = load_data("ROS_ST_K020_OSPCLAM_U_V1.OBJ")
main_w = MainWindow()
main_w.set_model(vertices, faces)
```

```{code-cell} ipython3
app.run()
```

![Stein model](stein.png)

## Author

**Rahul Ranjan Sah, Furman University**

## References
- https://naif.jpl.nasa.gov/pub/naif/generic_kernels/
- https://spiceypy.readthedocs.io/_/downloads/en/stable/pdf/
- https://vispy.org/
Binary file added docs/source/examples/stein.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 3 additions & 0 deletions docs/source/examples/stein.png:Zone.Identifier
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[ZoneTransfer]
ZoneId=3
HostUrl=https://github.com/
1 change: 1 addition & 0 deletions docs/source/gallery.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,6 @@ The following page contains a collection of practical examples and problems solv
/examples/tisserand.myst.md
/examples/detecting-events.myst.md
/examples/loading-OMM-and-TLE-satellite-data.myst.md
/examples/render-dsk-kernels.myst.md

```
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ dependencies = [
"plotly", # >=4.0,<6",
"pyerfa",
"scipy", # >=1.4.0",
"vispy",
]
dynamic = ["version", "description"]

Expand All @@ -80,11 +81,12 @@ doc = [
"ipywidgets",# >=7.6",
"jupyter-client",
"jupytext",
"jupyter_rfb",
"myst-parser", # >=0.13.1",
"nbsphinx", # >=0.8.6",
"nbconvert", # >=5.5",
"sgp4",
"sphinx", # !=3.5,!=4.0,<5.1", # https://github.com/readthedocs/sphinx-hoverxref/issues/119
"sphinx<8.0", # !=3.5,!=4.0,<5.1", # https://github.com/readthedocs/sphinx-hoverxref/issues/119
"sphinx-autoapi>=3",
"sphinx-gallery<=0.7.0", # Just to avoid the jupytext warning
"sphinx_rtd_theme", # ~=1.0.0",
Expand Down
36 changes: 36 additions & 0 deletions src/hapsira/render/data_loader.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
"""This module provides functionality to load 3D model data from a file.

The `load_data` function reads a file containing 3D model data, extracts the vertex and face
information, and returns them as numpy arrays. The file is expected to be in a specific format
where each line represents either a vertex or a face. Vertices are denoted by lines starting
with 'v' and faces by lines starting with 'f'.

The function uses pandas to read the file and process the data. The vertices and faces are
extracted based on their respective identifiers and returned as numpy arrays.
"""

import pandas as pd


def load_data(file_path):
"""
Load 3D model data from a file.

Parameters
----------
file_path : str
Path to the file containing the 3D model data.

Returns
-------
vertices : numpy.ndarray
Array of vertex coordinates.
faces : numpy.ndarray
Array of face indices.
"""
comet = pd.read_csv(file_path, sep=r"\s+", names=["TYPE", "X1", "X2", "X3"])
vertices = comet.loc[comet["TYPE"] == "v"][["X1", "X2", "X3"]].values
faces = comet.loc[comet["TYPE"] == "f"][["X1", "X2", "X3"]].values
faces = faces - 1
faces = faces.astype(int)
return vertices, faces
45 changes: 45 additions & 0 deletions src/hapsira/render/scene.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
from vispy import scene
from vispy.geometry import MeshData
from vispy.scene import visuals


class MainWindow(scene.SceneCanvas):
"""
Main window for rendering 3D models using Vispy.

This class provides a graphical interface for rendering 3D models using the Vispy library.
It sets up a scene canvas with interactive capabilities, allowing users to visualize and
interact with 3D models. The canvas includes a grid layout and a view with a turntable
camera for easy manipulation of the 3D scene.

Parameters
----------
*args : tuple
Additional arguments for the SceneCanvas.
"""

def __init__(self, *args):
super().__init__(keys="interactive", size=(800, 600), show=True)
self.unfreeze()
self.grid = self.central_widget.add_grid(margin=10)
self.view = self.grid.add_view(row=0, col=0, camera="turntable")
self.view.bgcolor = "black"
self.view.camera.fov = 60

def set_model(self, vertices, faces):
"""
Set the 3D model to be rendered.

This method takes arrays of vertices and faces, creates a mesh from them, and adds
the mesh to the view for rendering. The mesh is shaded smoothly and colored grey.

Parameters
----------
vertices : numpy.ndarray
Array of vertex coordinates.
faces : numpy.ndarray
Array of face indices.
"""
mesh_data = MeshData(vertices=vertices, faces=faces)
mesh = visuals.Mesh(meshdata=mesh_data, shading="smooth", color="grey")
self.view.add(mesh)
67 changes: 67 additions & 0 deletions src/hapsira/render/setup_dskexp.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/bin/bash

DSKEXP_URL="https://naif.jpl.nasa.gov/pub/naif/utilities/PC_Linux_64bit/dskexp"

# Functions
install_wget_arch() {
echo "Installing wget on Arch Linux..."
sudo pacman -Syu wget --noconfirm
if [ $? -ne 0 ]; then
echo "Failed to install wget. Please install it manually and re-run the script."
exit 1
fi
}

install_wget_fedora() {
echo "Installing wget on Fedora..."
sudo dnf install wget -y
if [ $? -ne 0 ]; then
echo "Failed to install wget. Please install it manually and re-run the script."
exit 1
fi
}

install_wget_ubuntu() {
echo "Installing wget on Ubuntu..."
sudo apt-get update
sudo apt-get install wget -y
if [ $? -ne 0 ]; then
echo "Failed to install wget. Please install it manually and re-run the script."
exit 1
fi
}

# wget installations
if ! command -v wget > /dev/null 2>&1; then
if [ -f /etc/os-release ]; then
. /etc/os-release
case "$ID" in
ubuntu|debian)
install_wget_ubuntu
;;
arch)
install_wget_arch
;;
fedora)
install_wget_fedora
;;
*)
echo "Unsupported operating system. Please install wget manually and re-run the script."
exit 1
;;
esac
fi
fi


# dskexp setup
if [ ! -f "dskexp" ]; then
echo "dskexp not found. Downloading..."
wget $DSKEXP_URL -O dskexp
if [ $? -ne 0 ]; then
echo "Failed to download dskexp. Please check the URL and your internet connection."
exit 1
fi
fi

chmod +x dskexp
26 changes: 26 additions & 0 deletions tests/tests_render/test_load_data.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
from io import StringIO

import numpy as np

from hapsira.render.data_loader import load_data


def test_load_data():
data = """v 1.0 2.0 3.0
v 4.0 2.0 3.0
f 2 2 4"""

file_path = StringIO(data)

test_vertices = np.array([[1.0, 2.0, 3.0], [4.0, 2.0, 3.0]])

# zero-index face
test_faces = np.array([[1, 1, 3]])

vertices, faces = load_data(file_path)

np.testing.assert_array_equal(vertices, test_vertices)
np.testing.assert_array_equal(faces, test_faces)


test_load_data()
20 changes: 20 additions & 0 deletions tests/tests_render/test_scene.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import unittest
from unittest.mock import MagicMock

import numpy as np

from hapsira.render.scene import MainWindow


class TestMainWindow(unittest.TestCase):
def test_set_model(self):
main_w = MainWindow()
main_w.view.add = MagicMock()

vertices = np.array([[0, 0, 0], [1, 0, 0], [0, 1, 0]])
faces = np.array([[0, 1, 2]])

main_w.set_model(vertices, faces)

# asserting that view.add was called once with proper arguments
main_w.add.assert_called_once_with(vertices, faces)