Skip to content

Commit

Permalink
Merge branch 'master' into release
Browse files Browse the repository at this point in the history
  • Loading branch information
harrisonv789 committed Nov 28, 2024
2 parents 3510ce1 + fcc880f commit 82a0e00
Show file tree
Hide file tree
Showing 13 changed files with 577 additions and 64 deletions.
80 changes: 67 additions & 13 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,11 @@ Example simulation scripts and usage of the NominalPy library can be found in th

Additional tutorials and functionality for the library can be found on the public documentation at [docs.nominalsys.com](https://docs.nominalsys.com) under the Guides page.

![Sun Pointing Simulation](https://docs.nominalsys.com/v0.8/articles/NominalSystems/guides/images/Untitled%203.png)

<br>
![Sun Pointing Simulation](https://docs.nominalsys.com/v1.0/articles/NominalSystems/guides/Python/GettingStarted/images/image.png)

---

### Installing `nominalpy`
## Installing `nominalpy`

NominalPy includes the interface for the Nominal API architecture, allowing for custom simulations to be constructed using API endpoints. For a non-local simulation, this requires correct authentication. To install `nominalpy`, install the project from the PyPi repository:

Expand All @@ -36,12 +34,11 @@ NominalPy requires the following Third-Party Python libraries to be installed al
- paho-mqtt
- matplotlib
- setuptools

<br>
- aiohttp

---

### Updating `nominalpy`
## Updating `nominalpy`

To upgrade a version of NominalPy, make sure to upgrade the python package. This can be done by the following command.

Expand All @@ -51,20 +48,77 @@ pip install nominalpy --user --upgrade

---

### Accessing your API Token
## Accessing your API Token

API Tokens are accessible via the [Nominal Website](https://www.nominalsys.com/account/log-in). Create a free account and start a 14-day trial of the software. This will give you access to unlimited sessions and requests during the trial period. The access token is available from the dashboard once the account is created and logged in.

To enable your token, create a Credential object and pass into the Simulation class when constructing simulations. Alternatively, if using the example library, update the token in the `credential_helper.py` class for easier access to the token over multiple files. More information can be found in the [API Access Keys](https://docs.nominalsys.com/v1.0/articles/NominalSystems/guides/Python/GettingStarted/3_APIAccessKeys/index.html).

---

API Tokens are accessible via the [Nominal Website](https://www.nominalsys.com/account/sign-in). Create a free account and start a 14-day free trial of the software. This will give you access to unlimited sessions and requests during the trial period. The access token is available from the dashboard once the account is created and logged in.
## API Sessions

To enable your token, create a Credential object and pass into the Simulation class when constructing simulations. Alternatively, if using the example library, update the token in the `credential_helper.py` class for easier access to the token over multiple files. More information can be found in the [Public Documentation](https://docs.nominalsys.com/).
Each token enables a single API session to be made. Multiple sessions cannot be created at once using the free-tier of the API. Each session is associated with a single cloud instance running the simulation that you have access to. This instance will run for 2 hours once connected. When first connected to the session, it may take between 30 seconds and 2 minutes for the instance to start. After that, the instance will be available without any load times for up to 2 hours. Upon the completion of the session, a new session can be started immediately after with a similar load-up time as before. There is no limit for the number of sessions available during a period of time, with the exception of having one session running concurrently.

---

### API Sessions
## Creating a Simulation

To create a simulation, use the `Simulation` class. Once the credentials have been created, this can be used to set up all configurations of objects and data within your instance.

```python
from nominalpy import Credentials, Simulation

credentials = Credentials("https://api.nominalsys.com", None, "$ACCESS_KEY$")
simulation = Simulation.get(credentials)
```

The simulation class can be used to manage the objects, tick the simulation and track the state of data over the lifetime of the session. As an example, a spacecraft could be created using the following code:

```python
from nominalpy import Object, types

spacecraft: Object = simulation.add_object(
types.SPACECRAFT,
Position=[6671000, 0, 0],
Velocity=[0, -7700, 0],
TotalMass=15.0,
AttitudeRate=[0.01, 0.02, -0.03]
)
```

---

## Ticking a Simulation

Once the objects have been created in the simulation, the simulation can be ticked. A `tick` can have a step-size, specified in seconds, and an optional duration. A `duration` will specify how long to tick the simulation for, with the specified step size, before finishing the process.

```python
simulation.tick(0.1)
simulation.tick_duration(time=1000.0, step=0.1)
```

---

## Recording Data

Before the simulation is ticked, specific objects, messages or instances can have their data tracked over the simulation lifetime. This allows for the data to be pulled back as a time-series set at the end of the simulation for data analysis and saving. By default, no components are recorded unless the simulation is told to start tracking them. To track an object, use the following.

```python
simulation.set_tracking_interval(10.0)
simulation.track_object(spacecraft)
simulation.track_object(spacecraft.get_message("Out_SpacecraftStateMsg"))
```

Once the simulation has finished ticking, to retrieve the data at the end of the simulation, use the `query` functions, which can also return a pandas data frame object that can be plotted. This will only work with ojects that have been tracked before the simulation started running.

Each token enables a single API session to be made. Multiple sessions cannot be created at once using the free-tier of the API. Each session is associated with a single cloud instance running the simulation that you have access to. This instance will run for 2 hours once connected. When first connected to the session, it may take between 30 seconds and 2 minutes for the instance to start when you run a simulation for the first time. After that, the instance will be available without any load times for 2 hours. Upon the completion of the session, a new session can be started immediately after with a similar load-up time as before. There is no limit for the number of sessions available during a period of time, with the exception of having one session running concurrently.
```python
data_sc = simulation.query_object(spacecraft)
data_msg = simulation.query_dataframe(spacecraft.get_message("Out_SpacecraftStateMsg"))
```

---

</br>

![Atmospheric Drag Analysis](https://docs.nominalsys.com/v0.8/articles/NominalSystems/guides/Python/GettingStarted/images/Untitled.png)
![Orbit Raising Maneuver](https://docs.nominalsys.com/v1.0/articles/NominalSystems/guides/images/image_2.png)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
# Setup the project
setup(
name='nominalpy',
version='1.0.0',
version='1.0.2',
packages=find_packages(where='src'),
package_dir={'': 'src'},
install_requires=["requests", "urllib3", "paho-mqtt", "numpy", "pandas", "matplotlib", "setuptools"],
install_requires=["aiohttp", "urllib3", "paho-mqtt", "numpy", "pandas", "matplotlib", "setuptools", "pytest-asyncio"],
author='Nominal Systems',
author_email='support@nominalsys.com',
description='Python Interface to the Nominal API for simulations',
Expand Down
2 changes: 1 addition & 1 deletion src/nominalpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Define the version
__version__ = "1.0.0"
__version__ = "1.0.1"

# Import the standard utilities
from .utils import NominalException, printer, types, helper
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@
# Disable the insecure request warning
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)

def __http_request (credentials: Credentials, method: str, path: str, data: dict = {}) -> dict:

def __http_request(credentials: Credentials, method: str, path: str, data: dict = {}) -> dict:
'''
Creates a generic HTTP request to the API with the specified type, path
and some data in the form of a JSON dictionary. This will return the JSON
Expand Down Expand Up @@ -99,7 +100,8 @@ def __http_request (credentials: Credentials, method: str, path: str, data: dict
return response.text
return None

def get (credentials: Credentials, path: str, data: dict = {}) -> dict:

def get(credentials: Credentials, path: str, data: dict = {}) -> dict:
'''
Performs a GET request to the API with the specified path and data. This
will return the JSON data from the response if the request was successful.
Expand All @@ -117,7 +119,8 @@ def get (credentials: Credentials, path: str, data: dict = {}) -> dict:

return __http_request(credentials, 'GET', path, data)

def post (credentials: Credentials, path: str, data: dict = {}) -> dict:

def post(credentials: Credentials, path: str, data: dict = {}) -> dict:
'''
Performs a POST request to the API with the specified path and data. This
will return the JSON data from the response if the request was successful.
Expand All @@ -135,7 +138,8 @@ def post (credentials: Credentials, path: str, data: dict = {}) -> dict:

return __http_request(credentials, 'POST', path, data)

def put (credentials: Credentials, path: str, data: dict = {}) -> dict:

def put(credentials: Credentials, path: str, data: dict = {}) -> dict:
'''
Performs a PUT request to the API with the specified path and data. This
will return the JSON data from the response if the request was successful.
Expand All @@ -153,7 +157,8 @@ def put (credentials: Credentials, path: str, data: dict = {}) -> dict:

return __http_request(credentials, 'PUT', path, data)

def patch (credentials: Credentials, path: str, data: dict = {}) -> dict:

def patch(credentials: Credentials, path: str, data: dict = {}) -> dict:
'''
Performs a PATCH request to the API with the specified path and data. This
will return the JSON data from the response if the request was successful.
Expand All @@ -171,7 +176,8 @@ def patch (credentials: Credentials, path: str, data: dict = {}) -> dict:

return __http_request(credentials, 'PATCH', path, data)

def delete (credentials: Credentials, path: str, data: dict = {}) -> dict:

def delete(credentials: Credentials, path: str, data: dict = {}) -> dict:
'''
Performs a DELETE request to the API with the specified path and data. This
will return the JSON data from the response if the request was successful.
Expand All @@ -187,4 +193,4 @@ def delete (credentials: Credentials, path: str, data: dict = {}) -> dict:
:rtype: dict
'''

return __http_request(credentials, 'DELETE', path, data)
return __http_request(credentials, 'DELETE', path, data)
70 changes: 69 additions & 1 deletion src/nominalpy/maths/astro.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from typing import Tuple
from . import constants
from . import utils
from .kinematics import euler2, euler3
from .utils import normalize_angle
from ..utils import NominalException


Expand Down Expand Up @@ -526,7 +528,7 @@ def geodetic_lla_to_pcpf (lla: np.ndarray, planet="Earth") -> np.ndarray:
return np.array([X, Y, Z], dtype=np.float64)


def geodetic_lla_to_pcpf_deg (lla: np.ndarray, planet="Earth") -> np.ndarray:
def geodetic_lla_to_pcpf_deg(lla: np.ndarray, planet="Earth") -> np.ndarray:
"""
Converts from Latitude/Longitude/Altitude (LLA) coordinates to Planet-Centered,
Planet-Fixed (PCPF) coordinates given a planet radius.
Expand All @@ -546,6 +548,72 @@ def geodetic_lla_to_pcpf_deg (lla: np.ndarray, planet="Earth") -> np.ndarray:
return geodetic_lla_to_pcpf(np.array([lat, lon, lla[2]], dtype=np.float64), planet=planet)


def t_pcpf_to_sez_using_geodetic_lla_deg(latitude: float, longitude: float) -> np.ndarray:
"""
Convert geodetic coordinates (latitude and longitude) to the SEZ (South, East, Zenith) rotation matrix.
:param latitude: Latitude in degrees.
:type latitude: float
:param longitude: Longitude in degrees.
:type longitude: float
:return: 3x3 SEZ rotation matrix.
:rtype: numpy.ndarray
"""
# Convert degrees to radians
t_pcpf_to_enu = t_pcpf_to_enu_using_geodetic_lla_deg(latitude, longitude)
# Convert ENU to SEZ
t_enu_to_sez = np.array([
[0, -1, 0],
[1, 0, 0],
[0, 0, 1]
])
return t_enu_to_sez @ t_pcpf_to_enu


def t_pcpf_to_enu_using_geodetic_lla_deg(latitude: float, longitude: float) -> np.ndarray:
"""
Convert geodetic coordinates (latitude and longitude) to the ENU (East, North, Up) rotation matrix.
:param latitude: Latitude in degrees.
:type latitude: float
:param longitude: Longitude in degrees.
:type longitude: float
:return: 3x3 ENU rotation matrix.
:rtype: numpy.ndarray
"""
# Convert the latitude and longitude into radians
lat_rad = np.deg2rad(latitude)
lon_rad = np.deg2rad(longitude)
# Create the transformation matrix for pcpf into enu
t_pcpf_to_enu = np.array([
[-np.sin(lon_rad), np.cos(lon_rad), 0],
[-np.cos(lon_rad) * np.sin(lat_rad), -np.sin(lon_rad) * np.sin(lat_rad), np.cos(lat_rad)],
[np.cos(lon_rad) * np.cos(lat_rad), np.sin(lon_rad) * np.cos(lat_rad), np.sin(lat_rad)]
])
return t_pcpf_to_enu


def enu_to_azimuth_elevation(enu_vector: np.ndarray) -> Tuple[float, float]:
"""
Convert an ENU vector to azimuth and elevation angles.
:param enu_vector: ENU vector.
:type enu_vector: numpy.ndarray
:return: Azimuth and elevation angles.
:rtype: tuple
"""
# Calculate azimuth angle
azimuth = np.arctan2(enu_vector[0], enu_vector[1])
azimuth = normalize_angle(azimuth, angle_max=2 * np.pi)
# Handle devision by zero
norm = np.linalg.norm(enu_vector)
if norm == 0:
raise ValueError("The input vector is a zero vector.")
# Calculate elevation angle
elevation = np.arcsin(enu_vector[2] / norm)
return azimuth, elevation


def calculate_orbital_velocity(r_bn_n_mag: float, semi_major_axis: float, gm: float = constants.EARTH_MU) -> float:
"""
Calculate the magnitude of the orbital velocity for a spacecraft in any orbit type.
Expand Down
37 changes: 37 additions & 0 deletions src/nominalpy/maths/kinematics.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,40 @@ def mrp_to_dcm(mrp: np.ndarray) -> np.ndarray:
c *= 1/d

return c


def euler2(angle_rad: float) -> np.ndarray:
"""
Create a rotation matrix for a rotation about the Y-axis.
:param angle_rad: Rotation angle in radians.
:type angle_rad: float
:return: 3x3 rotation matrix about the Y-axis.
:rtype: numpy.ndarray
"""
cos_x = np.cos(angle_rad)
sin_x = np.sin(angle_rad)
rotation_matrix = np.array([
[cos_x, 0.0, sin_x],
[0.0, 1.0, 0.0],
[-sin_x, 0.0, cos_x]
])
return rotation_matrix

def euler3(angle_rad: float) -> np.ndarray:
"""
Create a rotation matrix for a rotation about the Z-axis.
:param angle_rad: Rotation angle in radians.
:type angle_rad: float
:return: 3x3 rotation matrix about the Z-axis.
:rtype: numpy.ndarray
"""
cos_x = np.cos(angle_rad)
sin_x = np.sin(angle_rad)
rotation_matrix = np.array([
[cos_x, -sin_x, 0.0],
[sin_x, cos_x, 0.0],
[0.0, 0.0, 1.0]
])
return rotation_matrix
34 changes: 34 additions & 0 deletions src/nominalpy/maths/sensors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import numpy as np


def calculate_focal_length(area_mm2: float | np.ndarray, fov_degrees: float | np.ndarray) -> float | np.ndarray:
"""
Calculate the focal length of a camera given the sensor area and field of view.
:param area_mm2: The area of the sensor in square millimeters.
:type area_mm2: float
:param fov_degrees: The field of view of the camera in degrees.
:type fov_degrees: float
:return: The focal length of the camera in millimeters.
:rtype: float
"""
# Sanitise the inputs
if np.isnan(area_mm2) or np.isnan(fov_degrees):
raise ValueError("Input values cannot be NaN")
if np.isinf(area_mm2) or np.isinf(fov_degrees):
raise ValueError("Input values cannot be infinite")
if area_mm2 <= 0:
raise ValueError("Sensor area must be positive")
if fov_degrees <= 0 or fov_degrees >= 180:
raise ValueError("Field of view must be greater than 0 and less than 180")
# Assuming a square sensor
sensor_width_mm = np.sqrt(area_mm2)
# Convert FOV to radians
fov_radians = np.radians(fov_degrees)
# Calculate tan(fov/2) to use in the focal length calculation
tan_half_fov = np.tan(fov_radians / 2)
if tan_half_fov == 0:
raise ZeroDivisionError("tan(fov/2) is zero, invalid field of view")
# Calculate focal length
focal_length_mm = sensor_width_mm / (2 * tan_half_fov)
return focal_length_mm
Loading

0 comments on commit 82a0e00

Please sign in to comment.