Spaces:
Sleeping
Sleeping
Update app.py
Browse files
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 |
-
|
| 256 |
-
|
| 257 |
-
plate_num =
|
| 258 |
-
|
| 259 |
-
row_idx =
|
| 260 |
-
|
| 261 |
-
|
| 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 |
-
|
| 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 |
-
|
| 296 |
-
|
| 297 |
-
|
| 298 |
-
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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=["
|
| 326 |
kind="stable"
|
| 327 |
)
|
| 328 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 329 |
|
| 330 |
# --- Source summary ---
|
| 331 |
summary_rows = []
|
| 332 |
for src_idx in range(1, num_positions + 1):
|
| 333 |
-
|
| 334 |
-
|
| 335 |
-
|
| 336 |
-
|
| 337 |
-
|
| 338 |
-
|
| 339 |
-
|
| 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.")
|