Skip to content

Commit 533fa15

Browse files
manuleraCopilot
andauthored
Update EBIC primer design with more options (#536)
* update EBIC primer design with more options * Update src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx Co-authored-by: Copilot <[email protected]> * Apply suggestions from code review Co-authored-by: Copilot <[email protected]> --------- Co-authored-by: Copilot <[email protected]>
1 parent f948e9f commit 533fa15

File tree

4 files changed

+181
-8
lines changed

4 files changed

+181
-8
lines changed

src/components/primers/primer_design/SequenceTabComponents/PrimerDesignContext.jsx

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import error2String from '../../../../utils/error2String';
88
import useStoreEditor from '../../../../hooks/useStoreEditor';
99
import { cloningActions } from '../../../../store/cloning';
1010
import { stringIsNotDNA } from '../../../../store/cloning_utils';
11-
import { joinSequencesIntoSingleSequence, simulateHomologousRecombination } from '../../../../utils/sequenceManipulation';
11+
import { ebicTemplateAnnotation, joinSequencesIntoSingleSequence, simulateHomologousRecombination } from '../../../../utils/sequenceManipulation';
1212
import useHttpClient from '../../../../hooks/useHttpClient';
1313

1414
function changeValueAtIndex(current, index, newValue) {
@@ -98,6 +98,9 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
9898
newSequenceProduct.features.push(leftFeature);
9999
newSequenceProduct.features.push(rightFeature);
100100
setSequenceProduct(newSequenceProduct);
101+
} else if (designType === 'ebic') {
102+
newSequenceProduct = ebicTemplateAnnotation(sequences[0], rois[0].selectionLayer, primerDesignSettings);
103+
setSequenceProduct(newSequenceProduct);
101104
}
102105
}
103106
setSequenceProduct(newSequenceProduct);
@@ -249,15 +252,16 @@ export function PrimerDesignProvider({ children, designType, sequenceIds, primer
249252
} else if (designType === 'ebic') {
250253
endpoint = 'ebic';
251254
requestData = {
252-
sequence: sequences.find((e) => e.id === templateSequenceIds[0]),
253-
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[templateSequenceIds[0]].size),
254-
// forward_orientation: fragmentOrientations[0] === 'forward',
255+
template: {
256+
sequence: sequences.find((e) => e.id === templateSequenceIds[0]),
257+
location: selectedRegion2SequenceLocation(rois[0], teselaJsonCache[templateSequenceIds[0]].size),
258+
// forward_orientation: fragmentOrientations[0] === 'forward',
259+
},
255260
};
256261
params = {
257262
...paramsForRequest,
258263
};
259264
}
260-
261265
requestData.settings = globalPrimerSettings;
262266
const url = backendRoute(`primer_design/${endpoint}`);
263267

src/components/primers/primer_design/SequenceTabComponents/TabPanelEBICSettings.jsx

