Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Utilities to simplify the integration of command-line tools in TrackMate modules #299

Merged
merged 71 commits into from
Jun 25, 2024

Conversation

tinevez
Copy link
Member

@tinevez tinevez commented Jun 20, 2024

This PR ships a series of classes meant to simplify and accelerate the integration of command-lines in TrackMate modules. Typically such tools are Python tools that can be installed in a conda environment, can be called and configured from the command line, and yield results as files that can be imported in TrackMate. Good examples are Python scientific tools like cellpose or Trackastra.

These tools are in the fiji.plugin.trackmate.util.cli package.

An example implementation is in https://github.com/trackmate-sc/TrackMate-Trackastra/tree/main/src/main/java/fiji/plugin/trackmate/tracking/trackastra

A simple example is included in src/test/java/fiji/plugin/trackmate/util/cli/ExampleCommandCLI.java

Simple example.

Each tool is represented by one class, inheriting from CLIConfigurator. If the tool you want to run corresponds to an executable on disk, subclass CommandCLIConfigurator. If it based on a Python tool installed in a conda environment, subclass CondaCLIConfigurator.

Creating a CLI config.

For instance, a tool that depends on an executable with 3 arguments, 2 optional, 1 required, would look like this:

public class ExampleCommandCLI extends CommandCLIConfigurator
{
	...

