Scripting Guide¶
This guide explains many common tasks that you might want to do using scripting. It assumes you are familiar with the Python programming language and with the basic functionality of Nion Swift.
For a developer overview, including a description of basic components, see Scripting Concepts.
For detailed class and method references, see API Architecture.
The examples below work in the Python Console inside of Nion Swift.
If you are developing scripts using instrumentation and acquisition, refer to Nion Swift Instrumentation.
Contents
Opening the Python Console¶
Nion Swift includes an interactive Python Console for basic scripting. You can access a Python Console window using the menu item File > Python Console
(Ctrl-K on Windows/Linux; Cmd-K on macOS).
Once you have a Python Console, you can enter Python code in the interactive console. The Python Console automatically configures the api
variable for you.
>>> window = api.application.document_windows[0]
>>> len(window.library.data_items)
126
Accessing a Specific Display Item¶
Nion Swift makes a distinction between data items (the data arrays) and display items (the objects displaying the data). Often there is a 1:1 association between the two, but sometimes a data item will have multiple display items associated with it, and sometimes a display item will be displaying multiple data items simultaneously.
The workspace area is split into multiple display panels, each of which shows either a display item or a browser.
You may want to access a specific display item in a display panel. To do this, you assign a variable representing the display item.
You do this by clicking either on the display panel containing the display item or on the display item within the data panel list.
After you have selected the desired display item, press Ctrl+Shift+K (Windows/Linux) or Cmd+Shift+K (macOS). When you do this, Nion Swift will show the variable in the title of the display item shown in the display panel header; it will also print the variable in any Python Console windows that are open at the time.
>>> r01 = api.library.get_item_by_specifier(api.create_specifier(uuid.UUID("647feb15-8407-4693-8b9d-0cec90e94b7c")))
>>> numpy.amin(r01.data)
The first line above will be added automatically when you press Ctrl/Cmd+Shift+K. Once it appears, you can type the second line, substituting the actual r-variable for r01
.
The r-variables reference display items. Display items will often contain a single data item that can be referenced with the data_item
property. For convenience, xdata
, data
, and metadata
properties are available on display items. They return the parallel properties of the data_item
property if it exists or else they return None
.
Accessing the Target Data Item¶
In Nion Swift, when you select a display panel by clicking on its content and it becomes the target. The display panel will show a blue outline when it the target. You can access the target data item from scripts. First, click on a display panel that is showing an image. Now open a Python Console and type the following.
>>> window = api.application.document_windows[0]
>>> data_item = window.target_data_item
>>> data_shape = data_item.data.shape
>>> data_shape
(2048, 2048)
Now click on a different display panel (or drag a new Data Item into the current display panel).
>>> data_item = window.target_data_item
>>> data_shape = data_item.data.shape
>>> data_shape
(512, 128)
Changing Data in a Data Item¶
You overwrite the data in a data item. This example assumes that you have an existing data_item
that you wish to overwrite with new numpy
data.
>>> data_item.data = numpy.random.randn(16, 32)
You may want to change just part of the data in a data item without rewriting the entire data.
>>> with api.library.data_ref_for_data_item(data_item) as data_ref:
... data_ref[10:20, 10:20] = numpy.random.randn(10, 10)
...
Notice that you are assigning new data to a slice of the data_ref
, not assigning to data
as in the previous example.
Warning
From scripts, there is no protection against changing data. Changing data will permanently overwrite any old data. We recommend using caution with scripts that write to the target data item since the user may inadvertently choose a data item as the target which contains data that cannot be recovered.
Creating and Displaying a New Data Item¶
You can create a new data item and display it in an empty display panel. This example creates a new numpy
array, creates a new data item using the data, and displays it in the current workspace. Before running this script, if there are no empty display panels, you can right/control click on an existing display panel and choose the menu item Clear Display Panel Contents
to provide space in which the new data item can be placed.
The quick form (available in the console):
>>> data = numpy.random.randn(16, 32)
>>> show(data)
The most general form:
>>> window = api.application.document_windows[0]
>>> data = numpy.random.randn(16, 32)
>>> data_item = api.library.create_data_item_from_data(data)
>>> display_panel = window.display_data_item(data_item)
Note
If there is no empty display panel, the data item will not be displayed immediately and display_data_item
will return None
.
Creating a Data Item with Calibrated Dimensions¶
You can set a data item’s calibration. The API provides a create_calibration
method where the offset, scale, and unit name are specified.
>>> window = api.application.document_windows[0]
>>> data = numpy.random.randn(16, 32)
>>> data_item = api.library.create_data_item_from_data(data)
>>> intensity_calibration = api.create_calibration(offset=0.0, scale=4.0, units='counts')
>>> dimensional_calibration_0 = api.create_calibration(0.0, 10, 'µm')
>>> dimensional_calibration_1 = api.create_calibration(0.0, 19, 'µm')
>>> dimensional_calibrations = [dimensional_calibration_0, dimensional_calibration_1]
>>> data_item.set_intensity_calibration(intensity_calibration)
>>> data_item.set_dimensional_calibrations(dimensional_calibrations)
>>> show(data_item)
The calibration objects transform their values like this: x' = x * scale + offset
.
Adding Two Data Items¶
Assuming you have two data items of the same size, you can add them together and display the result by following these steps.
Click on each data item you want to add and assign an r-variable by pressing Ctrl/Cmd-Shift-K on each one. The r-variable will appear in the title of the data item, such as “My Data Item (r522)”.
r522
is the r-variable.Make sure you have an empty display panel by right/control clicking on one of the display panels and choose
Clear Display Panel Contents
from the menu.Open a script window (Ctrl/Cmd-K).
Write the follow script, substituting the r-variables assigned in step #1 for
r001
andr002
.>>> window = api.application.document_windows[0] >>> data = r001.data + r002.data >>> data_item = api.library.create_data_item_from_data(data) >>> show(data_item)
The new added data should be displayed in the display panel you freed up in step 2 or another free display panel.
Note
Nion Swift has the ability to configure live computations. In this case, though, the computation is not live. A description of how to set up a live computation is in the user’s guide.
Working with Extended Data¶
In the code snippets above, data items have been treated as having numpy
data. However, Nion Swift actually stores data in extended data structures (also called data and metadata and sometimes abbreviated as xdata).
- Extended data combines the following components:
The
numpy
compatible data array.Dimensional and intensity calibrations
Description of each of the dimensions (sequence, collection, datum)
Timestamps
Provenance/history (future feature)
>>> window = api.application.document_windows[0] >>> data = numpy.random.randn(16, 32) >>> intensity_calibration = api.create_calibration(offset=0.0, scale=4.0, units='counts') >>> dimensional_calibration_0 = api.create_calibration(0.0, 10, 'µm') >>> dimensional_calibration_1 = api.create_calibration(0.0, 19, 'µm') >>> dimensional_calibrations = [dimensional_calibration_0, dimensional_calibration_1] >>> xdata = api.create_data_and_metadata(data, intensity_calibration=intensity_calibration, ... dimensional_calibrations=dimensional_calibrations) ... >>> data_item = api.library.create_data_item_from_data_and_metadata(xdata)
Extended data also describes the usage of each dimension. Extended data can represent a sequence of data, a collection of data, and data with one or more datum dimensions. Extended data in Nion Swift is always organized with the sequence index (if any) in the first index, followed by the collection indexes, followed by the datum indexes.
For instance, a regular 2d visual image would be described as having two datum dimensions.
A scanned image might be represented as having 2 collection dimensions and only a scalar datum dimension or as having two datum dimensions.
A movie would be be described as being a sequence of two datum dimensions.
A spectrum image would be described as having two collection dimensions and a single datum dimension.
>>> spectrum_data = numpy.random.randn(2048)
>>> spectrum_data_descriptor = api.create_data_descriptor(is_sequence=False, collection_dimension_count=0, datum_dimension_count=1)
>>> spectrum_xdata = api.create_data_and_metadata(data, data_descriptor=spectrum_data_descriptor)
>>> image_data = numpy.random.randn(480, 640)
>>> image_data_descriptor = api.create_data_descriptor(is_sequence=False, collection_dimension_count=0, datum_dimension_count=2)
>>> image_xdata = api.create_data_and_metadata(data, data_descriptor=image_data_descriptor)
>>> movie_data = numpy.random.randn(1000, 480, 640)
>>> movie_data_descriptor = api.create_data_descriptor(is_sequence=True, collection_dimension_count=0, datum_dimension_count=2)
>>> movie_xdata = api.create_data_and_metadata(data, data_descriptor=movie_data_descriptor)
>>> line_spectrum_data = numpy.random.randn(500, 2048)
>>> line_spectrum_data_descriptor = api.create_data_descriptor(is_sequence=False, collection_dimension_count=1, datum_dimension_count=1)
>>> line_spectrum_xdata = api.create_data_and_metadata(data, data_descriptor=line_spectrum_data_descriptor)
>>> line_2d_data = numpy.random.randn(500, 1024, 1024)
>>> line_2d_data_descriptor = api.create_data_descriptor(is_sequence=False, collection_dimension_count=1, datum_dimension_count=2)
>>> line_2d_xdata = api.create_data_and_metadata(data, data_descriptor=line_2d_data_descriptor)
>>> si_data = numpy.random.randn(512, 512, 2048)
>>> si_data_descriptor = api.create_data_descriptor(is_sequence=False, collection_dimension_count=2, datum_dimension_count=1)
>>> si_xdata = api.create_data_and_metadata(data, data_descriptor=si_data_descriptor)
>>> data_4d = numpy.random.randn(64, 64, 1024, 1024)
>>> data_4d_data_descriptor = api.create_data_descriptor(is_sequence=False, collection_dimension_count=2, datum_dimension_count=2)
>>> data_4d_xdata = api.create_data_and_metadata(data, data_descriptor=data_4d_data_descriptor)
You can get extended from a data item and query its contents with many useful methods. Here are some examples.
>>> xdata = window.target_data_item.xdata
>>> xdata.dimensional_shape
(480, 640)
>>> xdata.data_dtype
dtype('float64')
>>> xdata.is_sequence
False
>>> xdata.collection_dimension_count
0
>>> xdata.datum_dimension_count
2
>>> xdata.intensity_calibration
x 1.0 + None
>>> xdata.dimensional_calibrations
[x 1.0 + None, x 1.0 + None]
>>> r650.xdata.timestamp
datetime.datetime(2016, 5, 26, 17, 11, 41, 918215)
Computations with Extended Data¶
You can do all sorts of computations with extended data. To begin with, you can use basic Python operators.
>>> xdata = xdata1 + xdata2 * xdata3
>>> xdata = -xdata4
You can also import the xdata
library and use the functions in that library. These functions will handle the data descriptions and calibrations properly.
>>> xdata = xd.fft(xdata1)
>>> xdata = xd.gaussian_blur(xdata2, 2.0)
>>> xdata = xd.pick(xdata3, (2, 3))
>>> xdata = xd.column(xdata1.collection_dimension_shape)
For a description of the full xdata
library, see Extended Data Guide.
For a quick description of the available methods or a specific method:
>>> help(xd)
>>> help(xd.fft)
Extracting Display Data from Data Items¶
In addition to the data that a data item stores, you can also access the secondary display data.
Reduced data refers to the original data sliced down to either 2d or 1d data. It has the data type of the original data.
Display data refers to the original data sliced down to either 2d or 1d data and then converted to a scalar or RGB data type. For instance, complex 128 data will have the complex display attribute applied and will result in float 64 data.
>>> window = api.application.document_windows[0]
>>> data_item = window.target_data_item
>>> data_item.xdata.is_sequence
True
>>> xdata.datum_dimension_count
2
>>> data_item.xdata.dimensional_shape
(60, 1024, 1024)
>>> data_item.xdata.data_dtype
dtype('complex128')
>>> data_item.display_xdata.is_sequence
False
>>> data_item.display_xdata.dimensional_shape
(1024, 1024)
>>> data_item.display_xdata.data_dtype
dtype('float64')
Display data can be useful when you want to operate on the data that is displayed. For instance, a line profile works with the display data rather than the original data.
Using Data Item Calibrations¶
There are a few convenience functions for accessing the calibrations of the data item. The intensity_calibration
and dimensional_calibrations
properties both return copies of the data item calibrations.
>>> window = api.application.document_windows[0]
>>> data_item = window.target_data_item
>>> intensity_calibration = data_item.intensity_calibration
>>> intensity_calibration.units
'counts'
>>> calibration_y = data_item.dimensional_calibrations[0]
>>> calibration_x = data_item.dimensional_calibrations[1]
>>> calibration_y.scale
0.11
>>> calibration_y.units
'nm'
You can set the calibrations of the data item too.
>>> window = api.application.document_windows[0]
>>> data_item = window.target_data_item
>>> intensity_calibration = data_item.intensity_calibration
>>> intensity_calibration.units = 'cd' # candela
>>> data_item.set_intensity_calibration(intensity_calibration)
>>> dimensional_calibrations = data_item.dimensional_calibrations
>>> dimensional_calibrations[0].scale = 0.12
>>> data_item.set_dimensional_calibrations(dimensional_calibrations)
You can convert between calibrated and uncalibrated pixels and strings using calibration objects:
>>> c = Calibration.Calibration(3, 5, "nm")
>>> c.convert_to_calibrated_value(20)
103.0
>>> c.convert_to_calibrated_size(20)
100.0
>>> c.convert_to_calibrated_value_str(20)
'103 nm'
>>> c.convert_to_calibrated_size_str(20)
'100 nm'
>>> c.convert_from_calibrated_value(90)
17.4
>>> c.convert_from_calibrated_size(10)
2.0
Note
The convenience functions for accessing data item calibrations work by setting the calibrations on the extended data associated with the data item. Storing new extended data will also change the calibrations. This can have unexpected consequences. For instance, calibrations can be overwritten if a live computation is executed. If you are using the API to perform a custom computation, and using these convenience functions, place them after the code that assigns new data
or xdata
to the target data item.
Using Data Item Created and Modified Timestamps¶
You can read the created
and modified
properties to get the created and modified datetime
objects,
specified in UTC. You can also read the timestamp
property of extended data.
>>> window = api.application.document_windows[0]
>>> data_item = window.target_data_item
>>> data_item.modified.isoformat()
'2017-02-09T05:10:18.427999'
>>> data_item.created.isoformat()
'2017-02-08T17:17:51.795207'
>>> data_item.xdata.timestamp.isoformat()
'2017-02-09T04:19:12.711283'
The created
datetime is never updated. The modified
datetime is updated whenever the data item or data changes. The xdata.timestamp
is updated whenever the data changes.
Finding Sources and Dependents of Data Items¶
The library keeps track of high level connections between data items. For instance, if data item A has a crop applied to it and generates data item B, then A is said to be a source of B and reciprocally B is said to be a dependent of A.
>>> window = api.application.document_windows[0]
>>> data_item = window.target_data_item
>>> dependents = api.library.get_dependent_data_items(data_item)
>>> sources = api.library.get_source_data_items(dependents[0])
>>> data_item is sources[0]
True
Uniquely Identifying Data Items¶
Persistent objects in the library have a unique uuid
identifier which is persistent for the lifetime of the object, even if exiting and relaunching Swift. The uuid
uniquely identifies that object.
>>> window = api.application.document_windows[0]
>>> data_item = window.target_data_item
>>> data_item.uuid
UUID('646bc502-6e8e-4e9f-8ac0-30c124822df3')
Note
The same object with the same uuid
can appear in two different libraries with different properties and data since the user may explicitly copy items between libraries. The uuid
is unique within a single library, however.
Managing Session Metadata¶
Metadata about the current session is stored with the library object and can be edited in the UI using the Session panel. You can access the metadata using Python:
>>> api.library.get_library_value("stem.session.instrument")
Nion UltraSTEM 200keV
>>> api.library.set_library_value("stem.session.microscopist", "Manfred Von Ardenne")
>>> api.library.delete_library_value("stem.session.task")
>>> api.library.has_library_value("stem.session.task")
False
Session Description |
|
---|---|
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
Reading and Writing Data Item Metadata¶
You can also access metadata associated with the data item.
>>> data_item.set_metadata_value("stem.session.site", "Hogwarts School of Witchcraft and Wizardry")
>>> data_item.set_metadata_value("stem.session.microscopist", "Albus Dumbledore")
>>> data_item.get_metadata_value("stem.high_tension_v")
120000
>>> data_item.delete_metadata_value("stem.session.task")
>>> data_item.has_metadata_value("stem.session.task")
False
The tables below show possible metadata keys and their data types.
You may also need to store metadata not defined by the keys below. You can do that using the metadata
property.
>>> metadata_dict = data_item.metadata
>>> metadata_dict.setdefault("astrology", dict())["moon-phase"] = "gibbous"
>>> data_item.set_metadata(metadata_dict)
Any value stored in the metadata
dict
must be convertible to json
, e.g. json.dumps(metadata_dict)
must succeed.
Using the keys has the advantage that when the data item is exported to another file format (such as TIFF), the keys can be used to flatten the metadata
dict
into well defined fields. If you use custom fields, they will only be available as a general metadata
json
string.
In addition, using the keys improves interoperability between applications.
If a key or set of keys should be added, Nion maintains a registry of keys. Please contact us to discuss.
Session Description |
|
---|---|
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
|
string |
STEM Values |
|
---|---|
|
integer |
|
string |
|
real |
|
real |
|
real |
|
real |
|
real |
STEM Data |
|
---|---|
|
string (EELS, EDS, CL, Ronchigram, HAADF, MAADF, BF) |
EELS Values |
|
---|---|
|
string |
|
real |
|
boolean |
Hardware Values |
|
---|---|
|
string |
|
string |
Camera Values |
||
---|---|---|
|
integer |
|
|
string |
|
|
integer |
|
|
string |
|
|
real |
|
|
integer |
high level index. reset when played. |
|
integer |
low level index. reset at application startup. |
|
integer |
|
|
real |
Scan Values |
|
---|---|
|
real |
|
real |
|
string |
|
integer |
|
string |
|
real |
|
real |
|
integer |
|
real |
|
real |
|
string |
|
integer |
Copying a Data Item¶
You may want to copy an existing data item and be able to modify it without affecting the original data item.
There are two ways to copy a data item. The copy technique copies the data item and maintains any live computation attached to the data item. The snapshot technique copies the data item but does not maintain any live computation.
Both copy operations copy the extended data, calibrations, metadata, display, and graphics. Neither operation copies data items dependent the one being copied.
>>> data = numpy.random.randn(16, 32)
>>> data_item = api.library.create_data_item_from_data(data)
>>> data_item_copy = api.library.copy_data_item(data_item)
>>> data_item_snap = api.library.snapshot_data_item(data_item)
>>> numpy.array_equal(data_item_copy.data, data)
True
>>> numpy.array_equal(data_item_snap.data, data)
True
It is also possible to make a new data item by copying only the extended data. This copies the extended data, calibrations, and metadata; but not session data, display, graphics or other items that are associated with the data item but not the extended data.
>>> data_item = api.library.create_data_item_from_data(numpy.random.randn(2, 2))
>>> data_item_copy = api.library.create_data_item_from_data_and_metadata(data_item.xdata)
>>> numpy.array_equal(data_item.data, data_item_copy.data)
True
>>> data_item.metadata == data_item_copy.metadata
True
Copying Metadata from One Data Item to Another¶
You can explicitly copy metadata from one data item to another. This is not recommended to use in production code since it will most likely break in future versions.
>>> data_item = api.library.create_data_item_from_data(numpy.random.randn(2, 2))
>>> data_item_copy = api.library.create_data_item_from_data(numpy.random.randn(2, 2))
>>> data_item_copy.set_intensity_calibration(data_item.intensity_calibration)
>>> data_item_copy.set_dimensional_calibrations(data_item.dimensional_calibrations)
>>> data_item_copy.set_metadata(data_item.metadata)
>>> session_keys = ['stem.session.instrument', 'stem.session.microscopist', 'stem.session.sample', \
... 'stem.session.sample_area', 'stem.session.site', 'stem.session.task']
...
>>> for session_key in session_keys:
... if data_item.has_metadata_value(session_key):
... data_item_copy.set_metadata_value(session_key, data_item.get_metadata_value(session_key))
...
Creating a Graphic to Annotate a Display¶
You can add graphics to annotate an image display item.
>>> data_item = api.library.create_data_item_from_data(numpy.random.randn(20, 20))
>>> p = data_item.display.add_point_region(0.4, 0.6)
>>> p.label = "Marker point"
>>> window = api.application.document_windows[0]
>>> display_panel = window.display_data_item(data_item)
You can also add graphics to annotate a line plot display item.
>>> plot_item = api.library.create_data_item_from_data(numpy.random.randn(32,))
>>> c = data_item2.display.add_channel_region(0.2)
>>> i = data_item2.display.add_interval_region(0.5, 0.6)
See the API documentation for adding other types of graphics such as rectangles, ellipses, lines, etc.
You can tag graphics with an identifier (separate from the label) and later look it up.
>>> data_item = api.library.create_data_item_from_data(numpy.random.randn(20, 20))
>>> p = data_item.display.add_point_region(0.4, 0.6)
>>> p.label = "Marker point"
>>> p.graphic_id = "feature_marker"
>>> pp = data_item.display.get_graphic_by_id("feature_marker")
>>> assert p == pp
You can remove graphics too.
>>> pp = data_item.display.get_graphic_by_id("feature_marker")
>>> if pp:
>>> data_item.display.remove_region(pp)
Storing Persistent Settings Files¶
You can store configuration files in a location provided by the API.
Note
By convention, you should log the settings file location so that the user has direct access to them.
Note
By convention, the settings files are stored in JSON format.
The following code shows how to access the configuration location:
>>> config_file = api.application.configuration_location / pathlib.Path("my_settings.json")
>>> logging.info("My plug-in configuration file: " + str(config_file))