Lines changed: 126 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,91 @@ import { Alert, Box, FormControl, FormLabel, InputAdornment, TextField } from '@
33
import TabPanel from '../../../navigation/TabPanel';
44
import { usePrimerDesign } from './PrimerDesignContext';
55
import StepNavigation from './StepNavigation';
6+
import { useSelector } from 'react-redux';
7+
import EnzymeMultiSelect from '../../../form/EnzymeMultiSelect';
8+
import { isEqual } from 'lodash-es';
9+
import { getSequenceWithinRange } from '@teselagen/range-utils';
10+
import { aliasedEnzymesByName, cutSequenceByRestrictionEnzyme } from '@teselagen/sequence-utils';
11+
12+
function trimPadding({ templateSequence, padding_left, padding_right, restrictionSitesToAvoid, roi, max_inside, max_outside }) {
13+
const { start, end } = roi.selectionLayer;
14+
const leftAnnotationRange = { start: start - padding_left, end: start - 1 };
15+
const leftArm = getSequenceWithinRange(leftAnnotationRange, templateSequence.sequence);
16+
const rightAnnotationRange = { start: end + 1, end: end + padding_right };
17+
const rightArm = getSequenceWithinRange(rightAnnotationRange, templateSequence.sequence);
18+
19+
const leftMargin = { start: start - max_outside, end: start + max_inside - 1 };
20+
const rightMargin = { start: end - max_inside, end: end + max_outside - 1 };
21+
const leftMarginArm = getSequenceWithinRange(leftMargin, templateSequence.sequence);
22+
const rightMarginArm = getSequenceWithinRange(rightMargin, templateSequence.sequence);
23+
24+
const enzymes = restrictionSitesToAvoid.map((enzyme) => aliasedEnzymesByName[enzyme.toLowerCase()]);
25+
if (enzymes.length === 0) {
26+
return { padding_left, padding_right, cutsitesInMargins: false };
27+
}
28+
29+
const cutsInLeftMargin = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
30+
leftMarginArm,
31+
true,
32+
enzyme
33+
));
34+
const cutsInRightMargin = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
35+
rightMarginArm,
36+
false,
37+
enzyme
38+
));
39+
40+
const cutsitesInMargins = cutsInLeftMargin.length > 0 || cutsInRightMargin.length > 0;
41+
42+
const leftCutsites = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
43+
leftArm,
44+
true,
45+
enzyme
46+
));
47+
const rightCutsites = enzymes.flatMap((enzyme) => cutSequenceByRestrictionEnzyme(
48+
rightArm,
49+
false,
50+
enzyme
51+
));
52+
53+
let paddingLeft = padding_left;
54+
let paddingRight = padding_right;
55+
if (leftCutsites.length > 0) {
56+
paddingLeft = leftArm.length - 1 - Math.max(...leftCutsites.map((cutsite) => cutsite.recognitionSiteRange.end));
57+
}
58+
if (rightCutsites.length > 0) {
59+
paddingRight = Math.min(...rightCutsites.map((cutsite) => cutsite.recognitionSiteRange.start));
60+
}
61+
return {
62+
padding_left: paddingLeft,
63+
padding_right: paddingRight,
64+
cutsitesInMargins,
65+
};
66+
67+
}
668

