diff --git a/README.md b/README.md index f1e25af78e1fc2a3f25820f2ba7fe8b05f83577e..5a735fe88fb68b35d4e7cefabde48e69684ffd35 100755 --- a/README.md +++ b/README.md @@ -19,3 +19,7 @@ Added burden UI, enabled selection of commonly used basic parts and search for p # Note on 2024.9.23 Added burden calculation, and users can now select parts from PartHub database (i.e. the iGEM registry). + +# Note on 2024.9.25 + +Added part file uploading functionality, and users can now upload their own parts and search for similar parts in PartHub database. diff --git a/app.py b/app.py index 10e7bddd9973860da7b1806f17b46c3b7d1209cc..d256e7b857186470b6bdbfd10f08f4ab64367324 100755 --- a/app.py +++ b/app.py @@ -80,21 +80,33 @@ def allowed_file(filename): return '.' in filename and \ filename.rsplit('.', 1)[1].lower() in ALLOWED_EXTENSIONS -@app.route('/api/parthub/upload_part_file', methods=['POST']) -# handle uploaded file in genbank or fasta format using Biopython -def handle_upload_part_file(): +def handle_upload_part_file(request, part_type): if 'file' not in request.files: app.logger.warning('Missing file') return jsonify({"message": "Missing file"}), 400 file = request.files['file'] if file and allowed_file(file.filename): filename = secure_filename(file.filename) - file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename)) - return parse_part_file(filename) + file_path = os.path.join(app.config['UPLOAD_FOLDER'], filename) + file.save(file_path) + return parse_part_file(file_path, part_type) else: app.logger.warning('Invalid file type') return jsonify({"message": "Invalid file type"}), 400 +@app.route('/api/parthub/upload_part_file/promoter', methods=['POST']) +def handle_upload_promoter_file(): + return handle_upload_part_file(request, 'promoter') + +@app.route('/api/parthub/upload_part_file/rbs', methods=['POST']) +def handle_upload_rbs_file(): + return handle_upload_part_file(request, 'rbs') + +@app.route('/api/parthub/upload_part_file/cds', methods=['POST']) +def handle_upload_cds_file(): + return handle_upload_part_file(request, 'cds') + + @app.route('/api/parthub/search', methods=['POST']) def handle_parthub_search(): data = request.json diff --git a/burden/burden_calculator.py b/burden/burden_calculator.py index 1c08e3ddbf554b30fde9de1d01e9c83e3de8f0c2..6198682b5db09849b338021a1e65cbed796fcb17 100644 --- a/burden/burden_calculator.py +++ b/burden/burden_calculator.py @@ -46,12 +46,15 @@ def burden_calculator(copy_number: float, prom_strength: float, tl_units: list[t ns = 0.5 # - heterologous wic = (copy_number / beta_copy_number) * (prom_strength / beta_prom) - rbsH = rbs_strengths * beta_rbs - nic = len_aa # BFP (GFP:238) - thetaic = 4.38 * np.ones(len(rbsH)) - Ric = np.ones(len(rbsH)) - gmaxic = 1260 * np.ones(len(rbsH)) - parameters = (thetar, s0, gmax, thetax, Kt, M, we, Km, vm, nx, Kq, Kg, vt, wr, wq, wic, nq, nr, ns, nic, thetaic, Ric, gmaxic) + rbsH = rbs_strengths * beta_rbs # array + nic = len_aa # array + thetaic = 4.38 * np.ones(len(rbsH)) # array + Ric = np.ones(len(rbsH)) # array + gmaxic = 1260 * np.ones(len(rbsH)) # array + parameters = [thetar, s0, gmax, thetax, Kt, M, we, Km, vm, nx, Kq, Kg, vt, wr, wq, wic, nq, nr, ns] + for i in nic, thetaic, Ric, gmaxic: + for j in i: + parameters.append(j) # define rate constants # - endogeneous @@ -60,11 +63,14 @@ def burden_calculator(copy_number: float, prom_strength: float, tl_units: list[t kb = 0.0095 ku = 1.0 # - heterologous - kbic = 1e-2 * rbsH - kuic = 1e-2 * np.ones(len(rbsH)) - dmic = np.log(2) / 2 * np.ones(len(rbsH)) - dpic = np.log(2) / 4 * np.ones(len(rbsH)) - rates = (b, dm, kb, ku, kbic, kuic, dmic, dpic) + kbic = 1e-2 * rbsH # array + kuic = 1e-2 * np.ones(len(rbsH)) # array + dmic = np.log(2) / 2 * np.ones(len(rbsH)) # array + dpic = np.log(2) / 4 * np.ones(len(rbsH)) # array + rates = [b, dm, kb, ku] + for i in kbic, kuic, dmic, dpic: + for j in i: + rates.append(j) # define initial conditions rmr_0 = 0 @@ -82,10 +88,13 @@ def burden_calculator(copy_number: float, prom_strength: float, tl_units: list[t r_0 = 10.0 a_0 = 1000.0 lam_0 = 0 - mic_0 = 0 - rmic_0 = 0 - pic_0 = 0 - init = (rmr_0, em_0, rmq_0, rmt_0, et_0, rmm_0, mt_0, mm_0, q_0, si_0, mq_0, mr_0, mic_0, rmic_0, pic_0, r_0, a_0, lam_0) + mic_0 = np.zeros(len(rbsH)) # array + rmic_0 = np.zeros(len(rbsH)) # array + pic_0 = np.zeros(len(rbsH)) # array + init = [rmr_0, em_0, rmq_0, rmt_0, et_0, rmm_0, mt_0, mm_0, q_0, si_0, mq_0, mr_0, r_0, a_0, lam_0] + for i in mic_0, rmic_0, pic_0: + for j in i: + init.append(j) # call solver routine t0 = 0 @@ -106,10 +115,10 @@ def burden_calculator(copy_number: float, prom_strength: float, tl_units: list[t si = y[9] mq = y[10] mr = y[11] - mic = y[12] - rmic = y[13] - pic = y[14] - r = y[15] - a = y[16] - lam = y[17] + r = y[12] + a = y[13] + lam = y[14] + mic = y[15:15+len(rbsH)] + rmic = y[15+len(rbsH):15+2*len(rbsH)] + pic = y[15+2*len(rbsH):15+3*len(rbsH)] return 1 - lam[-1] / growth_WT diff --git a/burden/cellmodel.py b/burden/cellmodel.py index 4e496c3ccce8a8c3bb4c2102e9f832b8aa84f222..96249b50cd8700ce725468aeed8049d9494050b9 100644 --- a/burden/cellmodel.py +++ b/burden/cellmodel.py @@ -15,14 +15,16 @@ import numpy as np def cellmodel_odes(t, y, rates, parameters): + assert len(rates) % 4 == 0 + n_cds = (len(rates) - 4) // 4 b = rates[0] dm = rates[1] kb = rates[2] ku = rates[3] - kbic = rates[4] - kuic = rates[5] - dmic = rates[6] - dpic = rates[7] + kbic = np.array(rates[4:4+n_cds]) + kuic = np.array(rates[4+n_cds:4+2*n_cds]) + dmic = np.array(rates[4+2*n_cds:4+3*n_cds]) + dpic = np.array(rates[4+3*n_cds:4+4*n_cds]) thetar = parameters[0] s0 = parameters[1] @@ -43,10 +45,10 @@ def cellmodel_odes(t, y, rates, parameters): nq = parameters[16] nr = parameters[17] ns = parameters[18] - nic = parameters[19] - thetaic = parameters[20] - Ric = parameters[21] - gmaxic = parameters[22] + nic = np.array(parameters[19:19+n_cds]) + thetaic = np.array(parameters[19+n_cds:19+2*n_cds]) + Ric = np.array(parameters[19+2*n_cds:19+3*n_cds]) + gmaxic = np.array(parameters[19+3*n_cds:19+4*n_cds]) rmr = y[0] em = y[1] @@ -60,16 +62,16 @@ def cellmodel_odes(t, y, rates, parameters): si = y[9] mq = y[10] mr = y[11] - mic = y[12] - rmic = y[13] - pic = y[14] - r = y[15] - a = y[16] + r = y[12] + a = y[13] + mic = np.array(y[15:15+n_cds]) + rmic = np.array(y[15+n_cds:15+2*n_cds]) + pic = np.array(y[15+2*n_cds:15+3*n_cds]) gamma = gmax * a / (Kg + a) gammaic = gmaxic * a / (Kg + a) ttrate = (rmq + rmr + rmt + rmm) * gamma - lam = (ttrate + gamma * rmic) / M + lam = (ttrate + gamma * np.sum(rmic)) / M vcat = em * vm * si / (Km + si) dydt = np.zeros(len(y)) @@ -85,11 +87,11 @@ def cellmodel_odes(t, y, rates, parameters): dydt[9] = (et * vt * s0 / (Kt + s0)) - vcat - lam * si dydt[10] = (wq * a / (thetax + a) / (1 + (q / Kq) ** nq)) + ku * rmq + gamma / nx * rmq - kb * r * mq - dm * mq - lam * mq dydt[11] = (wr * a / (thetar + a)) + ku * rmr + gamma / nr * rmr - kb * r * mr - dm * mr - lam * mr - dydt[12] = (wic * a / (thetaic + a) * Ric) + kuic * rmic - kbic * r * mic + gammaic / nic * rmic - dmic * mic - lam * mic - dydt[13] = kbic * r * mic - kuic * rmic - gammaic / nic * rmic - lam * rmic - dydt[14] = gammaic / nic * rmic - lam * pic - dpic * pic - dydt[15] = ku * rmr + ku * rmt + ku * rmm + ku * rmq + gamma / nr * rmr + gamma / nr * rmr + gamma / nx * rmt + gamma / nx * rmm + gamma / nx * rmq - kb * r * mr - kb * r * mt - kb * r * mm - kb * r * mq - lam * r + np.sum(gammaic / nic * rmic - kbic * r * mic + kuic * rmic) - dydt[16] = ns * vcat - ttrate - np.sum(gammaic * rmic) - lam * a - dydt[17] = lam + dydt[12] = ku * rmr + ku * rmt + ku * rmm + ku * rmq + gamma / nr * rmr + gamma / nr * rmr + gamma / nx * rmt + gamma / nx * rmm + gamma / nx * rmq - kb * r * mr - kb * r * mt - kb * r * mm - kb * r * mq - lam * r + np.sum(gammaic / nic * rmic - kbic * r * mic + kuic * rmic) + dydt[13] = ns * vcat - ttrate - np.sum(gammaic * rmic) - lam * a + dydt[14] = lam + dydt[15:15+n_cds] = (wic * a / (thetaic + a) * Ric) + kuic * rmic - kbic * r * mic + gammaic / nic * rmic - dmic * mic - lam * mic + dydt[15+n_cds:15+2*n_cds] = kbic * r * mic - kuic * rmic - gammaic / nic * rmic - lam * rmic + dydt[15+2*n_cds:15+3*n_cds] = gammaic / nic * rmic - lam * pic - dpic * pic return dydt \ No newline at end of file diff --git a/burden/config.py b/burden/config.py index 7a59ffe876519b0fa523dcf5db65f025fa4c1aad..68d0bd5e0df5904af4e79eec2660900d46ffb0ac 100644 --- a/burden/config.py +++ b/burden/config.py @@ -7,10 +7,10 @@ RBS_CDS_SCAR = 'TACTAG' # fitted parameters growth_WT = 20246867.33991941 -beta_prom = 35.75 -beta_rbs = 10.92073672 -K_prom = 3.229389306029437 -B_prom = 3428.628874021221 +beta_prom = 69.49128805 +beta_rbs = 50.31487379 +K_prom = 4.50839026 +B_prom = 1277.41 K_rbs = 0.21170254 b_rbs = 0.24290994 beta_copy_number = 200 \ No newline at end of file diff --git a/flask-compose.sh b/flask-compose.sh index 0bc6d1bfa01c3b8d2b573bf159d4c92227ca0c06..a83e0070403b04f574f05ee2ca98654b24e67c53 100644 --- a/flask-compose.sh +++ b/flask-compose.sh @@ -1,4 +1,6 @@ # wget https://ftp.ncbi.nlm.nih.gov/blast/executables/blast+/LATEST/ncbi-blast-2.16.0+-x64-linux.tar.gz # TO BE MODIFIED +mkdir /app/uploads +mkdir /app/similarity/data python parthub/upload_collections.py if [ $? -ne 0 ]; then @@ -8,7 +10,5 @@ else tar -zxvf ncbi-blast-2.16.0+-x64-linux.tar.gz mv ncbi-blast-2.16.0+ blast+ ./blast+/bin/makeblastdb -in similarity/data/seqdump.fasta -dbtype nucl - mkdir /app/uploads - mkdir /app/similarity/data python app.py --host=0.0.0.0 fi \ No newline at end of file diff --git a/similarity/utils.py b/similarity/utils.py index 1d05cb51cc1a92772e285fce54f356a6d710ccc8..70b9dabe433eba0c8fb1cbcf020688a71d6fd907 100644 --- a/similarity/utils.py +++ b/similarity/utils.py @@ -7,6 +7,8 @@ from Bio import SeqIO from Bio.Seq import Seq from Bio.SeqRecord import SeqRecord from flask import jsonify +from datetime import datetime +from time import time graph = Graph("bolt://parthub:7687", auth=("neo4j", "igem2024"), name="neo4j") node_matcher = NodeMatcher(graph) @@ -63,7 +65,7 @@ def query_similarity(curPart: str): # Calculate the similarity score try: - max_bs = float(df[(df.iloc[:, 1] == curPart) & (df.iloc[:, 3] == len(curSeq))].iloc[0, 11]) + max_bs = float(df.iloc[0, 11]) except: return None results = [] @@ -71,7 +73,7 @@ def query_similarity(curPart: str): matchedNode = node_matcher.match("Part", number=part).first() if matchedNode is None: continue - res_dict = {"part": part, "seq_score": 0.0, 'cat_score': 0.0} + res_dict = {"part": part, "name": matchedNode["name"], "seq_score": 0.0, 'cat_score': 0.0} for i in df[df.iloc[:, 1] == part].index: identity = float(df.iloc[i, 2]) bit_score = float(df.iloc[i, 11]) @@ -96,12 +98,20 @@ def query_similarity(curPart: str): graph.run(query) return results -def parse_part_file(filename: str): +def parse_part_file(filename: str, part_type: str): + if part_type != 'promoter': + part_type = part_type.upper() file_format = filename.rsplit('.', 1)[1].lower() try: records = list(SeqIO.parse(filename, file_format)) record = records[0] - seq = str(record.seq) - category = record.description.split(' ')[1] + seq = str(record.seq).upper() except: - return jsonify({"message": "File parse error. Please check the file format."}), 400 \ No newline at end of file + return jsonify({"message": "File parse error. Please check the file format."}), 400 + now = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + curtime = int((time()-1727325400)*1000) + query = "CREATE (n:Part{number:'New_part_" + str(curtime) + "',name:'New_part_" + now + \ + "',type:'" + part_type + "',sequence:'" + seq + "',contents:'',length:" + str(len(seq)) + \ + ",date:'',team:'User',designer:'User',category:'',url:''})" + graph.run(query) + return jsonify({"part_id": "New_part_" + str(curtime)}), 200 \ No newline at end of file diff --git a/webUI/src/views/burden/burden.vue b/webUI/src/views/burden/burden.vue index aef6d386fee868f94b3924bed710d8fc1a1dcbfb..b3ff6c5fc4a40e487999fbc2b73cc977d1ac482a 100755 --- a/webUI/src/views/burden/burden.vue +++ b/webUI/src/views/burden/burden.vue @@ -165,15 +165,13 @@ if (!regex.test(values.query)) { <a-form-item mode="horizontal"> <a-tooltip placement="bottomLeft"> <template #title> - <b - >Some average copy numbers for low, medium, and high - copy number plasmids:</b - > + <b>Copy numbers for common plasmids:</b> <br /> - <b>Low:</b> 15-20 copies<br /> - <b>Medium:</b> 20-100 copies<br /> - <b>High:</b> 500-700 copies<br /> + <b>Average Low:</b> 15-20 copies<br /> + <b>Average Medium:</b> 20-100 copies<br /> + <b>Average High:</b> 500-700 copies<br /> <b>pSB1C3, pSB1A2:</b> 100-300 copies<br /> + <b>J61002:</b> 25-30 copies<br /> </template> <a-input v-model:value="formState.copy_number" diff --git a/webUI/src/views/parthub/parthub.vue b/webUI/src/views/parthub/parthub.vue index 719712f5ea450e2944bc08270176f660542b7ddb..1b7511c89be9ff518be255f98de2f4cdae1dc6cf 100755 --- a/webUI/src/views/parthub/parthub.vue +++ b/webUI/src/views/parthub/parthub.vue @@ -13,7 +13,14 @@ alignItems: 'center', }" > - <div style="text-align: center; height: 85vh; width: 40%"> + <div + style=" + text-align: center; + height: 85vh; + width: 40%; + margin-top: 4vh; + " + > <a-form :model="formState" @finish="onFinish(formState)" @@ -26,7 +33,11 @@ { required: true, message: 'Please input your query!' }, ]" > - <a-input v-model:value="formState.query" placeholder="..."> + <a-input + v-model:value="formState.query" + style="width: 80%" + placeholder="Enter search content..." + > </a-input> <a-button slot="suffix" type="primary" html-type="submit"> Search @@ -51,6 +62,40 @@ </a-radio-group> </a-form-item> </a-form> + <p style="margin-top: 10vh; margin-bottom: 2vh"> + To search for similar parts of your own part, <br /> + please upload a part sequence file from your computer: + </p> + <a-select + v-model:value="uploadPartType" + style="width: 80%; margin-top: 2vh" + @focus="focus" + placeholder="Select part type" + > + <a-select-option value="promoter">promoter</a-select-option> + <a-select-option value="rbs">RBS</a-select-option> + <a-select-option value="cds">CDS</a-select-option> + </a-select> + <a-upload-dragger + v-model:fileList="fileList" + name="file" + :max-count="1" + :multiple="false" + :disabled="!uploadPartType" + :action="'/api/parthub/upload_part_file/' + uploadPartType" + @change="handleChange" + style="margin-top: 2vh; max-height: 25vh; display: block" + > + <p class="ant-upload-drag-icon"> + <InboxOutlined /> + </p> + <p class="ant-upload-text"> + Click or drag file to this area to upload + </p> + <p class="ant-upload-hint"> + Please upload a single .gb or .fasta file + </p> + </a-upload-dragger> </div> </div> </a-layout-content> @@ -66,6 +111,7 @@ import headermenu from "@/components/headermenu.vue"; import { reactive } from "vue"; import { message } from "ant-design-vue"; +import { InboxOutlined } from "@ant-design/icons-vue"; const formState = reactive({ query: "", type: "", @@ -73,12 +119,15 @@ const formState = reactive({ export default { components: { headermenu, + InboxOutlined, }, data() { return { searchType: "id", formState, defaultActivate: ["3"], + fileList: [], + uploadPartType: null, }; }, methods: { @@ -98,6 +147,19 @@ export default { onFinishFailed(errorInfo) { console.log(errorInfo); }, + handleChange(info) { + const status = info.file.status; + if (status !== "uploading") { + console.log(info.file, info.fileList); + } + if (status === "done") { + message.success(`${info.file.name} file uploaded successfully.`); + localStorage.setItem("curPart", info.file.response.part_id); + window.location.href = "/treemap"; + } else if (status === "error") { + message.error(`${info.file.name} file upload failed.`); + } + }, }, }; </script> diff --git a/webUI/src/views/treeMap/treeMap.vue b/webUI/src/views/treeMap/treeMap.vue index fa53fffb44cb48ea36fff903a8ac8c0a7542a092..ae9441219dff5a36445650244a9a07d13aed39f5 100755 --- a/webUI/src/views/treeMap/treeMap.vue +++ b/webUI/src/views/treeMap/treeMap.vue @@ -11,7 +11,6 @@ display: 'flex', flexDirection: 'row', justifyContent: 'flex-start', - alignItems: 'center', }" > <div style="margin-left: 1rem; width: 60%"> @@ -44,19 +43,61 @@ </div> </div> <div style="margin-left: 1rem; width: 35%"> - <a-card> - <p slot="title" id="title"></p> - <p id="name"></p> - <p id="type"></p> - <p id="date"></p> - <p id="team"></p> - <p id="designer"></p> - <p id="length"></p> - <p id="contents"></p> - <p id="category"></p> - <a id="sequence"> <DownloadOutlined /> Download sequence </a> - <a id="url"> <LinkOutlined /> View in iGEM Parts Registry </a> - </a-card> + <a-tabs v-model:activeKey="activeKey"> + <a-tab-pane key="1" tab="Similarity"> + <a-list + item-layout="horizontal" + :data-source="similarNodes" + style="max-height: 90vh; overflow-y: auto" + > + <template #renderItem="{ item }"> + <a-list-item> + <a + :href=" + 'https://parts.igem.org/wiki/index.php?title=Part:' + + item.part + " + > + <h3 style="color: #e37654">{{ item.part }}</h3> + </a> + {{ + item.name.length <= 70 + ? item.name + : item.name.slice(0, 67) + "..." + }} + <br /> + <b>Overall similarity:</b> {{ + item.overall_score.toFixed(2) + }} + <br /> + <b>Sequence similarity:</b> {{ + item.seq_score.toFixed(2) + }} + <br /> + <b>Category similarity:</b> {{ + item.cat_score.toFixed(2) + }} + <br /> + </a-list-item> + </template> + </a-list> + </a-tab-pane> + <a-tab-pane key="2" tab="Part/Relationship info" force-render> + <a-card style="max-height: 90vh; overflow-y: auto"> + <p slot="title" id="title"></p> + <p id="name"></p> + <p id="type"></p> + <p id="date"></p> + <p id="team"></p> + <p id="designer"></p> + <p id="length"></p> + <p id="contents"></p> + <p id="category"></p> + <a id="sequence"> <DownloadOutlined /> Download sequence </a> + <a id="url"> <LinkOutlined /> View in iGEM Parts Registry </a> + </a-card> + </a-tab-pane> + </a-tabs> </div> </div> </a-layout-content> @@ -162,6 +203,7 @@ export default { node: null, similarNodes: null, similarNodes_graph: null, + activeKey: "1", }; }, mounted() { @@ -174,8 +216,10 @@ export default { curPart: this.curPart, }) .then((response) => { - this.similarNodes = response.data.result.map((item) => item.part); - this.similarNodes_graph = this.similarNodes.slice(0, 30); + this.similarNodes = response.data.result; + this.similarNodes_graph = this.similarNodes + .map((item) => item.part) + .slice(0, 30); this.similarNodes_graph.push(this.curPart); this.similarNodes_graph = JSON.stringify(this.similarNodes_graph); this.draw();