├── .dockerignore ├── .gitattributes ├── .github ├── scripts │ ├── deploy-lambda-version.py │ └── prune-lambda-versions.py └── workflows │ ├── build-push-and-deploy.yml │ ├── deploy.yml │ ├── e2e.yml │ ├── pr.yml │ └── test.yml ├── .gitignore ├── .vscode └── settings.json ├── Dockerfile ├── LICENSE ├── README.md ├── TERMS.md ├── biome.json ├── docker-compose.yaml ├── e2e ├── nav.spec.ts └── optimize.spec.ts ├── frontend ├── .gitignore ├── README.md ├── index.html ├── package.json ├── postcss.config.cjs ├── public │ └── favicon.png ├── src │ ├── App.tsx │ ├── api │ │ └── index.ts │ ├── assets │ │ └── react.svg │ ├── components │ │ ├── logos │ │ │ ├── BASELogo.tsx │ │ │ └── BASELogoSmall.tsx │ │ └── pages │ │ │ └── optimize │ │ │ ├── Help.tsx │ │ │ ├── InputForm.tsx │ │ │ ├── OptimizePage.tsx │ │ │ ├── Output.tsx │ │ │ ├── ProgressLoader.tsx │ │ │ ├── TermsAndConditions.tsx │ │ │ ├── inputs │ │ │ ├── ParameterInput.tsx │ │ │ └── SequenceInput.tsx │ │ │ └── types.ts │ ├── constants │ │ └── index.ts │ ├── data │ │ ├── ref_codon_usage.txt │ │ └── restriction-sites.json │ ├── main.tsx │ ├── types │ │ ├── optimize.ts │ │ ├── sequence.ts │ │ ├── util.test.ts │ │ └── util.ts │ ├── utils │ │ └── sequence.ts │ └── vite-env.d.ts ├── tsconfig.app.json ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts ├── justfile ├── package.json ├── playwright.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── pyproject.toml ├── src └── mrnarchitect │ ├── __init__.py │ ├── app.py │ ├── cli.py │ ├── constants │ ├── __init__.py │ ├── codon_table.py │ └── sequences.py │ ├── data │ ├── __init__.py │ ├── codon-pair │ │ ├── .~lock.human.csv# │ │ └── human.csv │ ├── codon-tables │ │ ├── homo-sapiens.json │ │ └── mus-musculus.json │ ├── rare-codons.csv │ └── tAI │ │ ├── hg38-tRNAs.bed │ │ └── mm39-tRNAs.bed │ ├── organism.py │ ├── py.typed │ ├── sequence │ ├── __init__.py │ ├── manufacture-restriction-sites.txt │ ├── microRNAs.txt │ ├── optimize.py │ ├── sequence.py │ └── specifications │ │ ├── __init__.py │ │ ├── constraints.py │ │ └── objectives.py │ ├── tests │ ├── __init__.py │ ├── sequence │ │ ├── __init__.py │ │ ├── test_minimum_free_energy.py │ │ └── test_sequence.py │ ├── test_app.py │ └── test_cli.py │ ├── types.py │ └── utils │ ├── analysis.py │ └── fasta.py └── uv.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | .git 3 | .github 4 | .gitignore 5 | .nitro 6 | .output 7 | .pytest_cache 8 | .ruff-cache 9 | .tanstack 10 | .venv 11 | .vscode 12 | dist 13 | node_modules 14 | *.egg-info 15 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseUQ/mRNArchitect/500e9fbcc8298dba0c0fc79feb2ee951cbc7f43a/.gitattributes -------------------------------------------------------------------------------- /.github/scripts/deploy-lambda-version.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # dependencies = [ 3 | # "boto3" 4 | # ] 5 | # /// 6 | 7 | import argparse 8 | import json 9 | import logging 10 | 11 | import boto3 12 | 13 | LOG = logging.getLogger(__name__) 14 | 15 | FUNCTION_NAME = "app" 16 | 17 | if __name__ == "__main__": 18 | parser = argparse.ArgumentParser() 19 | parser.add_argument("--image-uri", required=True, type=str) 20 | parser.add_argument("--function-alias", required=True, type=str) 21 | 22 | args = parser.parse_args() 23 | image_uri = args.image_uri 24 | function_alias = args.function_alias 25 | 26 | lambda_client = boto3.client("lambda") 27 | 28 | LOG.info("Updating function code.") 29 | version = lambda_client.update_function_code( 30 | FunctionName=FUNCTION_NAME, ImageUri=image_uri, Publish=True 31 | )["Version"] 32 | 33 | LOG.info(f"Waiting for published version: {version}") 34 | waiter = lambda_client.get_waiter("published_version_active") 35 | waiter.wait(FunctionName=FUNCTION_NAME, Qualifier=version) 36 | 37 | try: 38 | LOG.info(f"Attempting to update existing alias: {function_alias}") 39 | lambda_client.update_alias( 40 | FunctionName=FUNCTION_NAME, Name=function_alias, FunctionVersion=version 41 | ) 42 | except lambda_client.exceptions.ResourceNotFoundException: 43 | LOG.info(f"Alias not found, creating new alias: {function_alias}") 44 | lambda_client.create_alias( 45 | FunctionName=FUNCTION_NAME, Name=function_alias, FunctionVersion=version 46 | ) 47 | lambda_client.create_function_url_config( 48 | FunctionName=FUNCTION_NAME, 49 | Qualifier=function_alias, 50 | AuthType="NONE", 51 | ) 52 | lambda_client.add_permission( 53 | FunctionName=FUNCTION_NAME, 54 | Qualifier=function_alias, 55 | StatementId="FunctionURLAllowPublicAccess", 56 | Action="lambda:InvokeFunctionUrl", 57 | Principal="*", 58 | FunctionUrlAuthType="NONE", 59 | ) 60 | 61 | function_url = lambda_client.get_function_url_config( 62 | FunctionName=FUNCTION_NAME, Qualifier=function_alias 63 | )["FunctionUrl"] 64 | 65 | print(json.dumps({"version": version, "function_url": function_url}), end="") 66 | -------------------------------------------------------------------------------- /.github/scripts/prune-lambda-versions.py: -------------------------------------------------------------------------------- 1 | # /// script 2 | # dependencies = [ 3 | # "boto3", 4 | # "PyGithub" 5 | # ] 6 | # /// 7 | 8 | import argparse 9 | import datetime 10 | import os 11 | 12 | import boto3 13 | import github 14 | 15 | FUNCTION_NAME = "app" 16 | 17 | ALIASES_TO_KEEP = ["PRODUCTION", "TEST"] 18 | 19 | if __name__ == "__main__": 20 | parser = argparse.ArgumentParser( 21 | description="Prune lambda versions/aliases that are no longer in use." 22 | ) 23 | parser.add_argument("repo", type=str, help="The GitHub repository name.") 24 | args = parser.parse_args() 25 | 26 | github_token = os.getenv("GH_TOKEN") 27 | if not github_token: 28 | raise RuntimeError( 29 | "Github token not available. Ensure the the GH_TOKEN env var is set." 30 | ) 31 | 32 | github_client = github.Github(auth=github.Auth.Token(github_token)) 33 | repo = github_client.get_repo(args.repo) 34 | open_pull_requests = repo.get_pulls(state="open") 35 | open_pull_request_numbers = [str(pr.number) for pr in open_pull_requests] 36 | 37 | lambda_client = boto3.client("lambda") 38 | 39 | versions = lambda_client.list_versions_by_function(FunctionName=FUNCTION_NAME)[ 40 | "Versions" 41 | ] 42 | print(f"Found {len(versions)} versions.") 43 | 44 | aliases = lambda_client.list_aliases(FunctionName=FUNCTION_NAME)["Aliases"] 45 | print(f"Found {len(aliases)} aliases.") 46 | 47 | deleted_count = 0 48 | for version in versions: 49 | if version["Version"] == "$LATEST": 50 | print(f"Keeping {version['Version']} version.") 51 | continue 52 | 53 | version_aliases = [ 54 | a for a in aliases if a["FunctionVersion"] == version["Version"] 55 | ] 56 | if any( 57 | a["Name"][3:] 58 | in open_pull_request_numbers # pull request aliases are of the form "pr-" 59 | or a["Name"] in ALIASES_TO_KEEP 60 | for a in version_aliases 61 | ): 62 | print( 63 | f"Keeping version {version['Version']} with aliases {[a['Name'] for a in version_aliases]}." 64 | ) 65 | continue 66 | 67 | version_last_modified = datetime.datetime.fromisoformat(version["LastModified"]) 68 | if version_last_modified + datetime.timedelta(hours=1) > datetime.datetime.now( 69 | tz=datetime.UTC 70 | ): 71 | print( 72 | f"Keeping version {version['Version']} with LastModified {version['LastModified']}." 73 | ) 74 | continue 75 | 76 | print( 77 | f"Deleting version {version['Version']} and aliases {[a['Name'] for a in version_aliases]}." 78 | ) 79 | for a in version_aliases: 80 | response = lambda_client.delete_alias( 81 | FunctionName=FUNCTION_NAME, Name=a["Name"] 82 | ) 83 | response = lambda_client.delete_function( 84 | FunctionName=FUNCTION_NAME, Qualifier=version["Version"] 85 | ) 86 | deleted_count += 1 87 | 88 | print(f"Deleted {deleted_count} versions.") 89 | -------------------------------------------------------------------------------- /.github/workflows/build-push-and-deploy.yml: -------------------------------------------------------------------------------- 1 | name: build-push-and-deploy 2 | 3 | on: 4 | workflow_call: 5 | inputs: 6 | image-tag: 7 | description: The image tag. 8 | type: string 9 | required: true 10 | lambda-function-alias: 11 | description: The function alias to deploy to. If not given, will not be deployed. 12 | type: string 13 | required: false 14 | default: "" 15 | outputs: 16 | image-uri: 17 | description: The URI of the newly pushed image. 18 | value: ${{ jobs.build-push-and-deploy.outputs.image-uri }} 19 | lambda-version: 20 | description: The newly created lambda version (if deployed). 21 | value: ${{ jobs.build-push-and-deploy.outputs.lambda-version }} 22 | lambda-function-url: 23 | description: The newly created lambda function URL (if deployed). 24 | value: ${{ jobs.build-push-and-deploy.outputs.lambda-function-url }} 25 | 26 | permissions: 27 | id-token: write # This is required for requesting the JWT 28 | contents: read # This is required for actions/checkout 29 | 30 | jobs: 31 | build-push-and-deploy: 32 | runs-on: ubuntu-latest 33 | outputs: 34 | image-uri: ${{ steps.ecr.outputs.registry }}/app:${{ inputs.image-tag }} 35 | lambda-version: ${{ steps.deploy-lambda.outputs.lambda-version }} 36 | lambda-function-url: ${{ steps.deploy-lambda.outputs.lambda-function-url }} 37 | steps: 38 | - uses: actions/checkout@v4 39 | - uses: aws-actions/configure-aws-credentials@v4 40 | with: 41 | aws-region: ap-southeast-2 42 | role-to-assume: arn:aws:iam::340752831962:role/deploy 43 | - uses: aws-actions/amazon-ecr-login@v2 44 | id: ecr 45 | - uses: docker/setup-buildx-action@v3 46 | - uses: docker/build-push-action@v6 47 | with: 48 | push: true 49 | tags: ${{ steps.ecr.outputs.registry }}/app:${{ inputs.image-tag }} 50 | provenance: false 51 | cache-from: type=registry,ref=${{ steps.ecr.outputs.registry }}/app:buildcache 52 | cache-to: type=registry,ref=${{ steps.ecr.outputs.registry }}/app:buildcache,mode=max 53 | - uses: astral-sh/setup-uv@v6 54 | - id: deploy-lambda 55 | if: ${{ inputs.lambda-function-alias }} 56 | shell: bash 57 | run: | 58 | OUTPUT=$( \ 59 | uv run .github/scripts/deploy-lambda-version.py \ 60 | --image-uri "${{ steps.ecr.outputs.registry }}/app:${{ inputs.image-tag }}" \ 61 | --function-alias "${{ inputs.lambda-function-alias }}" \ 62 | ) 63 | VERSION=$(echo -n $OUTPUT | jq -r .version ) 64 | FUNCTION_URL=$(echo -n $OUTPUT | jq -r .function_url ) 65 | echo "lambda-version=$VERSION" >> "$GITHUB_OUTPUT" 66 | echo "lambda-function-url=$FUNCTION_URL" >> "$GITHUB_OUTPUT" 67 | echo "### Lambda function deployed" >> $GITHUB_STEP_SUMMARY 68 | echo "Version: $VERSION" >> $GITHUB_STEP_SUMMARY 69 | echo "Function URL: $FUNCTION_URL" >> $GITHUB_STEP_SUMMARY 70 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: deploy 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | permissions: 8 | id-token: write # This is required for requesting the JWT 9 | contents: read # This is required for actions/checkout 10 | 11 | env: 12 | LAMBDA_FUNCTION_NAME: app 13 | 14 | jobs: 15 | build-push-and-deploy: 16 | uses: ./.github/workflows/build-push-and-deploy.yml 17 | with: 18 | image-tag: production-${{ github.sha }} 19 | lambda-function-alias: TEST 20 | 21 | e2e-test: 22 | needs: [build-push-and-deploy] 23 | uses: ./.github/workflows/e2e.yml 24 | with: 25 | playwright-base-url: ${{ needs.build-push-and-deploy.outputs.lambda-function-url }} 26 | 27 | deploy-production: 28 | needs: [build-push-and-deploy, e2e-test] 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: aws-actions/configure-aws-credentials@v4 32 | with: 33 | aws-region: ap-southeast-2 34 | role-to-assume: arn:aws:iam::340752831962:role/deploy 35 | - name: Publish lambda version for PRODUCTION 36 | run: | 37 | aws lambda update-alias \ 38 | --function-name app \ 39 | --name PRODUCTION \ 40 | --function-version ${{ needs.build-push-and-deploy.outputs.lambda-version }} 41 | 42 | e2e-production: 43 | needs: [deploy-production] 44 | uses: ./.github/workflows/e2e.yml 45 | with: 46 | playwright-base-url: https://app.basefacility.org.au 47 | 48 | -------------------------------------------------------------------------------- /.github/workflows/e2e.yml: -------------------------------------------------------------------------------- 1 | name: e2e 2 | on: 3 | workflow_call: 4 | inputs: 5 | playwright-base-url: 6 | description: "The playwright baseURL setting." 7 | type: string 8 | required: false 9 | default: '' 10 | 11 | permissions: 12 | id-token: write # This is required for requesting the JWT 13 | contents: read # This is required for actions/checkout 14 | 15 | jobs: 16 | e2e: 17 | runs-on: ubuntu-latest 18 | env: 19 | TAG: ${{ github.sha }} 20 | steps: 21 | - uses: aws-actions/configure-aws-credentials@v4 22 | with: 23 | aws-region: ap-southeast-2 24 | role-to-assume: arn:aws:iam::340752831962:role/deploy 25 | - uses: aws-actions/amazon-ecr-login@v2 26 | id: ecr 27 | - uses: docker/setup-buildx-action@v3 28 | with: 29 | # avoids having to export the local image 30 | # see: https://docs.docker.com/build/builders/drivers/ 31 | driver: docker 32 | - uses: docker/build-push-action@v6 33 | with: 34 | load: true 35 | tags: ${{ env.TAG }} 36 | target: e2e 37 | cache-from: type=registry,ref=${{ steps.ecr.outputs.registry }}/app:buildcache 38 | - run: | 39 | docker run --rm -e CI=true -e PLAYWRIGHT_BASE_URL=${{ inputs.playwright-base-url }} $TAG \ 40 | pnpm playwright test 41 | -------------------------------------------------------------------------------- /.github/workflows/pr.yml: -------------------------------------------------------------------------------- 1 | name: pr 2 | on: 3 | pull_request: 4 | 5 | permissions: 6 | id-token: write # This is required for requesting the JWT 7 | contents: read # This is required for actions/checkout 8 | pull-requests: write # This is required to interact with pull requests 9 | 10 | jobs: 11 | test: 12 | uses: ./.github/workflows/test.yml 13 | 14 | e2e: 15 | uses: ./.github/workflows/e2e.yml 16 | 17 | build-push-and-deploy: 18 | uses: ./.github/workflows/build-push-and-deploy.yml 19 | with: 20 | image-tag: pr-${{ github.sha }} 21 | lambda-function-alias: pr-${{ github.event.pull_request.number }} 22 | 23 | comment-function-url: 24 | needs: [build-push-and-deploy] 25 | runs-on: ubuntu-latest 26 | steps: 27 | - uses: actions/checkout@v4 28 | - env: 29 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | gh pr comment ${{ github.event.pull_request.number }} --edit-last --create-if-none --body "AWS Lambda URL: ${{ needs.build-push-and-deploy.outputs.lambda-function-url }}" 32 | 33 | prune-lambda-versions: 34 | needs: [build-push-and-deploy] 35 | runs-on: ubuntu-latest 36 | steps: 37 | - uses: actions/checkout@v4 38 | - uses: astral-sh/setup-uv@v6 39 | - uses: aws-actions/configure-aws-credentials@v4 40 | with: 41 | aws-region: ap-southeast-2 42 | role-to-assume: arn:aws:iam::340752831962:role/deploy 43 | - env: 44 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 45 | run: | 46 | uv run .github/scripts/prune-lambda-versions.py ${{ github.repository }} 47 | 48 | 49 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | on: 3 | workflow_call: 4 | 5 | permissions: 6 | id-token: write # This is required for requesting the JWT 7 | contents: read # This is required for actions/checkout 8 | 9 | jobs: 10 | test: 11 | name: test 12 | runs-on: ubuntu-latest 13 | env: 14 | TAG: ${{ github.sha }} 15 | steps: 16 | - uses: actions/checkout@v4 17 | - uses: aws-actions/configure-aws-credentials@v4 18 | with: 19 | aws-region: ap-southeast-2 20 | role-to-assume: arn:aws:iam::340752831962:role/deploy 21 | - uses: aws-actions/amazon-ecr-login@v2 22 | id: ecr 23 | - uses: docker/setup-buildx-action@v3 24 | with: 25 | # avoids having to export the local image 26 | # see: https://docs.docker.com/build/builders/drivers/ 27 | driver: docker 28 | - uses: docker/build-push-action@v6 29 | with: 30 | load: true 31 | tags: ${{ env.TAG }} 32 | target: dev 33 | cache-from: type=registry,ref=${{ steps.ecr.outputs.registry }}/app:buildcache 34 | - run: docker run --rm $TAG pnpm check 35 | - run: docker run --rm $TAG pnpm vitest run --dir frontend 36 | - run: docker run --rm $TAG uv run ruff check --diff 37 | - run: docker run --rm $TAG uv run ty check 38 | - run: docker run --rm $TAG uv run pytest 39 | - run: docker run --rm $TAG uv run mRNArchitect optimize ACGACG 40 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__/ 2 | .nitro/ 3 | .output/ 4 | .pytest_cache/ 5 | .ruff_cache/ 6 | .tanstack/ 7 | .venv/ 8 | .vinxi/ 9 | .vscode/ 10 | dist/ 11 | node_modules/ 12 | *.egg-info/ 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": true, 3 | "[javascript]": { 4 | "editor.tabSize": 2 5 | }, 6 | "[typescript]": { 7 | "editor.tabSize": 2 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:lts-slim AS base 2 | 3 | RUN apt-get update -qy && \ 4 | apt-get install -qy wget && \ 5 | rm -rf /var/lib/apt/lists/* 6 | 7 | # Install uv 8 | # see: https://docs.astral.sh/uv/guides/integration/docker/#installing-uv 9 | COPY --from=ghcr.io/astral-sh/uv:0.9.5 /uv /uvx /bin/ 10 | 11 | # Install pnpm 12 | # see: https://github.com/pnpm/pnpm/releases/ 13 | RUN wget -qO /usr/local/bin/pnpm https://github.com/pnpm/pnpm/releases/download/v10.20.0/pnpm-linux-x64 && \ 14 | chmod +x /usr/local/bin/pnpm 15 | 16 | # Install AWS Lambda Web Adapter 17 | # see: https://github.com/awslabs/aws-lambda-web-adapter 18 | COPY --from=public.ecr.aws/awsguru/aws-lambda-adapter:0.9.1 /lambda-adapter /opt/extensions/lambda-adapter 19 | 20 | # Install ViennaRNA 21 | # see: https://www.tbi.univie.ac.at/RNA/#binary_packages 22 | RUN wget -qO viennarna.deb https://www.tbi.univie.ac.at/RNA/download/debian/debian_12/viennarna_2.7.0-1_amd64.deb && \ 23 | apt-get update -qy && \ 24 | apt-get install -qy -f ./viennarna.deb && \ 25 | rm -rf viennarna.deb /var/lib/apt/lists/* 26 | 27 | # Install BLAST+ 28 | # see: https://blast.ncbi.nlm.nih.gov/doc/blast-help/downloadblastdata.html 29 | # see: https://hub.docker.com/r/ncbi/blast 30 | # COPY --from=docker.io/ncbi/blast:2.17.0 /blast /blast 31 | # RUN apt-get update -qy && \ 32 | # apt-get install -qy curl libidn12 libnet-perl perl-doc liblmdb-dev wget libsqlite3-dev perl && \ 33 | # rm -rf /var/lib/apt/lists/* 34 | # ENV PATH="/blast/bin:$PATH" 35 | # ENV BLASTDB="/blast/blastdb/" 36 | # WORKDIR ${BLASTDB} 37 | # RUN update_blastdb.pl --decompress --verbose taxdb 38 | 39 | # Setup the app directory 40 | RUN mkdir /app && chown node:node /app 41 | USER node 42 | WORKDIR /app 43 | 44 | ENV UV_COMPILE_BYTECODE=1 45 | RUN --mount=type=bind,source=uv.lock,target=uv.lock \ 46 | --mount=type=bind,source=pyproject.toml,target=pyproject.toml \ 47 | --mount=type=cache,target=/root/.cache/uv \ 48 | uv sync --locked --no-install-project --all-groups --compile-bytecode 49 | ENV PATH="/app/.venv/bin:$PATH" 50 | 51 | RUN --mount=type=bind,source=pnpm-lock.yaml,target=pnpm-lock.yaml \ 52 | --mount=type=bind,source=package.json,target=package.json \ 53 | --mount=type=cache,target=/root/.pnpm-store \ 54 | pnpm install --frozen-lockfile 55 | 56 | COPY --chown=node:node . . 57 | RUN --mount=type=cache,target=/root/.cache/uv \ 58 | uv sync --locked 59 | 60 | 61 | FROM base AS e2e 62 | 63 | RUN pnpm install @playwright/test 64 | USER root 65 | RUN pnpm playwright install-deps 66 | USER node 67 | RUN pnpm playwright install chromium --no-shell 68 | RUN pnpm run build 69 | CMD ["pnpm", "playwright", "test"] 70 | 71 | 72 | FROM base AS dev 73 | 74 | CMD ["pnpm", "run", "dev"] 75 | 76 | 77 | FROM base 78 | 79 | RUN pnpm install @playwright/test 80 | RUN pnpm run build 81 | CMD ["pnpm", "run", "start"] 82 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 The BASE facility, The University Of Queensland, Brisbane, Australia. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![deploy](https://github.com/BaseUQ/mRNArchitect/actions/workflows/deploy.yml/badge.svg)](https://github.com/BaseUQ/mRNArchitect/actions/workflows/deploy.yml) 2 | 3 | # mRNArchitect 4 | 5 | *mRNArchitect* is a software toolkit designed for optimizing mRNA vaccines and therapies to enhance stability, translation efficiency, and reduce reactogenicity. The software uses an optimization strategy based on the *DNAChisel* framework to generate and assemble mRNA sequences. 6 | 7 | ## Getting Started 8 | 9 | ### Accessing mRNArchitect 10 | 11 | You can access *mRNArchitect*: 12 | 13 | 1. **Online:** Use the web-based interface at [http://www.basefacility.org.au/software](http://www.basefacility.org.au/software). 14 | 2. **Local Installation:** Download the source code from the [GitHub repository](https://github.com/BaseUQ/mRNArchitect). 15 | 16 | 17 | ### Local Installation 18 | 19 | mRNArchitect may be used via a web or CLI interface. 20 | 21 | To use the web interface, you may build and run the provided docker image. 22 | 23 | ```sh 24 | > docker compose build 25 | > docker compose up 26 | ``` 27 | 28 | Then browse to [http://localhost:8080] to start optimizing. 29 | 30 | The CLI may tool may also be run via docker. 31 | 32 | ```sh 33 | > docker compose run app mRNArchitect optimize ACGACG 34 | ``` 35 | 36 | Or for an amino acid sequence: 37 | 38 | ```sh 39 | > docker compose run app mRNArchitect optimize MILK --sequence-type amino-acid 40 | ``` 41 | 42 | Use the `--help` option to see the full list of options: 43 | 44 | ```sh 45 | > docker compose run app mRNArchitect optimize --help 46 | ``` 47 | 48 | To run the CLI directly, first install the [uv](https://docs.astral.sh/uv/getting-started/installation/) package manager to setup a python environment. You can then run the above commands through `uv` rather than docker. 49 | 50 | ```sh 51 | > uv run mRNArchitect optimize ACGACG 52 | ``` 53 | 54 | ## Design of mRNA Sequence 55 | 56 | The mRNA sequence significantly affects its stability, translation, and reactogenicity. Therefore, optimizing an mRNA sequence is crucial for achieving desired outcomes in various applications. 57 | 58 | ### Steps to Use mRNArchitect 59 | 60 | 1. **Open the Application:** 61 | 62 | Visit the *mRNArchitect* website at [http://www.basefacility.org.au/software](http://www.basefacility.org.au/software). 63 | 64 | 2. **Input Sequence:** 65 | 66 | In the **Sequence Input** panel, input the sequences for different components of an mRNA. For example, paste the wild-type Firefly luciferase protein sequence (either in nucleotide or amino acid format) into the **Coding Sequence** field. 67 | 68 | 3. **Select UTR Sequences:** 69 | 70 | Choose the **Human alpha-globin** option for both the 5'UTR and 3'UTR fields. A poly(A) tail is not required for this protocol, as it will be added during PCR amplification. 71 | 72 | 4. **Modify Parameters:** 73 | 74 | Use the **Parameters** panel to adjust key variables that impact mRNA sequence optimization. Initially, it is recommended to use the default settings, but they can be modified as needed. For more information on each parameter, refer to the **HELP** section in *mRNArchitect*. 75 | 76 | 5. **Run Optimization:** 77 | 78 | Click **Optimize sequence** to start the sequence optimization. Once complete, the optimized sequence(s) can be viewed and downloaded. From the results page, you may click **< Back** to edit your sequence or parameters to run another optimization. 79 | 80 | 6. **Submit for Synthesis:** 81 | 82 | Copy the optimized mRNA sequence and submit it for synthesis by a third-party provider (e.g. IDT, GeneArt, Genscript, etc.). 83 | 84 | ### Example Sequences and Results 85 | 86 | Additional example sequences and result files can be downloaded from the [GitHub repository](https://github.com/BaseUQ/mRNArchitect) or *mRNArchitect* website at [http://www.basefacility.org.au/software](http://www.basefacility.org.au/software). 87 | 88 | ## Support and Documentation 89 | 90 | For further information, please refer to the **HELP** section available in the *mRNArchitect* interface. For issues, support or feedback, please open a ticket on the [GitHub Issues page](https://github.com/BaseUQ/mRNArchitect/issues) or contact us directly at **basedesign@uq.edu.au**. Please ensure your description is clear and has sufficient instructions to be able to reproduce the issue. 91 | 92 | ## License 93 | 94 | This project is licensed under the MIT License. 95 | -------------------------------------------------------------------------------- /TERMS.md: -------------------------------------------------------------------------------- 1 | **Terms and Conditions for Use of mRNArchitect Software** 2 | 3 | **Last updated: 14 August 2025** 4 | 5 | These Terms and Conditions (“Terms”) govern the use of the mRNArchitect software (the “Software”) made available by BASE Facility, The University of Queensland. 6 | 7 | By downloading, installing, or using the Software, you agree to be bound by these Terms. If you do not agree, you must not access or use the Software. 8 | 9 | 1. License and Permitted Use 10 | 11 | 1.1 University Of Queensland grants you a limited, non-exclusive, non-transferable, revocable license to use the Software for research purposes, including both non-profit and commercial research activities. 12 | 13 | 1.2 You must comply with all applicable laws, regulations, and institutional requirements when using the Software. 14 | 15 | 1.3 You must not: Sell, sublicense, rent, lease, or otherwise distribute the Software; Reverse engineer, decompile, or modify the Software, except as permitted by law; Use the Software for any unlawful, unethical, or harmful purposes. 16 | 17 | 2. Intellectual Property 18 | 19 | 2.1 The Software and all related documentation, updates, associated experimental results and materials remain the intellectual property of BASE Facility and/or its licensors. 20 | 21 | 2.2 No patent or other intellectual property license is granted or implied through use; users assume all responsibility for obtaining necessary rights to any third-party components or data they use with the Software. 22 | 23 | 2.3 All rights not expressly granted in these Terms are reserved. 24 | 25 | 3. User Responsibilities 26 | 27 | 3.1 You are responsible for: Ensuring the accuracy of any data you input into the Software; Backing up your work and maintaining your own data security. 28 | 29 | 3.2 You are responsible for: Using the Software in compliance with relevant biosafety and ethical guidelines; Ensuring compliance with applicable data protection and confidentiality requirements. 30 | 31 | 3.3 Citation Requirement: Publications, presentations, or other outputs resulting from use of the Software must acknowledge and cite BASE Facility, University Of Queensland and the mRNArchitect software in accordance with academic standards. 32 | 33 | 4. Data and Privacy 34 | 35 | 4.1 The Software may process user-provided data locally or via secure servers. 36 | 37 | 4.2 BASE Facility will handle personal data in accordance with the [UQ Privacy Policy](https://policies.uq.edu.au/). 38 | 39 | 4.3 BASE Facility may collect identifiers (e.g., name, email), usage data, and research-related information to support service provision and compliance. 40 | 41 | 4.4 BASE Facility will not sell data to third parties. 42 | 43 | 5. Disclaimers 44 | 45 | 5.1 The Software is provided “as is”, without any warranty of any kind, whether express or implied, including but not limited to warranties of merchantability, fitness for a particular purpose, or non-infringement. 46 | 47 | 5.2 BASE Facility does not warrant that the Software will be error-free, uninterrupted, or free of security vulnerabilities. 48 | 49 | 5.3 Outputs from the Software may require expert interpretation and verification; BASE Facility accepts no responsibility for any conclusions or decisions made based on such outputs. 50 | 51 | 6. Limitation of Liability 52 | 53 | 6.1 To the maximum extent permitted by law, BASE Facility will not be liable for any indirect, incidental, special, consequential, or punitive damages, or for any loss of data, profits, or business opportunities arising from the use of, or inability to use, the Software. 54 | 55 | 6.2 BASE Facility’s total liability in connection with the Software will not exceed the amount you paid (if any) for access to the Software. 56 | 57 | 7. Updates and Modifications 58 | 59 | 7.1 BASE Facility may update, modify, or discontinue the Software (in whole or in part) at any time without notice. 60 | 61 | 7.2 These Terms may be updated from time to time; continued use of the Software after changes are posted constitutes acceptance of the revised Terms. 62 | 63 | 8. Termination 64 | 65 | 8.1 BASE Facility may suspend or terminate your access to the Software at its sole discretion if you breach these Terms. 66 | 67 | 8.2 Upon termination, you must immediately cease all use of the Software and destroy any copies in your possession. 68 | 69 | 9. Governing Law 70 | 71 | 9.1 These Terms are governed by and construed in accordance with the laws of the State of Queensland, Australia, without regard to conflict of law principles. 72 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://biomejs.dev/schemas/2.3.2/schema.json", 3 | "vcs": { 4 | "enabled": false, 5 | "clientKind": "git", 6 | "useIgnoreFile": false 7 | }, 8 | "files": { 9 | "ignoreUnknown": false, 10 | "includes": [ 11 | "**", 12 | "!**/routeTree.gen.ts", 13 | "!**/.nitro", 14 | "!**/.output", 15 | "!**/.pnpm-store", 16 | "!**/.tanstack", 17 | "!**/.vinxi", 18 | "!**/.venv", 19 | "!**/data", 20 | "!**/dist" 21 | ] 22 | }, 23 | "formatter": { 24 | "enabled": true, 25 | "indentStyle": "space", 26 | "indentWidth": 2 27 | }, 28 | "assist": { "actions": { "source": { "organizeImports": "on" } } }, 29 | "linter": { 30 | "enabled": true, 31 | "rules": { 32 | "recommended": true, 33 | "style": { 34 | "noParameterAssign": "error", 35 | "useAsConstAssertion": "error", 36 | "useDefaultParameterLast": "error", 37 | "useEnumInitializers": "error", 38 | "useSelfClosingElements": "error", 39 | "useSingleVarDeclarator": "error", 40 | "noUnusedTemplateLiteral": "error", 41 | "useNumberNamespace": "error", 42 | "noInferrableTypes": "error", 43 | "noUselessElse": "error" 44 | } 45 | } 46 | }, 47 | "javascript": { 48 | "formatter": { 49 | "quoteStyle": "double" 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | app: 3 | restart: always 4 | build: 5 | context: . 6 | dockerfile: Dockerfile 7 | target: dev 8 | ports: 9 | - 8080:8080 10 | - 8081:8081 # hmr 11 | volumes: 12 | - .:/app 13 | - /app/node_modules 14 | - /app/.venv 15 | develop: 16 | watch: 17 | - path: ./pnpm-lock.yaml 18 | action: rebuild 19 | - path: ./uv.lock 20 | action: rebuild 21 | #- path: ./ 22 | # target: /app 23 | # action: sync 24 | # ignore: 25 | # - .venv/ 26 | # - node_modules/ 27 | # - notebooks/ 28 | e2e: 29 | profiles: [e2e] 30 | restart: always 31 | build: 32 | context: . 33 | dockerfile: Dockerfile 34 | target: e2e 35 | command: pnpm playwright test --ui-host 0.0.0.0 --ui-port 2345 36 | ports: 37 | - 2345:2345 38 | volumes: 39 | - .:/app 40 | - /app/node_modules 41 | - /app/.venv 42 | develop: 43 | watch: 44 | - path: ./pnpm-lock.yaml 45 | action: rebuild 46 | - path: ./uv.lock 47 | action: rebuild 48 | #- path: ./ 49 | # target: /app 50 | # action: rebuild 51 | # ignore: 52 | # - .venv/ 53 | # - node_modules/ 54 | # - notebooks/ 55 | jupyter: 56 | profiles: [jupyter] 57 | build: 58 | context: . 59 | dockerfile: Dockerfile 60 | target: dev 61 | command: jupyter lab --ip 0.0.0.0 --port 8888 --notebook-dir=./notebooks/ --IdentityProvider.token="" --ServerApp.password="" 62 | ports: 63 | - 8888:8888 64 | volumes: 65 | - ./notebooks/:/app/notebooks/ 66 | - $HOME/.aws:/home/node/.aws 67 | -------------------------------------------------------------------------------- /e2e/nav.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | test("home page", async ({ page }) => { 4 | await page.goto("/"); 5 | await expect(page).toHaveTitle(/mRNArchitect/); 6 | await expect( 7 | page.getByRole("button", { name: "Optimise sequence" }), 8 | ).toBeVisible(); 9 | }); 10 | -------------------------------------------------------------------------------- /e2e/optimize.spec.ts: -------------------------------------------------------------------------------- 1 | import { expect, test } from "@playwright/test"; 2 | 3 | const EGFP_NUCLEIC_ACID_SEQUENCE = 4 | "ATGGTGAGCAAGGGCGAGGAGCTGTTCACCGGCGTGGTGCCCATCCTGGTGGAGCTGGACGGCGACGTGAACGGCCACAAGTTCAGCGTGAGCGGCGAGGGAGAGGGCGACGCCACCTATGGCAAGCTGACCCTGAAGTTCATCTGCACCACCGGCAAGCTGCCCGTGCCCTGGCCCACACTGGTGACCACCCTGACCTACGGCGTGCAGTGCTTCAGCAGATACCCCGACCACATGAAGCAGCACGATTTCTTCAAGAGCGCCATGCCCGAGGGCTACGTGCAGGAGAGAACCATCTTCTTCAAGGACGACGGCAACTACAAGACCAGAGCCGAGGTGAAGTTCGAGGGCGACACCCTGGTGAACAGAATCGAGCTGAAGGGCATCGACTTCAAGGAGGATGGCAACATCCTGGGCCACAAGCTGGAGTACAACTACAACAGCCACAACGTGTACATCATGGCCGACAAGCAGAAGAACGGCATCAAGGTGAACTTCAAGATCAGACACAACATCGAGGACGGCAGCGTGCAGCTGGCCGACCACTACCAGCAGAACACCCCCATCGGCGACGGCCCCGTGCTGCTGCCCGACAACCACTACCTGAGCACCCAGAGCGCCCTGAGCAAGGACCCCAACGAGAAGAGAGACCACATGGTGCTGCTGGAGTTCGTGACCGCCGCCGGCATCACCCTGGGCATGGACGAGCTGTACAAG"; 5 | 6 | // test("run optimization - pre-fill random example", async ({ page }) => { 7 | // await page.goto("/"); 8 | // await page.waitForTimeout(1_000); // brief wait for the form to load, should make this better 9 | // await page.getByRole("button", { name: "Pre-fill random example" }).click(); 10 | // await expect(page.getByLabel("Coding sequence textarea")).not.toBeEmpty(); 11 | // await page.getByRole("button", { name: "Optimise sequence" }).click(); 12 | // 13 | // await expect(page.getByRole("tab", { name: "Output" })).toHaveAttribute( 14 | // "aria-selected", 15 | // "true", 16 | // { timeout: 30_000 }, 17 | // ); 18 | // }); 19 | 20 | test("run optimization - eGFP nucleic acid", async ({ page }) => { 21 | await page.goto("/"); 22 | await page.waitForTimeout(1_000); // brief wait for the form to load, should make this better 23 | 24 | await page 25 | .getByRole("textbox", { name: "Coding sequence textarea" }) 26 | .fill(EGFP_NUCLEIC_ACID_SEQUENCE); 27 | await page.getByLabel("Number of optimised sequences").fill("1"); 28 | 29 | await page.getByRole("button", { name: "Optimise sequence" }).click(); 30 | 31 | await expect(page.getByRole("tab", { name: "Output" })).toHaveAttribute( 32 | "aria-selected", 33 | "true", 34 | { timeout: 60_000 }, 35 | ); 36 | 37 | await expect(page.getByText(EGFP_NUCLEIC_ACID_SEQUENCE)).toHaveCount(2); 38 | }); 39 | -------------------------------------------------------------------------------- /frontend/.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # React + TypeScript + Vite 2 | 3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. 4 | 5 | Currently, two official plugins are available: 6 | 7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) (or [oxc](https://oxc.rs) when used in [rolldown-vite](https://vite.dev/guide/rolldown)) for Fast Refresh 8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh 9 | 10 | ## React Compiler 11 | 12 | The React Compiler is not enabled on this template because of its impact on dev & build performances. To add it, see [this documentation](https://react.dev/learn/react-compiler/installation). 13 | 14 | ## Expanding the ESLint configuration 15 | 16 | If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: 17 | 18 | ```js 19 | export default defineConfig([ 20 | globalIgnores(['dist']), 21 | { 22 | files: ['**/*.{ts,tsx}'], 23 | extends: [ 24 | // Other configs... 25 | 26 | // Remove tseslint.configs.recommended and replace with this 27 | tseslint.configs.recommendedTypeChecked, 28 | // Alternatively, use this for stricter rules 29 | tseslint.configs.strictTypeChecked, 30 | // Optionally, add this for stylistic rules 31 | tseslint.configs.stylisticTypeChecked, 32 | 33 | // Other configs... 34 | ], 35 | languageOptions: { 36 | parserOptions: { 37 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 38 | tsconfigRootDir: import.meta.dirname, 39 | }, 40 | // other options... 41 | }, 42 | }, 43 | ]) 44 | ``` 45 | 46 | You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: 47 | 48 | ```js 49 | // eslint.config.js 50 | import reactX from 'eslint-plugin-react-x' 51 | import reactDom from 'eslint-plugin-react-dom' 52 | 53 | export default defineConfig([ 54 | globalIgnores(['dist']), 55 | { 56 | files: ['**/*.{ts,tsx}'], 57 | extends: [ 58 | // Other configs... 59 | // Enable lint rules for React 60 | reactX.configs['recommended-typescript'], 61 | // Enable lint rules for React DOM 62 | reactDom.configs.recommended, 63 | ], 64 | languageOptions: { 65 | parserOptions: { 66 | project: ['./tsconfig.node.json', './tsconfig.app.json'], 67 | tsconfigRootDir: import.meta.dirname, 68 | }, 69 | // other options... 70 | }, 71 | }, 72 | ]) 73 | ``` 74 | -------------------------------------------------------------------------------- /frontend/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | mRNArchitect - BASE mRNA facility 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc -b && vite build", 9 | "preview": "vite preview", 10 | "check": "biome check && tsc --noEmit", 11 | "fix": "biome check --fix" 12 | }, 13 | "dependencies": { 14 | "@mantine/core": "^8.3.5", 15 | "@mantine/form": "^8.3.5", 16 | "@mantine/hooks": "^8.3.5", 17 | "@phosphor-icons/react": "^2.1.10", 18 | "date-fns": "^4.1.0", 19 | "react": "^19.1.1", 20 | "react-dom": "^19.1.1", 21 | "react-markdown": "^10.1.0", 22 | "zod": "^4.1.12" 23 | }, 24 | "devDependencies": { 25 | "@types/node": "^24.6.0", 26 | "@types/react": "^19.1.16", 27 | "@types/react-dom": "^19.1.9", 28 | "@vitejs/plugin-react": "^5.0.4", 29 | "globals": "^16.4.0", 30 | "postcss": "^8.5.6", 31 | "postcss-preset-mantine": "^1.18.0", 32 | "postcss-simple-vars": "^7.0.1", 33 | "typescript": "~5.9.3", 34 | "vite": "^7.1.7", 35 | "vite-tsconfig-paths": "^5.1.4", 36 | "vitest": "^4.0.4" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /frontend/postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | "postcss-preset-mantine": {}, 4 | "postcss-simple-vars": { 5 | variables: { 6 | "mantine-breakpoint-xs": "36em", 7 | "mantine-breakpoint-sm": "48em", 8 | "mantine-breakpoint-md": "62em", 9 | "mantine-breakpoint-lg": "75em", 10 | "mantine-breakpoint-xl": "88em", 11 | }, 12 | }, 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /frontend/public/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BaseUQ/mRNArchitect/500e9fbcc8298dba0c0fc79feb2ee951cbc7f43a/frontend/public/favicon.png -------------------------------------------------------------------------------- /frontend/src/App.tsx: -------------------------------------------------------------------------------- 1 | import { AppShell, Container, MantineProvider } from "@mantine/core"; 2 | import { lazy, Suspense } from "react"; 3 | 4 | const OptimizePage = lazy(() => 5 | import("~/components/pages/optimize/OptimizePage").then((module) => ({ 6 | default: module.OptimizePage, 7 | })), 8 | ); 9 | 10 | const App = () => ( 11 | 12 | 13 | 14 | 15 | Loading...}> 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | 24 | export default App; 25 | -------------------------------------------------------------------------------- /frontend/src/api/index.ts: -------------------------------------------------------------------------------- 1 | import z from "zod/v4"; 2 | import { 3 | Analysis, 4 | Optimization, 5 | OptimizationParameter, 6 | } from "~/types/optimize"; 7 | 8 | const API = import.meta.env.VITE_API ?? ""; 9 | 10 | const SequenceAndOrganism = z.object({ 11 | sequence: z.string().nonempty(), 12 | organism: z.string().nonempty(), 13 | }); 14 | 15 | type SequenceAndOrganism = z.infer; 16 | 17 | export const convert = (data: SequenceAndOrganism) => 18 | fetch(`${API}/api/convert`, { 19 | method: "POST", 20 | body: JSON.stringify(data), 21 | }) 22 | .then((response) => response.json()) 23 | .then((json) => 24 | z 25 | .object({ 26 | sequence: z.string().nonempty(), 27 | }) 28 | .parse(json), 29 | ); 30 | 31 | const OptimizationRequest = z.object({ 32 | sequence: z.string().nonempty(), 33 | parameters: z.array(OptimizationParameter), 34 | }); 35 | 36 | type OptimizationRequest = z.infer; 37 | 38 | export const optimize = (data: OptimizationRequest) => 39 | fetch(`${API}/api/optimize`, { 40 | method: "POST", 41 | body: JSON.stringify(data), 42 | }) 43 | .then((response) => response.json()) 44 | .then((json) => Optimization.parse(json)); 45 | 46 | export const analyze = (data: SequenceAndOrganism) => 47 | fetch(`${API}/api/analyze`, { 48 | method: "POST", 49 | body: JSON.stringify(data), 50 | }) 51 | .then((response) => response.json()) 52 | .then((json) => Analysis.parse(json)); 53 | -------------------------------------------------------------------------------- /frontend/src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /frontend/src/components/logos/BASELogo.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export const BASELogo = (props: SVGProps) => ( 4 | 10 | BASE logo 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | ); 23 | -------------------------------------------------------------------------------- /frontend/src/components/logos/BASELogoSmall.tsx: -------------------------------------------------------------------------------- 1 | import type { SVGProps } from "react"; 2 | 3 | export const BASELogoSmall = (props: SVGProps) => ( 4 | 10 | BASE logo small 11 | 12 | 13 | 14 | 15 | 16 | ); 17 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/Help.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Table, Text, Title } from "@mantine/core"; 2 | import { Fragment } from "react/jsx-runtime"; 3 | 4 | export const Help = () => { 5 | return ( 6 | 7 | 8 | Contact 9 | 15 | basedesign@uq.edu.au 16 | , 17 | ], 18 | [ 19 | "GitHub", 20 | 24 | https://github.com/BaseUQ/mRNArchitect 25 | , 26 | ], 27 | [ 28 | "Example", 29 | 30 | For guidance on how to design an mRNA, please see the 31 | step-by-step example{" "} 32 | 33 | here 34 | 35 | . 36 | , 37 | ], 38 | [ 39 | "Sequences", 40 | 41 | Please find useful sequences (promoters, UTRs etc.){" "} 42 | 43 | here 44 | 45 | . 46 | , 47 | ], 48 | ], 49 | }} 50 | /> 51 | 52 | 53 | Input sequence 54 |
60 | Add your coding sequence of interest here, for sequence 61 | optimisation1. You can paste either the amino acid, 62 | RNA or DNA sequence. You may also want to consider adding 63 | useful sequence elements such as nuclear localization signals, 64 | signal peptides, or other tags. Ensure your coding sequence 65 | starts with a MET codon and ends with a STOP codon. You may 66 | want to use two different stop codons for efficient 67 | termination (e.g., UAG/UGA). 68 | , 69 | ], 70 | [ 71 | "5' UTR", 72 | 73 | Paste your 5′ untranslated region (UTR) sequence here. The 5′ 74 | UTR is scanned by the ribosome prior to translation initiation 75 | and plays a key role in determining translation efficiency. We 76 | provide a well-validated 5' UTRs option, the human 77 | alpha-globin (ENSG00000206172) 5’ UTR sequence that has been 78 | validated in different cell types and applications. We provide 79 | further 5’ UTRs from housekeeping genes including beta globin 80 | (ENSG00000244734), beta actin (ENSG00000075624) and albumin 81 | (ENSG00000163631)2, a minimal 5' UTR3{" "} 82 | and a 5’ UTR with low secondary structure, NELL2 83 | (ENSG00000184613)4. By default, no 5' UTR is added. 84 | , 85 | ], 86 | [ 87 | "3' UTR", 88 | 89 | Paste your 3' untranslated sequence here. The 3' untranslated 90 | region (UTR) is regulated by microRNAs and RNA-binding 91 | proteins and plays a key role in cell-specific mRNA stability 92 | and expression. We provide a well-validated option, the human 93 | alpha-globin (ENSG00000206172) 3' UTR sequence that has been 94 | validated in different cell types and applications. 95 | Additionally, we provide a human alpha-globin 3’ UTR with an 96 | added microRNA 122 binding site, which was shown to reduce 97 | mRNA expression in Huh7 (liver) cells5. By default, 98 | no 3' UTR will be added. 99 | , 100 | ], 101 | [ 102 | "Poly(A) tail", 103 | "Specify the length of the poly(A) tail or alternatively paste more complex designs. The length of the poly(A) tail plays a critical role in mRNA translation and stability. By default, no poly(A) tail will be added.", 104 | ], 105 | ], 106 | }} 107 | /> 108 | 109 | 110 | Input optimisation parameter region 111 |
121 | The coordinates within the coding region to optimise. Note 122 | that the coordinates are 1-based, and are inclusive of the end 123 | coordinate. Selecting "Full sequence" will optimise the whole 124 | sequence. 125 | , 126 | ], 127 | [ 128 | "Don't optimise region", 129 | "If set, the given sub-region is not optimised. That is, the input and output sequences will be exactly the same between the specified nucleotide coordinates.", 130 | ], 131 | [ 132 | "Enable uridine depletion", 133 | "If selected, this minimizes the use of uridine nucleosides in the mRNA sequence. This is achieved by avoiding codons that encode uridine at the third wobble position and can impact the reactogenicity of the mRNA sequence.", 134 | ], 135 | [ 136 | "Avoid ribosome slip", 137 | 138 | Avoid more than 3 consecutive Us in the open-reading frame, 139 | where ribosomes can +1 frameshift at consecutive 140 | N1-methylpseudouridines6. 141 | , 142 | ], 143 | [ 144 | "Avoid manufacture restriction sites", 145 | 146 | Avoid restriction enzyme binding sites that can interfere with 147 | DNA template synthesis. 148 | , 149 | ], 150 | [ 151 | "Avoid microRNA seed sites", 152 | 153 | Avoid binding sites for microRNA seed sequences that can 154 | result in unwanted degradation. 155 | , 156 | ], 157 | [ 158 | "GC content", 159 | 160 | Defines the minimum or maximum fraction of the mRNA sequence 161 | comprising G/C nucleotides that is associated with stability 162 | and hairpins of the mRNA. We recommend 0.4 and 0.7. 163 | , 164 | ], 165 | [ 166 | "Avoid cut sites", 167 | "Specify restriction enzyme sites that should be avoided in the mRNA sequence.", 168 | ], 169 | [ 170 | "Avoid sequences", 171 | "Specify sequences that should be avoided in the mRNA sequence.", 172 | ], 173 | [ 174 | "Avoid homopolymer tracts", 175 | "Avoid homopolymer tracts that can be difficult to synthesise and translate. We recommend 9 for poly(U)/poly(A) and 6 for poly(C)/poly(G).", 176 | ], 177 | [ 178 | "Avoid hairpins", 179 | "Avoid stable hairpins longer than the given length within the given window size. We recommend a length of 10 and window size of 60.", 180 | ], 181 | ], 182 | }} 183 | /> 184 | 185 | 186 | Output 187 |
209 | The Codon Adaptation Index (CAI) is a measure of deviation 210 | between the codon usage of an mRNA sequence from the preferred 211 | codon usage of the organism7. The CAI score ranges 212 | from 0 (totally dissimilar) to 1 (all mRNA codons match the 213 | organism's codon usage reference table). 214 | , 215 | ], 216 | [ 217 | "CDS MFE", 218 | 219 | The Minimum Free Energy (MFE) is the lowest Gibbs free energy 220 | change associated with the formation of secondary structures 221 | in RNA molecules due to intramolecular base pairing 222 | 8. Lower values of MFE are associated with the 223 | formation of stable secondary structures and hairpins that can 224 | occlude protein expression. 225 | , 226 | ], 227 | [ 228 | "5' UTR MFE", 229 | "The Minimum Free Energy of the 5' UTR sequence. Lower values of MFE are associated with the formation of stable secondary structures.", 230 | ], 231 | [ 232 | "3' UTR MFE", 233 | "The Minimum Free Energy of the 3' UTR sequences. Lower values of MFE are associated with the formation of stable secondary structures.", 234 | ], 235 | [ 236 | "Total MFE", 237 | "The Minimum Free Energy of the full sequence (5' UTR, coding sequence, 3' UTR and poly(A) tail). Lower values of MFE are associated with the formation of stable secondary structures.", 238 | ], 239 | ], 240 | }} 241 | /> 242 | 243 | 244 | References 245 | 246 |
  • 247 | Zulkower, V. & Rosser, S. DNA Chisel, a versatile sequence 248 | optimizer. Bioinformatics 36, 4508-4509 249 | (2020). 250 |
  • 251 |
  • 252 | Ma, Q. et al. Optimization of the 5ʹ untranslated region of mRNA 253 | vaccines. Scientific Reports 14: 19845 254 | (2024). 255 |
  • 256 |
  • 257 | Trepotec, Z. et al. Maximizing the translational yield of mRNA 258 | therapeutics by minimizing 5′-UTRs. Tissue Engineering Part A{" "} 259 | 25, 69-79 (2019). 260 |
  • 261 |
  • 262 | Lewis, C. et al. Quantitative profiling of human translation 263 | initiation reveals elements that potently regulate endogenous and 264 | therapeutically modified mRNAs. Molecular Cell{" "} 265 | 85, 445-459 (2025). 266 |
  • 267 |
  • 268 | Jain R. et al. MicroRNAs enable mRNA therapeutics to selectively 269 | program cancer cells to self-destruct. Nucleic Acid Therapies{" "} 270 | 28, 285-296 (2018). 271 |
  • 272 |
  • 273 | Mulroney, T. E. et al. N 1-methylpseudouridylation of mRNA causes+ 1 274 | ribosomal frameshifting. Nature 625, 189-194 275 | (2024). 276 |
  • 277 |
  • 278 | Sharp, P. M. & Li, W.-H. The codon adaptation index-a measure of 279 | directional synonymous codon usage bias, and its potential 280 | applications. Nucleic acids research 15, 281 | 1281-1295 (1987). 282 |
  • 283 |
  • 284 | Lorenz, R. et al. ViennaRNA Package 2.0.{" "} 285 | Algorithms for molecular biology 6, 26 286 | (2011). 287 |
  • 288 |
    289 |
    290 | 291 | ); 292 | }; 293 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/InputForm.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Accordion, 3 | type AccordionControlProps, 4 | ActionIcon, 5 | Alert, 6 | Box, 7 | Button, 8 | Center, 9 | Divider, 10 | Fieldset, 11 | LoadingOverlay, 12 | NumberInput, 13 | SegmentedControl, 14 | Stack, 15 | Text, 16 | TextInput, 17 | Tooltip, 18 | } from "@mantine/core"; 19 | import { useForm } from "@mantine/form"; 20 | import { PlusIcon, TrashIcon } from "@phosphor-icons/react"; 21 | import { useState } from "react"; 22 | import type { OptimizationParameter } from "~/types/optimize"; 23 | import type { Sequence } from "~/types/sequence"; 24 | import { nucleotideCDSLength } from "~/utils/sequence"; 25 | import { ParameterInput } from "./inputs/ParameterInput"; 26 | import { SequenceInput } from "./inputs/SequenceInput"; 27 | import { ProgressLoader } from "./ProgressLoader"; 28 | import { OptimizationInput } from "./types"; 29 | 30 | const createDefaultParameter = ( 31 | startCoordinate: number | null = null, 32 | endCoordinate: number | null = null, 33 | ): OptimizationParameter => ({ 34 | startCoordinate, 35 | endCoordinate, 36 | enforceSequence: false, 37 | codonUsageTable: "homo-sapiens", 38 | optimizeCai: true, 39 | optimizeTai: null, 40 | avoidRepeatLength: 10, 41 | enableUridineDepletion: false, 42 | avoidRibosomeSlip: false, 43 | avoidManufactureRestrictionSites: false, 44 | avoidMicroRnaSeedSites: false, 45 | gcContentMin: 0.4, 46 | gcContentMax: 0.7, 47 | gcContentWindow: 100, 48 | avoidRestrictionSites: [], 49 | avoidSequences: [], 50 | avoidPolyT: 9, 51 | avoidPolyA: 9, 52 | avoidPolyC: 6, 53 | avoidPolyG: 6, 54 | hairpinStemSize: 10, 55 | hairpinWindow: 60, 56 | }); 57 | 58 | const parameterTitle = ( 59 | sequence: Sequence, 60 | parameter: OptimizationParameter, 61 | ) => { 62 | const start = parameter.startCoordinate ?? 1; 63 | const end = parameter.endCoordinate ?? nucleotideCDSLength(sequence); 64 | return `Region [${start}-${end}]`; 65 | }; 66 | 67 | const checkSequence = (v: OptimizationInput) => { 68 | const { codingSequence, codingSequenceType } = v.sequence; 69 | const aaStopCodonMissing = 70 | codingSequenceType === "amino-acid" && !codingSequence.endsWith("*"); 71 | const naStopCodonMissing = 72 | codingSequenceType === "nucleic-acid" && 73 | codingSequence.search("(TGA|TAA|TAG)$") === -1; 74 | if (aaStopCodonMissing || naStopCodonMissing) { 75 | return ( 76 | 77 | The sequence does not end with a stop codon ('*' for amino acid 78 | sequences, and 'TGA', 'TAA' or 'TAG' for nucleotide sequences). 79 | Sequences that lack a stop codon may change the protein's function. 80 | 81 | ); 82 | } 83 | }; 84 | 85 | const AccordionControl = ({ 86 | showDelete, 87 | onClickDelete, 88 | ...props 89 | }: { 90 | showDelete: boolean; 91 | onClickDelete: () => void; 92 | } & AccordionControlProps) => ( 93 |
    94 | 95 | {showDelete && ( 96 | 97 | 98 | 99 | 100 | 101 | )} 102 |
    103 | ); 104 | 105 | interface InputFormProps { 106 | onSubmit: (v: OptimizationInput) => Promise; 107 | } 108 | 109 | export const InputForm = ({ onSubmit }: InputFormProps) => { 110 | const [optimisationMode, setOptimisationMode] = useState("simple"); 111 | const [accordionValue, setAccordionValue] = useState("0"); 112 | 113 | const form = useForm({ 114 | initialValues: { 115 | name: "", 116 | numberOfSequences: 3, 117 | sequence: { 118 | codingSequenceType: "nucleic-acid", 119 | codingSequence: "", 120 | fivePrimeUtr: "", 121 | threePrimeUtr: "", 122 | polyATail: "", 123 | }, 124 | parameters: [createDefaultParameter()], 125 | }, 126 | transformValues: (values) => OptimizationInput.parse(values), 127 | validate: (values) => { 128 | const result = OptimizationInput.safeParse(values); 129 | if (result.success) { 130 | return {}; 131 | } 132 | return Object.fromEntries( 133 | result.error.issues 134 | .filter((issue) => issue.path.length) 135 | .map((issue) => [issue.path.join("."), issue.message]), 136 | ); 137 | }, 138 | }); 139 | 140 | const handleOptimisationModeOnChange = (v: string) => { 141 | setOptimisationMode(v); 142 | if (v === "simple") { 143 | form.setFieldValue("parameters", [createDefaultParameter()]); 144 | } else { 145 | form.setFieldValue("parameters", [ 146 | createDefaultParameter( 147 | 1, 148 | nucleotideCDSLength(form.getValues().sequence) || 90, 149 | ), 150 | ]); 151 | setAccordionValue("0"); 152 | } 153 | }; 154 | 155 | const handleOnAddParameter = () => { 156 | const { sequence, parameters } = form.getValues(); 157 | const numParameters = parameters.length; 158 | const startCoordinate = numParameters ? 1 : null; 159 | const endCoordinate = numParameters 160 | ? nucleotideCDSLength(sequence) || 90 161 | : null; 162 | form.insertListItem( 163 | "parameters", 164 | createDefaultParameter(startCoordinate, endCoordinate), 165 | ); 166 | setAccordionValue(numParameters.toString()); 167 | }; 168 | 169 | const handleOnDeleteParameter = (index: number) => { 170 | form.removeListItem("parameters", index); 171 | }; 172 | 173 | return ( 174 |
    175 | 176 |
    177 | 182 | 189 |
    190 |
    191 | 192 |
    193 |
    194 | 203 | 204 | {optimisationMode === "simple" && ( 205 | 206 | 207 | 208 | )} 209 | {optimisationMode === "advanced" && ( 210 | <> 211 | 216 | {form.getValues().parameters.map((p, index) => ( 217 | // biome-ignore lint/suspicious/noArrayIndexKey: No other suitable key 218 | 219 | 1} 221 | onClickDelete={() => handleOnDeleteParameter(index)} 222 | > 223 | 224 | {parameterTitle(form.getValues().sequence, p)} 225 | 226 | 227 | 228 | 229 | 230 | 231 | ))} 232 | 233 |
    234 | 243 |
    244 | 245 | )} 246 |
    247 | 255 | {form.submitting && ( 256 | 269 | {checkSequence(form.getTransformedValues())} 270 | 271 | ), 272 | }} 273 | /> 274 | )} 275 |
    276 | 277 | ); 278 | }; 279 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/OptimizePage.tsx: -------------------------------------------------------------------------------- 1 | import { Alert, Box, Modal, Stack, Tabs, Text } from "@mantine/core"; 2 | import { 3 | ClipboardTextIcon, 4 | FileIcon, 5 | InfoIcon, 6 | QuestionIcon, 7 | ScribbleIcon, 8 | } from "@phosphor-icons/react"; 9 | import { useState } from "react"; 10 | import { analyze, convert, optimize } from "~/api"; 11 | import { OptimizationError } from "~/types/optimize"; 12 | import { Help } from "./Help"; 13 | import { InputForm } from "./InputForm"; 14 | import { Output, type OutputProps } from "./Output"; 15 | import { TermsAndConditions } from "./TermsAndConditions"; 16 | import type { OptimizationInput, OptimizationOutput } from "./types"; 17 | 18 | export const OptimizePage = () => { 19 | const [activeTab, setActiveTab] = useState("input"); 20 | const [outputProps, setOutputProps] = useState(); 21 | const [optimizationError, setOptimizationError] = useState< 22 | OptimizationError | string 23 | >(); 24 | 25 | const handleTabsOnChange = (tab: string | null) => { 26 | if (tab === "paper") { 27 | window 28 | ?.open( 29 | "https://www.biorxiv.org/content/10.1101/2024.12.03.626696v3", 30 | "_blank", 31 | ) 32 | ?.focus(); 33 | } else { 34 | setActiveTab(tab); 35 | } 36 | }; 37 | 38 | const handleOnSubmit = async (values: OptimizationInput) => { 39 | const parseInput = async ( 40 | optimizationInput: OptimizationInput, 41 | ): Promise => { 42 | let codingSequence = optimizationInput.sequence.codingSequence; 43 | if (optimizationInput.sequence.codingSequenceType === "amino-acid") { 44 | codingSequence = ( 45 | await convert({ sequence: codingSequence, organism: "homo-sapiens" }) 46 | ).sequence; 47 | } 48 | return { 49 | ...optimizationInput, 50 | sequence: { 51 | ...optimizationInput.sequence, 52 | codingSequenceType: "nucleic-acid", 53 | codingSequence, 54 | }, 55 | }; 56 | }; 57 | 58 | const analyzeSequence = async (sequence: string, organism: string) => { 59 | if (sequence) { 60 | return await analyze({ sequence, organism }); 61 | } 62 | return null; 63 | }; 64 | 65 | const optimizeAndAnalyze = async ( 66 | optimizationForm: OptimizationInput, 67 | ): Promise => { 68 | const { sequence, parameters } = optimizationForm; 69 | const optimization = await optimize({ 70 | sequence: sequence.codingSequence, 71 | parameters, 72 | }); 73 | if (!optimization.success) { 74 | throw optimization; 75 | } 76 | 77 | const cdsAnalysis = await analyze({ 78 | sequence: optimization.result.sequence.nucleicAcidSequence, 79 | organism: parameters[0].codonUsageTable, 80 | }); 81 | 82 | const fullSequenceAnalysis = await analyze({ 83 | sequence: `${sequence.fivePrimeUtr}${optimization.result.sequence.nucleicAcidSequence}${sequence.threePrimeUtr}${sequence.polyATail}`, 84 | organism: parameters[0].codonUsageTable, 85 | }); 86 | 87 | return { optimization, cdsAnalysis, fullSequenceAnalysis }; 88 | }; 89 | 90 | setOutputProps(undefined); 91 | setOptimizationError(undefined); 92 | try { 93 | const formValues = await parseInput(values); 94 | const { sequence, parameters, numberOfSequences } = formValues; 95 | const organism = parameters[0].codonUsageTable; 96 | 97 | const [ 98 | cdsAnalysis, 99 | fivePrimeUtrAnalysis, 100 | threePrimeUtrAnalysis, 101 | fullSequenceAnalysis, 102 | ...outputs 103 | ] = await Promise.all([ 104 | analyze({ 105 | sequence: sequence.codingSequence, 106 | organism, 107 | }), 108 | analyzeSequence(sequence.fivePrimeUtr, organism), 109 | analyzeSequence(sequence.threePrimeUtr, organism), 110 | analyze({ 111 | sequence: `${sequence.fivePrimeUtr}${sequence.codingSequence}${sequence.threePrimeUtr}${sequence.polyATail}`, 112 | organism: organism, 113 | }), 114 | ...Array(numberOfSequences) 115 | .fill(0) 116 | .map(() => optimizeAndAnalyze(formValues)), 117 | ]); 118 | 119 | setOutputProps({ 120 | input: formValues, 121 | output: { 122 | input: { 123 | cdsAnalysis, 124 | fivePrimeUtrAnalysis, 125 | threePrimeUtrAnalysis, 126 | fullSequenceAnalysis, 127 | }, 128 | outputs: outputs.sort((a, b) => 129 | (a.fullSequenceAnalysis.codonAdaptationIndex ?? 0) > 130 | (b.cdsAnalysis.codonAdaptationIndex ?? 0) 131 | ? -1 132 | : 1, 133 | ), 134 | }, 135 | }); 136 | setActiveTab("output"); 137 | } catch (e) { 138 | console.error(e); 139 | setOutputProps(undefined); 140 | const error = OptimizationError.safeParse(e); 141 | if (error.success) { 142 | setOptimizationError(error.data); 143 | } else { 144 | setOptimizationError("N/A"); 145 | } 146 | } finally { 147 | } 148 | }; 149 | 150 | return ( 151 | 152 | 153 | } 156 | > 157 | Input 158 | 159 | } 162 | disabled={!outputProps} 163 | > 164 | Output 165 | 166 | } 169 | ml="auto" 170 | > 171 | Help 172 | 173 | } 176 | > 177 | Terms 178 | 179 | }> 180 | Paper 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | {outputProps && } 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | {optimizationError && ( 198 | setOptimizationError(undefined)}> 199 | 200 | Error resolving constraints. Sequence cannot be optimised. Please 201 | verify your input sequence or adjust input parameters (e.g. increase 202 | GC content/window). 203 | 204 | {typeof optimizationError === "string" 205 | ? optimizationError 206 | : optimizationError.error.message 207 | .split("\n") 208 | .map((v) => {v})} 209 | 210 | 211 | 212 | )} 213 | 214 | ); 215 | }; 216 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/Output.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Card, Group, Stack, Text } from "@mantine/core"; 2 | import { DownloadSimpleIcon } from "@phosphor-icons/react"; 3 | import { format } from "date-fns"; 4 | import { useMemo } from "react"; 5 | import { Fragment } from "react/jsx-runtime"; 6 | import type { OptimizationParameter } from "~/types/optimize"; 7 | import type { Sequence } from "~/types/sequence"; 8 | import { nucleotideCDSLength } from "~/utils/sequence"; 9 | import type { OptimizationInput, OptimizationOutput } from "./types"; 10 | 11 | const parameterTitle = ( 12 | sequence: Sequence, 13 | parameter: OptimizationParameter, 14 | ): string => { 15 | const start = parameter.startCoordinate ?? 1; 16 | const end = parameter.endCoordinate ?? nucleotideCDSLength(sequence); 17 | return `Parameter region [${start}-${end}]`; 18 | }; 19 | 20 | const generateReport = ({ 21 | input: { name, sequence, parameters }, 22 | output, 23 | }: OutputProps): string => { 24 | const date = new Date(); 25 | const inputReport = [ 26 | "---mRNArchitect", 27 | "Version\t\t0.4", 28 | `Date\t\t${format(date, "do MMM yyyy")}`, 29 | `Time\t\t${format(date, "HH:mm:ss x")}`, 30 | `Sequence name\t${name}`, 31 | "", 32 | "---Input Sequence", 33 | `CDS\t\t${sequence.codingSequence}`, 34 | `5' UTR\t\t${sequence.fivePrimeUtr}`, 35 | `3' UTR\t\t${sequence.threePrimeUtr}`, 36 | `Poly(A) tail\t${sequence.polyATail}`, 37 | ]; 38 | 39 | const parameterReports = parameters 40 | .map((c) => { 41 | const report: string[] = [ 42 | `---${parameterTitle(sequence, c)}`, 43 | `Start coordinate\t\t\t${c.startCoordinate || "1"}`, 44 | `End coordinate\t\t\t\t${c.endCoordinate || output.outputs[0].optimization.result.sequence.nucleicAcidSequence.length}`, 45 | `Don't optimise region\t\t\t${c.enforceSequence}`, 46 | ]; 47 | if (!c.enforceSequence) { 48 | report.push( 49 | ...[ 50 | //`Optimise CAI\t\t\t\t${c.optimizeCai}`, 51 | //`Optimise tAI\t\t\t\t${c.optimizeTai}`, 52 | `Organism\t\t\t\t${c.codonUsageTable}`, 53 | `Avoid repeat length\t\t\t${c.avoidRepeatLength}`, 54 | `Enable uridine depletion\t\t${c.enableUridineDepletion}`, 55 | `Avoid ribosome slip\t\t\t${c.avoidRibosomeSlip}`, 56 | `Avoid manufacture restriction sites\t${c.avoidManufactureRestrictionSites}`, 57 | `Avoid microRNA seed sites\t\t${c.avoidMicroRnaSeedSites}`, 58 | `GC content minimum\t\t\t${c.gcContentMin}`, 59 | `GC content maximum\t\t\t${c.gcContentMax}`, 60 | `GC content window\t\t\t${c.gcContentWindow}`, 61 | `Avoid cut sites\t\t\t\t${c.avoidRestrictionSites}`, 62 | `Avoid sequences\t\t\t\t${c.avoidSequences}`, 63 | `Avoid poly(U)\t\t\t\t${c.avoidPolyT}`, 64 | `Avoid poly(A)\t\t\t\t${c.avoidPolyA}`, 65 | `Avoid poly(C)\t\t\t\t${c.avoidPolyC}`, 66 | `Avoid poly(G)\t\t\t\t${c.avoidPolyG}`, 67 | `Hairpin stem size\t\t\t${c.hairpinStemSize}`, 68 | `Hairpin window\t\t\t\t${c.hairpinWindow}`, 69 | ], 70 | ); 71 | } 72 | return report; 73 | }) 74 | .reduce((prev, current) => prev.concat([""], current), []); 75 | 76 | const outputReports = output.outputs.map( 77 | ({ optimization, cdsAnalysis, fullSequenceAnalysis }, index) => [ 78 | `---Optimised Sequence #${index + 1}`, 79 | "", 80 | `CDS:\t\t\t${optimization.result.sequence.nucleicAcidSequence}`, 81 | "", 82 | `Full-length mRNA:\t${sequence.fivePrimeUtr + optimization.result.sequence.nucleicAcidSequence + sequence.threePrimeUtr + sequence.polyATail}`, 83 | "", 84 | "---Results", 85 | "Metric\t\t\tInput\tOptimised", 86 | `A ratio\t\t\t${output.input.cdsAnalysis.aRatio.toFixed(2)}\t${cdsAnalysis.aRatio.toFixed(2)}`, 87 | `T/U ratio\t\t${output.input.cdsAnalysis.tRatio.toFixed(2)}\t${cdsAnalysis.tRatio.toFixed(2)}`, 88 | `G ratio\t\t\t${output.input.cdsAnalysis.gRatio.toFixed(2)}\t${cdsAnalysis.gRatio.toFixed(2)}`, 89 | `C ratio\t\t\t${output.input.cdsAnalysis.cRatio.toFixed(2)}\t${cdsAnalysis.cRatio.toFixed(2)}`, 90 | `AT ratio\t\t${output.input.cdsAnalysis.atRatio.toFixed(2)}\t${cdsAnalysis.atRatio.toFixed(2)}`, 91 | `GA ratio\t\t${output.input.cdsAnalysis.gaRatio.toFixed(2)}\t${cdsAnalysis.gaRatio.toFixed(2)}`, 92 | `GC ratio\t\t${output.input.cdsAnalysis.gcRatio.toFixed(2)}\t${cdsAnalysis.gcRatio.toFixed(2)}`, 93 | `Uridine depletion\t${output.input.cdsAnalysis.uridineDepletion?.toFixed(2) ?? "-"}\t${cdsAnalysis.uridineDepletion?.toFixed(2) ?? "-"}`, 94 | `CAI\t\t\t${output.input.cdsAnalysis.codonAdaptationIndex?.toFixed(2) ?? "-"}\t${cdsAnalysis.codonAdaptationIndex?.toFixed(2) ?? "-"}`, 95 | `tAI\t\t\t${output.input.cdsAnalysis.trnaAdaptationIndex?.toFixed(2) ?? "-"}\t${cdsAnalysis.trnaAdaptationIndex?.toFixed(2) ?? "-"}`, 96 | `CDS MFE (kcal/mol)\t${output.input.cdsAnalysis.minimumFreeEnergy.energy.toFixed(2)}\t${cdsAnalysis.minimumFreeEnergy.energy.toFixed(2)}`, 97 | `5' UTR MFE (kcal/mol)\t${output.input.fivePrimeUtrAnalysis?.minimumFreeEnergy.energy.toFixed(2) ?? "-"}\t${output.input.fivePrimeUtrAnalysis?.minimumFreeEnergy.energy.toFixed(2) ?? "-"}`, 98 | `3' UTR MFE (kcal/mol)\t${output.input.threePrimeUtrAnalysis?.minimumFreeEnergy.energy.toFixed(2) ?? "-"}\t${output.input.threePrimeUtrAnalysis?.minimumFreeEnergy.energy.toFixed(2) ?? "-"}`, 99 | `Total MFE (kcal/mol)\t${output.input.fullSequenceAnalysis?.minimumFreeEnergy.energy.toFixed(2) ?? "-"}\t${fullSequenceAnalysis?.minimumFreeEnergy.energy.toFixed(2) ?? "-"}`, 100 | "", 101 | "---Logs", 102 | ...optimization.result.constraints.trim().split("\n"), 103 | ...optimization.result.objectives.trim().split("\n"), 104 | ], 105 | ); 106 | 107 | const reportText = [ 108 | ...inputReport, 109 | "", 110 | "---Optimisation parameters:", 111 | ...parameterReports, 112 | "", 113 | ...outputReports.reduce( 114 | (accumulator, current) => accumulator.concat([""], current), 115 | [], 116 | ), 117 | "", 118 | ]; 119 | 120 | return reportText.join("\n"); 121 | }; 122 | 123 | export interface OutputProps { 124 | input: OptimizationInput; 125 | output: OptimizationOutput; 126 | } 127 | 128 | export const Output = ({ input, output }: OutputProps) => { 129 | const report = useMemo( 130 | () => generateReport({ input, output }), 131 | [input, output], 132 | ); 133 | 134 | return ( 135 | 136 | 137 | 145 | 146 | 147 | 148 | 149 |
    150 |             {report.split("\n").map((line) => (
    151 |               
    152 |                 {line}
    153 |                 
    154 |
    155 | ))} 156 |
    157 |
    158 |
    159 |
    160 | ); 161 | }; 162 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/ProgressLoader.tsx: -------------------------------------------------------------------------------- 1 | import { Card, Center, Container, Loader, Stack, Text } from "@mantine/core"; 2 | import { useInterval } from "@mantine/hooks"; 3 | import { formatDuration, intervalToDuration } from "date-fns"; 4 | import { type PropsWithChildren, useState } from "react"; 5 | 6 | interface ProgressLoaderProps { 7 | estimatedTimeInSeconds: number; 8 | } 9 | 10 | export const ProgressLoader = ({ 11 | children, 12 | estimatedTimeInSeconds, 13 | }: ProgressLoaderProps & PropsWithChildren) => { 14 | const [elapsedSeconds, setElapsedSeconds] = useState(0); 15 | useInterval(() => setElapsedSeconds((s) => s + 1), 1000, { 16 | autoInvoke: true, 17 | }); 18 | 19 | const formattedEstimatedTime = formatDuration( 20 | intervalToDuration({ start: 0, end: estimatedTimeInSeconds * 1000 }), 21 | { format: ["minutes"] }, 22 | ); 23 | const formattedElapsedTime = formatDuration( 24 | intervalToDuration({ start: 0, end: elapsedSeconds * 1000 }), 25 | { format: ["minutes", "seconds"], zero: true }, 26 | ); 27 | 28 | return ( 29 | 30 |
    31 | 32 | 33 | 34 | Optimisation in progress... 35 | {`Estimated time: < ${formattedEstimatedTime}`} 36 | {`Elapsed time: ${formattedElapsedTime}`} 37 | {children} 38 | 39 | 40 |
    41 |
    42 | ); 43 | }; 44 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/TermsAndConditions.tsx: -------------------------------------------------------------------------------- 1 | import { Stack, Text } from "@mantine/core"; 2 | 3 | export const TermsAndConditions = () => ( 4 | 5 | Terms and Conditions for Use of mRNArchitect Software 6 | Last updated: 14 August 2025 7 | 8 | These Terms and Conditions (“Terms”) govern the use of the mRNArchitect 9 | software (the “Software”) made available by BASE Facility, The University 10 | of Queensland. 11 | 12 | 13 | By downloading, installing, or using the Software, you agree to be bound 14 | by these Terms. If you do not agree, you must not access or use the 15 | Software. 16 | 17 | 18 | 1. License and Permitted Use 19 |
    20 | 1.1 University Of Queensland grants you a limited, non-exclusive, 21 | non-transferable, revocable license to use the Software for research 22 | purposes, including both non-profit and commercial research activities. 23 |
    24 | 1.2 You must comply with all applicable laws, regulations, and 25 | institutional requirements when using the Software. 26 |
    27 | 1.3 You must not: Sell, sublicense, rent, lease, or otherwise distribute 28 | the Software; Reverse engineer, decompile, or modify the Software, except 29 | as permitted by law; Use the Software for any unlawful, unethical, or 30 | harmful purposes. 31 |
    32 | 33 | 2. Intellectual Property 34 |
    35 | 2.1 The Software and all related documentation, updates, associated 36 | experimental results and materials remain the intellectual property of 37 | BASE Facility and/or its licensors. 38 |
    39 | 2.2 No patent or other intellectual property license is granted or implied 40 | through use; users assume all responsibility for obtaining necessary 41 | rights to any third-party components or data they use with the Software. 42 |
    43 | 2.3 All rights not expressly granted in these Terms are reserved. 44 |
    45 | 46 | 3. User Responsibilities 47 |
    48 | 3.1 You are responsible for: Ensuring the accuracy of any data you input 49 | into the Software; Backing up your work and maintaining your own data 50 | security. 51 |
    52 | 3.2 You are responsible for: Using the Software in compliance with 53 | relevant biosafety and ethical guidelines; Ensuring compliance with 54 | applicable data protection and confidentiality requirements. 55 |
    56 | 3.3 Citation Requirement: Publications, presentations, or other outputs 57 | resulting from use of the Software must acknowledge and cite BASE 58 | Facility, University Of Queensland and the mRNArchitect software in 59 | accordance with academic standards. 60 |
    61 | 62 | 4. Data and Privacy 63 |
    64 | 4.1 The Software may process user-provided data locally or via secure 65 | servers. 66 |
    67 | 4.2 BASE Facility will handle personal data in accordance with the{" "} 68 | 69 | UQ Privacy Policy 70 | 71 | . 72 |
    73 | 4.3 BASE Facility may collect identifiers (e.g., name, email), usage data, 74 | and research-related information to support service provision and 75 | compliance. 76 |
    77 | 4.4 BASE Facility will not sell data to third parties. 78 |
    79 | 80 | 5. Disclaimers 81 |
    82 | 5.1 The Software is provided “as is”, without any warranty of any kind, 83 | whether express or implied, including but not limited to warranties of 84 | merchantability, fitness for a particular purpose, or non-infringement. 85 |
    86 | 5.2 BASE Facility does not warrant that the Software will be error-free, 87 | uninterrupted, or free of security vulnerabilities. 88 |
    89 | 5.3 Outputs from the Software may require expert interpretation and 90 | verification; BASE Facility accepts no responsibility for any conclusions 91 | or decisions made based on such outputs. 92 |
    93 | 94 | 6. Limitation of Liability 95 |
    96 | 6.1 To the maximum extent permitted by law, BASE Facility will not be 97 | liable for any indirect, incidental, special, consequential, or punitive 98 | damages, or for any loss of data, profits, or business opportunities 99 | arising from the use of, or inability to use, the Software. 100 |
    101 | 6.2 BASE Facility’s total liability in connection with the Software will 102 | not exceed the amount you paid (if any) for access to the Software. 103 |
    104 | 105 | 7. Updates and Modifications 106 |
    107 | 7.1 BASE Facility may update, modify, or discontinue the Software (in 108 | whole or in part) at any time without notice. 109 |
    110 | 7.2 These Terms may be updated from time to time; continued use of the 111 | Software after changes are posted constitutes acceptance of the revised 112 | Terms. 113 |
    114 | 115 | 8. Termination 116 |
    117 | 8.1 BASE Facility may suspend or terminate your access to the Software at 118 | its sole discretion if you breach these Terms. 119 |
    120 | 8.2 Upon termination, you must immediately cease all use of the Software 121 | and destroy any copies in your possession. 122 |
    123 | 124 | 9. Governing Law 125 |
    126 | 9.1 These Terms are governed by and construed in accordance with the laws 127 | of the State of Queensland, Australia, without regard to conflict of law 128 | principles. 129 |
    130 |
    131 | ); 132 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/inputs/ParameterInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Flex, 3 | Group, 4 | InputWrapper, 5 | MultiSelect, 6 | NativeSelect, 7 | NumberInput, 8 | Radio, 9 | RangeSlider, 10 | SegmentedControl, 11 | Stack, 12 | Switch, 13 | TagsInput, 14 | } from "@mantine/core"; 15 | import type { UseFormReturnType } from "@mantine/form"; 16 | import type { OptimizationInput } from "~/components/pages/optimize/types"; 17 | import { ORGANISMS } from "~/constants"; 18 | import RESTRICTION_SITES from "~/data/restriction-sites.json"; 19 | 20 | export const ParameterInput = ({ 21 | form, 22 | hideCoordinates = false, 23 | index, 24 | }: { 25 | form: UseFormReturnType; 26 | hideCoordinates?: boolean; 27 | index: number; 28 | }) => { 29 | const coordinateType = 30 | Number.isInteger(form.getValues().parameters[index].startCoordinate) || 31 | Number.isInteger(form.getValues().parameters[index].endCoordinate) 32 | ? "sub-region" 33 | : "full-sequence"; 34 | 35 | const handleOnChangeCoordinateType = (value: string) => { 36 | const { 37 | sequence: { codingSequence, codingSequenceType }, 38 | } = form.getValues(); 39 | const nucleotideSequenceLength = 40 | codingSequence.length * (codingSequenceType === "nucleic-acid" ? 1 : 3); 41 | const end = nucleotideSequenceLength > 3 ? nucleotideSequenceLength : 90; 42 | form.setFieldValue( 43 | `parameters.${index}.startCoordinate`, 44 | value === "sub-region" ? 1 : null, 45 | ); 46 | form.setFieldValue( 47 | `parameters.${index}.endCoordinate`, 48 | value === "sub-region" ? end : null, 49 | ); 50 | }; 51 | 52 | return ( 53 | 54 | {!hideCoordinates && ( 55 | 56 | 57 | 65 | {coordinateType === "sub-region" && ( 66 | 72 | 79 | 86 | 87 | )} 88 | 97 | 98 | 99 | )} 100 | {!form.getValues().parameters[index].enforceSequence && ( 101 | <> 102 | {false && ( 103 | { 110 | const optimizeCai = v === "cai"; 111 | const optimizeTai = v === "cai" ? null : 1.0; 112 | form.setFieldValue( 113 | `parameters.${index}.optimizeCai`, 114 | optimizeCai, 115 | ); 116 | form.setFieldValue( 117 | `parameters.${index}.optimizeTai`, 118 | optimizeTai, 119 | ); 120 | }} 121 | > 122 | 123 | 124 | 125 | 126 | 127 | )} 128 | 134 | 141 | 147 | 148 | 160 | 172 | 173 | 174 | 186 | 198 | 199 | 200 | 201 | 208 | 209 | { 227 | form.setFieldValue(`parameters.${index}.gcContentMin`, min); 228 | form.setFieldValue(`parameters.${index}.gcContentMax`, max); 229 | }} 230 | /> 231 | 232 | 239 | 240 | 241 | ({ 248 | label: v, 249 | value: v, 250 | }))} 251 | key={form.key(`parameters.${index}.avoidRestrictionSites`)} 252 | {...form.getInputProps(`parameters.${index}.avoidRestrictionSites`)} 253 | /> 254 | 261 | 262 | 269 | 276 | 283 | 290 | 297 | 298 | 299 | 300 | 307 | 314 | 321 | 322 | 323 | 324 | )} 325 | 326 | ); 327 | }; 328 | -------------------------------------------------------------------------------- /frontend/src/components/pages/optimize/inputs/SequenceInput.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Button, 3 | Group, 4 | InputWrapper, 5 | NativeSelect, 6 | NumberInput, 7 | SegmentedControl, 8 | Stack, 9 | Textarea, 10 | } from "@mantine/core"; 11 | import type { UseFormReturnType } from "@mantine/form"; 12 | import { useState } from "react"; 13 | import type { OptimizationInput } from "~/components/pages/optimize/types"; 14 | import { 15 | EXAMPLE_SEQUENCES, 16 | FIVE_PRIME_UTRS, 17 | THREE_PRIME_UTRS, 18 | } from "~/constants"; 19 | import type { Sequence } from "~/types/sequence"; 20 | 21 | export interface SequenceFormProps { 22 | initialSequence?: Sequence; 23 | onSave: (sequence: Sequence) => void; 24 | } 25 | 26 | export const SequenceInput = ({ 27 | form, 28 | }: { 29 | form: UseFormReturnType; 30 | }) => { 31 | const [fivePrimeUTRSequenceType, setFivePrimeUTRSequenceType] = 32 | useState(""); 33 | const [threePrimeUTRSequenceType, setThreePrimeUTRSequenceType] = 34 | useState(""); 35 | const [polyATailType, setPolyATailType] = useState< 36 | "none" | "generate" | "custom" 37 | >("none"); 38 | const [polyATailGenerate, setPolyATailGenerate] = useState( 39 | 120, 40 | ); 41 | 42 | const handleOnChangeFivePrimeUtrSequenceType = (v: string) => { 43 | setFivePrimeUTRSequenceType(v); 44 | form.setFieldValue("sequence.fivePrimeUtr", v); 45 | }; 46 | 47 | const handleOnChangeThreePrimeUtrSequenceType = (v: string) => { 48 | setThreePrimeUTRSequenceType(v); 49 | form.setFieldValue("sequence.threePrimeUtr", v); 50 | }; 51 | 52 | const handleOnChangePolyATailType = (v: string) => { 53 | setPolyATailType(v as typeof polyATailType); 54 | if (v === "none") { 55 | form.setFieldValue("sequence.polyATail", ""); 56 | } else if (v === "generate") { 57 | const length = 58 | typeof polyATailGenerate === "string" 59 | ? Number.parseInt(polyATailGenerate, 10) 60 | : polyATailGenerate; 61 | form.setFieldValue("sequence.polyATail", "A".repeat(length)); 62 | } 63 | }; 64 | 65 | return ( 66 | 67 | 68 | 69 | 70 | 78 | 95 | 96 |