Skip to content

Commit

Permalink
Issues demo (#320)
Browse files Browse the repository at this point in the history
* closes #318, also fixes it for PCR

* closes #316, tests missing
  • Loading branch information
manulera authored Dec 9, 2024
1 parent aba0fbf commit 1df316b
Show file tree
Hide file tree
Showing 4 changed files with 127 additions and 20 deletions.
2 changes: 1 addition & 1 deletion ShareYourCloning_backend
61 changes: 61 additions & 0 deletions cypress/e2e/primer_design.cy.js
Original file line number Diff line number Diff line change
Expand Up @@ -348,16 +348,22 @@ describe('Test primer designer functionality', () => {
// One enzyme is enough to submit, either one
setAutocompleteValue('Left enzyme', 'EcoRI', '.primer-design');
cy.contains('.primer-design button', 'Design primers').should('exist');
cy.get('.veCutsiteLabel').filter(':contains("EcoRI")').should('exist');
clearAutocompleteValue('Left enzyme', '.primer-design');
cy.get('.veCutsiteLabel').filter(':contains("EcoRI")').should('not.exist');

cy.contains('.primer-design button', 'Design primers').should('not.exist');
setAutocompleteValue('Right enzyme', 'BamHI', '.primer-design');
cy.contains('.primer-design button', 'Design primers').should('exist');
cy.get('.veCutsiteLabel').filter(':contains("BamHI")').should('exist');

// There should be a single primer tail feature displayed
cy.get('.veLabelText').filter(':contains("primer tail")').should('have.length', 1);
setAutocompleteValue('Left enzyme', 'EcoRI', '.primer-design');

// There should be two now
cy.get('.veCutsiteLabel').filter(':contains("BamHI")').should('exist');
cy.get('.veCutsiteLabel').filter(':contains("EcoRI")').should('exist');
cy.get('.veLabelText').filter(':contains("primer tail")').should('have.length', 2);
// Go to sequence tab
cy.get('.veTabSequenceMap').contains('Sequence Map').click();
Expand All @@ -377,6 +383,8 @@ describe('Test primer designer functionality', () => {
cy.wait('@primerDesign').then((interception) => {
expect(interception.request.query.minimal_hybridization_length).to.equal('10');
expect(interception.request.query.target_tm).to.equal('40');
expect(interception.request.query.left_enzyme_inverted).to.equal('false');
expect(interception.request.query.right_enzyme_inverted).to.equal('false');
});

// We should be on the Results tab
Expand All @@ -403,6 +411,59 @@ describe('Test primer designer functionality', () => {
cy.get('li').contains('PCR with primers seq_2_EcoRI_fwd and seq_2_BamHI_rvs').should('exist');
});

it.only('Restriction ligation primer design - invert site', () => {
const sequence = 'ATCTAACTTTACTTGGAAAGCGTTTCACGT'.toLowerCase();
manuallyTypeSequence(sequence);
addSource('PCRSource');
const ampSequence = sequence.slice(1, 30);
return;
// Click on design primers
cy.get('button').contains('Design primers').click();
clickMultiSelectOption('Purpose of primers', 'Restriction and Ligation', 'li');

// Click on axis tick 1
cy.get('.veAxisTick[data-test="1"]').first().click({ force: true });

// Click on axis tick 30 while holding shift
cy.get('.veAxisTick[data-test="30"]').first().click({ shiftKey: true });

// Set selection
cy.get('button').contains('Set from selection').click();

// Go to other settings tab
cy.get('button.MuiTab-root').contains('Other settings').click();

// Select EcoRI, should not be possible to select inverted
setAutocompleteValue('Left enzyme', 'EcoRI', '.primer-design');
cy.get('.primer-design').contains('Invert site').should('not.exist');

// Should be possible to select when BsaI is selected
setAutocompleteValue('Left enzyme', 'BsaI', '.primer-design');
cy.get('.primer-design').contains('Invert site').should('exist');

// Go to sequence tab
cy.get('.veTabSequenceMap').contains('Sequence Map').click();

// THere should be the forward sequence displayed:
cy.get('svg.rowViewTextContainer text').contains(`TTTggtctc${ampSequence}`);

// Invert the site, should show the reverse sequence
cy.get('.primer-design').contains('Invert site').click();
cy.get('svg.rowViewTextContainer text').contains(`TTTgagacc${ampSequence}`);

// Select EcoRI, should not be possible to select inverted
setAutocompleteValue('Right enzyme', 'EcoRI', '.primer-design');
cy.get('.primer-design').contains('Invert site').should('not.exist');
cy.get('.primer-design').filter(':contains("Invert site")').should('have.length', 1);

// Should be possible to select when BsaI is selected
setAutocompleteValue('Right enzyme', 'BsaI', '.primer-design');
cy.get('.primer-design').filter(':contains("Invert site")').should('have.length', 2);
cy.get('svg.rowViewTextContainer text').contains(`TTTgagacc${ampSequence}ggtctcAAA`);
cy.get('.primer-design').contains('Invert site').eq(1).click();
cy.get('svg.rowViewTextContainer text').contains(`TTTgagacc${ampSequence}gagaccAAA`);
});

it('Simple pair primer design', () => {
const sequence = 'ATCTAACTTTACTTGGAAAGCGTTTCACGT';
manuallyTypeSequence(sequence);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import * as React from 'react';
import { Box, Tab, Tabs, FormControl, Button, Alert, TextField, Tooltip, FormLabel } from '@mui/material';
import { Box, Tab, Tabs, FormControl, Button, Alert, TextField, Tooltip, FormLabel, FormControlLabel, Checkbox } from '@mui/material';
import { batch, useDispatch, useSelector, useStore } from 'react-redux';
import InfoIcon from '@mui/icons-material/Info';
import { aliasedEnzymesByName, getReverseComplementSequenceString as reverseComplement } from '@teselagen/sequence-utils';
import { updateEditor } from '@teselagen/ove';
import { cloningActions } from '../../../../store/cloning';
import useStoreEditor from '../../../../hooks/useStoreEditor';
import TabPanel from '../../../navigation/TabPanel';
Expand Down Expand Up @@ -31,6 +32,11 @@ function getRecognitionSequence(enzyme) {
return recognitionSeq.split('').map((base) => (base in ambiguousDnaBases ? ambiguousDnaBases[base] : base)).join('');
}

function isEnzymePalyndromic(enzyme) {
const recognitionSeq = getRecognitionSequence(enzyme);
return recognitionSeq === reverseComplement(recognitionSeq);
}

function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) {
const templateSequenceId = pcrSource.input[0];
const { setMainSequenceId, setCurrentTab, addPrimersToPCRSource } = cloningActions;
Expand All @@ -45,6 +51,8 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) {
const primerDesignSettings = usePrimerDesignSettings({ homologyLength: null, hybridizationLength: 20, targetTm: 55 });
const [leftEnzyme, setLeftEnzyme] = React.useState(null);
const [rightEnzyme, setRightEnzyme] = React.useState(null);
const [leftEnzymeInverted, setLeftEnzymeInverted] = React.useState(false);
const [rightEnzymeInverted, setRightEnzymeInverted] = React.useState(false);
const [spacers, setSpacers] = React.useState(['', '']);
const [fillerBases, setFillerBases] = React.useState('TTT');
const [amplificationDirection, setAmplificationDirection] = React.useState('forward');
Expand All @@ -61,8 +69,10 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) {

React.useEffect(() => {
if (rois.every((region) => region !== null) && spacersAreValid && fillersAreValid) {
const forwardPrimerStartingSeq = (leftEnzyme ? fillerBases : '') + getRecognitionSequence(leftEnzyme) + spacers[0];
const reversePrimerStartingSeq = reverseComplement((rightEnzyme ? fillerBases : '') + getRecognitionSequence(rightEnzyme) + reverseComplement(spacers[1]));
const leftEnzymeSeq = leftEnzymeInverted ? reverseComplement(getRecognitionSequence(leftEnzyme)) : getRecognitionSequence(leftEnzyme);
const rightEnzymeSeq = rightEnzymeInverted ? reverseComplement(getRecognitionSequence(rightEnzyme)) : getRecognitionSequence(rightEnzyme);
const forwardPrimerStartingSeq = (leftEnzyme ? fillerBases : '') + leftEnzymeSeq + spacers[0];
const reversePrimerStartingSeq = reverseComplement((rightEnzyme ? fillerBases : '') + rightEnzymeSeq + reverseComplement(spacers[1]));
const { teselaJsonCache } = store.getState().cloning;
const templateSequence = teselaJsonCache[templateSequenceId];
const newSequenceProduct = joinEntitiesIntoSingleSequence([templateSequence], rois.map((s) => s.selectionLayer), [amplificationDirection], [forwardPrimerStartingSeq, reversePrimerStartingSeq], false, 'primer tail');
Expand All @@ -71,7 +81,25 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) {
} else {
setSequenceProduct(null);
}
}, [fillerBases, rightEnzyme, leftEnzyme, spacers, rois, spacersAreValid, fillersAreValid, templateSequenceId, store, setSequenceProduct, amplificationDirection]);
}, [fillerBases, rightEnzyme, leftEnzyme, spacers, rois, spacersAreValid, fillersAreValid, templateSequenceId, store, setSequenceProduct, amplificationDirection, leftEnzymeInverted, rightEnzymeInverted]);

// When the user changes enzyme, reset the enzyme inversion state
React.useEffect(() => {
setLeftEnzymeInverted(false);
}, [leftEnzyme]);
React.useEffect(() => {
setRightEnzymeInverted(false);
}, [rightEnzyme]);

// When enzymes change, update the displayed enzymes in the editor
React.useEffect(() => {
const allEnzymes = [leftEnzyme, rightEnzyme].filter((enzyme) => enzyme !== null);
const filteredRestrictionEnzymes = allEnzymes.map((enzyme) => ({
canBeHidden: true,
value: enzyme,
}));
updateEditor(store, 'mainEditor', { annotationVisibility: { cutsites: leftEnzyme || rightEnzyme }, restrictionEnzymes: { filteredRestrictionEnzymes, isEnzymeFilterAnd: false } });
}, [leftEnzyme, rightEnzyme]);

const addPrimers = () => {
batch(() => {
Expand All @@ -87,6 +115,7 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) {
onTabChange(null, 0);
document.getElementById(`source-${pcrSource.id}`)?.scrollIntoView();
updateStoreEditor('mainEditor', null);
updateEditor(store, 'mainEditor', { annotationVisibility: { cutsites: false } });
};

const onPrimerDesign = async () => {
Expand All @@ -95,6 +124,8 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) {
target_tm: primerDesignSettings.targetTm,
left_enzyme: leftEnzyme,
right_enzyme: rightEnzyme,
left_enzyme_inverted: leftEnzymeInverted,
right_enzyme_inverted: rightEnzymeInverted,
filler_bases: fillerBases,
};
const serverError = await designPrimers(rois, params, [amplificationDirection], spacers);
Expand All @@ -119,13 +150,29 @@ function PrimerDesignSimplePair({ pcrSource, restrictionLigation = false }) {
{restrictionLigation && (
<>
<FormLabel>Restriction enzyme sites</FormLabel>
<Box>
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
<EnzymeMultiSelect value={leftEnzyme} setEnzymes={setLeftEnzyme} label="Left enzyme" multiple={false} />
</FormControl>
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
<EnzymeMultiSelect value={rightEnzyme} setEnzymes={setRightEnzyme} label="Right enzyme" multiple={false} />
</FormControl>
<Box sx={{ display: 'flex', flexDirection: 'row', justifyContent: 'center' }}>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
<EnzymeMultiSelect value={leftEnzyme} setEnzymes={setLeftEnzyme} label="Left enzyme" multiple={false} />
</FormControl>
{leftEnzyme && !isEnzymePalyndromic(leftEnzyme) && (
<FormControlLabel
control={<Checkbox checked={leftEnzymeInverted} onChange={(e) => setLeftEnzymeInverted(e.target.checked)} />}
label="Invert site"
/>
)}
</Box>
<Box sx={{ display: 'flex', flexDirection: 'column', alignItems: 'center' }}>
<FormControl sx={{ width: '10em', mt: 1.5, mr: 2 }}>
<EnzymeMultiSelect value={rightEnzyme} setEnzymes={setRightEnzyme} label="Right enzyme" multiple={false} />
</FormControl>
{rightEnzyme && !isEnzymePalyndromic(rightEnzyme) && (
<FormControlLabel
control={<Checkbox checked={rightEnzymeInverted} onChange={(e) => setRightEnzymeInverted(e.target.checked)} />}
label="Invert site"
/>
)}
</Box>
<FormControl sx={{ width: '10em', mt: 1.5 }}>
<TextField
label={(
Expand Down
15 changes: 7 additions & 8 deletions src/utils/transformCoords.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { flipContainedRange, isRangeWithinRange, translateRange } from '@teselagen/range-utils';
import { flipContainedRange, getRangeLength, isRangeWithinRange, translateRange } from '@teselagen/range-utils';

export default function getTransformCoords({ assembly, type: sourceType }, parentSequenceData, productLength) {
if (!assembly) {
Expand All @@ -9,7 +9,7 @@ export default function getTransformCoords({ assembly, type: sourceType }, paren

let count = 0;
if (sourceType === 'PCRSource') {
count = fragments[0].left_location.start;
count = assembly[0].right_location.start;
}
// Special case for insertion assemblies
// else if (fragments[0].sequence === fragments[fragments.length - 1].sequence && !circular) {
Expand All @@ -22,15 +22,14 @@ export default function getTransformCoords({ assembly, type: sourceType }, paren
const sequence = parentSequenceData.find((e) => e.id === f.sequence);
const { size } = sequence;
const { left_location: left, right_location: right } = f;
const leftEdge = count;
const leftStart = left?.start || 0;
const rightStart = right?.start || 0;
const rightEnd = right?.end || size;
const rightEdge = count + rightEnd - (left?.start || 0);
// Ranges are 0-based, but [0:0] is not empty, it's the equivalent to python's [0:1]
f.rangeInAssembly = translateRange({ start: leftEdge, end: rightEdge - 1 }, 0, productLength);
const rangeLength = getRangeLength({ start: leftStart, end: rightEnd - 1 }, size);
f.rangeInAssembly = translateRange({ start: 0, end: rangeLength - 1 }, count, productLength);
f.size = size;

count += (rightStart - (left?.start || 0));
count += getRangeLength({ start: leftStart, end: rightStart - 1 }, size);
});
const rangeInParent = (selection, id) => {
if (selection.start === -1) {
Expand All @@ -43,7 +42,7 @@ export default function getTransformCoords({ assembly, type: sourceType }, paren
const { rangeInAssembly, left_location: left, reverse_complemented, size } = fragment;
const startInParent = left?.start || 0;
if (isRangeWithinRange(selection, rangeInAssembly, productLength)) {
const selectionShifted = selection.start < selection.end ? selection : { start: selection.start, end: selection.end + productLength };
const selectionShifted = selection.start <= selection.end ? selection : { start: selection.start, end: selection.end + productLength };
const outRange = translateRange(selectionShifted, -rangeInAssembly.start + startInParent, size);
if (reverse_complemented) {
return flipContainedRange(outRange, { start: 0, end: size - 1 }, size);
Expand Down

0 comments on commit 1df316b

Please sign in to comment.