Cloud Computing with SAOImageDS9: $geturl()for full documentation on Analysis Files, please see Analysis
Table of Contents
Introduction
Analysis menu digest
Information about examples used in this doc
Detailed Examples
Summary
SAOImageDS9 (DS9) can be used as an interface to cloud computing resources. Another way of saying this is that DS9 can run analysis programs remotely and receive results in many different formats: text, images, plots, and regions.
This is made possible by using the $geturl() Analysis Command macro. This macro will “call” the specified URL with whatever parameters have been supplied and will display the results.
The key is that DS9 recognizes some application specific data types that allows it to process specialized results.
In this document we assume the reader has some cursory understanding of developing web services. Users should have some understanding of Content-Type’s (used interchangeably with MIME types), HTTP GET versus POST requests, and URL syntax. An even more cursory knowledge of Tcl/Tk and XPA is helpful.
The DS9 Analysis command syntax is specified in the Reference Manual. The basics are as follows:
The Analysis commands are written in an text file. The unofficial standard is to name the file with the .ans suffix, though any legal filename/suffix can be used.
Each command uses 4 lines. For example
My Analysis Task * menu my_awesome_command $parameters | $textBreakdown:
The name of the analysis task. For example My Analysis Task
The types of files the analysis task can be used with. This is based on the file name, not the content of the file. * is a wildcard meaning all file names. If multiple file names are expected they are separated with spaces. For example *evt* *EVT* would work with file names that contain the string evt or EVT. File names are case sensitive.
Whether this analysis task should be added to the Analysis menu or whether it should be bound (ie bind) to a Tk event. For example bind m would run the analysis task when the m button is pressed.
The actual analysis command. This can include parameters such as Analysis macros: $filename or $regions. It may also include user parameters (more on this below). The output from the command is then sent to a particular output based on where it is piped, |. $text will send the output from the command to a text box. $image will send the results to the imager.
User settable parameters can be specific using the param section:
param my_pars par1 entry {Hello World} {} {Enter your message in this box} par2 menu {Favorite Color} {red|green|blue|green|black} {Select your fav. color} endparamand then used in the analysis task like this:
My Parameters * menu $param(my_pars); my_command $par1 $par2 | $textThis is just a quick introduction. There are many more details and options described in the documentation.
Information about examples used in this doc
The details about setting up a production ready web server and writing robust web services is beyond the scope of this document.
The basic requirement is that whatever web service is being used it has to accept an HTTP/S GET request where the URL contains both the location of the web resource as well as any processing parameters. For example
http://my_awesome.service.com/my_tool?par1=foo&par2=bar
GET differs from POST in that with GET the parameters are included in the URL whereas POST parameters are not. POST services allows for things like file upload which DS9 does not support.
In this document we will be using the Flask Python based web service framework to run a local web server. By default it runs on the local machine on port 5000, ie IP address 127.0.0.1:5000.
The nature of the web server setup is up to the user. It could be a CGI script written in any language (C/C++, Python, Shell, Perl, etc) or a RESTful service like the examples below. The only requirement is that the service has to accept/process GET requests.
This section demonstrates several different remote analysis tasks. It includes a simple example of how to setup the flask web service and the DS9 Analysis Command file for various types of analysis tasks which return different types of outputs.
Getting Started: Hello World
Let’s create a simple file called hello_world.py
from flask import Flask, make_response app = Flask(__name__) @app.route("/") def hello_world(): retval = "Hello, World!" response = make_response(retval) response.mimetype = "text/plain" return responseThe first two lines are common to all the examples below.
We can then start a local web server running on http://127.0.0.1:5000
flask --app hello_world runNext we crate the DS9 analysis command file hello_world.ans
Hello World * menu $geturl(http://127.0.0.1:5000/) | $textand then run ds9
ds9 -analysis hello_world.ans(Or go to Analysis -> Load Analysis Commands and select the file hello_world.ans.)
Then go to Analysis -> Hello World which will open a text box displaying the message: Hello World.
The remainder of the examples in this document will follow a similar pattern where we have to write some code to be run on the web server, and add an option to the DS9 analysis file to execute that code.
Passing parameters
Users should be familiar with URL’s that look like
https://linux.is.awesome.edu/some_command?this=1&that=the_other
Where some_command is the web access point which is taking some parameters: this and that. These are identified in the URL as they follow the question mark (“?”) and are separated by ampersands (“&”). They take the form
parameter name = value
This is how HTTP/S “GET” requests are encoded. HTTP/S “PUT” requests are processed differently; however, ds9 only supports “GET” requests.
It will often be the case that these parameters will be set via the DS9 Analysis menu syntax using in the param section.
Below is a simple example that will just print back to the user the text they supply when prompted for the parameter:
Simple, Single Parameter
from flask import request @app.route("/simple_parameter/") def simple_parameter(): user_text = request.args.get("text") retval = f"The user wrote '{user_text}'" response = make_response(retval) response.mimetype = "text/plain" return responsethe request.args.get method is used to retrieve specific parameters by name. Here the web service end-point simple_parameter is expecting to find a parameter named text.
The DS9 Analysis File for this example looks like:
param simpletxt usr_txt entry {Enter some text} {} {This text will be echoed back} endparam Simple Text * menu $param(simpletxt); $geturl(http://127.0.0.1:5000/simple_parameter/?text=$usr_txt) | $textThe the param section identifies a new parameter section: simpletxt. It has a single parameter named usr_txt, which is a text entry box. In the $geturl macro we see that we have used the “?” to identify that there are parameters and added a single parameter text=$usr_txt. When this analysis task is invoked from the menu, it will open a window asking the use the Enter some text. Upon submission, a text box will open with the message The user wrote ‘blah’ where blah is whatever the user wrote.
Multiple parameters
Multiple parameters are handled in the same way. They are simply separated by ampersands.
It is beyond the scope of this document, but users should be aware that certain characters have special encoding when present in a GET URL. This includes question marks, ampersands, and spaces.
Below is the python bit that can read multiple parameter values
@app.route("/multi_parameter/") def multi_parameter(): retval = "Found the following parameters\n" for key in request.args: retval += f"{key} = {request.args[key]}\n" response = make_response(retval) response.mimetype = "text/plain" return responseThis bit of code just loops over whatever parameters are present in the URL. It will print the key = value pairs in a text window.
The DS9 Analysis bit looks like:
param multiparam usr_txt2 entry {Enter some text} {} {This text will be echoed back} func menu {Function} min|max|median {An example of a select box} check checkbox {Yes/No} 1 {Checked is 1, Unchecked is 0} endparam Multi Params * menu $param(multiparam); $geturl(http://127.0.0.1:5000/multi_parameter/?text=$usr_txt2&func=$func&par3=$check) | $textWhere here again we have defined a new set of parameters in the param section.
You cannot use the same parameter name in different parameter sections. It can result in unpredictable results. Use unique parameter names in each param section.
The $geturl macro then has the parameters encoded using ampersand seperated key=value pairs.
X-XPA/XPASET mime-type
HTTP/S allows content creators to use custom Content-Type’s for specialized “media” types. The convention is to preface the custom type with an x-. DS9 recognizes the x-xpa/xpaset content type. With this Content-Type services can send xpaset commands to DS9.
The X-XPA/XPASET requires two parameters:
target : this is the XPA access point to send the commands to. Note that setting this to simply “ds9” will send the same XPA command to all instances of DS9 that are running. To specify a specific instance of DS9, ie the one that actually sent the $geturl request, you need to use the $xpa_method macro.
paramlist : this is the actual XPA command to run.
Additional data needed by the xpaset command should then be included in the body of the response.
This example shows how to create a region and return it back to DS9
@app.route("/region/") def send_region(): target = request.args.get("target") retval = "physical; circle(4096.4,4096.5,100)" response = make_response(retval) response.headers['Content-Type'] = f'x-xpa/xpaset; target="{target}"; paramlist="regions -format ds9"' return responsewould be the equivalent of running
echo "physical; circle(4096.4,4096.5,100)" | xpaset $target regions -format ds9
from the command line. That is the data that on the command line would be input via stdin, is included in the body of the response.
The DS9 analysis command looks like:
Simple Region * menu $geturl(http://127.0.0.1:5000/region/?target=$xpa_method) | $textDS9 cannot accept xpaget requests. get requests requires two way communication with the application. The $geturl requires that all data and parameters that the service requires be supplied in the $geturl request itself.
Multi-part mime-type
Some services may need to send back information in more than one xpaset request. To accommodate this, DS9 recognizes the multipart/mixed Content-Type. While this is common when dealing with email messages, it is not common for HTTP/S. However it is perfectly legal/valid.
The syntax for a multipart/mixed message includes that as the Content-Type, and includes a boundary string that is used to separate the content of each embedded message. The boundary should be a unique character string that is guaranteed not to be part of the body of any of the embedded messages.
Below is a simple example that runs 3 xpaset commands
- xpaset -p $target cmap bb to set the color map to “bb”
- xpaset -p $target scale log to change to log scaling
- xpaset -p $target scale limits 0 10 to change the scale limits to go from 0 to 10.
BOUNDARY = "xpamime:142857:285714:428571:571428:714285:857142" @app.route("/cmap/") def multi_cmds(): def wrap_cmd(cmd): retval = f'--{BOUNDARY}\n' retval += f'Content-Type: x-xpa/xpaset; target="{target}"; paramlist="{cmd}"\n\n' return retval target = request.args.get("target") cmd1 = "cmap bb" cmd2 = "scale log" cmd3 = "scale limits 0 10" retval = wrap_cmd(cmd1) retval += wrap_cmd(cmd2) retval += wrap_cmd(cmd3) retval += f'--{BOUNDARY}' response = make_response(retval) response.mimetype = f'multipart/mixed; boundary="{BOUNDARY}"' return responseWhen the boundary is used it is prefixed with two dashes, --
The last command must be followed by a trailing boundary line.
The DS9 analysis command menu syntax looks like:
Multi Commands * menu $geturl(http://127.0.0.1:5000/cmap/?target=$xpa_method) | $textNote that these commands are completely independent. Setting the color map, changing the scaling, and changing the scale limits are independent actions. Each command in a multipart/mixed message is independent. They are processed in order but do not have to be related. Also note that none of these commands require any additional data; that is the body of each part if blank. (This is the two new-line characters, \n, after the paramlist.)
Returning a plot
Plots can be created using the XPA plot command.
However plot commands must be wrapped inside a multipart/mixed content.
In this example we plot X vs X^2 for x in the range from 0 to 9 and then modify the plots appearance with additional xpa commands:
@app.route("/plot/") def send_plot(): def make_plot(xx,yy): vals = [f"{x} {y}" for x, y in zip(xx, yy)] plot_cmd = 'plot line {My Title } {X Label} {Y Label } xy' retval = f'--{BOUNDARY}\n' retval += f'Content-Type: x-xpa/xpaset; target="{target}"; paramlist="{plot_cmd}"\n\n' retval += "\n".join(vals)+"\n" plot_cmd = 'plot line smooth step' retval += f'--{BOUNDARY}\n' retval += f'Content-Type: x-xpa/xpaset; target="{target}"; paramlist="{plot_cmd}"\n\n' plot_cmd = 'plot background lightblue' retval += f'--{BOUNDARY}\n' retval += f'Content-Type: x-xpa/xpaset; target="{target}"; paramlist="{plot_cmd}"\n\n' retval += f'--{BOUNDARY}\n' return retval target = request.args.get("target") xx = list(range(10)) yy = [x*x for x in xx] retval = make_plot(xx, yy) response = make_response(retval) response.mimetype = f'multipart/mixed; boundary="{BOUNDARY}"' return responseNote that Python (sometimes) will treat curly brackets special and so does Tcl. In Python you may sometimes need to used double curly brackets when you want to pass a single curly bracket to Tcl. In this example since the plot_cmd variable does not do any formatting so we need to used single curly brackets. In Tcl, strings that contain spaces need to be wrapped in curly brackets.
The DS9 analysis command for this is:
Simple Plot * menu $geturl(http://127.0.0.1:5000/plot/?target=$xpa_method) | $textNote: It is possible to send bare plots (just values, no title, no labels, all default plot options) using the $plot macro (without any options). In this case the values can be returned as just a simple text/plain Content-Type.
Returning images
Returning an image can be a bit more complicated.
Simple case: Single images with properly configured web server
In the most simple case, if the web service is only going to return a single image then rather than using $geturl() you should just use $url() which is piped into the $image macro.
In this example we have a local file: /export/img.fits that we want to return via the /get_img end point. We can do something like this:
@app.route("/get_img") def get_img(): retval = open("/export/img.fits", "rb").read() response = make_response(retval) response.headers['Content-Type'] = "image/fits" return responseIt is important that the FITS image is read in as a pure binary/byte array (the b in the rb mode). Equally important is that the Content-Type has been set to the standard image/fits mime-type used for FITS images. If the Content-Type is not properly set to image/fits then the results are unpredictable.
Then in the DS9 analysis file the command looks like:
Basic Image * menu $url(http://127.0.0.1:5000/get_img) | $image(new)$url is used instead of $geturl. The output is piped to the $image(new) macro which will create a new frame to load the image.
The URL passed to $url can contain the same GET encoded syntax as shown before, eg
$param(something); $url(https://awesome.edu/endpoint?par1=$par1&par2=$par2) | $imageUsing fits XPA access point.
The second technique to load an image is to use the fits XPA access point. This is necessary if there are multiple images to be returned (eg if doing a 2D fit which requires sending back separate images for the model and residual). It can also be useful if the output from the service contains multiple different outputs such as an Image and a Region to draw on that image.
The trick is that the raw, binary FITS image cannot be used directly. The image must be gzip’ed and it must be encoded into ASCII using base64 encoding.
This is an example of how to convert the binary FITS file to ASCII:
def _convert_image(infile): 'Load image, gzip, and base64 encode it' import gzip import io import base64 import textwrap # Load the image as raw bytes with open(infile, 'rb') as fp: img_data = fp.read() # Compress the image in memory compressed_buffer = io.BytesIO() with gzip.GzipFile(fileobj=compressed_buffer, mode="wb") as fp: fp.write(img_data) # Encode compressed image w/ base64 compressed_buffer.seek(0) # rewind img_base64 = base64.b64encode(compressed_buffer.getvalue()) # Format into 80 chars wide retval = textwrap.fill(img_base64.decode('ascii'), width=80) return retvalThe data are read-in as binary data. The data are then compressed, in memory, using gzip. The compressed data are then base64 encoded to ASCII. Then finally the ASCII is formatted to be 80 characters wide, which is generally standard for base64 messages.
We can use this conversion routine to setup our image server end point. Just as with the Plot example, the fits commands have to be wrapped inside a multipart/mixed container. This technique requires setting Content-Transfer-Encoding: base64 in the header.
@app.route("/image/") def send_image(): def multipart_wrap(image): retval = f'--{BOUNDARY}\n' retval += f'Content-Type: x-xpa/xpaset; target="{target}"; paramlist="fits new sample_img.fits.gz"\n' retval += 'Content-Transfer-Encoding: base64\n\n' retval += image retval += f'\n--{BOUNDARY}\n' return retval target = request.args.get("target") image = _convert_image("/export/img.fits") retval = multipart_wrap(image) response = make_response(retval) response.mimetype = f'multipart/mixed; boundary="{BOUNDARY}"' return responseIn this example we used the fits new command so that the image will be loaded into a new frame.
The DS9 analysis command looks like:
Complex Image * menu $geturl(http://127.0.0.1:5000/image/?target=$xpa_method) | $textUsing this technique users can return multiple images (eg if needed for RGB, HSV or HLS frames) or images and associated regions, etc.
Download an arbitrary file
This technique uses XPA to control the built in web browser to download an arbitrary file. The browser window will open and then close when the download is complete. This requires two different end-points to our service
- The end point URL that is called from DS9 to initiate the download, similar to our other examples. This first URL will have the DS9 browser open the 2nd URL.
- The end point that serves the file.
@app.route("/web/") def send_web(): target = request.args.get("target") retval = f'--{BOUNDARY}\n' retval += f'Content-Type: x-xpa/xpaset; target="{target}"; paramlist="web http://127.0.0.1:5000/get_file"\n\n' retval += f'--{BOUNDARY}\n' retval += f'Content-Type: x-xpa/xpaset; target="{target}"; paramlist="web close"\n\n' retval += f'--{BOUNDARY}\n' response = make_response(retval) response.mimetype = f'multipart/mixed; boundary="{BOUNDARY}"' return response @app.route("/get_file") def get_file(): import os infile = "/export/img.fits" fname = os.path.basename(infile) retval = open(infile, "rb").read() response = make_response(retval) response.headers['Content-Type'] = f"Content-Type: application/octet-stream;name={fname}" return responseThis first URL end point, /web is what DS9 will be use in the analysis menu, ie
Download File * menu $geturl(http://127.0.0.1:5000/web/?target=$xpa_method) | $textThe /web endpoint then makes two XPA calls. The first call is to open the web browser and load the second URL endpoint, /get_file. The /get_file end point then open a file and reads it into memory as raw bytes (the b in rb). It then serves the file with Content-Type as application/octet-stream. This MIME type is used to convey an arbitrary data file which will cause the DS9 browser to prompt the user to save the file. The file name name={fname} will be the default name. After the file is saved then the /web endpoint closes the web browser window. For small files, served locally the browser window may just “flash” on the screen for an instance.
This technique could be used to return any arbitrary results including thing such as PDF files, non-image FITS files (ie FITS tables), or any random file that is not intended to be “consumed” by DS9 directly.
This document describes how to use DS9 as an interface to cloud computing resources. It demonstrates how to handle various types of returned information including text, regions, plots, images, and arbitrary data files. It provides some guidance with respect to best practices when writing the cloud based applications.