Scripting
Gaia Sky exposes an API that can be accessed via Python scripts and via an HTTP server. In this section we focus on the Python method. The API calls can be called from Python programs (scripts), that must be run with the system Python interpreter. They connect to a gateway service offered by a running instance of Gaia Sky.
Quick start
If you just need some examples to get started, look up the test and showcase scripts in scripts folder of the project.
Requirements
In order to connect to the gateway server, you need a Python 3.5+
interpreter and the Py4J package. You can install it with pip
as a user package like this:
$ pip install --user py4j
You may also use your distribution or operating system package manager to install Py4J. Please, refer to your distribution or operating system documentation for more information. Find more information on the library at the Py4J homepage.
Running a test script
Then, launch Gaia Sky, download this script, open a terminal window (PowerShell in Windows) and run:
$ python asteroids-tour.py
The directory from which you run the script does not matter. If all goes well Gaia Sky should be showing a nice tour of the asteroids in the DR2 catalog.
Have a look at the script. All lines which start with gs.
are API calls which call methods in the Gaia Sky gateway server. What are API calls, you ask? See next section.
The scripting API
The scripting API is a set of methods which may be called from Python scripts to interact with Gaia Sky. The available methods differ depending on the version of Gaia Sky.
Using the API remotely
Gaia Sky provides a REST server that enables the remote execution of API calls over HTTP. This is described in the REST server section.
API documentation
The only up-to-date API documentation for each version is in the interface header files themselves. Below is a list of links to the different APIs.
Writing scripts for Gaia Sky
Gaia Sky uses the single-threaded model of Py4J.
In order to connect to Gaia Sky from Python, import ClientServer
and JavaParameters
, and then
create a gateway and get its entry point. The entry point is the object you can use to call
API methods on. Since Gaia Sky uses a server per script, the gateway must be shut down at the end of the script
so that the Python program can terminate correctly and Gaia Sky can create a new server to deal with further
scripts listening to the Py4J port.
from py4j.clientserver import ClientServer, JavaParameters
gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True))
gs = gateway.entry_point
# User code goes here
[...]
gateway.shutdown()
The JavaParameters(auto_convert=True)
is not strictly necessary, but if you don’t use it you need
to convert Python lists to Java arrays yourself before calling the API.
Now, we can start calling API methods on the object gs
.
# Disable input
gs.disableInput()
gs.cameraStop()
gs.minimizeInterfaceWindow()
# Welcome
gs.setHeadlineMessage("Welcome to the Gaia Sky")
gs.setSubheadMessage("Explore Gaia, the Solar System and the whole Galaxy!")
[...]
Find lots of example scripts here.
Backing up and restoring settings
Typically, scripts modify various program settings when they run (camera speed, star brightness, field of view, etc.). In order to leave Gaia Sky in the state it was before, scripts have the option to back up and restore the entire settings state of Gaia Sky. To do that, the API includes a few calls to push and pull settings states from an internal LIFO stack:
backupSettings() — push the current settings state to the settings stack.
restoreSettings() — restore the top-most settings state from the settings stack so that they become immediately effective. This call re-initializes the user interface of Gaia Sky, so be aware that the UI will be reset.
clearSettingsStack() — clears the settings stack. Calling restoreSettings() after this will have no effect.
These calls can be used at the start and end of scripts to back up and restore the user settings, so that everything is left unchanged after a script execution.
from py4j.clientserver import ClientServer, JavaParameters
gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True))
gs = gateway.entry_point
# 1. Back up settings before anything
gs.backupSettings()
# 2. Script does things and modifies the settings
[...]
# 3. Restore the settings backed up at point 1.
gs.restoreSettings()
gateway.shutdown()
Logging to Gaia Sky and Python
When printing messages, you can either log to Gaia Sky or print to the standard output of the terminal where Python runs:
gs.print("This goes to the Gaia Sky log")
print("This goes to the Python output")
In order to log messages to both outputs, you can define a function which takes a string and prints it out to both sides:
def pprint(text):
gs.print(text)
print(text)
pprint("Hey, this is printed in both Gaia Sky AND Python!")
Method and attribute access
Py4J allows accessing public class methods but not public attrbiutes. In case you get objects from Gaia Sky, you can’t directly call public attributes, but need to access them via public methods:
# Get the Mars model object
body = gs.getObject("Mars")
# Get spherical coordinates
radec = body.getPosSph()
# DO NOT do this, it crashes!
gs.print("RA/DEC: %f / %f" % (radec.x, radec.y))
# DO THIS instead
gs.print("RA/DEC: %f / %f" % (radec.x(), radec.y()))
Strict parameter types
Please, be strict with the parameter types. Use floats when the method signature has floats and integers when it has integers. The scripting interface still tries to perform conversions under the hood but it is better to do it right from the beginning. For example, for the API method:
double[] galacticToInternalCartesian(double l, double b, double r);
may not work if called like this from Python:
gs.galacticToInternalCartesian(10, 43.5, 2)
Note that the first and third parameters are integers rather than floating-point numbers. Call it like this instead:
gs.galacticToInternalCartesian(10.0, 43.5, 2.0)
Loading datasets from scripts
Gaia Sky supports data loading from scripts using the STIL data provider. It is really easy to load a VOTable file from a script:
from py4j.clientserver import ClientServer, JavaParameters
gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True))
gs = gateway.entry_point
# Load dataset
gs.loadDataset("dataset-name", "/path/to/dataset.vot")
# Async insertion, let's make sure the data is available
gs.sleep(2)
# Now we can play around with it
gs.hideDataset("dataset-name")
# Show it again
gs.showDataset("dataset-name")
# Shutdown
gateway.shutdown()
Find an example of how to load a star catalog from a script here. This one showcases how to load a dataset with generic particles (only positions).
Additionally, you can also load JSON data files and dataset descriptors made for Gaia Sky (see the JSON dataset format section).
Synchronizing with the main loop
Sometimes, when updating animations or creating camera paths, it is necessary to
sync the execution of scripts with the thread which runs the main loop (main thread).
However, the scripting engine runs scripts in separate threads asynchronously,
making it a non-obvious task to achieve this synchronization.
In order to fix this, a new mechanism has been added in Gaia Sky 2.0.3
. Now, runnables
can be parked so that they run at the end of the update-render processing of each loop
cycle. A runnable is a class which extends java.lang.Runnable
, and implements
a very simple public void run()
method.
Runnables can be posted, meaning that they are run only once at the end fo the current cycle, or parked, meaning that they run until they stop or they are unparked. Parked runnables must provide a name identifier in order to be later accessed and unparked.
Let’s see an example of how to implement a frame counter in Python using py4j
:
from py4j.clientserver import ClientServer, JavaParameters, PythonParameters
class FrameCounterRunnable(object):
def __init__(self):
self.n = 0
def run(self):
self.n = self.n + 1
if self.n % 30 == 0:
gs.print("Number of frames: %d" % self.n)
class Java:
implements = ["java.lang.Runnable"]
gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True),
python_parameters=PythonParameters())
gs = gateway.entry_point
# We park a runnable which counts the frames and prints the current number
# of frames every 30 of them
gs.parkRunnable("frame_counter", FrameCounterRunnable())
gs.sleep(15.0)
# We unpark the frame counter
gs.unparkRunnable("frame_counter")
gateway.shutdown()
In this example, we park a runnable which counts frames for 15 seconds. Note that here we need
to pass a PythonParameters
instance to the ClientServer
constructor.
A more useful example can be found here. In this one, a polyline is created between the Earth and the Moon. Then, a parked runnable is used to update the line points with the new positions of the bodies. Finally, time is started so that the bodies start moving and the line positions are updated correctly and in synch with the main thread.
Camera and scene runnables
The Gaia Sky main loop updates first the camera position and orientation, and then updates the objects in the scene. In order to maintain sufficient precision, the scene is floated at the position of the camera, meaning that the camera is always effectively at the origin of coordinates, and the scene objects are moved around. This means that the effective position of every objects in the scene at every frame depends on the position of the camera.
So far, we have seen the parkRunnable()
method, which parks a runnable that runs only after the camera-scene update cycle. However, sometimes we need to modify the positions of objects in the scene with respect to other objects. If we use the current method, we will always be using the position in the last frame. However, we need to use the position in the current frame, and we can do so by introducing two new park methods:
parkCameraRunnable()
— parks a runnable that runs after the camera has updated, but before the scene has done so. Use this to fetch the predicted position of an object to have the position in the current frame. (seefetchPredictedPosition()
).parkSceneRunnable()
— parks a runnable that runs after the camera and scene have updated. This is exactly the same as theparkRunnable()
we already know.
An example of this can be found here. It needs a couple of JSON data files (also in the repository).
Overriding object coordinates provider
The positions of most objects in Gaia Sky are computed internally using coordinate providers. It is possible to override the coordinate providers of objects and implement your own in Python. This way of setting the position of an object is the best way to ensure internal consistency and overall system stability. When the coordinates provider is overriden, the user code runs naturally during the scene graph update stage.
To implement a coordinates provider and set it to an object, you first need to create a class that implements IPythonCoordinatesProvider
, and submit it to Gaia Sky via the API call setObjectCoordinatesProvider(name, provider)
. The provider class needs to have a getEquatorialCartesianCoordinates(self, julianDate, outVector)
method, which you need to implement. In it, you need to compute the coordinates of your object for the given Julian date (double-precision floating point number), in the internal reference system and units, and put the result in outVector
, using the method outVector.set(x, y, z)
.
Here is an example:
from py4j.clientserver import ClientServer, JavaParameters, PythonParameters
from py4j.java_collections import ListConverter
import os
# This is the coordinates provider class.
# It implements the method getEquatorialCartesianCoordinates().
class MyCoordinatesProvider(object):
def __init__(self, gateway):
self.gateway = gateway
self.gs = gateway.entry_point
self.converter = ListConverter()
self.km_to_u = self.gs.kilometresToInternalUnits(1.0)
self.pc_to_u = self.gs.parsecsToInternalUnits(1.0)
def getEquatorialCartesianCoordinates(self, julianDate, outVector):
# Here we need internal coordinates.
x_km = 150000000 * self.km_to_u
z_km = 200000000 * self.km_to_u
v = [x_km, (julianDate - 2460048.0) * 100.0, z_km]
# We need to set the result in the out vector.
outVector.set(v[0], v[1], v[2])
return outVector
def toString():
return "my-coordinates-provider"
class Java:
implements = ["gaiasky.util.coord.IPythonCoordinatesProvider"]
gateway = ClientServer(java_parameters=JavaParameters(auto_convert=True),
python_parameters=PythonParameters())
gs = gateway.entry_point
# Load test star system.
gs.loadDataset("Test star system", os.path.abspath("./particles-body-coordinates.json"))
# Set coordinates provider.
provider = MyCoordinatesProvider(gateway)
gs.setObjectCoordinatesProvider("Test Coord Star", provider)
gs.startSimulationTime()
gs.setCameraFocus("Test Coord Star")
print("Coordinates provider set.")
input("Press a key to finish...")
gs.stopSimulationTime()
# Clean up before shutting down, otherwise Gaia Sky will crash
# due to the closed connection.
gs.removeObjectCoordinatesProvider("Test Coord Star")
gs.removeDataset("Coordinates test system")
gs.sleep(2.0)
gateway.shutdown()
You can find this script, along with the necessary JSON data file, here.
More examples
As we said, you can find more examples in the scripts
folder in the
repository.
Running and debugging scripts
In order to run scripts, you need a Python interpreter with the
python-py4j
module installed in your system.
Load up Gaia Sky, open a new terminal window and run your script:
$ python script.py
Please, note that Gaia Sky needs to be running before the script is started for the connection to succeed.
To debug a script in the terminal using pudb
run this:
$ python -m pudb script.py