wenjun99 commited on
Commit
aea4424
·
verified ·
1 Parent(s): 852fe6e

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -45
app.py CHANGED
@@ -173,7 +173,6 @@ with tab2:
173
  # TAB 3: Pipetting Command Generator
174
  # --------------------------------------------------
175
 
176
-
177
  with tab3:
178
  st.header("🧪 Pipetting Command Generator")
179
  st.markdown("""
@@ -181,6 +180,7 @@ with tab3:
181
  The app will:
182
  - Auto-detect or create `Sample`, `Position#`, `Total edited`, and `Volume per "1"` columns
183
  - Generate pipetting commands dynamically for any number of positions
 
184
  - Display and allow CSV download for both commands and source volume summaries
185
  """)
186
 
@@ -212,7 +212,6 @@ with tab3:
212
  # --- Detect Position columns ---
213
  position_cols = [c for c in df.columns if re.match(r"(?i)^position\s*\d+", c)]
214
  if not position_cols:
215
- # If not found, auto-generate for numeric-like columns
216
  non_pos_cols = {"sample", "total edited", 'volume per "1"', "volume per 1"}
217
  candidate_cols = [c for c in df.columns if c.lower() not in non_pos_cols]
218
  if not candidate_cols:
@@ -239,7 +238,6 @@ with tab3:
239
  # --- Constants ---
240
  ROWS_96 = ["A", "B", "C", "D", "E", "F", "G", "H"]
241
  COLS_96 = list(range(1, 13))
242
- GROUP_START_COLS = [1, 5, 9]
243
  num_positions = len(position_cols)
244
 
245
  def well_name(row_letter, col_number):
@@ -252,19 +250,13 @@ with tab3:
252
  col_idx = within_plate % 12
253
  return plate_num, well_name(ROWS_96[row_idx], COLS_96[col_idx])
254
 
