File size: 8,564 Bytes
c4a6de4
 
580207d
c4a6de4
 
580207d
 
 
c4a6de4
 
 
 
 
 
 
 
 
 
 
 
 
d6e7437
c4a6de4
 
 
 
 
 
 
 
 
 
 
 
d6e7437
 
 
c4a6de4
 
3c883fe
d6e7437
 
 
 
 
c4a6de4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d6e7437
c4a6de4
 
 
 
 
 
 
 
 
d6e7437
c4a6de4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c34e683
 
 
 
 
 
 
 
 
 
 
 
 
c4a6de4
c34e683
 
 
 
c4a6de4
 
c34e683
 
 
 
 
 
c4a6de4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76e2406
 
 
 
c4a6de4
 
 
 
 
3c883fe
76e2406
 
c4a6de4
d6e7437
3c883fe
 
c4a6de4
3c883fe
c4a6de4
 
d6e7437
 
 
 
 
 
 
c4a6de4
 
 
 
 
 
 
 
 
 
 
 
d6e7437
 
 
c4a6de4
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
import streamlit as st
import pandas as pd
import math

# === App Title ===
st.set_page_config(page_title="Robot Script Generator", layout="wide")
st.title("🧪 Robot Script Generator")

# === Voyager ASCII 6-bit conversion table ===
voyager_table = {
    i: ch for i, ch in enumerate([
        ' ', 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I',
        'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S',
        'T', 'U', 'V', 'W', 'X', 'Y', 'Z', '0', '1', '2',
        '3', '4', '5', '6', '7', '8', '9', '.', ',', '(',
        ')', '+', '-', '*', '/', '=', '$', '!', ':', '%',
        '"', '#', '@', "'", '?', '&'
    ])
}
reverse_voyager_table = {v: k for k, v in voyager_table.items()}

# === Binary → String conversion ===
def binary_labels_to_string(bits: list[int]) -> str:
    chars = []
    for i in range(0, len(bits), 6):
        chunk = bits[i:i+6]
        if len(chunk) < 6:
            chunk += [0] * (6 - len(chunk))
        val = sum(b << (5 - j) for j, b in enumerate(chunk))
        chars.append(voyager_table.get(val, '?'))
    return ''.join(chars)

