Skip to content

Commit c45e84e

Browse files
committed
Add results processing/display
1 parent 0ad1e21 commit c45e84e

File tree

5 files changed

+61
-4
lines changed

5 files changed

+61
-4
lines changed

examples/run_ndip_tool/model.py

Lines changed: 27 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
"""Model implementation for NDIP tool example."""
22

33
from enum import Enum
4-
from typing import Tuple
4+
from io import BytesIO
5+
from typing import List, Tuple
56

67
from nova.galaxy import Parameters, Tool
78
from nova.galaxy.interfaces import BasicTool
9+
from PIL import Image
10+
from PIL.ImageStat import Stat
811
from pydantic import BaseModel, Field
912

1013

@@ -23,11 +26,21 @@ class FormData(BaseModel):
2326
fractal_type: FractalOptions = Field(default=FractalOptions.mandelbrot, title="Type")
2427

2528

29+
class ImageStatistics(BaseModel):
30+
"""Pydantic model for holding image statistics."""
31+
32+
count: List[int] = Field(default=[])
33+
extrema: List[Tuple[int, int]] = Field(default=[])
34+
mean: List[float] = Field(default=[])
35+
median: List[int] = Field(default=[])
36+
37+
2638
class Model:
2739
"""Model implementation for NDIP tool example."""
2840

2941
def __init__(self) -> None:
3042
self.form = FormData()
43+
self.stats = ImageStatistics()
3144

3245

3346
class FractalsTool(BasicTool):
@@ -47,6 +60,17 @@ def prepare_tool(self) -> Tuple[Tool, Parameters]:
4760

4861
return self.tool, tool_params
4962

50-
def get_results(self) -> None:
51-
# TODO: add some processing of the results.
63+
def compute_stats(self) -> None:
64+
outputs = self.tool.get_results()
65+
output = outputs.get_dataset("output").get_content()
66+
67+
img = Image.open(BytesIO(output))
68+
stat = Stat(img)
69+
70+
self.model.stats.count = stat.count
71+
self.model.stats.extrema = stat.extrema
72+
self.model.stats.mean = [round(mean, 3) for mean in stat.mean]
73+
self.model.stats.median = stat.median
74+
75+
def get_results(self, tool: Tool) -> None:
5276
pass

examples/run_ndip_tool/view.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from nova.trame import ThemedApp
55
from nova.trame.view.components import ExecutionButtons, InputField, ProgressBar, ToolOutputWindows
66
from nova.trame.view.layouts import VBoxLayout
7+
from trame.widgets import html
78
from trame.widgets import vuetify3 as vuetify
89

910
from .model import Model
@@ -19,6 +20,7 @@ def __init__(self) -> None:
1920
self.create_vm()
2021
# If you forget to call connect, then the application will crash when you attempt to update the view.
2122
self.view_model.form_data_bind.connect("data")
23+
self.view_model.stats_bind.connect("stats")
2224
# Generally, we want to initialize the view state before creating the UI for ease of use. If initialization
2325
# is expensive, then you can defer it. In this case, you must handle the view state potentially being
2426
# uninitialized in the UI via v_if statements.
@@ -38,6 +40,22 @@ def create_ui(self) -> None:
3840
ProgressBar("fractals")
3941
ToolOutputWindows("fractals")
4042

43+
with html.Div(v_if="stats.count.length > 0"):
44+
html.P(
45+
"PIL computed {{ stats.count.length }} bands for the generated fractal image.",
46+
classes="mb-2",
47+
)
48+
html.P(
49+
(
50+
"Band {{ index + 1 }} Stats: "
51+
"Count={{ stats.count[index] }}, "
52+
"Extrema={{ stats.extrema[index] }}, "
53+
"Mean={{ stats.mean[index] }}, "
54+
"Median={{ stats.median[index] }}"
55+
),
56+
v_for="(_, index) in stats.count.length",
57+
)
58+
4159
with layout.post_content:
4260
# This adds UI components for running and stopping the tool.
4361
ExecutionButtons("fractals")

examples/run_ndip_tool/view_model.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""View model implementation for NDIP tool example."""
22

33
import os
4+
from typing import Any, Dict
45

6+
from blinker import signal
7+
from nova.common.job import WorkState
8+
from nova.common.signals import Signal, get_signal_id
59
from nova.galaxy.tool_runner import ToolRunner
610
from nova.mvvm.interface import BindingInterface
711

@@ -23,12 +27,22 @@ def __init__(self, model: Model, binding: BindingInterface) -> None:
2327
self.model = model
2428

2529
self.form_data_bind = binding.new_bind(self.model.form)
30+
self.stats_bind = binding.new_bind(self.model.stats)
2631

2732
# Using the ToolRunner will allow the tool's progress to be automatically communicated to the view.
2833
self.tool = FractalsTool(model)
2934
# The "fractals" string needs to be consistent with the view components.
3035
self.tool_runner = ToolRunner("fractals", self.tool, self.store_factory, galaxy_url, galaxy_api_key)
3136

37+
# Now we need to listen for when the tool finished running.
38+
self.completion_signal = signal(get_signal_id("fractals", Signal.PROGRESS))
39+
self.completion_signal.connect(self.on_completion, weak=False)
40+
41+
async def on_completion(self, _sender: Any, state: WorkState, details: Dict[str, Any]) -> None:
42+
if state == WorkState.FINISHED:
43+
self.tool.compute_stats()
44+
self.stats_bind.update_in_view(self.model.stats)
45+
3246
def store_factory(self) -> str:
3347
# This will cause a history named "fractals" to be created on NDIP to store the tool's outputs, errors, and
3448
# results.

poetry.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ requests = "*"
2323
ruff = "*"
2424
trame-code = "*"
2525
nltk = "*"
26+
pillow = "^11.3.0"
2627

2728
[build-system]
2829
requires = ["poetry-core"]

0 commit comments

Comments
 (0)