NDIP Platform (Neutron Data Interpretation Platform)
```

In essence, you will build your **User Application** using the **NOVA Libraries**, which in turn will interact with the **NDIP Platform** to perform neutron data analysis tasks.
@@ -76,6 +64,15 @@ In this tutorial, you will learn how to use these three core NOVA libraries to b
We\'ll be using example tools as a demonstration for this tutorial, however, the lessons learned here can be applied to a wide variety of neutron scattering data analysis applications. This hands-on tutorial will guide you through each step of the process, empowering you to build your own interactive tools.
## Downloading the tutorial repository
The tutorial is hosted on the ORNL gitlab at [https://code.ornl.gov/ndip/public-packages/nova-carpentry-tutorial](https://code.ornl.gov/ndip/public-packages/nova-carpentry-tutorial). The simplest and recommended way to download the tutorial repository is by using git and the command:
It is also possible to download a zipped copy of the repository directly from the repository\'s gitlab web site.
## Code Examples Directory
All of the code examples used in this tutorial are available in the `code` directory of the tutorial repository. These examples are built upon the template application that you will clone in the next episode. The code is organized by episode, with each episode having its own subdirectory (e.g., `code/episode_2`, `code/episode_3`, etc.).
- Explain the purpose of the `Nova`, `Tool`, and `Parameters` classes in `nova-galaxy`.
- Explain the purpose of the `Connection`, `Outputs`, `Datastore`, `Tool`, and `Parameters` classes in `nova-galaxy`.
- Describe the basic workflow for running an NDIP tool using `nova-galaxy`.
- Connect to NDIP using the `Nova` class.
- Connect to NDIP using the `Connection` class.
- Define an NDIP tool and set its parameters using the `Tool` and `Parameters` classes.
- Run the tool and create a datastore.
@@ -26,7 +26,7 @@ exercises: 3
# Using NDIP for Backend Computations
In this section, we will start using the `nova-galaxy` library to interact with the NDIP platform and run a neutron analysis tool. First, ensure you have set your `GALAXY_URL` and `GALAXY_API_KEY` as environment variables, as explained in the notes at the end of this episode. We also need to add `nova-galaxy` as a project dependency.
From the command line, type `poetry add nova-galaxy@^0.4.0`. This command will add the nova-galaxy library to the pyproject.toml file as a project dependency. Then run `poetry install` to update your project dependencies.
From the command line, type `poetry add nova-galaxy@^0.7.0`. This command will add the nova-galaxy library to the pyproject.toml file as a project dependency. Then run `poetry install` to update your project dependencies.
## Interacting with NDIP via `nova-galaxy`
@@ -34,30 +34,37 @@ The `nova-galaxy` library is your gateway to interacting with NDIP programmatica
We will be using the following key classes from `nova-galaxy` in this episode:
***`Nova`**: The main entry point for interacting with NDIP. You instantiate the `Nova` class with your NDIP URL and API key to establish a connection.
***`Connection`**: The main entry point for interacting with NDIP. You instantiate the `Connection` class with your NDIP URL and API key to establish a connection.
***`Tool`**: Represents a tool available on the NDIP platform. You can define a `Tool` object by its ID (which corresponds to a tool XML definition in NDIP).
***`Parameters`**: Used to define the input parameters for a tool. You add parameters to a `Parameters` object, specifying the parameter names and values.
***`Datastore`**: Configures Galaxy to group outputs of a tool to group outputs of a tool together.
***`Output`**: Contains the output datasets and collections for a tool.
***`Dataset`**: A singular file which can be uploaded to Galaxy to be used in tools or downloaded from Galaxy to local storage.
***`DatasetCollection`**: A group of files which can be uploaded to Galaxy to be used in tools or downloaded from Galaxy to local storage.
The basic workflow for running a tool with `nova-galaxy` involves these steps:
1.**Connect to NDIP**: Create a `Nova` instance with your credentials.
1.**Connect to NDIP**: Create a `Connection` instance with your credentials.
2.**Define the Tool**: Create a `Tool` instance, specifying the ID of the NDIP tool you want to run.
3.**Set Parameters**: Create a `Parameters` instance and add the necessary input parameters and their values for the tool.
4.**Run the Tool**: Use the `tool.run()` method to submit the job to NDIP. This typically involves creating a datastore to hold the job\'s input and output data.
## Setting up the Fractal tool
Let\'s create a `Fractal` class that uses `nova-galaxy` to run the `neutrons_fractal` tool on NDIP. You can find the complete code for this episode in the `code/episode_3` directory. Here, we will focus on the key code snippets and explain the important parts.
Let\'s create a `Fractal` class that uses `nova-galaxy` to run the `neutrons_fractal` tool on NDIP. You can find the complete code for this episode in the `code/episode_3` directory.
**1. `Fractal` Class (`src/nova_tutorial/models/fractal.py`):**
**1. `Fractal` Class (`src/nova_tutorial/app/models/fractal.py`):**
***Imports**: The `Fractal Model` starts by importing necessary classes from `nova-galaxy`:
To get started, let\'s create the Fractal class. Create an empty file at `src/nova_tutorial/app/models/fractal.py`. Add the following pieces of code to the newly created file.
***Imports**: The `Fractal Class` will start by importing the necessary classes from `nova-galaxy`:
```python
from nova.galaxy import Nova, Parameters, Tool
importos
fromnova.galaxyimportConnection,Parameters,Tool
```
***`__init__` method**: In the `__init__` method, we instantiate the `Fractal` class. Note how we retrieve `GALAXY_URL` and `GALAXY_API_KEY` from environment variables. This establishes how we will connect to NDIP:
***`__init__` method**: In the `__init__` method, we initialize the `Fractal` class. Note how we retrieve `GALAXY_URL` and `GALAXY_API_KEY` from environment variables. This establishes how we will connect to NDIP:
```python
classFractal:
@@ -69,38 +76,45 @@ Let\'s create a `Fractal` class that uses `nova-galaxy` to run the `neutrons_fra
***`run_fractal_tool` method**: This method encapsulates the logic for running the `fractal` tool. Let\'s examine the key steps within this method:
* **Instantiate `Nova`, `Tool`, and `Parameters`**: We create instances of the `Nova`, `Tool`, and `Parameters` classes:
* **Instantiate `Connection`, `Tool`, and `Parameters`**: We create instances of the `Connection`, `Tool`, and `Parameters` classes:
Note that we create a `Tool` object with the `id="neutrons_fractal"`. This tells `nova-galaxy` which NDIP tool we want to run. The obvious question at this point is how do we know the id of the tool and what parameters it expects? We can look at the tool\'s launch page in calvera for some hints but ultimately we have to look at the tool\'s [xml file](https://code.ornl.gov/ndip/galaxy-tools/-/blob/dev/tools/neutrons/test_tools/fractal.xml?ref_type=heads).
* **Connect and Run the Tool**: The `with nova.connect() as galaxy_connection:` block establishes a connection to NDIP and ensures proper handling of the connection:
***Connect and Run the Tool**: The `with conn.connect() as galaxy_connection:` block establishes a connection to NDIP and ensures proper handling of the connection:
**2. `main.py` - Calling the Model (`src/nova_tutorial/app/main.py`):**
**2. `main.py` - Calling the Model (`src/nova_tutorial/main.py`):**
We are now going to modify the existing`main.py`file. Change the main method to match the code below.
***Instantiate and Run**: In the `main()` function, we create an instance of `Fractal` and call the `run_fractal_tool()` method, wrapped in a `try...except` block for basic error handling:
```python
defmain():
importsys
from.models.fractalimportFractal
defmain()->None:
fractal=Fractal()
try:
fractal.run_fractal_tool()
exceptExceptionase:
print(f"Error running fractal tool: {e}")
```
## Running the tool
@@ -128,7 +142,22 @@ At times, it may be desirable to execute a tool or workflow without waiting on t
## Tool output
After the tool finishes running on NDIP, the result of the tool is returned as an output. In this example, the output is a single image file of the generated fractal. Tools can return single files or a collection of files as a zip. The output can be used by the rest of your application, saved, or simply discarded. A copy of the output also resides on the NDIP platform, so it is not necessary to maintain a local copy.
Tool execution often results in some type of output. In the Fractal example, the tool output is a singular image file. A tool can have multiple outputs and sometimes these outputs are grouped together in a collection. In `nova-galaxy`, a singular file is called a Dataset and a group of files is called a DatasetCollection. The `Dataset` and `DatasetCollection` classes support the following methods:
***upload(Datastore):** Uploads the Dataset(DatasetCollection) to the specified Datastore on Galaxy.
***download(file_path):** Downloads the Dataset(DatasetCollection) from Galaxy to the local path.
***get_content():** Retreives the content of the Dataset(DatasetCollection) without saving it to a local file path.
If a tool run results in a `Dataset` or `DatasetCollection`, an `Output` is returned from the run method. `Output` is an encapsulation of the output datasets and collections from a Tool. A tool execution can result in multiple `Dataset` and `DatasetCollection`, therefore, these are all grouped in the `Outputs` class for easier consumption.
In the Fractal example, the Tool.run comman returns an instance of the `Output` class which we save to the variable `output`. The Fractal tool [xml file](https://code.ornl.gov/ndip/galaxy-tools/-/blob/dev/tools/neutrons/test_tools/fractal.xml?ref_type=heads) defines that successful execution of the tool will result in a `Dataset` named `output`. This `Dataset` is then downloaded to the local file path `image.png`.
The Outputs can be used by the rest of your application, saved, or simply discarded. A copy of the Datasets and DatasetCollections also resides on the NDIP platform, so it is not necessary to maintain a local copy.
@@ -40,6 +40,7 @@ MVVM is an architectural design pattern specifically designed for applications w

The MVVM pattern consists of three core components:
***Model:** The Model represents the *data* and the *business logic* of the application. It\'s responsible for:
* Data storage (e.g., reading from and writing to a database, a file, or an API).
* Data validation (ensuring the data is in a valid state).
@@ -63,7 +64,7 @@ The Model is agnostic to the UI. It doesn\'t know anything about how the data wi
The ViewModel knows about the View and the data that the View needs, but it doesn\'t know about the specific UI components that are used to display the data. It also orchestrates the interaction between the View and the Model.
*The ViewModel is where we\'ll use `nova-mvvm` to create bindings between the ViewModel and the View, enabling the reactive updates.*
The ViewModel is where we\'ll use `nova-mvvm` to create bindings between the ViewModel and the View, enabling the reactive updates.
from nova.galaxy import Connection, Parameters, Tool
```
***Update class variables:** Now we'll update fractal_type to support pydantic and add an image variable to store the image. Modify the variable declarations to the following:
* **Update class variables:** Now we'll update fractal_type and other class variables to support pydantic. We'll also add an image variable to store the image. Modify the variable declarations to the following:
* **Decode the image data:** Finally, we need to decode the image that we receive as the output from the tool execution. Modify the section where we execute the tool to the following:
@@ -185,7 +187,21 @@ Let\'s see how to implement the MVVM pattern using `nova-mvvm` and incorporate P
**4. Modify the tab panel (`src/nova_tutorial/views/tab_panel.py`):**
Modify the tab panel to add our new Fractal tab
**5. Modify the tab panel (`src/nova_tutorial/app/views/tabs_panel.py`):**
* **Add Fractal Tab to the tab panel**: Modify the tab panel to add our new Fractal tab
```python
with vuetify.VTabs(v_model=("active_tab", 0), classes="pl-5"):
@@ -219,8 +235,14 @@ Let\'s see how to implement the MVVM pattern using `nova-mvvm` and incorporate P
vuetify.VTab("Sample Tab 2", value=3)
```
**4. Modify the tab panel (`src/nova_tutorial/views/tab_content_panel.py`):**
Add our new Fractal Tab to the tab content panel.
**6. Modify the tab panel content (`src/nova_tutorial/app/views/tab_content_panel.py`):**
* **Add FractalTab to imports**: Import the newly created FractalTab class into our tab_content_panel.
```python
from .fractal_tab import FractalTab # Import the FractalTab
```
* **Add the Fractal Tab to our existing tabs**: Add the Fractal Tab lines to the vuetify.VWindow section and modify the values.
```python
with vuetify.VWindow(v_model="active_tab"):
@@ -232,6 +254,30 @@ Let\'s see how to implement the MVVM pattern using `nova-mvvm` and incorporate P
SampleTab2()
```
**7. `main.py` - Calling the Model (`src/nova_tutorial/app/main.py`):**
We are now going to modify the existing `main.py` file. Change the main method to match the code below.
* **Instantiate and Run**: In the `main()` function, we create an instance of `Fractal` and call the `run_fractal_tool()` method, wrapped in a `try...except` block for basic error handling:
```python
import sys
from .models.fractal import Fractal
def main() -> None:
kwargs = {}
from .views.main import MainApp
app = MainApp()
for arg in sys.argv[2:]:
try:
key, value = arg.split("=")
kwargs[key] = int(value)
except Exception:
pass
app.server.start(**kwargs)
```
## Running the application
!!!! You need more explanation of why you added view related code but here you say we have not created a UI yet. !!!!
@@ -248,7 +294,7 @@ You should see `Fractal tool finished successfully.` printed to the console, alt
* In `FractalViewModel` in `src/nova_tutorial/view_models/fractal_view_model.py`, modify the `update_fractal_programmatically` function from the previous exercise to use an *invalid* fractal type:
* In `FractalViewModel` in `src/nova_tutorial/app/view_models/fractal_view_model.py`, modify the `update_fractal_programmatically` function from the previous exercise to use an *invalid* fractal type:
self.fractal_type = new_type # Use the setter which includes validation
@@ -267,7 +313,7 @@ You should see `Fractal tool finished successfully.` printed to the console, alt
::::::::::::::::::::::::::::::::::::::: challenge
**Inspect ViewModel State**
* In `src/nova_tutorial/view_models/fractal_view_model.py`, add `print` statements within the `FractalViewModel.__init__` method to print the initial values of `self._fractal_type`, `self._job_status`, and `self._message`.
* In `src/nova_tutorial/app/view_models/fractal_view_model.py`, add `print` statements within the `FractalViewModel.__init__` method to print the initial values of `self._fractal_type`, `self._job_status`, and `self._message`.
* Run the application (`poetry run app`). Observe the output in the console. Verify that the initial values are printed as expected.
* Now, modify the `FractalViewModel.__init__` method to change the initial value of `self._message` to "Application starting...". Run the application again and confirm that the printed initial message has changed.
@@ -275,7 +321,7 @@ You should see `Fractal tool finished successfully.` printed to the console, alt
::::::::::::::::::::::::::::::::::::::: challenge
**Programmatic State Update and Binding:**
* In `FractalViewModel` in `src/nova_tutorial/view_models/fractal_view_model.py`, after the line `self.fractal_type_bind = binding.new_bind(...)` in `__init__`, add the following lines:
* In `FractalViewModel` in `src/nova_tutorial/app/view_models/fractal_view_model.py`, after the line `self.fractal_type_bind = binding.new_bind(...)` in `__init__`, add the following lines:
```python
print("Initial fractal type:",self._fractal_type)# Print initial value
@@ -75,7 +75,7 @@ Layouts are responsible for arraging your content in a consistent manner. In Tra
`nova-trame` provides a basic layout and theme that you can access via the `ThemedApp` class. The template app will setup your main view class to inherit from `ThemedApp` already, so let\'s look at how the layout is defined and how we can add content to slots.
**1. `nova_tutorial/views/main.py`:**
**1. `nova_tutorial/app/views/main.py`:**
```python
classMainApp(ThemedApp):
@@ -215,7 +215,7 @@ For a more detailed explanation of how to work with our layout and theme, please
Now, let\'s add some UI components to the Sample Tabs in our application to demonstrate how to use these components. We\'ll modify the `sample_tab_1.py` and `sample_tab_2.py` files to include these components.