# === Well mapping ===
def get_well_position(sample_index):
    """Convert sample index (1-based) into A1–H12 pattern within its plate"""
    row_letter = chr(65 + ((sample_index - 1) % 96) // 12)  # 8 rows (A–H)
    col_number = ((sample_index - 1) % 12) + 1              # 12 columns
    return f"{row_letter}{col_number}"


def get_plate_id(sample_index):
    """Return Plate number based on 96 samples per plate"""
    plate_number = math.ceil(sample_index / 96)
    return f"Plate {plate_number}"

# === Track and replace source if volume exceeded ===
def track_and_replace_source(source_list, robot_script, volume_limit=150):
    source_volumes = {}
    adjusted_sources = []
    for entry in robot_script:
        src = entry['Source']
        vol = entry['Volume']
        source_volumes[src] = source_volumes.get(src, 0) + vol
        if source_volumes[src] > volume_limit:
            row_letter = src[0]
            col_number = src[1:]
            new_row_letter = chr(ord(row_letter) + 4)
            new_src = f"{new_row_letter}{col_number}"
            entry['Source'] = new_src
            source_volumes[new_src] = source_volumes.get(new_src, 0) + vol
            source_volumes[src] -= vol
        adjusted_sources.append(entry)
    return adjusted_sources, source_volumes

# === Fixed D-source transfers ===
def generate_fixed_d_source_instructions_to_all_samples(n_samples, fixed_volume=16, volume_limit=170):
    d_source_volumes = {}
    d_source_script = []
    current_d_index = 1
    for i in range(n_samples):
        dest = get_well_position(i + 1)
        plate = get_plate_id(i + 1)
        current_d_well = f"D{current_d_index}"
        d_source_volumes.setdefault(current_d_well, 0)
        if d_source_volumes[current_d_well] + fixed_volume > volume_limit:
            current_d_index += 1
            current_d_well = f"D{current_d_index}"
            d_source_volumes[current_d_well] = 0
        d_source_volumes[current_d_well] += fixed_volume
        tool = 'TS_50' if fixed_volume > 10 else 'TS_10'
        d_source_script.append({
            'Plate': plate,
            'Source': current_d_well,
            'Destination': dest,
            'Volume': fixed_volume,
            'Tool': tool
        })
    return d_source_script, d_source_volumes

def generate_source_wells(n):
    wells, rows = [], 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'
    for i in range(n):
        row, col = rows[i // 12], (i % 12) + 1
        wells.append(f"{row}{col}")
    return wells

# === Main UI ===
st.header("Upload Binary Data (0/1)")
binary_file = st.file_uploader("Upload Binary CSV", type=["csv"])
st.divider()

st.subheader("Optional Metadata")
barcode_id_input = st.text_input("Barcode ID (optional)", value="")
labware_source_input = st.text_input("Labware Source ID", value="1")
labware_dest_input = st.text_input("Labware Destination ID", value="1")
name_input = st.text_input("Name field (optional)", value="")
volume_limit_input = st.number_input("Maximum Volume per Source Well (µL)", value=150, min_value=10, step=10)

# === Load Data ===
if binary_file:
    df_binary = pd.read_csv(binary_file, header=None)
    df_binary.columns = [str(i+1) for i in range(df_binary.shape[1])]
else:
    st.info("No file uploaded — manually enter or paste your binary data below.")
    st.caption("💡 Tip: You can copy and paste entire datasets (Ctrl+V) directly here — rows and columns will adjust automatically.")

    # Ask the user how many columns (if they want to define manually)
    n_cols = st.number_input(
        "Number of columns (adjust if pasting a dataset)", 
        min_value=1, value=8, step=1
    )

    # Create an empty DataFrame with that many columns
    initial_df = pd.DataFrame(columns=[str(i) for i in range(1, n_cols + 1)])

    # Dynamic editor — allows copy-paste of arbitrary rows
    df_binary = st.data_editor(
        initial_df,
        num_rows="dynamic",
        key="manual_input",
        use_container_width=True,
    )

    # If user pastes a larger dataset (more columns than n_cols)
    # detect and rename automatically
    if not df_binary.empty:
        n_detected = df_binary.shape[1]
        df_binary.columns = [str(i+1) for i in range(n_detected)]

if not df_binary.empty:
    st.subheader("Binary Matrix")
    st.dataframe(df_binary.style.applymap(lambda v: "background-color: lightgreen" if v == 1 else "background-color: lightcoral"))
    st.download_button("⬇️ Download Binary CSV", df_binary.to_csv(index=False), "binary_matrix.csv")

    # Decode to string
    decoded = binary_labels_to_string(df_binary.values.flatten().astype(int).tolist())
    st.subheader("Decoded String Output")
    st.code(decoded)
    st.download_button("⬇️ Download Decoded String", decoded, "decoded_string.txt")

    # === Generate Robot Script ===
    st.divider()
    st.subheader("Generated Robot Script")

    df_robot = df_binary.copy()
    df_robot.insert(0, 'Sample', range(1, len(df_robot) + 1))
    df_robot['# donors'] = df_robot.iloc[:, 1:].astype(int).sum(axis=1)
    df_robot['volume donors (µL)'] = df_robot['# donors'].apply(
        lambda x: 64 / x if x > 0 else 0
    )


    robot_script = []
    source_wells = generate_source_wells(df_robot.shape[1] - 1)

    for i, col in enumerate(df_robot.columns[1:]):
        for row_idx, sample in df_robot.iterrows():
            if sample['# donors'] == 0:
                continue  # skip samples with no donors
            if int(sample[col]) == 1:
                sample_id = int(sample['Sample'])
                sample_index = row_idx + 1
                plate = get_plate_id(sample_index)
                source = source_wells[i]
                dest = get_well_position(sample_index)
                vol = round(sample['volume donors (µL)'], 2)
                tool = 'TS_50' if vol > 10 else 'TS_10'
                robot_script.append({
                    'Plate': plate,  # ✅ New Column
                    'Source': source,
                    'Destination': dest,
                    'Volume': vol,
                    'Tool': tool
                })

    robot_script, source_volumes = track_and_replace_source(source_wells, robot_script, volume_limit=volume_limit_input)
    d_script, d_volumes = generate_fixed_d_source_instructions_to_all_samples(
        len(df_robot), fixed_volume=16, volume_limit=volume_limit_input
    )
    full_script = robot_script + d_script

    robot_script_df = pd.DataFrame(full_script)
    robot_script_df.insert(0, 'Barcode ID', barcode_id_input)
    robot_script_df.insert(1, 'Labware_Source', labware_source_input)
    robot_script_df.insert(3, 'Labware_Destination', labware_dest_input)
    robot_script_df['Name'] = name_input
    robot_script_df = robot_script_df[['Barcode ID', 'Labware_Source', 'Plate',
                                       'Source', 'Labware_Destination', 'Destination',
                                       'Volume', 'Tool', 'Name']]

    st.dataframe(robot_script_df)
    st.download_button("⬇️ Download Robot Script", robot_script_df.to_csv(index=False), "robot_script.csv")

    # === Source Volume Summary ===
    st.divider()
    st.subheader("Total Volume Used Per Source")
    combined_volumes = {**source_volumes, **d_volumes}
    volume_df = pd.DataFrame(list(combined_volumes.items()), columns=['Source', 'Total Volume (µL)'])
    st.dataframe(volume_df)
    st.download_button("⬇️ Download Volume Summary", volume_df.to_csv(index=False), "source_volumes.csv")