	public ExampleCommandCLI()
	{
		executable
				.name( "Path to the executable." )
				.help( "Browse to the executable location on your computer." )
				.key( "PATH_TO_EXECUTABLE" );

executable is the executable that will be called by the TrackMate module.

CLI configs that inherit from 'CommandCLIConfigurator' are all based on an actual executable, that is a file with exec rights somewhere on the user computer. They need to set it themselves, so the config part only specifies the name, the help and the key of this command. The help and name will be used in the UI.

  • name() sets a user-friendly name to use in UIs and messages.
  • help() is a text help that is shown in the tooltip of the UIs.
  • key() is used to serialize the config to a Map< String, Object >, for TrackMate integration.

In this example we will assume that the tool we want to run accepts a few arguments.

The first one is the --nThreads argument, that accept integer larger than 1 and smaller than 24.

		this.nThreads = addIntArgument()
				.argument( "--nThreads" ) // arg in the command line
				.name( "N threads" ) // convenient name
				.help( "Sets the number of threads to use for computation." ) // help
				.defaultValue( 1 )
				.min( 1 )
				.max( 24 ) // will be used to create an adequate UI widget
				.key( "N_THREADS" ) // use to serialize to Map<String, Object>
				.get();

The 'argument()' part must be something the tool can understand. This is what is passed to it before the value.
This example argument is not required, but has a default value of 1. The default value is used only in the command line. If an argument is not required, is not set, but has a default value, then the argument will appear in the command line with this default value.

Adding arguments is done via 'adder' methods, that are only visible in inhering classes. The get() method of the adder returns the created argument. It also adds it to the inner parts of the mother class, so that it is handled automatically when creating a GUI or a command line. But it is a good idea to expose it in this concrete class so that you can expose it to the user and let them set it.

The second argument is a double. It is required, which means that an error will be thrown when making a command line from this config if the user forgot to set a value. It also has a unit, which is only used in the UI.

Because it does not specify a min or a max, any numerical value can be entered in the GUI. The implementation will have to add an extra check to verify consistency of values.

		this.diameter = addDoubleArgument()
				.argument( "--diameter" )
				.name( "Diameter" )
				.help( "The diameter of objects to process." )
				.key( "DIAMETER" )
				.defaultValue( 1.5 )
				.units( "microns" )
				.required( true ) // required flag
				.get();

The third argument is a a double argument that has a min and a max will generate another widget in the UI: a slider.

		this.time = addDoubleArgument()
				.argument( "--time" )
				.name( "Time" )
				.help( "Time to wait after processing." )
				.key( "TIME" )
				.min( 1. )
				.max( 100. )
				.units( "seconds" )
				.get();

Using a CLI config to yield a command line.

Here is how this CLI config could be used to generate a command line. The class CommandBuilder contains a static method that generates a command line as a list of tokens, so that they can be used directly by Java ProcessBuilder.

final ExampleCommandCLI cli = new ExampleCommandCLI();

//Configure the CLI.
cli.getCommandArg().set( "/path/to/my/executable" );

The following will generate an error because 'diameter' is required and not set.

The argument 'nThreads' is not set either, but it has a default value and is not required -> no error, the command line will use the default value.

The argument 'time' is not set either, does not have a default, but it is not required -> no error, the command line will just miss the 'time' argument.

// Play with the command line.
// This will generate an error because 'diameter' is not set and is required.
try
{
	final List< String > cmd = CommandBuilder.build( cli );
	System.out.println( "To run: " + cmd ); // error
}
catch ( final IllegalArgumentException e )
{
	System.err.println( e.getMessage() );
}

This prints:

Required argument 'Diameter' is not set.

We can fix this by actually setting the diameter:

// Set the diameter. Now it should be ok.
cli.diameter().set( 2.5 );
try
{
	final List< String > cmd = CommandBuilder.build( cli );
	System.out.println( "To run: " + cmd );
}
catch ( final IllegalArgumentException e )
{
	System.err.println( e.getMessage() );
}

This prints:

To run: [/path/to/my/executable, --nThreads, 1, --diameter, 2.5]

Generating a UI from a CLI config.

A CLI object can also be used to generate a UI that will configure it. This is done with the CliGuiBuilder class. There is one important point: the UI builder requires all arguments and commands to have a value set:

// The UI cannot be created wit arguments that do not have a value. This
// will generate an error:
try
{
	CliGuiBuilder.build( cli );
}
catch ( final IllegalArgumentException e )
{
	System.err.println( e.getMessage() );
}

This prints:

The GUI builder requires all arguments and commands to have a value set. The following miss one: [N threads, Time]

To fix this we can set a value for all arguments:

cli.time().set( 5. );
cli.nThreads().set( 2 );
// This should be ok now.
final CliConfigPanel panel = CliGuiBuilder.build( cli );
final JFrame frame = new JFrame( "Demo CLI tool" );
frame.getContentPane().add( panel, BorderLayout.CENTER );
final JButton btn = new JButton( "echo" );
btn.addActionListener( e -> System.out.println( CommandBuilder.build( cli ) ) );
frame.getContentPane().add( btn, BorderLayout.SOUTH );
frame.setLocationRelativeTo( null );
frame.pack();
frame.setVisible( true );
frame.setDefaultCloseOperation( JFrame.EXIT_ON_CLOSE );

And it yields this:
Screenshot 2024-06-25 at 15 07 43

The UI directly modifies the values of the arguments. When pressing the echo button we get this:

[/path/to/my/executable-trololo, --nThreads, 19, --diameter, 55.0, --time, 88.0]

tinevez and others added 30 commits June 15, 2024 23:08
For instance, in cellpose we have the --pretrained_model arg that
can accept either a string from a pretrained model list, or a path
to a custom model. This corresponds to two different types of
Argument<?> : a list and a path.

To make it possible to choose between one OR the other in a GUI,
there is a SelectableArguments that lets the user specifies that
one argument can be active OR another one, but not bother at the same
time.

	// State that we can set one or the other.
	cli.addSelectableArguments()
			.add( ptm )
			.add( cm )
			.select( ptm );

This is supported in the GUI (but what a mess) and the args that are
in a selectable group appear with a radio button. Selecting one
properly sets the CLI.

The arguments in a selectable groups do not have to be following
in the arg list, but it is prettier that way.
TODO: also make it in the TrackMate settings map bridge.
- restrict visibility once the CLI config is built.
- shorten the class using abstract classes more.
Sometimes we need to change the output of an argument so that it
'looks good' in TrackMate GUI but still translates into something
the executable can understand. For instance in cellpose we want
to display for the optional channel 2:
- 0 (don't use)
- 1 (red)
- 2 (green)
- 3 (blue)
into the accepted values by the CLI:
- 0
- 1
- 2
- 3
This mechanism offers the possibility to do so.
Typically, a TrackMate detector or tracker has more parameters
than the CLI of the tool it will call. For instance to simplify
contours after a detection step.
This commit allows passing these extra parameters to the UI builder
and to the map serializer so that a TrackMate module that calls a
command line tool can be handled by the CLI framework.
tinevez and others added 27 commits June 23, 2024 16:02
Do not use Elements. Instead just gives the possibity of adding
extra arguments, making them visible in the GUI, but not used in
the command. For this, add a method arg.inCLI(boolean) that specified
whether this arg is to be used in the CLI.
This allows using the same system to generate a command line, a GUI,
and manage all of this via TrackMate settings.

Also add javadocs and fix some typos.
For many tools we want to integrate are Python tools that can be
run from a specific conda environment. If the implementing CLI
config knows already what is the Python command, the user should
just have to specify in what conda env it is installed.
It is more convenient than to browse to a Python executable and
less prone to error.
Right now, the conda CLI config only works on windows.
- Offer a utility to let the user sets the path to the conda
executable on their system. Stores it in a SciJava Prefs.
- Make a method with defaults, from known installation of conda
flavor.
- Use it the conda exec to build a map from conda env name
to the path of the python executable they are using.
- Rework the conda env list command.
We CANNOT change the conda environment with the Java ProcessBuilder.
At least after me trying very hard. So we have to defer on this
platform to calling the Python tool as a module, from the python
executable. It is very sad. And it limits what we can run this
way to tools that works as a module ('python -m blablabla').
I see no other way yey.

To do so:

- Make a special command for the arg that allows selecting a conda
environment: CondaEnvironmentCommand. Use it in the GUI and in the
command builder.

- Its translator turns it into either a 'conda activate' thing on
Windows, or in a 'python -m blablabla' on Mac.
when trying to create a GUI with a CLI config that has some arguments
that miss a value.
@tinevez tinevez marked this pull request as ready for review June 25, 2024 13:16
@tinevez tinevez merged commit a1b7d70 into master Jun 25, 2024
1 check passed
@tinevez tinevez deleted the cli-tools branch June 25, 2024 13:17
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

1 participant