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.

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.

  1. 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.

  2. 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.

  3. Open a script window (Ctrl/Cmd-K).

  4. Write the follow script, substituting the r-variables assigned in step #1 for r001 and r002.

    >>> 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

stem.session.instrument

string

stem.session.detector

string

stem.session.microscopist

string

stem.session.sample

string

stem.session.sample_area

string

stem.session.sample_source

string

stem.session.sample_formula

string

stem.session.site

string

stem.session.task

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

stem.session.instrument

string

stem.session.detector

string

stem.session.microscopist

string

stem.session.sample

string

stem.session.sample_area

string

stem.session.sample_source

string

stem.session.sample_formula

string

stem.session.site

string

stem.session.task

string


STEM Values

stem.high_tension_v

integer

stem.gun_type

string

stem.convergence_angle_rad

real

stem.collection_angle_rad

real

stem.probe_size_m2

real

stem.beam_current_a

real

stem.defocus_m

real


STEM Data

stem.signal_type

string (EELS, EDS, CL, Ronchigram, HAADF, MAADF, BF)


EELS Values

stem.eels.spectrum_type

string

stem.eels.resolution_eV

real

stem.eels.is_monochromated

boolean


Hardware Values

stem.hardware_source.id

string

stem.hardware_source.name

string


Camera Values

stem.camera.binning

integer

stem.camera.channel_id

string

stem.camera.channel_index

integer

stem.camera.channel_name

string

stem.camera.exposure_s

real

stem.camera.frame_index

integer

high level index. reset when played.

stem.camera.frame_number

integer

low level index. reset at application startup.

stem.camera.valid_rows

integer

stem.camera.detector_current

real


Scan Values

stem.scan.center_x_nm

real

stem.scan.center_y_nm

real

stem.scan.channel_id

string

stem.scan.channel_index

integer

stem.scan.channel_name

string

stem.scan.frame_time_s

real

stem.scan.fov_nm

real

stem.scan.frame_index

integer

stem.scan.pixel_time_us

real

stem.scan.rotation_rad

real

stem.scan.scan_id

string

stem.scan.valid_rows

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))