Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
<query xmlns="http://labkey.org/data/xml/query">
<description>Missed-cleavage peptides lacking a grouping. PTM reports show these as separate rows instead of combining them per site; set a shared Attribute Group ID in Skyline to merge them.</description>
</query>
40 changes: 40 additions & 0 deletions resources/queries/targetedms/UngroupedMissedCleavagePeptides.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
* Copyright (c) 2026 LabKey Corporation
*
* Licensed under the Apache License, Version 2.0: http://www.apache.org/licenses/LICENSE-2.0
*/
-- Missed-cleavage peptides whose Attribute Group ID doesn't match any shorter peptide they contain, so PTM reports split the site across rows
SELECT
l.RunId,
l.ProteinId,
l.Protein,
l.Sequence AS MissedCleavageSequence,
l.StartIndex,
l.EndIndex,
l.NumMissedCleavages,
l.GroupKey AS CurrentGroup,
GROUP_CONCAT(s.Sequence, ', ') AS ContainedPeptides
FROM (
SELECT DISTINCT p.PeptideGroupId.RunId AS RunId, p.PeptideGroupId AS ProteinId, p.PeptideGroupId.Label AS Protein,
p.Sequence, p.StartIndex, p.EndIndex, p.NumMissedCleavages,
-- Skyline groups by unmodified sequence unless Attribute Group ID is set
COALESCE(NULLIF(p.AttributeGroupId, ''), p.Sequence) AS GroupKey
FROM Peptide p
WHERE p.Decoy IS NOT TRUE
AND p.NumMissedCleavages > 0
AND p.PeptideGroupId.RunId.ReplacedByRun IS NULL
) l
INNER JOIN (
SELECT DISTINCT p.PeptideGroupId AS ProteinId, p.Sequence, p.StartIndex, p.EndIndex, p.NumMissedCleavages,
COALESCE(NULLIF(p.AttributeGroupId, ''), p.Sequence) AS GroupKey
FROM Peptide p
WHERE p.Decoy IS NOT TRUE
) s
ON s.ProteinId = l.ProteinId
AND s.Sequence <> l.Sequence
AND s.StartIndex >= l.StartIndex
AND s.EndIndex <= l.EndIndex
AND s.NumMissedCleavages < l.NumMissedCleavages
GROUP BY l.RunId, l.ProteinId, l.Protein, l.Sequence, l.StartIndex, l.EndIndex, l.NumMissedCleavages, l.GroupKey
-- A missed-cleavage peptide spans several cleaved peptides but can only join one group
HAVING SUM(CASE WHEN s.GroupKey = l.GroupKey THEN 1 ELSE 0 END) = 0
8 changes: 7 additions & 1 deletion src/org/labkey/targetedms/TargetedMSController.java
Original file line number Diff line number Diff line change
Expand Up @@ -249,6 +249,7 @@
import org.labkey.targetedms.query.SampleFileTable;
import org.labkey.targetedms.query.SkylineListManager;
import org.labkey.targetedms.query.SkylineListSchema;
import org.labkey.targetedms.query.TargetedMSQueryView;
import org.labkey.targetedms.query.TargetedMSTable;
import org.labkey.targetedms.query.TransitionManager;
import org.labkey.targetedms.search.ModificationSearchWebPart;
Expand Down Expand Up @@ -5353,7 +5354,12 @@ protected QueryView createQueryView(RunDetailsForm form, BindException errors, b
{
QuerySettings settings = new QuerySettings(getViewContext(), _dataRegionName, "PeptideIds");
settings.setBaseFilter(new SimpleFilter(FieldKey.fromParts("PeptideGroupId", "RunId"), form.getId()));
return createViewWithNoContainerFilterOptions(settings, errors);
QueryView result = createViewWithNoContainerFilterOptions(settings, errors);
if (result instanceof TargetedMSQueryView view)
{
view.setSplitCrossLinkedPeptides(true);
}
return result;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,22 +39,25 @@ class CDRConditionalFormattingDisplayColumnFactory implements DisplayColumnFacto
private final Function<Long, Protein> _proteinGetter;
private final Map<Pair<String, Long>, Pair<Boolean, String>> _stressedSamples;
private final long _runId;
private final Set<String> _terminalModifications;

public CDRConditionalFormattingDisplayColumnFactory(Function<Long, Protein> proteinGetter, Map<Pair<String, Long>, Pair<Boolean, String>> stressedSamples, long runId)
public CDRConditionalFormattingDisplayColumnFactory(Function<Long, Protein> proteinGetter, Map<Pair<String, Long>, Pair<Boolean, String>> stressedSamples, long runId, Set<String> terminalModifications)
{
_proteinGetter = proteinGetter;
_stressedSamples = stressedSamples;
_runId = runId;
_terminalModifications = terminalModifications;
}

public static RiskLevel getRiskLevel(Number value, boolean inCDR, boolean stressed, String modification, String modifiedSequence)
public static RiskLevel getRiskLevel(Number value, boolean inCDR, boolean stressed, boolean terminalModification, String modifiedSequence)
{
if (value == null)
{
return null;
}

if ("Gln->pyro-Glu (N-term Q)".equalsIgnoreCase(modification))
// Issue 55036: N- and C-terminal modifications aren't risk-scored
if (terminalModification)
{
return null;
}
Expand Down Expand Up @@ -120,7 +123,7 @@ public static boolean isInCDR(FieldKey parentFieldKey, RenderContext ctx, Functi
@Override
public DisplayColumn createRenderer(ColumnInfo boundCol)
{
return new Col(boundCol, _proteinGetter, _stressedSamples, _runId);
return new Col(boundCol, _proteinGetter, _stressedSamples, _runId, _terminalModifications);
}

public enum RiskLevel
Expand Down Expand Up @@ -150,13 +153,15 @@ private static class Col extends DataColumn
private final Function<Long, Protein> _proteinGetter;
private final Map<Pair<String, Long>, Pair<Boolean, String>> _stressedSamples;
private final long _runId;
private final Set<String> _terminalModifications;

public Col(ColumnInfo colInfo, Function<Long, Protein> proteinGetter, Map<Pair<String, Long>, Pair<Boolean, String>> stressedSamples, long runId)
public Col(ColumnInfo colInfo, Function<Long, Protein> proteinGetter, Map<Pair<String, Long>, Pair<Boolean, String>> stressedSamples, long runId, Set<String> terminalModifications)
{
super(colInfo);
_proteinGetter = proteinGetter;
_stressedSamples = stressedSamples;
_runId = runId;
_terminalModifications = terminalModifications;
}

@Override
Expand All @@ -173,7 +178,7 @@ public void addQueryFieldKeys(Set<FieldKey> keys)
@Nullable
private ConditionalFormat createConditionalFormat(Number value, boolean inCDR, boolean stressed, String modification, String modifiedSequence)
{
RiskLevel level = getRiskLevel(value, inCDR, stressed, modification, modifiedSequence);
RiskLevel level = getRiskLevel(value, inCDR, stressed, modification != null && _terminalModifications.contains(modification), modifiedSequence);
if (level == null)
{
return null;
Expand Down
116 changes: 91 additions & 25 deletions src/org/labkey/targetedms/query/CrossLinkedPeptideDisplayColumn.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import org.apache.commons.lang3.StringUtils;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.junit.Assert;
import org.junit.Test;
import org.labkey.api.collections.LongHashMap;
Expand All @@ -34,6 +35,7 @@
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

/**
* Renders the cross-linked peptide info in a few different variants. For unlinked peptides, passes through the primary
Expand Down Expand Up @@ -87,34 +89,41 @@ public Object getValue(RenderContext ctx)
CrossLinkedPeptideInfo i = new CrossLinkedPeptideInfo(modifiedSequence);
List<Protein> proteins = getProteinsFromRun(ctx);
List<CrossLinkedPeptideInfo.PeptideSequence> sequences = i.getAllSequences();
return render(sequences, proteins, modification);
Integer peptideIndex = ctx.get(CrossLinkedPeptideExcelWriter.PEPTIDE_INDEX) instanceof Integer index ? index : null;
return render(sequences, proteins, modification, peptideIndex);
}
return defaultValue;
}

protected @NotNull String render(List<CrossLinkedPeptideInfo.PeptideSequence> sequences, List<Protein> proteins, String modification)
/** @param peptideIndex non-null when the Excel export is writing each cross-linked peptide on its own row */
protected @NotNull String render(List<CrossLinkedPeptideInfo.PeptideSequence> sequences, List<Protein> proteins, String modification, @Nullable Integer peptideIndex)
{
StringBuilder result = new StringBuilder();
String outerSeparator = "";
return formatAll(sequences, proteins, modification).stream()
.filter(value -> !value.isEmpty())
.collect(Collectors.joining("\n"));
}

for (CrossLinkedPeptideInfo.PeptideSequence sequence : sequences)
{
List<CrossLinkedPeptideInfo.Match> matches = sequence.findMatches(proteins);
if (!matches.isEmpty())
{
result.append(outerSeparator);
outerSeparator = "\n";
/** @return the formatted value for each peptide, blank when it matches no protein */
protected @NotNull List<String> formatAll(List<CrossLinkedPeptideInfo.PeptideSequence> sequences, List<Protein> proteins, String modification)
{
return sequences.stream().map(sequence -> formatSequence(sequence, proteins, modification)).toList();
}

String innerSeparator = "";
for (CrossLinkedPeptideInfo.Match match : matches)
{
result.append(innerSeparator);
innerSeparator = "; ";
result.append(_formatter.format(match, sequence, modification));
}
}
protected @NotNull List<String> getPeptideValues(RenderContext ctx)
{
String modifiedSequence = ctx.get(getModifiedSequenceFieldKey(), String.class);
if (modifiedSequence == null)
{
return List.of();
}
return result.toString();
return formatAll(new CrossLinkedPeptideInfo(modifiedSequence).getAllSequences(), getProteinsFromRun(ctx), ctx.get(getModificationFieldKey(), String.class));
}

protected @NotNull String formatSequence(CrossLinkedPeptideInfo.PeptideSequence sequence, List<Protein> proteins, String modification)
{
return sequence.findMatches(proteins).stream()
.map(match -> _formatter.format(match, sequence, modification))
.collect(Collectors.joining("; "));
}

private List<Protein> getProteinsFromRun(RenderContext ctx)
Expand All @@ -127,7 +136,7 @@ private List<Protein> getProteinsFromRun(RenderContext ctx)
return Collections.emptyList();
}

private FieldKey getModifiedSequenceFieldKey()
protected FieldKey getModifiedSequenceFieldKey()
{
return FieldKey.fromString(getColumnInfo().getFieldKey().getParent(), "PeptideModifiedSequence");
}
Expand All @@ -151,12 +160,69 @@ public void addQueryFieldKeys(Set<FieldKey> keys)
keys.add(getRunIdFieldKey());
}

/** Renders one value per cross-linked peptide, so the Excel export puts each on its own row */
private static class PerPeptideColumn extends CrossLinkedPeptideDisplayColumn implements CrossLinkedPeptideExcelWriter.PerPeptideColumn
{
private PerPeptideColumn(ColumnInfo col, Formatter formatter)
{
super(col, formatter);
}

@Override
public int getPeptideCount(RenderContext ctx)
{
String modifiedSequence = ctx.get(getModifiedSequenceFieldKey(), String.class);
return modifiedSequence == null ? 1 : new CrossLinkedPeptideInfo(modifiedSequence).getAllSequences().size();
}

@Override
protected @NotNull String render(List<CrossLinkedPeptideInfo.PeptideSequence> sequences, List<Protein> proteins, String modification, @Nullable Integer peptideIndex)
{
if (peptideIndex == null)
{
return super.render(sequences, proteins, modification, null);
}
return peptideIndex < sequences.size() ? formatSequence(sequences.get(peptideIndex), proteins, modification) : "";
}
}

/** Splits across the Excel rows only when the cross-linked peptides are on different chains */
private static class ChainColumn extends PerPeptideColumn
{
private ChainColumn(ColumnInfo col)
{
super(col, (match, _, _) -> match.protein().getLabel() == null ? match.protein().getName() : match.protein().getLabel());
}

@Override
public boolean isSplit(RenderContext ctx)
{
return getDistinctChains(getPeptideValues(ctx)).size() > 1;
}

@Override
protected @NotNull String render(List<CrossLinkedPeptideInfo.PeptideSequence> sequences, List<Protein> proteins, String modification, @Nullable Integer peptideIndex)
{
List<String> chains = getDistinctChains(formatAll(sequences, proteins, modification));
if (peptideIndex == null || chains.size() > 1)
{
return super.render(sequences, proteins, modification, peptideIndex);
}
return String.join("\n", chains);
}

private static List<String> getDistinctChains(List<String> values)
{
return values.stream().filter(value -> !value.isEmpty()).distinct().toList();
}
}

public static class ChainFactory implements DisplayColumnFactory
{
@Override
public DisplayColumn createRenderer(ColumnInfo colInfo)
{
return new CrossLinkedPeptideDisplayColumn(colInfo, (match, sequence, modification) -> match.protein().getLabel() == null ? match.protein().getName() : match.protein().getLabel());
return new ChainColumn(colInfo);
}
}

Expand All @@ -165,7 +231,7 @@ public static class PeptideLocationFactory implements DisplayColumnFactory
@Override
public DisplayColumn createRenderer(ColumnInfo colInfo)
{
return new CrossLinkedPeptideDisplayColumn(colInfo, (match, sequence, modification) ->
return new PerPeptideColumn(colInfo, (match, sequence, modification) ->
(match.index() + 1) + "-" + (match.index() + getPeptideLength(sequence.getUnmodified(), modification)));
}
}
Expand All @@ -185,7 +251,7 @@ public static class PeptideIdentityFactory implements DisplayColumnFactory
@Override
public DisplayColumn createRenderer(ColumnInfo colInfo)
{
return new CrossLinkedPeptideDisplayColumn(colInfo, (match, sequence, modification) ->
return new PerPeptideColumn(colInfo, (match, sequence, modification) ->
{
String label = match.protein().getLabel();
String prefix = getChainPrefix(label);
Expand Down Expand Up @@ -224,7 +290,7 @@ public DisplayColumn createRenderer(ColumnInfo colInfo)
return new CrossLinkedPeptideDisplayColumn(colInfo, null)
{
@Override
protected @NotNull String render(List<CrossLinkedPeptideInfo.PeptideSequence> sequences, List<Protein> proteins, String modification)
protected @NotNull String render(List<CrossLinkedPeptideInfo.PeptideSequence> sequences, List<Protein> proteins, String modification, @Nullable Integer peptideIndex)
{
List<List<String>> allBonds = new ArrayList<>();

Expand Down
Loading
Loading