Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
|
@@ -717,3 +717,180 @@ with tab5:
|
|
| 717 |
st.dataframe(source_volume_df)
|
| 718 |
st.download_button("Download Source Volumes", source_volume_df.to_csv(index=False), "source_total_volumes.csv", key="download_volume_any")
|
| 719 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 717 |
st.dataframe(source_volume_df)
|
| 718 |
st.download_button("Download Source Volumes", source_volume_df.to_csv(index=False), "source_total_volumes.csv", key="download_volume_any")
|
| 719 |
|
| 720 |
+
|
| 721 |
+
|
| 722 |
+
import streamlit as st
|
| 723 |
+
import pandas as pd
|
| 724 |
+
|
| 725 |
+
# === App Title ===
|
| 726 |
+
with tab5:
|
| 727 |
+
st.header("Robot Script Generator")
|
| 728 |
+
|
| 729 |
+
# === Voyager ASCII 6-bit conversion table ===
|
| 730 |
+
voyager_table = {
|
| 731 |
+
i: ch for i, ch in enumerate([
|
| 732 |
+
' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
|
| 733 |
+
'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
|
| 734 |
+
'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2',
|
| 735 |
+
'3', '4', '5', '6', '7', '8', '9', '.', ',', '(',
|
| 736 |
+
')', '+', '-', '*', '/', '=', '$', '!', ':', '%',
|
| 737 |
+
'"', '#', '@', "'", '?', '&'
|
| 738 |
+
])
|
| 739 |
+
}
|
| 740 |
+
reverse_voyager_table = {v: k for k, v in voyager_table.items()}
|
| 741 |
+
|
| 742 |
+
# === Binary ↔ String conversion ===
|
| 743 |
+
def binary_labels_to_string(bits: list[int]) -> str:
|
| 744 |
+
chars = []
|
| 745 |
+
for i in range(0, len(bits), 6):
|
| 746 |
+
chunk = bits[i:i+6]
|
| 747 |
+
if len(chunk) < 6:
|
| 748 |
+
chunk += [0] * (6 - len(chunk))
|
| 749 |
+
val = sum(b << (5 - j) for j, b in enumerate(chunk))
|
| 750 |
+
chars.append(voyager_table.get(val, '?'))
|
| 751 |
+
return ''.join(chars)
|
| 752 |
+
|
| 753 |
+
# === Well mapping ===
|
| 754 |
+
def get_well_position(sample_index):
|
| 755 |
+
row_letter = chr(65 + (sample_index - 1) // 12)
|
| 756 |
+
col_number = ((sample_index - 1) % 12) + 1
|
| 757 |
+
return f"{row_letter}{col_number}"
|
| 758 |
+
|
| 759 |
+
# === Track and replace source if volume exceeded ===
|
| 760 |
+
def track_and_replace_source(source_list, robot_script, volume_limit=150):
|
| 761 |
+
source_volumes = {}
|
| 762 |
+
adjusted_sources = []
|
| 763 |
+
for entry in robot_script:
|
| 764 |
+
src = entry['Source']
|
| 765 |
+
vol = entry['Volume']
|
| 766 |
+
source_volumes[src] = source_volumes.get(src, 0) + vol
|
| 767 |
+
if source_volumes[src] > volume_limit:
|
| 768 |
+
row_letter = src[0]
|
| 769 |
+
col_number = src[1:]
|
| 770 |
+
new_row_letter = chr(ord(row_letter) + 4)
|
| 771 |
+
new_src = f"{new_row_letter}{col_number}"
|
| 772 |
+
entry['Source'] = new_src
|
| 773 |
+
source_volumes[new_src] = source_volumes.get(new_src, 0) + vol
|
| 774 |
+
source_volumes[src] -= vol
|
| 775 |
+
adjusted_sources.append(entry)
|
| 776 |
+
return adjusted_sources, source_volumes
|
| 777 |
+
|
| 778 |
+
# === Fixed D-source transfers ===
|
| 779 |
+
def generate_fixed_d_source_instructions_to_all_samples(n_samples, fixed_volume=16, volume_limit=170):
|
| 780 |
+
d_source_volumes = {}
|
| 781 |
+
d_source_script = []
|
| 782 |
+
current_d_index = 1
|
| 783 |
+
for i in range(n_samples):
|
| 784 |
+
dest = get_well_position(i + 1)
|
| 785 |
+
current_d_well = f"D{current_d_index}"
|
| 786 |
+
d_source_volumes.setdefault(current_d_well, 0)
|
| 787 |
+
|
| 788 |
+
if d_source_volumes[current_d_well] + fixed_volume > volume_limit:
|
| 789 |
+
current_d_index += 1
|
| 790 |
+
current_d_well = f"D{current_d_index}"
|
| 791 |
+
d_source_volumes[current_d_well] = 0
|
| 792 |
+
|
| 793 |
+
d_source_volumes[current_d_well] += fixed_volume
|
| 794 |
+
|
| 795 |
+
# ✅ Updated: use TS_50 if volume >10 µL, else TS_10
|
| 796 |
+
tool = 'TS_50' if fixed_volume > 10 else 'TS_10'
|
| 797 |
+
d_source_script.append({
|
| 798 |
+
'Source': current_d_well,
|
| 799 |
+
'Destination': dest,
|
| 800 |
+
'Volume': fixed_volume,
|
| 801 |
+
'Tool': tool
|
| 802 |
+
})
|
| 803 |
+
|
| 804 |
+
return d_source_script, d_source_volumes
|
| 805 |
+
|
| 806 |
+
# === Source well generation ===
|
| 807 |
+
def generate_source_wells(n):
|
| 808 |
+
wells, rows = [], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
|
| 809 |
+
for i in range(n):
|
| 810 |
+
row, col = rows[i // 12], (i % 12) + 1
|
| 811 |
+
wells.append(f"{row}{col}")
|
| 812 |
+
return wells
|
| 813 |
+
|
| 814 |
+
# === Main UI ===
|
| 815 |
+
st.header("Upload Binary Data (0/1)")
|
| 816 |
+
|
| 817 |
+
binary_file = st.file_uploader("Upload Binary CSV", type=["csv"])
|
| 818 |
+
st.divider()
|
| 819 |
+
|
| 820 |
+
st.subheader("Optional Metadata")
|
| 821 |
+
barcode_id_input = st.text_input("Barcode ID (optional)", value="")
|
| 822 |
+
labware_source_input = st.text_input("Labware Source ID", value="1")
|
| 823 |
+
labware_dest_input = st.text_input("Labware Destination ID", value="1")
|
| 824 |
+
name_input = st.text_input("Name field (optional)", value="")
|
| 825 |
+
volume_limit_input = st.number_input("Maximum Volume per Source Well (µL)", value=150, min_value=10, step=10)
|
| 826 |
+
|
| 827 |
+
# === Load Data ===
|
| 828 |
+
if binary_file:
|
| 829 |
+
df_binary = pd.read_csv(binary_file, header=None)
|
| 830 |
+
df_binary.columns = [str(i+1) for i in range(df_binary.shape[1])]
|
| 831 |
+
else:
|
| 832 |
+
st.info("No file uploaded — manually enter binary data below.")
|
| 833 |
+
df_binary = st.data_editor(
|
| 834 |
+
pd.DataFrame(columns=[str(i) for i in range(1, 33)]),
|
| 835 |
+
num_rows="dynamic", key="manual_input"
|
| 836 |
+
)
|
| 837 |
+
|
| 838 |
+
if not df_binary.empty:
|
| 839 |
+
st.subheader("Binary Matrix")
|
| 840 |
+
st.dataframe(df_binary.style.applymap(lambda v: "background-color: lightgreen" if v == 1 else "background-color: lightcoral"))
|
| 841 |
+
st.download_button("⬇️ Download Binary CSV", df_binary.to_csv(index=False), "binary_matrix.csv")
|
| 842 |
+
|
| 843 |
+
# Decode to string
|
| 844 |
+
decoded = binary_labels_to_string(df_binary.values.flatten().astype(int).tolist())
|
| 845 |
+
st.subheader("Decoded String Output")
|
| 846 |
+
st.code(decoded)
|
| 847 |
+
st.download_button("⬇️ Download Decoded String", decoded, "decoded_string.txt")
|
| 848 |
+
|
| 849 |
+
# === Generate Robot Script ===
|
| 850 |
+
st.divider()
|
| 851 |
+
st.subheader("Generated Robot Script")
|
| 852 |
+
|
| 853 |
+
df_robot = df_binary.copy()
|
| 854 |
+
df_robot.insert(0, 'Sample', range(1, len(df_robot) + 1))
|
| 855 |
+
df_robot['# donors'] = df_robot.iloc[:, 1:].astype(int).sum(axis=1)
|
| 856 |
+
df_robot['volume donors (µL)'] = 64 / df_robot['# donors']
|
| 857 |
+
|
| 858 |
+
robot_script = []
|
| 859 |
+
source_wells = generate_source_wells(df_robot.shape[1] - 1)
|
| 860 |
+
|
| 861 |
+
for i, col in enumerate(df_robot.columns[1:]):
|
| 862 |
+
for _, sample in df_robot.iterrows():
|
| 863 |
+
if int(sample[col]) == 1:
|
| 864 |
+
source = source_wells[i]
|
| 865 |
+
dest = get_well_position(int(sample['Sample']))
|
| 866 |
+
vol = round(sample['volume donors (µL)'], 2)
|
| 867 |
+
|
| 868 |
+
# ✅ Updated: use TS_50 for volumes >10 µL, TS_10 otherwise
|
| 869 |
+
tool = 'TS_50' if vol > 10 else 'TS_10'
|
| 870 |
+
robot_script.append({'Source': source, 'Destination': dest, 'Volume': vol, 'Tool': tool})
|
| 871 |
+
|
| 872 |
+
robot_script, source_volumes = track_and_replace_source(source_wells, robot_script, volume_limit=volume_limit_input)
|
| 873 |
+
d_script, d_volumes = generate_fixed_d_source_instructions_to_all_samples(
|
| 874 |
+
len(df_robot), fixed_volume=16, volume_limit=volume_limit_input
|
| 875 |
+
)
|
| 876 |
+
|
| 877 |
+
full_script = robot_script + d_script
|
| 878 |
+
|
| 879 |
+
robot_script_df = pd.DataFrame(full_script)
|
| 880 |
+
robot_script_df.insert(0, 'Barcode ID', barcode_id_input)
|
| 881 |
+
robot_script_df.insert(1, 'Labware_Source', labware_source_input)
|
| 882 |
+
robot_script_df.insert(3, 'Labware_Destination', labware_dest_input)
|
| 883 |
+
robot_script_df['Name'] = name_input
|
| 884 |
+
robot_script_df = robot_script_df[['Barcode ID', 'Labware_Source', 'Source',
|
| 885 |
+
'Labware_Destination', 'Destination', 'Volume', 'Tool', 'Name']]
|
| 886 |
+
|
| 887 |
+
st.dataframe(robot_script_df)
|
| 888 |
+
st.download_button("⬇️ Download Robot Script", robot_script_df.to_csv(index=False), "robot_script.csv")
|
| 889 |
+
|
| 890 |
+
# === Source Volume Summary ===
|
| 891 |
+
st.divider()
|
| 892 |
+
st.subheader("Total Volume Used Per Source")
|
| 893 |
+
combined_volumes = {**source_volumes, **d_volumes}
|
| 894 |
+
volume_df = pd.DataFrame(list(combined_volumes.items()), columns=['Source', 'Total Volume (µL)'])
|
| 895 |
+
st.dataframe(volume_df)
|
| 896 |
+
st.download_button("⬇️ Download Volume Summary", volume_df.to_csv(index=False), "source_volumes.csv")
|