raylim commited on
Commit
7c7a1ba
·
unverified ·
1 Parent(s): e66cef9

Add progressive results display and sex validation to UI

Browse files

- Add sex requirement check before analysis starts
- Display AEON and PALADIN results progressively as slides are processed
- Show settings table during multi-slide processing for progress tracking
- Add initial yield to make UI responsive immediately
- Fix sex dropdown to use None instead of 'Unknown' as default
- Convert None to empty string when building settings DataFrame
- Ensure Score column is numeric before rounding in PALADIN results
- Hide settings table for single-slide mode, show for multi-slide mode

Files changed (1) hide show
  1. src/mosaic/ui/app.py +75 -14
src/mosaic/ui/app.py CHANGED
@@ -96,10 +96,25 @@ def analyze_slides(
96
  if len(slides) != len(settings_input):
97
  raise gr.Error("Missing settings for uploaded slides")
98
 
 
 
 
 
99
  all_slide_masks = []
100
  all_aeon_results = []
101
  all_paladin_results = []
102
 
 
 
 
 
 
 
 
 
 
 
 
103
  # Load models once (for batch) or per-slide (for single)
104
  model_cache = None
105
  if len(slides) > 1:
@@ -123,7 +138,7 @@ def analyze_slides(
123
  slide_path=slide_path,
124
  seg_config=row["Segmentation Config"],
125
  site_type=row["Site Type"],
126
- sex=row.get("Sex", "Unknown"),
127
  tissue_site=row.get("Tissue Site", "Unknown"),
128
  cancer_subtype=row["Cancer Subtype"],
129
  cancer_subtype_name_map=cancer_subtype_name_map,
@@ -146,18 +161,54 @@ def analyze_slides(
146
  )
147
  all_paladin_results.append(paladin_results)
148
 
149
- # Yield intermediate update to show slide masks as they're generated
150
- # This allows the UI to update incrementally during processing
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  yield (
 
152
  all_slide_masks.copy(), # Current slide masks
153
- gr.DataFrame(visible=False), # aeon_output_table (not ready yet)
154
- gr.DownloadButton(
155
- visible=False
156
- ), # aeon_download_button (not ready yet)
157
- None, # paladin_output_table (not ready yet)
158
- gr.DownloadButton(
159
- visible=False
160
- ), # paladin_download_button (not ready yet)
161
  user_dir, # user_dir_state
162
  )
163
 
@@ -206,6 +257,9 @@ def analyze_slides(
206
  for code in combined_paladin_results["Cancer Subtype"]
207
  ]
208
  combined_paladin_results["Cancer Subtype"] = cancer_subtype_names
 
 
 
209
  combined_paladin_results["Score"] = combined_paladin_results["Score"].round(3)
210
 
211
  paladin_output_path = user_dir / f"paladin_results-{timestamp}.csv"
@@ -215,7 +269,10 @@ def analyze_slides(
215
  progress(1.0, desc="All done!")
216
 
217
  # Final yield with complete results
 
 
218
  yield (
 
219
  all_slide_masks,
220
  combined_aeon_results,
221
  aeon_output,
@@ -249,7 +306,7 @@ def launch_gradio(server_name, server_port, share):
249
  sex_dropdown = gr.Dropdown(
250
  choices=SEX_OPTIONS,
251
  label="Sex",
252
- value="Unknown",
253
  )
254
  tissue_site_dropdown = gr.Dropdown(
255
  choices=get_tissue_sites(),
@@ -357,7 +414,7 @@ def launch_gradio(server_name, server_port, share):
357
  [
358
  slide_name,
359
  site_type,
360
- sex,
361
  tissue_site,
362
  cancer_subtype,
363
  ihc_subtype,
@@ -373,6 +430,9 @@ def launch_gradio(server_name, server_port, share):
373
  return settings_df
374
  # Create a copy to avoid modifying the original
375
  updated_df = settings_df.copy()
 
 
 
376
  updated_df[column_name] = new_value
377
  return updated_df
378
 
@@ -510,6 +570,7 @@ def launch_gradio(server_name, server_port, share):
510
  user_dir_state,
511
  ],
512
  outputs=[
 
513
  slide_masks,
514
  aeon_output_table,
515
  aeon_download_button,
@@ -518,7 +579,7 @@ def launch_gradio(server_name, server_port, share):
518
  user_dir_state,
519
  ],
520
  queue=True,
521
- show_progress_on=paladin_output_table,
522
  )
523
  settings_input.change(
524
  lambda df: validate_settings(
 
96
  if len(slides) != len(settings_input):
97
  raise gr.Error("Missing settings for uploaded slides")
98
 
99
+ # Check that all slides have sex specified
100
+ if settings_input["Sex"].isna().any() or (settings_input["Sex"] == "").any() or (settings_input["Sex"] == None).any():
101
+ raise gr.Error("Sex is required for all slides. Please select Male or Female.")
102
+
103
  all_slide_masks = []
104
  all_aeon_results = []
105
  all_paladin_results = []
106
 
107
+ # Yield initial state to make settings table visible immediately
108
+ yield (
109
+ gr.Dataframe(value=settings_input, visible=True), # Make settings visible
110
+ [], # Empty slide masks
111
+ gr.DataFrame(visible=False), # Hidden AEON table
112
+ gr.DownloadButton(visible=False), # Hidden AEON download
113
+ None, # No PALADIN results yet
114
+ gr.DownloadButton(visible=False), # Hidden PALADIN download
115
+ user_dir, # user_dir_state
116
+ )
117
+
118
  # Load models once (for batch) or per-slide (for single)
119
  model_cache = None
120
  if len(slides) > 1:
 
138
  slide_path=slide_path,
139
  seg_config=row["Segmentation Config"],
140
  site_type=row["Site Type"],
141
+ sex=row["Sex"],
142
  tissue_site=row.get("Tissue Site", "Unknown"),
143
  cancer_subtype=row["Cancer Subtype"],
144
  cancer_subtype_name_map=cancer_subtype_name_map,
 
161
  )
162
  all_paladin_results.append(paladin_results)
163
 
164
+ # Build partial AEON results for display
165
+ partial_aeon_df = gr.DataFrame(visible=False)
166
+ if all_aeon_results:
167
+ partial_aeon = pd.concat(all_aeon_results, axis=1)
168
+ partial_aeon.reset_index(inplace=True)
169
+ partial_aeon = partial_aeon.round(3)
170
+
171
+ # Convert OncoTree codes to names for display
172
+ cancer_subtype_names = [
173
+ f"{get_oncotree_code_name(code)} ({code})"
174
+ for code in partial_aeon["Cancer Subtype"]
175
+ ]
176
+ partial_aeon["Cancer Subtype"] = cancer_subtype_names
177
+
178
+ partial_aeon_df = gr.DataFrame(
179
+ partial_aeon,
180
+ visible=True,
181
+ column_widths=["4px"] + ["2px"] * (partial_aeon.shape[1] - 1),
182
+ )
183
+
184
+ # Build partial PALADIN results for display
185
+ partial_paladin_df = None
186
+ if all_paladin_results:
187
+ partial_paladin = pd.concat(all_paladin_results, ignore_index=True)
188
+
189
+ # Convert OncoTree codes to names for display
190
+ cancer_subtype_names = [
191
+ f"{get_oncotree_code_name(code)} ({code})"
192
+ for code in partial_paladin["Cancer Subtype"]
193
+ ]
194
+ partial_paladin["Cancer Subtype"] = cancer_subtype_names
195
+
196
+ # Ensure Score is numeric before rounding
197
+ partial_paladin["Score"] = pd.to_numeric(partial_paladin["Score"], errors='coerce')
198
+ partial_paladin["Score"] = partial_paladin["Score"].round(3)
199
+
200
+ partial_paladin_df = partial_paladin
201
+
202
+ # Yield intermediate update to show progressive results
203
+ # Download buttons stay hidden until all slides are processed
204
+ # Make settings visible during processing (for progress bar display)
205
  yield (
206
+ gr.Dataframe(value=settings_input, visible=True), # Settings visible for progress
207
  all_slide_masks.copy(), # Current slide masks
208
+ partial_aeon_df, # Partial AEON results (growing)
209
+ gr.DownloadButton(visible=False), # Download button hidden until complete
210
+ partial_paladin_df, # Partial PALADIN results (growing)
211
+ gr.DownloadButton(visible=False), # Download button hidden until complete
 
 
 
 
212
  user_dir, # user_dir_state
213
  )
214
 
 
257
  for code in combined_paladin_results["Cancer Subtype"]
258
  ]
259
  combined_paladin_results["Cancer Subtype"] = cancer_subtype_names
260
+
261
+ # Ensure Score is numeric before rounding
262
+ combined_paladin_results["Score"] = pd.to_numeric(combined_paladin_results["Score"], errors='coerce')
263
  combined_paladin_results["Score"] = combined_paladin_results["Score"].round(3)
264
 
265
  paladin_output_path = user_dir / f"paladin_results-{timestamp}.csv"
 
269
  progress(1.0, desc="All done!")
270
 
271
  # Final yield with complete results
272
+ # Hide settings table if only one slide, keep visible for multiple slides
273
+ settings_visible = len(slides) > 1
274
  yield (
275
+ gr.Dataframe(value=settings_input, visible=settings_visible), # Hide if single slide
276
  all_slide_masks,
277
  combined_aeon_results,
278
  aeon_output,
 
306
  sex_dropdown = gr.Dropdown(
307
  choices=SEX_OPTIONS,
308
  label="Sex",
309
+ value=None,
310
  )
311
  tissue_site_dropdown = gr.Dropdown(
312
  choices=get_tissue_sites(),
 
414
  [
415
  slide_name,
416
  site_type,
417
+ sex if sex is not None else "",
418
  tissue_site,
419
  cancer_subtype,
420
  ihc_subtype,
 
430
  return settings_df
431
  # Create a copy to avoid modifying the original
432
  updated_df = settings_df.copy()
433
+ # Convert None to empty string for display (especially for Sex column)
434
+ if new_value is None:
435
+ new_value = ""
436
  updated_df[column_name] = new_value
437
  return updated_df
438
 
 
570
  user_dir_state,
571
  ],
572
  outputs=[
573
+ settings_input,
574
  slide_masks,
575
  aeon_output_table,
576
  aeon_download_button,
 
579
  user_dir_state,
580
  ],
581
  queue=True,
582
+ show_progress_on=settings_input,
583
  )
584
  settings_input.change(
585
  lambda df: validate_settings(