Skip to content

Commit

Permalink
Merge pull request #6 from PeterHindes/SPUR-Rosalie
Browse files Browse the repository at this point in the history
This update fixes the gui and makes it look better and work on mac, and work in a more extensible way.
  • Loading branch information
rhughes65 authored Aug 19, 2024
2 parents 76d0a81 + fa4618e commit d644610
Show file tree
Hide file tree
Showing 26 changed files with 386 additions and 160 deletions.
5 changes: 4 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,7 @@ dmypy.json
.pyre/

#vscode
.vscode/
.vscode/

#Excel files
*.xlsx
23 changes: 23 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# This docker file coppies the src folder and installs the dependencies
# we use python:3.12 as the base image
# we also mount the output folder to the WORKDIR/output folder

FROM python:3.12

# Set the working directory
WORKDIR /app

# Copy the current directory contents into the container at /app
COPY ./src /app

# Install any needed packages specified in requirements.txt
RUN pip install --trusted-host pypi.python.org -r requirements.txt

# Create a folder to store the output
RUN mkdir output

# Expose the port the app runs on
EXPOSE 4269

# Run main.py when the container launches
CMD ["python", "main.py"]
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# Xperimental-data-convertor
# Xperimental Data Connector

This is a utility to link excel2sbol and excel to flapjack converter, interlink the resulting data, and upload it to synbiohub and flapjack.

<img src="https://github.com/SynBioDex/Xperimental-Data-Connector/blob/Gonza10V-patch-1/images/xdc%20logo.png" alt="XDC logo" width="250"/>


The Xperimental Data Connector (XDC) is a Python package that links excel2sbol and excel2flapjack, interlink the resulting data, and upload it to synbiohub and flapjack.

This branch is only used to generate an Excel sheet for experiments. It does this by taking the cartesian product of the options selected to generate an experiment that covers all possible variations of the experiment.
9 changes: 9 additions & 0 deletions docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
services:
main:
build:
context: .
dockerfile: Dockerfile
volumes:
- ./output:/app/output
ports:
- "4269:4269"
Binary file removed images/Dependency_structure.pptx
Binary file not shown.
Binary file removed images/Excel_Sheet_Parts.png
Binary file not shown.
Binary file removed images/dependency_structure_20210611.png
Binary file not shown.
Binary file removed images/excel2sbol_spreadsheet.PNG
Binary file not shown.
Binary file removed images/excel2sbol_synbiohub.PNG
Binary file not shown.
Binary file removed images/excel2sbol_xml.PNG
Binary file not shown.
2 changes: 0 additions & 2 deletions requirements.txt

This file was deleted.

19 changes: 19 additions & 0 deletions run-native.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/bash

if [[ "$@" == *"--clean"* ]]; then
rm -r venv
fi

if [ ! -d "venv" ]; then
python3 -m venv venv
. venv/bin/activate
pip install -r src/requirements.txt
else
. venv/bin/activate
fi

if [[ "$@" == *"--debug"* ]]; then
python3 src/main.py --debug
else
python src/main.py
fi
Binary file added src/favicon.ico
Binary file not shown.
106 changes: 106 additions & 0 deletions src/gensheet.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
from datetime import datetime
import itertools
from openpyxl import Workbook
from openpyxl.styles import Font

# Below is excel sheet generation code
# it takes an argument that looks like [{'lb'}, {'top10', 'dh5a'}, {'repressilator'}, {'atc'}]
def excel_sheet(lists, replicates):
product = list(itertools.product(*lists))
wb = Workbook()
sheet1 = wb.active
sheet1.title = "Sample Design"
titles = [
"Sample Design ID",
"Media ID",
"Strain ID",
"Vector ID",
"Supplement ID",
]

# Populate the first row with the titles
for col_num, title in enumerate(titles, start=1):
cell = sheet1.cell(row=1, column=col_num, value=title)
cell.font = Font(bold=True)

global sample_design_id
sample_design_id = 1

# Populating the excel sheet with generated data
for row_num, item in enumerate(product, start=2):
# Sample ID counter for first row
sheet1.cell(row=row_num, column=1, value=f"SampleDesign{sample_design_id}")
sample_design_id += 1

# Continue to populate the rest of the row
for col_num, value in enumerate(item, start=2):
sheet1.cell(row=row_num, column=col_num, value=value)

plate_96_wells_coordinates = [
(row, col) for row in range(1, 9) for col in range(1, 13)
]

def generate_sample_names(num_designs):
global sample_names
sample_names = []
for i in range(0, sample_design_id - 1):
sample_names.append(f"SampleDesign{i+1}")
return sample_names

num_designs = 4
generate_sample_names(num_designs)

# This creates/formats sheet 2 with the samples and their positions in a well plate
sheet2 = wb.create_sheet(title="Sample")

headers = ["Sample ID", "Row", "Column", "Well ID", "Sample Design ID"]

for col_num, header in enumerate(headers, start=1):
sheet2.cell(row=1, column=col_num, value=header)

# Initializes the well count and sample ID count
well_count = 0
sample_id = 1

for sd in sample_names:
for r in range(replicates):
row_data = [
f"Sample{sample_id}",
plate_96_wells_coordinates[well_count][0],
plate_96_wells_coordinates[well_count][1],
f"Assay{+1}",
sd,
]
sheet2.append(row_data)
well_count += 1
sample_id += 1