769
function TabPanelEBICSettings() {
8-
const { error, selectedTab, sequenceIds, primers, submissionPreventedMessage, designPrimers, primerDesignSettings } = usePrimerDesign();
9-
const { max_inside, max_outside, target_tm, target_tm_tolerance, updateSettings } = primerDesignSettings;
70+
const { error, selectedTab, sequenceIds, primers, submissionPreventedMessage, designPrimers, primerDesignSettings, rois } = usePrimerDesign();
71+
const { max_inside, max_outside, target_tm, target_tm_tolerance, updateSettings, restrictionSitesToAvoid, padding_left, padding_right } = primerDesignSettings;
72+
const [cutsitesInMarginsError, setCutsitesInMarginsError] = React.useState(false);
73+
74+
const templateSequence = useSelector((state) => state.cloning.teselaJsonCache[sequenceIds[0]], isEqual);
75+
76+
React.useEffect(() => {
77+
if (rois.length > 0 && rois[0] !== null) {
78+
const { padding_left: newPaddingLeft, padding_right: newPaddingRight, cutsitesInMargins } = trimPadding({
79+
templateSequence,
80+
padding_left,
81+
padding_right,
82+
restrictionSitesToAvoid,
83+
roi: rois[0],
84+
max_inside,
85+
max_outside,
86+
});
87+
updateSettings({ padding_left: newPaddingLeft, padding_right: newPaddingRight });
88+
setCutsitesInMarginsError(cutsitesInMargins);
89+
}
90+
}, [templateSequence, restrictionSitesToAvoid, rois, max_inside, max_outside, padding_left, padding_right]);
1091

1192
return (
1293
<TabPanel value={selectedTab} index={sequenceIds.length}>
@@ -73,11 +154,54 @@ function TabPanelEBICSettings() {
73154
}}
74155
/>
75156
</FormControl>
157+
158+
</Box>
159+
<Box sx={{ mt: 2 }}>
160+
<FormControl sx={{ mr: 2 }}>
161+
<TextField
162+
label="Padding left"
163+
value={padding_left}
164+
onChange={(e) => { updateSettings({ padding_left: Number(e.target.value) }); }}
165+
type="number"
166+
InputProps={{
167+
endAdornment: <InputAdornment position="end">bp</InputAdornment>,
168+
sx: { width: '10em' },
169+
}}
170+
/>
171+
</FormControl>
172+
<FormControl sx={{ mr: 2 }}>
173+
<TextField
174+
label="Padding right"
175+
value={padding_right}
176+
onChange={(e) => { updateSettings({ padding_right: Number(e.target.value) }); }}
177+
type="number"
178+
InputProps={{
179+
endAdornment: <InputAdornment position="end">bp</InputAdornment>,
180+
sx: { width: '10em' },
181+
}}
182+
/>
183+
</FormControl>
184+
</Box>
185+
<Box sx={{ mt: 2 }}>
186+
<FormControl sx={{ mr: 2, width: '15em' }}>
187+
<EnzymeMultiSelect
188+
value={restrictionSitesToAvoid}
189+
setEnzymes={(v) => updateSettings({ restrictionSitesToAvoid: v })}
190+
label="Sites to avoid"
191+
multiple={true}
192+
/>
193+
</FormControl>
194+
76195
</Box>
77196
</Box>
78197
</Box>
79198
</Box>
80199
{error && <Alert severity="error" sx={{ width: 'fit-content', margin: 'auto', mt: 2 }}>{error}</Alert>}
200+
{cutsitesInMarginsError && (
201+
<Alert severity="error" sx={{ width: 'fit-content', margin: 'auto', mt: 2 }}>
202+
Restriction enzyme cut sites were detected in the margin regions. Please adjust the margin size or select different restriction sites to avoid this issue.
203+
</Alert>
204+
)}
81205
<StepNavigation
82206
onStepCompletion={designPrimers}
83207
stepCompletionText="Design primers"

src/components/primers/primer_design/SequenceTabComponents/useEBICPrimerDesignSettings.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ export default function useEBICPrimerDesignSettings() {
1616
max_outside: 20,
1717
target_tm: 61,
1818
target_tm_tolerance: 3,
19+
restrictionSitesToAvoid: [],
20+
padding_left: 1000,
21+
padding_right: 1000,
1922
});
2023
const [error, setError] = React.useState(getError(settings));
2124
const updateSettings = (newSettings) => {

src/utils/sequenceManipulation.js

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { getReverseComplementSequenceAndAnnotations, getReverseComplementSequenceString, getSequenceDataBetweenRange, insertSequenceDataAtPositionOrRange } from '@teselagen/sequence-utils';
2-
import { convertBasePosTraceToPerBpTrace, fastaToJson } from '@teselagen/bio-parsers';
2+
import { fastaToJson } from '@teselagen/bio-parsers';
3+
import { tidyUpSequenceData } from '@teselagen/sequence-utils';
34

45
function getSpacerSequence(spacer, spacerFeatureName = 'spacer') {
56
if (!spacer) {
@@ -176,3 +177,44 @@ export function syncChromatogramDataWithAlignment(chromatogramData, alignmentStr
176177
}
177178
return originalChromatogramData;
178179
}
180+
181+
export function ebicTemplateAnnotation(templateSequence, roi, {max_inside, max_outside, padding_left, padding_right}) {
182+
const leftFeature = {
183+
start: roi.start - padding_left,
184+
end: roi.start - 1,
185+
type: 'misc_feature',
186+
name: 'left_homology_arm',
187+
strand: 1,
188+
forward: true,
189+
};
190+
const leftMargin = {
191+
start: roi.start - max_outside,
192+
end: roi.start + max_inside - 1,
193+
type: 'misc_feature',
194+
name: 'left_margin',
195+
strand: null,
196+
forward: true,
197+
};
198+
const rightMargin = {
199+
start: roi.end - max_inside,
200+
end: roi.end + max_outside - 1,
201+
type: 'misc_feature',
202+
name: 'right_margin',
203+
strand: -1,
204+
};
205+
const rightFeature = {
206+
start: roi.end + 1,
207+
end: roi.end + padding_right,
208+
type: 'misc_feature',
209+
name: 'right_homology_arm',
210+
strand: -1,
211+
};
212+
213+
const annotatedSequence = structuredClone(templateSequence);
214+
annotatedSequence.features.push(leftFeature);
215+
annotatedSequence.features.push(leftMargin);
216+
annotatedSequence.features.push(rightMargin);
217+
annotatedSequence.features.push(rightFeature);
218+
219+
return tidyUpSequenceData(annotatedSequence);
220+
}

0 commit comments

Comments
 (0)