dsh-molbio-panel 0.1.7 → 0.1.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/client.js +327 -93
- package/package.json +3 -2
package/lib/client.js
CHANGED
|
@@ -4964,6 +4964,16 @@ window.__ModuleLoader__.load({
|
|
|
4964
4964
|
return entries.map((entry) => `>${entry.id}${entry.description !== '' ? ' ' + entry.description : ''}\n${entry.sequence}`).join('\n') + '\n';
|
|
4965
4965
|
}
|
|
4966
4966
|
|
|
4967
|
+
/** Render entries as four-line FASTQ text (quality is preserved verbatim). */
|
|
4968
|
+
function toFastq(entries) {
|
|
4969
|
+
return entries.map((entry) => {
|
|
4970
|
+
if (typeof entry.quality !== 'string' || entry.quality.length !== entry.sequence.length) {
|
|
4971
|
+
throw new MolbioInputError(`entry ${entry.id}: sequence and quality lengths differ`);
|
|
4972
|
+
}
|
|
4973
|
+
return `@${entry.id}${entry.description !== undefined && entry.description !== '' ? ' ' + entry.description : ''}\n${entry.sequence}\n+\n${entry.quality}`;
|
|
4974
|
+
}).join('\n') + '\n';
|
|
4975
|
+
}
|
|
4976
|
+
|
|
4967
4977
|
/** Summary statistics over parsed entries. */
|
|
4968
4978
|
function entryStats(entries, qualityMode = false) {
|
|
4969
4979
|
const lengths = entries.map((entry) => entry.sequence.length);
|
|
@@ -5008,6 +5018,7 @@ window.__ModuleLoader__.load({
|
|
|
5008
5018
|
exports.parseFasta = parseFasta;
|
|
5009
5019
|
exports.parseFastq = parseFastq;
|
|
5010
5020
|
exports.toFasta = toFasta;
|
|
5021
|
+
exports.toFastq = toFastq;
|
|
5011
5022
|
exports.entryStats = entryStats;
|
|
5012
5023
|
return exports;
|
|
5013
5024
|
};
|
|
@@ -5051,17 +5062,17 @@ window.__ModuleLoader__.load({
|
|
|
5051
5062
|
* from `anchor` and not from the RC-scan `start`. It is the REVERSE primer,
|
|
5052
5063
|
* and the amplicon ends at `anchor + length - 1`.
|
|
5053
5064
|
*
|
|
5054
|
-
*
|
|
5055
|
-
*
|
|
5056
|
-
*
|
|
5057
|
-
*
|
|
5058
|
-
*
|
|
5059
|
-
*
|
|
5060
|
-
*
|
|
5061
|
-
*
|
|
5062
|
-
*
|
|
5063
|
-
*
|
|
5064
|
-
*
|
|
5065
|
+
* `designIntronSpanningPrimers` (tool: `molbio_design_intron_primers`)
|
|
5066
|
+
* follows the SAME convention since 0.23.0. The junction is carried by the
|
|
5067
|
+
* REVERSE primer (the assay sits 5' of the junction), so the reverse candidate
|
|
5068
|
+
* is scanned on the reverse complement of the spliced transcript: every 3'-end
|
|
5069
|
+
* constraint is then evaluated on the molecule that is ordered, and the hit is
|
|
5070
|
+
* mapped back to sense coordinates through `anchor` (its 3' end, the LEFT edge
|
|
5071
|
+
* of its site). `forward` is upstream of `reverse`, the spliced amplicon runs
|
|
5072
|
+
* from the forward 5' end to the reverse 5' end, and the reported reverse
|
|
5073
|
+
* sequence is what goes on the order form. Before 0.23.0 the tool reported both
|
|
5074
|
+
* primers as sense substrings, so `forward` sat downstream of `reverse` and the
|
|
5075
|
+
* reported reverse was the reverse complement of the molecule to order.
|
|
5065
5076
|
*/
|
|
5066
5077
|
|
|
5067
5078
|
const { DNA_BASES, MolbioInputError, baseCounts, complement, dimerThermo, endGcCount5, endStability5, findRuns, hairpinThermo, normalizeSequence, primerTm, reverseComplement, selfAnyScore, selfEndScore } = __molbio_require("lib.mjs");
|
|
@@ -5737,37 +5748,60 @@ window.__ModuleLoader__.load({
|
|
|
5737
5748
|
// ── cross-intron primer design ──────────────────────────────────────────────
|
|
5738
5749
|
|
|
5739
5750
|
/**
|
|
5740
|
-
* Mismatch report for cross-intron candidates.
|
|
5741
|
-
*
|
|
5742
|
-
*
|
|
5743
|
-
*
|
|
5744
|
-
*
|
|
5745
|
-
* both
|
|
5751
|
+
* Mismatch report for cross-intron candidates. The forward primer is the
|
|
5752
|
+
* sense-strand substring at its binding site; the reverse primer is the REVERSE
|
|
5753
|
+
* COMPLEMENT of the sense-strand substring at its site (the molecule that is
|
|
5754
|
+
* ordered). `anchorStart` is the 0-based sense-strand position of the primer's
|
|
5755
|
+
* 3' end — for the reverse primer that is the LEFT edge of its site — and
|
|
5756
|
+
* `isReverse` flips both the perfect-match base and how a primer offset maps
|
|
5757
|
+
* onto the transcript.
|
|
5746
5758
|
*/
|
|
5747
|
-
function intronMismatchReport(candidate, spliced, splicedToGenomic) {
|
|
5759
|
+
function intronMismatchReport(candidate, spliced, splicedToGenomic, anchorStart, isReverse) {
|
|
5748
5760
|
const len = candidate.length;
|
|
5749
|
-
return candidate.mismatches.map(({ p, base }) =>
|
|
5750
|
-
|
|
5751
|
-
|
|
5752
|
-
|
|
5753
|
-
|
|
5754
|
-
|
|
5755
|
-
|
|
5756
|
-
|
|
5761
|
+
return candidate.mismatches.map(({ p, base }) => {
|
|
5762
|
+
const offset = isReverse ? len - 1 - p : p;
|
|
5763
|
+
const index = anchorStart + offset;
|
|
5764
|
+
return {
|
|
5765
|
+
position: p + 1,
|
|
5766
|
+
template_base: isReverse ? complement(spliced[index]) : spliced[index],
|
|
5767
|
+
primer_base: base,
|
|
5768
|
+
spliced_position: index + 1,
|
|
5769
|
+
genomic_position: splicedToGenomic[index] + 1,
|
|
5770
|
+
distance_from_3prime: len - 1 - p,
|
|
5771
|
+
};
|
|
5772
|
+
});
|
|
5757
5773
|
}
|
|
5758
5774
|
|
|
5759
5775
|
/**
|
|
5760
|
-
*
|
|
5761
|
-
*
|
|
5762
|
-
*
|
|
5763
|
-
*
|
|
5776
|
+
* The same weights as `mismatchPenalty`, over the snake_case report fields.
|
|
5777
|
+
* The old intron path handed snake_case reports to the camelCase helper, so the
|
|
5778
|
+
* 3'-zone surcharge never applied there; this keeps the two conventions apart on
|
|
5779
|
+
* purpose instead of loosening the shared helper.
|
|
5780
|
+
*/
|
|
5781
|
+
function intronMismatchPenalty(report, zone) {
|
|
5782
|
+
return report.reduce((sum, mismatch) => sum + 8 + (mismatch.distance_from_3prime <= zone ? 4 : 0), 0);
|
|
5783
|
+
}
|
|
5784
|
+
|
|
5785
|
+
/**
|
|
5786
|
+
* Design qPCR primer pairs across an exon-exon junction so genomic DNA cannot
|
|
5787
|
+
* be amplified: the REVERSE primer straddles the junction (>= minJunctionBases
|
|
5788
|
+
* on each side), the FORWARD primer sits upstream inside a single exon, and the
|
|
5789
|
+
* spliced amplicon crosses the junction inside the reverse primer's site.
|
|
5790
|
+
*
|
|
5791
|
+
* Coordinates are reported both on the spliced transcript and on the genomic
|
|
5792
|
+
* sequence. Orientation follows `designPrimerPairs` (see the file header):
|
|
5793
|
+
* `forward.sequence` is the sense-strand substring at its site and
|
|
5794
|
+
* `reverse.sequence` is the reverse complement of the sense-strand substring at
|
|
5795
|
+
* its site. The reverse candidate is scanned on the reverse complement of the
|
|
5796
|
+
* spliced transcript, so every 3'-end constraint (GC clamp, end stability, end
|
|
5797
|
+
* GC, self-end, hairpin) applies to the molecule that is ordered.
|
|
5764
5798
|
*/
|
|
5765
5799
|
function designIntronSpanningPrimers(genomic, exons, opts) {
|
|
5766
5800
|
if (!Array.isArray(exons) || exons.length < 2) throw new MolbioInputError('exons must be an array of at least two {start, end} spans');
|
|
5767
5801
|
const sorted = [...exons].sort((a, b) => a.start - b.start);
|
|
5768
5802
|
for (const exon of sorted) {
|
|
5769
5803
|
if (!Number.isInteger(exon.start) || !Number.isInteger(exon.end) || exon.start < 1 || exon.end > genomic.length || exon.start > exon.end) {
|
|
5770
|
-
throw new MolbioInputError(
|
|
5804
|
+
throw new MolbioInputError('exon ' + JSON.stringify(exon) + ' is outside the genomic sequence (length ' + genomic.length + ')');
|
|
5771
5805
|
}
|
|
5772
5806
|
}
|
|
5773
5807
|
for (let i = 1; i < sorted.length; i++) {
|
|
@@ -5790,33 +5824,39 @@ window.__ModuleLoader__.load({
|
|
|
5790
5824
|
}
|
|
5791
5825
|
});
|
|
5792
5826
|
if (spliced.length < 24) throw new MolbioInputError('the spliced transcript is too short for primer design');
|
|
5827
|
+
const splicedLength = spliced.length;
|
|
5828
|
+
const rcSpliced = reverseComplement(spliced);
|
|
5793
5829
|
|
|
5794
5830
|
// v13 target position preference (coordinates on the spliced transcript).
|
|
5795
5831
|
const targetPosition = opts.targetPosition;
|
|
5796
|
-
if (targetPosition !== undefined && (!Number.isInteger(targetPosition) || targetPosition < 1 || targetPosition >
|
|
5797
|
-
throw new MolbioInputError(
|
|
5832
|
+
if (targetPosition !== undefined && (!Number.isInteger(targetPosition) || targetPosition < 1 || targetPosition > splicedLength)) {
|
|
5833
|
+
throw new MolbioInputError('target_position ' + targetPosition + ' is outside the spliced transcript (length ' + splicedLength + ')');
|
|
5798
5834
|
}
|
|
5799
5835
|
const targetWeight = opts.targetPenalty ?? DESIGN_DEFAULTS.targetPenalty;
|
|
5800
5836
|
|
|
5801
|
-
// Forward candidates
|
|
5802
|
-
const fwd =
|
|
5803
|
-
|
|
5837
|
+
// Forward candidates: entirely inside one exon on the sense strand.
|
|
5838
|
+
const fwd = scanCandidates(spliced, 0, splicedLength - opts.lenMin, opts)
|
|
5839
|
+
.filter((candidate) => splicedToExon[candidate.start] === splicedToExon[candidate.start + candidate.length - 1])
|
|
5840
|
+
.map((candidate) => ({ ...candidate, exon: splicedToExon[candidate.start] }))
|
|
5841
|
+
.sort((a, b) => a.start - b.start);
|
|
5842
|
+
|
|
5843
|
+
// Reverse candidates: scanned on the reverse complement (the ordered
|
|
5844
|
+
// molecule), then mapped back to the sense-strand 3' end `anchor`.
|
|
5845
|
+
const rev = [];
|
|
5846
|
+
for (const candidate of scanCandidates(rcSpliced, 0, splicedLength - opts.lenMin, opts)) {
|
|
5847
|
+
const anchor = splicedLength - (candidate.start + candidate.length);
|
|
5848
|
+
const topEnd = anchor + candidate.length - 1;
|
|
5804
5849
|
for (const junction of junctionPositions) {
|
|
5805
|
-
const left = junction.splicedPos -
|
|
5806
|
-
const right =
|
|
5850
|
+
const left = junction.splicedPos - anchor;
|
|
5851
|
+
const right = topEnd + 1 - junction.splicedPos;
|
|
5807
5852
|
if (left >= minSide && right >= minSide) {
|
|
5808
|
-
|
|
5853
|
+
rev.push({ ...candidate, anchor, junction, junction_left: left, junction_right: right });
|
|
5809
5854
|
break;
|
|
5810
5855
|
}
|
|
5811
5856
|
}
|
|
5812
5857
|
}
|
|
5813
|
-
|
|
5814
|
-
|
|
5815
|
-
const rev = scanCandidates(spliced, 0, spliced.length - opts.lenMin, opts)
|
|
5816
|
-
.filter((candidate) => splicedToExon[candidate.start] === splicedToExon[candidate.start + candidate.length - 1])
|
|
5817
|
-
.map((candidate) => ({ ...candidate, exon: splicedToExon[candidate.start] }))
|
|
5818
|
-
.sort((a, b) => a.start - b.start);
|
|
5819
|
-
const revStarts = rev.map((candidate) => candidate.start);
|
|
5858
|
+
rev.sort((a, b) => a.anchor - b.anchor);
|
|
5859
|
+
const revAnchors = rev.map((candidate) => candidate.anchor);
|
|
5820
5860
|
|
|
5821
5861
|
const tmCenter = (opts.tmMin + opts.tmMax) / 2;
|
|
5822
5862
|
const misprimingIndex = opts.checkMispriming
|
|
@@ -5825,48 +5865,52 @@ window.__ModuleLoader__.load({
|
|
|
5825
5865
|
const maxSites = opts.misprimingMaxSites ?? DESIGN_DEFAULTS.misprimingMaxSites;
|
|
5826
5866
|
const pairs = [];
|
|
5827
5867
|
for (const f of fwd) {
|
|
5828
|
-
|
|
5829
|
-
|
|
5830
|
-
|
|
5868
|
+
// The reverse site must start after the forward 3' end, and the amplicon
|
|
5869
|
+
// (forward 5' -> reverse 5') must fit the window. The bounds are widened by
|
|
5870
|
+
// the primer-length range and the exact length is checked below, because a
|
|
5871
|
+
// long reverse primer can start earlier and still close inside the window.
|
|
5872
|
+
const aMin = Math.max(f.start + f.length, f.start + opts.ampliconMin - opts.lenMax);
|
|
5873
|
+
const aMax = f.start + opts.ampliconMax - opts.lenMin;
|
|
5874
|
+
if (aMax < aMin) continue;
|
|
5831
5875
|
let lo = 0;
|
|
5832
|
-
let hi =
|
|
5876
|
+
let hi = revAnchors.length;
|
|
5833
5877
|
while (lo < hi) {
|
|
5834
5878
|
const mid = (lo + hi) >> 1;
|
|
5835
|
-
if (
|
|
5879
|
+
if (revAnchors[mid] < aMin) lo = mid + 1;
|
|
5836
5880
|
else hi = mid;
|
|
5837
5881
|
}
|
|
5838
5882
|
for (let i = lo; i < rev.length; i++) {
|
|
5839
5883
|
const r = rev[i];
|
|
5840
|
-
if (r.
|
|
5841
|
-
|
|
5842
|
-
const
|
|
5843
|
-
const
|
|
5884
|
+
if (r.anchor > aMax) break;
|
|
5885
|
+
const ampliconStart = f.start;
|
|
5886
|
+
const ampliconEnd = r.anchor + r.length - 1;
|
|
5887
|
+
const ampliconLength = ampliconEnd - ampliconStart + 1;
|
|
5888
|
+
if (ampliconLength < opts.ampliconMin || ampliconLength > opts.ampliconMax) continue;
|
|
5889
|
+
const gStart = splicedToGenomic[ampliconStart];
|
|
5890
|
+
const gEnd = splicedToGenomic[ampliconEnd];
|
|
5844
5891
|
if (gEnd - gStart + 1 < minGenomicSpan) continue;
|
|
5845
5892
|
if (Math.abs(f.tm - r.tm) > opts.maxTmDelta) continue;
|
|
5846
5893
|
const dimer = dimerThermo(f.sequence, r.sequence, opts.ctMolar ?? DEFAULT_CT_MOLAR);
|
|
5847
5894
|
if (dimer.any_tm > opts.maxDimerTm || dimer.end_tm > opts.maxDimerEndTm) continue;
|
|
5895
|
+
const misK = opts.mispriming3PrimeBases ?? DESIGN_DEFAULTS.mispriming3PrimeBases;
|
|
5848
5896
|
let fMispriming = { count: 0, sites: [] };
|
|
5849
5897
|
let rMispriming = { count: 0, sites: [] };
|
|
5850
5898
|
if (misprimingIndex !== undefined) {
|
|
5851
|
-
|
|
5852
|
-
fMispriming = misprimingForCandidate(f, f.sequence.slice(-misK), misprimingIndex, opts, f.start);
|
|
5899
|
+
// Exclude each primer's own 3'-tail window (not its 5' end).
|
|
5900
|
+
fMispriming = misprimingForCandidate(f, f.sequence.slice(-misK), misprimingIndex, opts, f.start + f.length - misK);
|
|
5853
5901
|
if (fMispriming.count > maxSites) continue;
|
|
5854
|
-
|
|
5855
|
-
// the real oligo is its reverse complement, so its 3' tail is the RC
|
|
5856
|
-
// of the FIRST k bases and the intended site is r.start
|
|
5857
|
-
const rTail = reverseComplement(r.sequence.slice(0, misK));
|
|
5858
|
-
rMispriming = misprimingForCandidate(r, rTail, misprimingIndex, opts, r.start);
|
|
5902
|
+
rMispriming = misprimingForCandidate(r, r.sequence.slice(-misK), misprimingIndex, opts, r.anchor);
|
|
5859
5903
|
if (rMispriming.count > maxSites) continue;
|
|
5860
5904
|
}
|
|
5861
|
-
const fMismatches = intronMismatchReport(f, spliced, splicedToGenomic);
|
|
5862
|
-
const rMismatches = intronMismatchReport(r, spliced, splicedToGenomic);
|
|
5905
|
+
const fMismatches = intronMismatchReport(f, spliced, splicedToGenomic, f.start, false);
|
|
5906
|
+
const rMismatches = intronMismatchReport(r, spliced, splicedToGenomic, r.anchor, true);
|
|
5863
5907
|
const zone = opts.mismatch3PrimeZone ?? DESIGN_DEFAULTS.mismatch3PrimeZone;
|
|
5864
5908
|
let fTargetDistance;
|
|
5865
5909
|
let rTargetDistance;
|
|
5866
5910
|
let targetDistance;
|
|
5867
5911
|
if (targetPosition !== undefined) {
|
|
5868
|
-
fTargetDistance = Math.abs(f.start + f.length - targetPosition); //
|
|
5869
|
-
rTargetDistance = Math.abs(r.
|
|
5912
|
+
fTargetDistance = Math.abs(f.start + f.length - targetPosition); // forward 3' end (1-based)
|
|
5913
|
+
rTargetDistance = Math.abs(r.anchor + 1 - targetPosition); // reverse 3' end (1-based)
|
|
5870
5914
|
targetDistance = Math.min(fTargetDistance, rTargetDistance);
|
|
5871
5915
|
}
|
|
5872
5916
|
const penalty = 0.6 * Math.abs(f.tm - r.tm) + Math.abs(f.tm - tmCenter) + Math.abs(r.tm - tmCenter)
|
|
@@ -5874,8 +5918,8 @@ window.__ModuleLoader__.load({
|
|
|
5874
5918
|
+ 1.0 * Math.max(0, f.self_end - 1) + 1.0 * Math.max(0, r.self_end - 1)
|
|
5875
5919
|
+ 0.2 * Math.max(0, f.hairpin_tm - 40) + 0.2 * Math.max(0, r.hairpin_tm - 40)
|
|
5876
5920
|
+ 0.2 * Math.max(0, dimer.any_tm - 40) + 0.2 * Math.max(0, dimer.end_tm - 40)
|
|
5877
|
-
+
|
|
5878
|
-
+
|
|
5921
|
+
+ intronMismatchPenalty(fMismatches, zone)
|
|
5922
|
+
+ intronMismatchPenalty(rMismatches, zone)
|
|
5879
5923
|
+ 8 * fMispriming.count + 8 * rMispriming.count
|
|
5880
5924
|
+ (targetDistance !== undefined ? targetWeight * targetDistance : 0);
|
|
5881
5925
|
pairs.push({
|
|
@@ -5893,9 +5937,7 @@ window.__ModuleLoader__.load({
|
|
|
5893
5937
|
spliced_end: f.start + f.length,
|
|
5894
5938
|
genomic_start: splicedToGenomic[f.start] + 1,
|
|
5895
5939
|
genomic_end: splicedToGenomic[f.start + f.length - 1] + 1,
|
|
5896
|
-
|
|
5897
|
-
junction_left: f.junction_left,
|
|
5898
|
-
junction_right: f.junction_right,
|
|
5940
|
+
exon: f.exon + 1,
|
|
5899
5941
|
mismatch_count: fMismatches.length,
|
|
5900
5942
|
mismatches: fMismatches,
|
|
5901
5943
|
mispriming_count: fMispriming.count,
|
|
@@ -5917,11 +5959,13 @@ window.__ModuleLoader__.load({
|
|
|
5917
5959
|
hairpin_tm: r.hairpin_tm,
|
|
5918
5960
|
end_stability_kcal: r.end_stability_kcal,
|
|
5919
5961
|
end_gc_count: r.end_gc_count,
|
|
5920
|
-
spliced_start: r.
|
|
5921
|
-
spliced_end: r.
|
|
5922
|
-
genomic_start: splicedToGenomic[r.
|
|
5923
|
-
genomic_end: splicedToGenomic[r.
|
|
5924
|
-
|
|
5962
|
+
spliced_start: r.anchor + 1,
|
|
5963
|
+
spliced_end: r.anchor + r.length,
|
|
5964
|
+
genomic_start: splicedToGenomic[r.anchor] + 1,
|
|
5965
|
+
genomic_end: splicedToGenomic[r.anchor + r.length - 1] + 1,
|
|
5966
|
+
exons: [String(r.junction.up + 1), String(r.junction.down + 1)],
|
|
5967
|
+
junction_left: r.junction_left,
|
|
5968
|
+
junction_right: r.junction_right,
|
|
5925
5969
|
mismatch_count: rMismatches.length,
|
|
5926
5970
|
mismatches: rMismatches,
|
|
5927
5971
|
mispriming_count: rMispriming.count,
|
|
@@ -5933,7 +5977,7 @@ window.__ModuleLoader__.load({
|
|
|
5933
5977
|
})),
|
|
5934
5978
|
...(rTargetDistance !== undefined ? { target_distance: rTargetDistance } : {}),
|
|
5935
5979
|
},
|
|
5936
|
-
spliced_amplicon: { start:
|
|
5980
|
+
spliced_amplicon: { start: ampliconStart + 1, end: ampliconEnd + 1, length: ampliconLength },
|
|
5937
5981
|
genomic_amplicon_length: gEnd - gStart + 1,
|
|
5938
5982
|
...(targetDistance !== undefined ? { target_distance: targetDistance } : {}),
|
|
5939
5983
|
penalty: Math.round(penalty * 100) / 100,
|
|
@@ -5944,7 +5988,7 @@ window.__ModuleLoader__.load({
|
|
|
5944
5988
|
const seen = new Set();
|
|
5945
5989
|
const unique = [];
|
|
5946
5990
|
for (const pair of pairs) {
|
|
5947
|
-
const key =
|
|
5991
|
+
const key = pair.spliced_amplicon.start + ':' + pair.spliced_amplicon.end;
|
|
5948
5992
|
if (seen.has(key)) continue;
|
|
5949
5993
|
seen.add(key);
|
|
5950
5994
|
unique.push(pair);
|
|
@@ -7093,12 +7137,27 @@ window.__ModuleLoader__.load({
|
|
|
7093
7137
|
};
|
|
7094
7138
|
const WATER = 18.015;
|
|
7095
7139
|
|
|
7096
|
-
|
|
7097
|
-
|
|
7098
|
-
|
|
7099
|
-
|
|
7140
|
+
/**
|
|
7141
|
+
* Selectable pK sets for the Henderson-Hasselbalch charge model. `bjellqvist`
|
|
7142
|
+
* is the set `proteinProperties` has always used for its pI; `emboss` is the
|
|
7143
|
+
* Epk.dat table EMBOSS ships with `iep`. They are NOT interchangeable — the
|
|
7144
|
+
* reported set name travels with every charge/pI value.
|
|
7145
|
+
*/
|
|
7146
|
+
const PK_SETS = {
|
|
7147
|
+
bjellqvist: {
|
|
7148
|
+
label: 'Bjellqvist 1993',
|
|
7149
|
+
Nterm: 8.6, Cterm: 3.6, C: 9.0, D: 4.05, E: 4.45, H: 6.04,
|
|
7150
|
+
K: 10.8, R: 12.0, Y: 10.3,
|
|
7151
|
+
},
|
|
7152
|
+
emboss: {
|
|
7153
|
+
label: 'EMBOSS Epk.dat',
|
|
7154
|
+
Nterm: 8.6, Cterm: 3.6, C: 8.5, D: 3.9, E: 4.1, H: 6.5,
|
|
7155
|
+
K: 10.8, R: 12.5, Y: 10.1,
|
|
7156
|
+
},
|
|
7100
7157
|
};
|
|
7101
7158
|
|
|
7159
|
+
const PK = PK_SETS.bjellqvist;
|
|
7160
|
+
|
|
7102
7161
|
const HYDROPATHY = {
|
|
7103
7162
|
A: 1.8, R: -4.5, N: -3.5, D: -3.5, C: 2.5, Q: -3.5, E: -3.5,
|
|
7104
7163
|
G: -0.4, H: -3.2, I: 4.5, L: 3.8, K: -3.9, M: 1.9, F: 2.8,
|
|
@@ -7111,30 +7170,37 @@ window.__ModuleLoader__.load({
|
|
|
7111
7170
|
return counts;
|
|
7112
7171
|
}
|
|
7113
7172
|
|
|
7114
|
-
|
|
7173
|
+
/**
|
|
7174
|
+
* Net charge at a given pH by Henderson-Hasselbalch summation. The terminal
|
|
7175
|
+
* groups count once each; K/R/H are positive, D/E/C/Y negative. `pkaSet` is a
|
|
7176
|
+
* key of PK_SETS (or a set object), so callers can name the table they used.
|
|
7177
|
+
*/
|
|
7178
|
+
function netCharge(seq, counts, pH, pkaSet = 'bjellqvist') {
|
|
7179
|
+
const pk = typeof pkaSet === 'string' ? PK_SETS[pkaSet] : pkaSet;
|
|
7180
|
+
if (pk === undefined) throw new MolbioInputError('unknown pK set ' + JSON.stringify(pkaSet) + '; expected one of ' + Object.keys(PK_SETS).join(', '));
|
|
7115
7181
|
let charge = 0;
|
|
7116
7182
|
const pos = (pKa) => 1 / (1 + Math.pow(10, pH - pKa));
|
|
7117
7183
|
const neg = (pKa) => 1 / (1 + Math.pow(10, pKa - pH));
|
|
7118
|
-
charge += pos(
|
|
7119
|
-
for (const [aa, pKa] of [['K',
|
|
7184
|
+
charge += pos(pk.Nterm);
|
|
7185
|
+
for (const [aa, pKa] of [['K', pk.K], ['R', pk.R], ['H', pk.H]]) {
|
|
7120
7186
|
if (counts[aa] !== undefined) charge += counts[aa] * pos(pKa);
|
|
7121
7187
|
}
|
|
7122
|
-
charge -= neg(
|
|
7123
|
-
for (const [aa, pKa] of [['D',
|
|
7188
|
+
charge -= neg(pk.Cterm);
|
|
7189
|
+
for (const [aa, pKa] of [['D', pk.D], ['E', pk.E], ['C', pk.C], ['Y', pk.Y]]) {
|
|
7124
7190
|
if (counts[aa] !== undefined) charge -= counts[aa] * neg(pKa);
|
|
7125
7191
|
}
|
|
7126
7192
|
return charge;
|
|
7127
7193
|
}
|
|
7128
7194
|
|
|
7129
7195
|
/** Isoelectric point by bisection over the net-charge function. */
|
|
7130
|
-
function isoelectricPoint(seq, counts) {
|
|
7196
|
+
function isoelectricPoint(seq, counts, pkaSet = 'bjellqvist') {
|
|
7131
7197
|
let lo = 0;
|
|
7132
7198
|
let hi = 14;
|
|
7133
|
-
let chargeLo =
|
|
7134
|
-
let chargeHi =
|
|
7199
|
+
let chargeLo = netCharge(seq, counts, lo, pkaSet);
|
|
7200
|
+
let chargeHi = netCharge(seq, counts, hi, pkaSet);
|
|
7135
7201
|
for (let i = 0; i < 60; i++) {
|
|
7136
7202
|
const mid = (lo + hi) / 2;
|
|
7137
|
-
const chargeMid =
|
|
7203
|
+
const chargeMid = netCharge(seq, counts, mid, pkaSet);
|
|
7138
7204
|
if (Math.abs(chargeMid) < 1e-4) return Math.round(mid * 100) / 100;
|
|
7139
7205
|
if (Math.sign(chargeMid) === Math.sign(chargeLo)) {
|
|
7140
7206
|
lo = mid;
|
|
@@ -7439,6 +7505,9 @@ window.__ModuleLoader__.load({
|
|
|
7439
7505
|
}
|
|
7440
7506
|
|
|
7441
7507
|
exports.normalizeProtein = normalizeProtein;
|
|
7508
|
+
exports.PK_SETS = PK_SETS;
|
|
7509
|
+
exports.countAminoAcids = countAminoAcids;
|
|
7510
|
+
exports.netCharge = netCharge;
|
|
7442
7511
|
exports.isoelectricPoint = isoelectricPoint;
|
|
7443
7512
|
exports.proteinProperties = proteinProperties;
|
|
7444
7513
|
exports.peptideDigest = peptideDigest;
|
|
@@ -8394,6 +8463,91 @@ window.__ModuleLoader__.load({
|
|
|
8394
8463
|
return parts.join(' · ');
|
|
8395
8464
|
}
|
|
8396
8465
|
|
|
8466
|
+
// ── figure cards (the same `tool.call.toolview` seat) ───────────────────────
|
|
8467
|
+
|
|
8468
|
+
/** The meta kind the sequence-logo and gRNA tools attach to their results. */
|
|
8469
|
+
const FIGURE_CARD_KIND = 'molbio-figure';
|
|
8470
|
+
|
|
8471
|
+
const CARD_SUMMARY_MAX = 300;
|
|
8472
|
+
const CARD_PATH_MAX = 500;
|
|
8473
|
+
const CARD_GUIDES_MAX = 5;
|
|
8474
|
+
|
|
8475
|
+
/** A bounded string, or '' when the value is not a string. */
|
|
8476
|
+
function cardText(value, max) {
|
|
8477
|
+
return typeof value === 'string' ? value.slice(0, max) : '';
|
|
8478
|
+
}
|
|
8479
|
+
|
|
8480
|
+
/** One guide row, validated field by field; malformed rows are dropped. */
|
|
8481
|
+
function cardGuide(row) {
|
|
8482
|
+
if (row === null || typeof row !== 'object' || Array.isArray(row)) return undefined;
|
|
8483
|
+
const sequence = cardText(row.sequence, 60);
|
|
8484
|
+
if (sequence === '') return undefined;
|
|
8485
|
+
return {
|
|
8486
|
+
rank: Number.isInteger(row.rank) ? row.rank : 0,
|
|
8487
|
+
sequence,
|
|
8488
|
+
pam: cardText(row.pam, 12),
|
|
8489
|
+
strand: row.strand === 'reverse' ? 'reverse' : 'forward',
|
|
8490
|
+
start: Number.isInteger(row.start) ? row.start : 0,
|
|
8491
|
+
end: Number.isInteger(row.end) ? row.end : 0,
|
|
8492
|
+
score: typeof row.score === 'number' && Number.isFinite(row.score) ? row.score : 0,
|
|
8493
|
+
offTargets: Number.isInteger(row.off_target_count) ? row.off_target_count : 0,
|
|
8494
|
+
};
|
|
8495
|
+
}
|
|
8496
|
+
|
|
8497
|
+
/**
|
|
8498
|
+
* Read a tool-result block's meta into what the figure card draws.
|
|
8499
|
+
*
|
|
8500
|
+
* Like `mapCardView`, the value is a projection this package produced itself,
|
|
8501
|
+
* but the card renders inside the conversation, so every field is checked and
|
|
8502
|
+
* bounded rather than trusted: a block from an older version, another tool, a
|
|
8503
|
+
* running call or a failed run must degrade to a notice, never throw.
|
|
8504
|
+
*
|
|
8505
|
+
* @param {unknown} meta the block's meta, as the client received it.
|
|
8506
|
+
* @returns {{kind: 'notice', message: string, ...} | {kind: 'figure', figure:
|
|
8507
|
+
* 'logo'|'grna', summary: string, svg: string, svgPath: string,
|
|
8508
|
+
* csvPath: string, mapPath: string, guides: Array}}
|
|
8509
|
+
*/
|
|
8510
|
+
function figureCardView(meta) {
|
|
8511
|
+
if (meta === null || typeof meta !== 'object' || Array.isArray(meta)) {
|
|
8512
|
+
return { kind: 'notice', message: 'No figure data was attached to this call.' };
|
|
8513
|
+
}
|
|
8514
|
+
if (meta.kind !== FIGURE_CARD_KIND) {
|
|
8515
|
+
return { kind: 'notice', message: 'This call carries no figure.' };
|
|
8516
|
+
}
|
|
8517
|
+
const figure = meta.figure === 'grna' ? 'grna' : meta.figure === 'logo' ? 'logo' : '';
|
|
8518
|
+
if (figure === '') {
|
|
8519
|
+
return { kind: 'notice', message: 'This call carries an unknown figure type.' };
|
|
8520
|
+
}
|
|
8521
|
+
const base = {
|
|
8522
|
+
figure,
|
|
8523
|
+
summary: cardText(meta.summary, CARD_SUMMARY_MAX),
|
|
8524
|
+
svgPath: cardText(meta.svg_path, CARD_PATH_MAX),
|
|
8525
|
+
csvPath: cardText(meta.csv_path, CARD_PATH_MAX),
|
|
8526
|
+
mapPath: cardText(meta.map_path, CARD_PATH_MAX),
|
|
8527
|
+
guides: Array.isArray(meta.guides)
|
|
8528
|
+
// Bound the work before validating, then keep at most five VALID rows:
|
|
8529
|
+
// malformed entries must not consume the budget and hide good ones.
|
|
8530
|
+
? meta.guides.slice(0, 50).map(cardGuide).filter((guide) => guide !== undefined).slice(0, CARD_GUIDES_MAX)
|
|
8531
|
+
: [],
|
|
8532
|
+
svg: '',
|
|
8533
|
+
};
|
|
8534
|
+
if (!isSvgMarkup(meta.svg)) {
|
|
8535
|
+
const message = meta.svg_missing === true
|
|
8536
|
+
? 'No picture was requested for this call; the numbers are in the tool result.'
|
|
8537
|
+
: meta.svg_omitted === true
|
|
8538
|
+
? `The figure is too large to draw inline (${Math.round((Number.isInteger(meta.svg_bytes) ? meta.svg_bytes : 0) / 1024)} KB); the written file is named below.`
|
|
8539
|
+
: 'The figure markup did not travel with this call.';
|
|
8540
|
+
return { kind: 'notice', message, ...base };
|
|
8541
|
+
}
|
|
8542
|
+
return { kind: 'figure', ...base, svg: meta.svg };
|
|
8543
|
+
}
|
|
8544
|
+
|
|
8545
|
+
/** The header line for a figure card, with a stable fallback. */
|
|
8546
|
+
function figureCardSummary(view) {
|
|
8547
|
+
if (view.summary !== '') return view.summary;
|
|
8548
|
+
return view.figure === 'logo' ? 'Sequence logo' : 'gRNA design';
|
|
8549
|
+
}
|
|
8550
|
+
|
|
8397
8551
|
// ── literature library (papers.json) ────────────────────────────────────────
|
|
8398
8552
|
|
|
8399
8553
|
/** The library file the molbio_paper_* tools write, relative to the workspace. */
|
|
@@ -8571,6 +8725,9 @@ window.__ModuleLoader__.load({
|
|
|
8571
8725
|
exports.MAP_CARD_KIND = MAP_CARD_KIND;
|
|
8572
8726
|
exports.mapCardView = mapCardView;
|
|
8573
8727
|
exports.mapCardSummary = mapCardSummary;
|
|
8728
|
+
exports.FIGURE_CARD_KIND = FIGURE_CARD_KIND;
|
|
8729
|
+
exports.figureCardView = figureCardView;
|
|
8730
|
+
exports.figureCardSummary = figureCardSummary;
|
|
8574
8731
|
exports.LIBRARY_FILE = LIBRARY_FILE;
|
|
8575
8732
|
exports.readWorkspaceText = readWorkspaceText;
|
|
8576
8733
|
exports.parseLibrary = parseLibrary;
|
|
@@ -8619,7 +8776,7 @@ window.__ModuleLoader__.load({
|
|
|
8619
8776
|
* inside the factory would also have to be torn down by hand on unload.
|
|
8620
8777
|
*/
|
|
8621
8778
|
const { createElement: h, useEffect, useMemo, useRef, useState } = __ext0;
|
|
8622
|
-
const { LIBRARY_FILE, NCBI_KEY_REF, childPath, classifyEntry, credentialInfo, isMolbioDetailSubject, remoteFailure, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
|
|
8779
|
+
const { LIBRARY_FILE, NCBI_KEY_REF, childPath, classifyEntry, credentialInfo, figureCardSummary, figureCardView, isMolbioDetailSubject, remoteFailure, decodeWorkspaceBytes, filterPapers, isOpenable, libraryTags, logoSvg, mapCardSummary, mapCardView, paperFields, paperLink, paperSummary, parseAlignmentFile, parseLibrary, parsePlasmidFile, plasmidSvg, readWorkspaceText, sortEntries } = __molbio_require("build/panel-core.mjs");
|
|
8623
8780
|
/** This implementation's identity, and the key its body/title register under. */
|
|
8624
8781
|
const PANEL_ID = 'dsh-molbio-tools';
|
|
8625
8782
|
/** Type discriminator the tab is opened by. */
|
|
@@ -8858,8 +9015,10 @@ window.__ModuleLoader__.load({
|
|
|
8858
9015
|
return 'Papers';
|
|
8859
9016
|
}
|
|
8860
9017
|
|
|
8861
|
-
/** The tool names whose transcript card this package draws. */
|
|
9018
|
+
/** The tool names whose transcript card this package draws (plasmid maps). */
|
|
8862
9019
|
const MAP_TOOL_KEYS = ['molbio_plasmid_map', 'molbio_plasmid_map_file'];
|
|
9020
|
+
/** Figure tools: a drawn logo / gRNA map plus (for gRNA) the ranked list. */
|
|
9021
|
+
const FIGURE_TOOL_KEYS = ['molbio_sequence_logo', 'molbio_grna_design'];
|
|
8863
9022
|
|
|
8864
9023
|
/**
|
|
8865
9024
|
* The transcript card for the map tools (`tool.call.toolview`, keyed by tool
|
|
@@ -8928,6 +9087,72 @@ window.__ModuleLoader__.load({
|
|
|
8928
9087
|
return h('div', { style: styles.card }, ...content);
|
|
8929
9088
|
}
|
|
8930
9089
|
|
|
9090
|
+
/**
|
|
9091
|
+
* The transcript card for the sequence-logo and gRNA tools. The logo card draws
|
|
9092
|
+
* the logo; the gRNA card draws the map when `map_path` was requested AND lists
|
|
9093
|
+
* the top-ranked guides (a gRNA call without a map still gets the table). Like
|
|
9094
|
+
* the map card it is a pure function of the block, and everything it reads is
|
|
9095
|
+
* validated and bounded by `figureCardView`.
|
|
9096
|
+
*/
|
|
9097
|
+
function MolbioFigureCard({ block, inspect }) {
|
|
9098
|
+
const settled = block !== null && typeof block === 'object' && block.kind === 'tool-result';
|
|
9099
|
+
const view = figureCardView(settled ? block.meta : undefined);
|
|
9100
|
+
const host = useRef(null);
|
|
9101
|
+
const svg = view.kind === 'figure' ? view.svg : '';
|
|
9102
|
+
const [drawError, setDrawError] = useState(null);
|
|
9103
|
+
|
|
9104
|
+
useEffect(() => {
|
|
9105
|
+
const node = host.current;
|
|
9106
|
+
if (node === null) return;
|
|
9107
|
+
node.replaceChildren();
|
|
9108
|
+
if (svg === '') return;
|
|
9109
|
+
const parsed = new DOMParser().parseFromString(svg, 'image/svg+xml');
|
|
9110
|
+
if (parsed.querySelector('parsererror') !== null) {
|
|
9111
|
+
setDrawError('the figure markup could not be parsed as SVG');
|
|
9112
|
+
return;
|
|
9113
|
+
}
|
|
9114
|
+
setDrawError(null);
|
|
9115
|
+
const element = parsed.documentElement;
|
|
9116
|
+
element.setAttribute('style', 'width:100%;height:auto;max-width:640px');
|
|
9117
|
+
node.append(document.importNode(element, true));
|
|
9118
|
+
}, [svg]);
|
|
9119
|
+
|
|
9120
|
+
const content = [h('div', { key: 'head', style: styles.cardHead }, settled ? figureCardSummary(view) : 'Drawing the figure…')];
|
|
9121
|
+
if (view.kind === 'figure') {
|
|
9122
|
+
if (drawError !== null) content.push(h('div', { key: 'drawerr', style: styles.cardNote }, drawError));
|
|
9123
|
+
content.push(h('div', { key: 'svg', ref: host, style: styles.cardSvg }));
|
|
9124
|
+
} else {
|
|
9125
|
+
content.push(h('div', { key: 'note', style: styles.cardNote }, view.message));
|
|
9126
|
+
}
|
|
9127
|
+
if (view.guides.length > 0) {
|
|
9128
|
+
const head = ['#', 'Guide', 'PAM', 'Position', 'Score', 'Off-targets'];
|
|
9129
|
+
const rows = view.guides.map((guide) => h('tr', { key: guide.rank },
|
|
9130
|
+
h('td', { style: styles.td }, String(guide.rank)),
|
|
9131
|
+
h('td', { style: styles.td }, guide.sequence),
|
|
9132
|
+
h('td', { style: styles.td }, guide.pam === '' ? '—' : guide.pam),
|
|
9133
|
+
h('td', { style: styles.td }, `${guide.strand === 'reverse' ? '−' : '+'}${guide.start}-${guide.end}`),
|
|
9134
|
+
h('td', { style: styles.td }, String(guide.score)),
|
|
9135
|
+
h('td', { style: styles.td }, String(guide.offTargets)),
|
|
9136
|
+
));
|
|
9137
|
+
content.push(h('table', { key: 'guides', style: styles.table },
|
|
9138
|
+
h('thead', null, h('tr', null, ...head.map((label) => h('th', { key: label, style: styles.th }, label)))),
|
|
9139
|
+
h('tbody', null, ...rows)));
|
|
9140
|
+
}
|
|
9141
|
+
if (view.svgPath !== '') content.push(h('div', { key: 'path', style: styles.cardPath }, view.svgPath));
|
|
9142
|
+
if (view.csvPath !== '') content.push(h('div', { key: 'csv', style: styles.cardPath }, view.csvPath));
|
|
9143
|
+
if (settled && block.isError === true) {
|
|
9144
|
+
const text = (Array.isArray(block.content) ? block.content : [])
|
|
9145
|
+
.map((item) => (item !== null && typeof item === 'object' && item.type === 'text' ? String(item.text ?? '') : ''))
|
|
9146
|
+
.join('\n')
|
|
9147
|
+
.trim();
|
|
9148
|
+
content.push(h('div', { key: 'err', style: styles.error }, text === '' ? 'The call failed.' : text));
|
|
9149
|
+
if (typeof inspect === 'function') {
|
|
9150
|
+
content.push(h('button', { key: 'inspect', style: styles.cardButton, onClick: () => inspect() }, 'Inspect'));
|
|
9151
|
+
}
|
|
9152
|
+
}
|
|
9153
|
+
return h('div', { style: styles.card }, ...content);
|
|
9154
|
+
}
|
|
9155
|
+
|
|
8931
9156
|
/**
|
|
8932
9157
|
* The literature panel: the workspace `papers.json` the molbio_paper_* tools
|
|
8933
9158
|
* maintain, rendered as a searchable reading list with a detail pane.
|
|
@@ -9276,6 +9501,15 @@ window.__ModuleLoader__.load({
|
|
|
9276
9501
|
key: toolName,
|
|
9277
9502
|
}, MolbioMapCard)), `molbio panel: ${toolName} card`);
|
|
9278
9503
|
}
|
|
9504
|
+
// Figure cards: the sequence-logo and gRNA tools declare a `molbio-figure`
|
|
9505
|
+
// projection; the same component draws a logo or a gRNA map and, for gRNA,
|
|
9506
|
+
// lists the top-ranked guides even when no map was requested.
|
|
9507
|
+
for (const toolName of FIGURE_TOOL_KEYS) {
|
|
9508
|
+
ctx.effect(() => ctx.slots.inject('tool.call.toolview', () => ctx.slots.register({
|
|
9509
|
+
name: 'tool.call.toolview',
|
|
9510
|
+
key: toolName,
|
|
9511
|
+
}, MolbioFigureCard)), `molbio panel: ${toolName} card`);
|
|
9512
|
+
}
|
|
9279
9513
|
// Plugins -> the installed plugin's detail page: the one place a user can
|
|
9280
9514
|
// store the NCBI key without editing a YAML file or exporting a variable.
|
|
9281
9515
|
//
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-molbio-panel",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Browser panel for the DeepSeek Harness Web GUI: a Molbio tab in the right sidebar that draws plasmid maps and sequence logos from workspace sequence files, using the same computation modules as the dsh-molbio-tools plugin.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.mjs",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"0.2.1-rc.2": "compatible",
|
|
28
28
|
"0.2.1-rc.3": "compatible",
|
|
29
29
|
"0.2.1-rc.4": "unknown",
|
|
30
|
-
"0.2.1-rc.5": "compatible"
|
|
30
|
+
"0.2.1-rc.5": "compatible",
|
|
31
|
+
"0.2.2-rc.1": "compatible"
|
|
31
32
|
}
|
|
32
33
|
},
|
|
33
34
|
"bundle": {
|