# Auto fit column width code
for col in sheet1.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = max_length + 2
sheet1.column_dimensions[column].width = adjusted_width

for col in sheet2.columns:
max_length = 0
column = col[0].column_letter
for cell in col:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
adjusted_width = max_length + 2
sheet2.column_dimensions[column].width = adjusted_width

timestamp = datetime.now().strftime("%Y-%m-%d_%H:%M:%S")
filename = f"output/SampleDesign_{timestamp}.xlsx"

wb.save(filename)
66 changes: 66 additions & 0 deletions src/main.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import argparse
from flask import Flask, request, render_template, send_file
import webbrowser
from gensheet import excel_sheet

app = Flask(__name__)

@app.route("/")
@app.route('/index.html')
def index():
return send_file('templates/index.html', mimetype='text/html')

@app.route('/options.json')
def options():
return send_file('options.json', mimetype='application/json')

@app.route('/favicon.ico')
def favicon():
return send_file('favicon.ico', mimetype='image/vnd.microsoft.icon')

# This takes an html form post and prints it to the console returning success to the browser
@app.route('/submit', methods=['POST'])
def submit():
# get the lists from the form
lists = [
request.form.getlist('media'),
request.form.getlist('strain'),
request.form.getlist('supplement'),
request.form.getlist('vector')
]

#check that all lists have at least one item
for l in lists:
if len(l) == 0:
return render_template('response.html',
message="Error, at least one item must be selected from each list, please go back.",
color="red"
)

# get the number of replicates from the form
replicates = int(request.form["replicates"])

# generate the excel sheet
try:
excel_sheet(lists, replicates)
# Below is error handling code
except Exception as e:
return render_template('response.html',
message=f"An error occurred during generation: {str(e)}\n\nPlease select a lower number of replicates and try again.",
color="red"
)

# return success to the browser
return render_template('response.html',
message="Success",
color="green"
)

port = 4269
# webbrowser.open('http://localhost:'+str(port))
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Run Flask app with optional debug mode.')
parser.add_argument('--debug', action='store_true', help='Run the Flask app in debug mode')
args = parser.parse_args()

app.run(debug=args.debug, port=port, host='0.0.0.0')
6 changes: 6 additions & 0 deletions src/options.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"media": ["lb", "m9"],
"strain": ["top10", "dh5a"],
"supplement": ["atc", "iptg"],
"vector": ["repressilator", "toggleswitch"]
}
2 changes: 2 additions & 0 deletions src/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
flask==3.0.3
openpyxl==3.1.5
131 changes: 131 additions & 0 deletions src/templates/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LabGen UI</title>
<style>
#page {
display: flex;
flex-direction: column;
align-items: center;
padding: 25px;
margin: 25px;
border-radius: 15px;
background-color: rgb(0, 55, 77);
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
}
#submit {
background-color: rgb(0, 126, 176);
border-radius: 15px;
border: none;
cursor: pointer;
}
#form {
width: 100%;
}
form {
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
}
form > .section {
background-color: rgb(0, 84, 118);
box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
padding: 10px;
margin: 5px;
border-radius: 10px;
color: wheat;
width: calc(100% - 20px);
}

body {
background-color: rgb(4, 4, 41);
color: wheat;
margin: 0;
}

</style>
</head>
<body>
<div id="page">
<h1>Welcome to LabGen</h1>
<div id="form"></div>
</div>

<script src="https://unpkg.com/react@18/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx"">

// takes the section title and the options for the section
function OptionsSection({title, name, options, selectFirst=false}) {
return (
<div className="section">
<label>{title}</label>
<div className="options">
{options.map((option, index) => (
<label key={name+index}><input type="checkbox" name={name} value={option} autoFocus={(index==0 && selectFirst)} />{option}</label>
))}
</div>
</div>
)
}

function OptionSlider({title, name, min, max, initialVal}) {
const [value, setValue] = React.useState(initialVal);

const handleChange = (event) => {
setValue(event.target.value);
};

return (
<div className="section">
<label>{title}</label>
<div>
<input type="range" name={name} min={min} max={max} defaultValue={value} onChange={handleChange} />
<span>{value}</span>
</div>
</div>
)
}

function Form() {
// Start with empty options list
const [options, setOptions] = React.useState({
"media": [""],
"strain": [""],
"supplement": [""],
"vector": [""]
});


// Fetch the options JSON data when the component mounts
React.useEffect(() => {
fetch('/options.json')
.then(response => response.json())
.then(data => {console.log(data);setOptions(data)})
.catch(error => console.error('Error fetching the JSON data:', error));
}, []);

return (
<form action="/submit" method="post">
<OptionsSection title="Select Media" name="media" options={options.media} selectFirst={true} />
<OptionsSection title="Select a strain" name="strain" options={options.strain} />
<OptionsSection title="Select a supplement" name="supplement" options={options.supplement} />
<OptionsSection title="Select a vector" name="vector" options={options.vector} />

<OptionSlider title="Number of replicates" name="replicates" min="1" max="10" initialVal="1" />

<button className="section" type="submit" id="submit">Submit</button>
</form>
)
}

const form = document.getElementById('form');
const root = ReactDOM.createRoot(form);
root.render(<Form />);
</script>
</body>
</html>
Loading

0 comments on commit d644610

Please sign in to comment.