255
- def source_index_to_group_coords(source_idx):
256
- group_idx = source_idx - 1
257
- plate_num = 1 + (group_idx // 24)
258
- within_plate_group = group_idx % 24
259
- row_idx = within_plate_group // 3
260
- group_in_row = within_plate_group % 3
261
- row_letter = ROWS_96[row_idx]
262
- start_col = GROUP_START_COLS[group_in_row]
263
- return plate_num, row_letter, start_col
264
-
265
- def source_index_to_wells(source_idx):
266
- plate_num, row_letter, start_col = source_index_to_group_coords(source_idx)
267
- return [(plate_num, well_name(row_letter, start_col + offset)) for offset in range(4)]
268
 
269
  def pick_tool(volume_ul):
270
  return "TS_10" if volume_ul <= 10.0 else "TS_50"
@@ -272,8 +264,7 @@ with tab3:
272
  # --- Core logic ---
273
  commands = []
274
  source_volume_totals = {}
275
- per_source_well_cum = {i: [0.0, 0.0, 0.0, 0.0] for i in range(1, num_positions + 1)}
276
- per_source_wells = {i: source_index_to_wells(i) for i in range(1, num_positions + 1)}
277
 
278
  for _, row in df.iterrows():
279
  sample_id = int(row["Sample"])
@@ -292,23 +283,38 @@ with tab3:
292
  if not is_one:
293
  continue
294
 
295
- wells_list = per_source_wells[pos_idx]
296
- cum_list = per_source_well_cum[pos_idx]
297
-
298
- chosen = None
299
- for w_i, ((plate_num, well_str), current_vol) in enumerate(zip(wells_list, cum_list)):
300
- if current_vol + vol_per_one <= max_per_well_ul:
301
- chosen = (w_i, plate_num, well_str)
302
- break
303
- if chosen is None:
304
- st.error(f"Source {pos_idx} has all 4 wells full. Cannot add {vol_per_one} µL.")
305
- st.stop()
306
-
307
- w_i, src_plate, src_well = chosen
308
- cum_list[w_i] += vol_per_one
309
- per_source_well_cum[pos_idx] = cum_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
310
  source_volume_totals[(src_plate, src_well)] = source_volume_totals.get((src_plate, src_well), 0.0) + vol_per_one
311
-
312
  commands.append({
313
  "SourceIdx": pos_idx,
314
  "Source plate": src_plate,
@@ -322,22 +328,28 @@ with tab3:
322
  # --- Compile results ---
323
  commands_df = pd.DataFrame(commands)
324
  commands_df = commands_df.sort_values(
325
- by=["SourceIdx", "Source plate", "Source well", "Destination plate", "Destination well"],
326
  kind="stable"
327
  )
328
- commands_df = commands_df[["Source plate", "Source well", "Destination plate", "Destination well", "Volume", "Tool"]]
 
 
 
 
 
 
329
 
330
  # --- Source summary ---
331
  summary_rows = []
332
  for src_idx in range(1, num_positions + 1):
333
- for plate_num, well_str in per_source_wells[src_idx]:
334
- total = source_volume_totals.get((plate_num, well_str), 0.0)
335
- summary_rows.append({
336
- "Source": src_idx,
337
- "Source plate": plate_num,
338
- "Source well": well_str,
339
- "Total volume taken (µL)": round(total, 2)
340
- })
341
  summary_df = pd.DataFrame(summary_rows)
342
 
343
  st.success(f"✅ Generated {len(commands_df)} commands across {num_positions} positions.")
@@ -363,4 +375,4 @@ with tab3:
363
  except Exception as e:
364
  st.error(f"❌ Error processing file: {e}")
365
  else:
366
- st.info("👆 Upload an Excel/CSV/TXT file to start generating pipetting commands.")
 
173
  # TAB 3: Pipetting Command Generator
174
  # --------------------------------------------------
175
 
 
176
  with tab3:
177
  st.header("🧪 Pipetting Command Generator")
178
  st.markdown("""
 
180
  The app will:
181
  - Auto-detect or create `Sample`, `Position#`, `Total edited`, and `Volume per "1"` columns
182
  - Generate pipetting commands dynamically for any number of positions
183
+ - Use one source well per source index and reroute to the next source if a well is full
184
  - Display and allow CSV download for both commands and source volume summaries
185
  """)
186
 
 
212
  # --- Detect Position columns ---
213
  position_cols = [c for c in df.columns if re.match(r"(?i)^position\s*\d+", c)]
214
  if not position_cols:
 
215
  non_pos_cols = {"sample", "total edited", 'volume per "1"', "volume per 1"}
216
  candidate_cols = [c for c in df.columns if c.lower() not in non_pos_cols]
217
  if not candidate_cols:
 
238
  # --- Constants ---
239
  ROWS_96 = ["A", "B", "C", "D", "E", "F", "G", "H"]
240
  COLS_96 = list(range(1, 13))
 
241
  num_positions = len(position_cols)
242
 
243
  def well_name(row_letter, col_number):
 
250
  col_idx = within_plate % 12
251
  return plate_num, well_name(ROWS_96[row_idx], COLS_96[col_idx])
252
 
253
+ # one well per source index
254
+ def source_index_to_well(source_idx):
255
+ plate_num = ((source_idx - 1) // 96) + 1
256
+ within_plate = (source_idx - 1) % 96
257
+ row_idx = within_plate // 12
258
+ col_idx = within_plate % 12
259
+ return plate_num, well_name(ROWS_96[row_idx], COLS_96[col_idx])
 
 
 
 
 
 
260
 
261
  def pick_tool(volume_ul):
262
  return "TS_10" if volume_ul <= 10.0 else "TS_50"
 
264
  # --- Core logic ---
265
  commands = []
266
  source_volume_totals = {}
267
+ per_source_vol = {i: 0.0 for i in range(1, num_positions + 1)}
 
268
 
269
  for _, row in df.iterrows():
270
  sample_id = int(row["Sample"])
 
283
  if not is_one:
284
  continue
285
 
286
+ src_plate, src_well = source_index_to_well(pos_idx)
287
+ current_vol = per_source_vol[pos_idx]
288
+
289
+ # reroute if source full
290
+ if current_vol + vol_per_one > max_per_well_ul:
291
+ rerouted = False
292
+ for next_src in range(pos_idx + 1, num_positions + 1):
293
+ next_plate, next_well = source_index_to_well(next_src)
294
+ if per_source_vol[next_src] + vol_per_one <= max_per_well_ul:
295
+ st.warning(f"⚠️ Source {pos_idx} full rerouted {vol_per_one:.2f} µL to Source {next_src}.")
296
+ per_source_vol[next_src] += vol_per_one
297
+ source_volume_totals[(next_plate, next_well)] = source_volume_totals.get((next_plate, next_well), 0.0) + vol_per_one
298
+ commands.append({
299
+ "SourceIdx": f"{pos_idx}→{next_src}",
300
+ "Source plate": next_plate,
301
+ "Source well": next_well,
302
+ "Destination plate": dest_plate,
303
+ "Destination well": dest_well,
304
+ "Volume": round(vol_per_one, 2),
305
+ "Tool": tool,
306
+ "Note": "Rerouted due to full source"
307
+ })
308
+ rerouted = True
309
+ break
310
+ if not rerouted:
311
+ st.error(f"❌ All sources from {pos_idx} onward full. Cannot add {vol_per_one} µL for Sample {sample_id}.")
312
+ st.stop()
313
+ continue # skip normal addition
314
+
315
+ # normal case
316
+ per_source_vol[pos_idx] += vol_per_one
317
  source_volume_totals[(src_plate, src_well)] = source_volume_totals.get((src_plate, src_well), 0.0) + vol_per_one
 
318
  commands.append({
319
  "SourceIdx": pos_idx,
320
  "Source plate": src_plate,
 
328
  # --- Compile results ---
329
  commands_df = pd.DataFrame(commands)
330
  commands_df = commands_df.sort_values(
331
+ by=["Source plate", "Source well", "Destination plate", "Destination well"],
332
  kind="stable"
333
  )
334
+
335
+ if "Note" in commands_df.columns:
336
+ commands_df = commands_df[["SourceIdx", "Source plate", "Source well",
337
+ "Destination plate", "Destination well", "Volume", "Tool", "Note"]]
338
+ else:
339
+ commands_df = commands_df[["SourceIdx", "Source plate", "Source well",
340
+ "Destination plate", "Destination well", "Volume", "Tool"]]
341
 
342
  # --- Source summary ---
343
  summary_rows = []
344
  for src_idx in range(1, num_positions + 1):
345
+ src_plate, src_well = source_index_to_well(src_idx)
346
+ total = source_volume_totals.get((src_plate, src_well), 0.0)
347
+ summary_rows.append({
348
+ "Source": src_idx,
349
+ "Source plate": src_plate,
350
+ "Source well": src_well,
351
+ "Total volume taken (µL)": round(total, 2)
352
+ })
353
  summary_df = pd.DataFrame(summary_rows)
354
 
355
  st.success(f"✅ Generated {len(commands_df)} commands across {num_positions} positions.")
 
375
  except Exception as e:
376
  st.error(f"❌ Error processing file: {e}")
377
  else:
378
+ st.info("👆 Upload an Excel/CSV/TXT file to start generating pipetting commands.")