archaeopteryx 3.12.0 → 3.14.0
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/README.md +72 -7
- package/archaeopteryx.js +343 -54
- package/forester.js +593 -82
- package/package.json +3 -3
package/forester.js
CHANGED
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
|
-
// v 3.
|
|
23
|
+
// v 3.14.0
|
|
24
24
|
// 2026-09-17
|
|
25
25
|
//
|
|
26
26
|
// forester.js is a general suite for dealing with phylogenetic trees.
|
|
@@ -3814,6 +3814,230 @@
|
|
|
3814
3814
|
};
|
|
3815
3815
|
|
|
3816
3816
|
|
|
3817
|
+
// Everything the Tree Properties dialog reports, gathered in ONE pass:
|
|
3818
|
+
// the desktop's TreeFacts.scan, minus its histogram (Christian, 2026-09-23:
|
|
3819
|
+
// "we don't need a histogram ... but a more detailed stats would be
|
|
3820
|
+
// useful, min, median, max, n, also type ... also need to include the
|
|
3821
|
+
// properties").
|
|
3822
|
+
//
|
|
3823
|
+
// Side-effect free and DOM-free, so it can be tested in Node -- the whole
|
|
3824
|
+
// reason it lives here rather than in the dialog that renders it, which is
|
|
3825
|
+
// the same split the desktop makes between TreeFacts and
|
|
3826
|
+
// TreePropertiesForm.
|
|
3827
|
+
//
|
|
3828
|
+
// Support values are grouped BY TYPE, because a tree can carry a bootstrap
|
|
3829
|
+
// and a posterior on the same branch and their ranges are not comparable
|
|
3830
|
+
// (0-100 against 0-1); averaging them together would produce a number that
|
|
3831
|
+
// describes neither. MAD values are excluded for the same reason they are
|
|
3832
|
+
// excluded from the Newick support slot: they rate a root position, not a
|
|
3833
|
+
// clade.
|
|
3834
|
+
forester.treeStatistics = function (phyOrNode) {
|
|
3835
|
+
// Callers hand this either a phylogeny wrapper or the root node, and
|
|
3836
|
+
// the difference is one extra internal node, one extra branch and one
|
|
3837
|
+
// more of depth -- small enough to read as plausible. getTreeRoot
|
|
3838
|
+
// normalizes both, as collectBasicTreeProperties does.
|
|
3839
|
+
let root = phyOrNode ? forester.getTreeRoot(phyOrNode) : null;
|
|
3840
|
+
let stats = {
|
|
3841
|
+
tips: 0, internal: 0, nodes: 0, branches: 0,
|
|
3842
|
+
polytomies: 0, maxChildren: 0,
|
|
3843
|
+
depth: 0, height: 0, heightKnown: false,
|
|
3844
|
+
collapsed: 0,
|
|
3845
|
+
branchLengths: null,
|
|
3846
|
+
support: [],
|
|
3847
|
+
ultrametric: false,
|
|
3848
|
+
tipsWithTaxonomy: 0, distinctTaxonomies: 0, tipsWithTaxonomyId: 0,
|
|
3849
|
+
tipsWithSequence: 0, tipsWithMolSeq: 0, tipsWithDomains: 0,
|
|
3850
|
+
nodesWithDate: 0, tipsWithDistribution: 0, namedInternal: 0,
|
|
3851
|
+
duplications: 0, speciations: 0, nodesWithEvents: 0,
|
|
3852
|
+
propertyRefs: []
|
|
3853
|
+
};
|
|
3854
|
+
if (!root) {
|
|
3855
|
+
return stats;
|
|
3856
|
+
}
|
|
3857
|
+
let lengths = [];
|
|
3858
|
+
let zeroLengths = 0;
|
|
3859
|
+
let negativeLengths = 0;
|
|
3860
|
+
let byType = Object.create(null);
|
|
3861
|
+
let taxa = Object.create(null);
|
|
3862
|
+
let refs = Object.create(null);
|
|
3863
|
+
let refOrder = [];
|
|
3864
|
+
let tipDepths = [];
|
|
3865
|
+
let maxDepth = 0;
|
|
3866
|
+
|
|
3867
|
+
(function walk(n, depth, distance) {
|
|
3868
|
+
++stats.nodes;
|
|
3869
|
+
let kids = n.children || [];
|
|
3870
|
+
if (depth > maxDepth) {
|
|
3871
|
+
maxDepth = depth;
|
|
3872
|
+
}
|
|
3873
|
+
if (n !== root) {
|
|
3874
|
+
++stats.branches;
|
|
3875
|
+
let b = n.branch_length;
|
|
3876
|
+
if (typeof b === 'number' && isFinite(b)) {
|
|
3877
|
+
lengths.push(b);
|
|
3878
|
+
if (b === 0) {
|
|
3879
|
+
++zeroLengths;
|
|
3880
|
+
} else if (b < 0) {
|
|
3881
|
+
++negativeLengths;
|
|
3882
|
+
}
|
|
3883
|
+
distance += b;
|
|
3884
|
+
}
|
|
3885
|
+
}
|
|
3886
|
+
// support, by type, MAD left out
|
|
3887
|
+
if (n.confidences) {
|
|
3888
|
+
for (let i = 0; i < n.confidences.length; ++i) {
|
|
3889
|
+
let c = n.confidences[i];
|
|
3890
|
+
if (c.type === forester.MAD_CONFIDENCE_TYPE) {
|
|
3891
|
+
continue;
|
|
3892
|
+
}
|
|
3893
|
+
if (typeof c.value !== 'number' || !isFinite(c.value)) {
|
|
3894
|
+
continue;
|
|
3895
|
+
}
|
|
3896
|
+
let key = c.type || '';
|
|
3897
|
+
if (!byType[key]) {
|
|
3898
|
+
byType[key] = [];
|
|
3899
|
+
stats.support.push({type: key, values: byType[key]});
|
|
3900
|
+
}
|
|
3901
|
+
byType[key].push(c.value);
|
|
3902
|
+
}
|
|
3903
|
+
}
|
|
3904
|
+
if (n.date && typeof n.date.value === 'number' && isFinite(n.date.value)) {
|
|
3905
|
+
++stats.nodesWithDate;
|
|
3906
|
+
}
|
|
3907
|
+
if (n.properties) {
|
|
3908
|
+
let seen = Object.create(null);
|
|
3909
|
+
for (let i = 0; i < n.properties.length; ++i) {
|
|
3910
|
+
let ref = n.properties[i] && n.properties[i].ref;
|
|
3911
|
+
if (!ref || seen[ref]) {
|
|
3912
|
+
continue; // a node carrying a ref twice counts once
|
|
3913
|
+
}
|
|
3914
|
+
seen[ref] = true;
|
|
3915
|
+
if (refs[ref] === undefined) {
|
|
3916
|
+
refs[ref] = 0;
|
|
3917
|
+
refOrder.push(ref);
|
|
3918
|
+
}
|
|
3919
|
+
++refs[ref];
|
|
3920
|
+
}
|
|
3921
|
+
}
|
|
3922
|
+
if (n.events) {
|
|
3923
|
+
++stats.nodesWithEvents;
|
|
3924
|
+
if (n.events.duplications > 0) {
|
|
3925
|
+
stats.duplications += n.events.duplications;
|
|
3926
|
+
}
|
|
3927
|
+
if (n.events.speciations > 0) {
|
|
3928
|
+
stats.speciations += n.events.speciations;
|
|
3929
|
+
}
|
|
3930
|
+
}
|
|
3931
|
+
if (kids.length > 0) {
|
|
3932
|
+
++stats.internal;
|
|
3933
|
+
if (kids.length > stats.maxChildren) {
|
|
3934
|
+
stats.maxChildren = kids.length;
|
|
3935
|
+
}
|
|
3936
|
+
if (kids.length > 2) {
|
|
3937
|
+
++stats.polytomies;
|
|
3938
|
+
}
|
|
3939
|
+
if (n !== root && n.name) {
|
|
3940
|
+
++stats.namedInternal;
|
|
3941
|
+
}
|
|
3942
|
+
for (let i = 0; i < kids.length; ++i) {
|
|
3943
|
+
walk(kids[i], depth + 1, distance);
|
|
3944
|
+
}
|
|
3945
|
+
return;
|
|
3946
|
+
}
|
|
3947
|
+
++stats.tips;
|
|
3948
|
+
tipDepths.push(distance);
|
|
3949
|
+
if (n.taxonomies && n.taxonomies.length > 0) {
|
|
3950
|
+
++stats.tipsWithTaxonomy;
|
|
3951
|
+
let t = n.taxonomies[0];
|
|
3952
|
+
let label = t.scientific_name || t.code || t.common_name || '';
|
|
3953
|
+
if (label) {
|
|
3954
|
+
taxa[label] = true;
|
|
3955
|
+
}
|
|
3956
|
+
if (t.id && t.id.value) {
|
|
3957
|
+
++stats.tipsWithTaxonomyId;
|
|
3958
|
+
}
|
|
3959
|
+
}
|
|
3960
|
+
if (n.sequences && n.sequences.length > 0) {
|
|
3961
|
+
++stats.tipsWithSequence;
|
|
3962
|
+
let q = n.sequences[0];
|
|
3963
|
+
if (q.mol_seq && q.mol_seq.value) {
|
|
3964
|
+
++stats.tipsWithMolSeq;
|
|
3965
|
+
}
|
|
3966
|
+
if (forester.domainArchitectureOf(n)) {
|
|
3967
|
+
++stats.tipsWithDomains;
|
|
3968
|
+
}
|
|
3969
|
+
}
|
|
3970
|
+
if (n.distributions && n.distributions.length > 0) {
|
|
3971
|
+
++stats.tipsWithDistribution;
|
|
3972
|
+
}
|
|
3973
|
+
})(root, 0, 0);
|
|
3974
|
+
|
|
3975
|
+
stats.depth = maxDepth;
|
|
3976
|
+
stats.distinctTaxonomies = Object.keys(taxa).length;
|
|
3977
|
+
stats.propertyRefs = refOrder.map(function (r) {
|
|
3978
|
+
return {ref: r, nodes: refs[r]};
|
|
3979
|
+
});
|
|
3980
|
+
stats.branchLengths = forester.describeValues(lengths);
|
|
3981
|
+
if (stats.branchLengths) {
|
|
3982
|
+
stats.branchLengths.zero = zeroLengths;
|
|
3983
|
+
stats.branchLengths.negative = negativeLengths;
|
|
3984
|
+
}
|
|
3985
|
+
stats.support = stats.support.map(function (g) {
|
|
3986
|
+
let d = forester.describeValues(g.values);
|
|
3987
|
+
d.type = g.type;
|
|
3988
|
+
return d;
|
|
3989
|
+
}).filter(function (d) {
|
|
3990
|
+
return d.n > 0;
|
|
3991
|
+
});
|
|
3992
|
+
if (tipDepths.length > 0) {
|
|
3993
|
+
// running, not Math.min.apply: the spread of 150k tip depths
|
|
3994
|
+
// overflows the call stack, as timeAxisInfo found before this
|
|
3995
|
+
let lo = Infinity;
|
|
3996
|
+
let hi = -Infinity;
|
|
3997
|
+
for (let i = 0; i < tipDepths.length; ++i) {
|
|
3998
|
+
if (tipDepths[i] < lo) {
|
|
3999
|
+
lo = tipDepths[i];
|
|
4000
|
+
}
|
|
4001
|
+
if (tipDepths[i] > hi) {
|
|
4002
|
+
hi = tipDepths[i];
|
|
4003
|
+
}
|
|
4004
|
+
}
|
|
4005
|
+
stats.height = hi;
|
|
4006
|
+
stats.heightKnown = lengths.length > 0;
|
|
4007
|
+
// ultrametric: every tip the same distance from the root, to a
|
|
4008
|
+
// tolerance scaled to the tree, since branch lengths are floats
|
|
4009
|
+
stats.ultrametric = stats.heightKnown && (hi - lo) <= (Math.max(hi, 1e-12) * 1e-6);
|
|
4010
|
+
}
|
|
4011
|
+
return stats;
|
|
4012
|
+
};
|
|
4013
|
+
|
|
4014
|
+
// n, minimum, median, maximum, mean and sum of a list of numbers, or null
|
|
4015
|
+
// when there are none. The median is the middle value of the sorted list,
|
|
4016
|
+
// or the mean of the two middle ones -- taken on a COPY, because sorting
|
|
4017
|
+
// the caller's array would reorder data it still owns.
|
|
4018
|
+
forester.describeValues = function (values) {
|
|
4019
|
+
if (!values || values.length === 0) {
|
|
4020
|
+
return null;
|
|
4021
|
+
}
|
|
4022
|
+
let v = values.slice().sort(function (a, b) {
|
|
4023
|
+
return a - b;
|
|
4024
|
+
});
|
|
4025
|
+
let n = v.length;
|
|
4026
|
+
let sum = 0;
|
|
4027
|
+
for (let i = 0; i < n; ++i) {
|
|
4028
|
+
sum += v[i];
|
|
4029
|
+
}
|
|
4030
|
+
let mid = Math.floor(n / 2);
|
|
4031
|
+
return {
|
|
4032
|
+
n: n,
|
|
4033
|
+
min: v[0],
|
|
4034
|
+
max: v[n - 1],
|
|
4035
|
+
median: (n % 2 === 1) ? v[mid] : ((v[mid - 1] + v[mid]) / 2),
|
|
4036
|
+
mean: sum / n,
|
|
4037
|
+
sum: sum
|
|
4038
|
+
};
|
|
4039
|
+
};
|
|
4040
|
+
|
|
3817
4041
|
forester.calcMaxBranchLength = function (node) {
|
|
3818
4042
|
let max = 0;
|
|
3819
4043
|
forester.preOrderTraversalAll(node, function (n) {
|
|
@@ -5010,6 +5234,40 @@
|
|
|
5010
5234
|
});
|
|
5011
5235
|
};
|
|
5012
5236
|
|
|
5237
|
+
// Residue normalization for a Nexus character matrix, ported from the
|
|
5238
|
+
// desktop's BasicSequence.createAaSequence / createDnaSequence /
|
|
5239
|
+
// createRnaSequence, which is what its Nexus reader builds a sequence
|
|
5240
|
+
// with. Three steps, in this order:
|
|
5241
|
+
// 1. uppercase;
|
|
5242
|
+
// 2. '.' becomes the gap '-' (before step 3, or it would become X/N);
|
|
5243
|
+
// 3. anything outside the type's alphabet becomes the unspecified
|
|
5244
|
+
// residue, 'X' for protein and 'N' for nucleotides.
|
|
5245
|
+
// So the Nexus missing symbol '?' arrives as X or N, which is what
|
|
5246
|
+
// Christian decided on 2026-09-23 ("normalize to X, same as desktop").
|
|
5247
|
+
// '?' is simply one character outside the alphabet; doing it alone would
|
|
5248
|
+
// have left us differing on '.' and on stray letters instead.
|
|
5249
|
+
//
|
|
5250
|
+
// This is the NEXUS MATRIX reader only. A phyloXML <mol_seq> is kept
|
|
5251
|
+
// exactly as written by both programs -- measured, not assumed: the
|
|
5252
|
+
// desktop reads "MK??AL.N" out of phyloXML unchanged.
|
|
5253
|
+
const AA_NOT_ALPHABET = /[^ARNDBCQEZGHILKMFPSTWYVXUO\-*]/g;
|
|
5254
|
+
const DNA_NOT_ALPHABET = /[^ACGTRYMKWSN\-*]/g;
|
|
5255
|
+
const RNA_NOT_ALPHABET = /[^ACGURYMKWSN\-*]/g;
|
|
5256
|
+
|
|
5257
|
+
function normalizeMatrixResidues(block, datatype) {
|
|
5258
|
+
let out = block.toUpperCase().replace(/\./g, '-');
|
|
5259
|
+
if (datatype === 'protein') {
|
|
5260
|
+
return out.replace(AA_NOT_ALPHABET, 'X');
|
|
5261
|
+
}
|
|
5262
|
+
if (datatype === 'dna') {
|
|
5263
|
+
return out.replace(DNA_NOT_ALPHABET, 'N');
|
|
5264
|
+
}
|
|
5265
|
+
if (datatype === 'rna') {
|
|
5266
|
+
return out.replace(RNA_NOT_ALPHABET, 'N');
|
|
5267
|
+
}
|
|
5268
|
+
return out;
|
|
5269
|
+
}
|
|
5270
|
+
|
|
5013
5271
|
// Parses a Nexus-formatted string and returns an ARRAY of tree objects,
|
|
5014
5272
|
// each in the same shape parseNewHampshire produces (a Nexus file can
|
|
5015
5273
|
// hold any number of trees). Ported from the desktop's
|
|
@@ -5031,6 +5289,10 @@
|
|
|
5031
5289
|
const RESIDUES_RE = /^[A-Za-z\-_*?.]+$/;
|
|
5032
5290
|
const DATATYPE_RE = /datatype\s*=\s*([a-z]+)/;
|
|
5033
5291
|
const MATCHCHAR_RE = /matchchar\s*=\s*['"]?(\S)/;
|
|
5292
|
+
// the desktop's patterns, character for character: one optional space
|
|
5293
|
+
// either side of the '=', then the first non-space as the symbol
|
|
5294
|
+
const MISSING_RE = /missing\s?.\s?(\S)/;
|
|
5295
|
+
const GAP_RE = /gap\s?.\s?(\S)/;
|
|
5034
5296
|
|
|
5035
5297
|
let trees = [];
|
|
5036
5298
|
let taxlabels = [];
|
|
@@ -5058,6 +5320,22 @@
|
|
|
5058
5320
|
let isRooted = false;
|
|
5059
5321
|
let matchchar = null;
|
|
5060
5322
|
let matrixReferenceId = null;
|
|
5323
|
+
// The symbols a block DECLARES for missing data and for a gap. Nexus
|
|
5324
|
+
// lets a file choose them, so assuming '?' and '-' reads an all-missing
|
|
5325
|
+
// row of a "Missing=N" file as real residues -- the invented-sequence
|
|
5326
|
+
// bug the residue scan exists to prevent. Defaults are the conventions.
|
|
5327
|
+
let missingChar = '?';
|
|
5328
|
+
let gapChar = '-';
|
|
5329
|
+
// Matrix ids whose row carried at least one real residue. A row of
|
|
5330
|
+
// nothing but the missing and gap symbols states that the taxon has NO
|
|
5331
|
+
// data, and must not become a sequence: our own writer emits exactly
|
|
5332
|
+
// such rows to keep the matrix rectangular, so without this a save and
|
|
5333
|
+
// reopen invents a sequence of question marks for every tip that never
|
|
5334
|
+
// had one. Tracked here rather than tested on the finished sequence
|
|
5335
|
+
// because the desktop turns '?' into 'X' when it builds an AA
|
|
5336
|
+
// sequence, after which a missing row is indistinguishable from a
|
|
5337
|
+
// genuinely ambiguous one.
|
|
5338
|
+
let seqIdsWithResidues = Object.create(null);
|
|
5061
5339
|
|
|
5062
5340
|
// Nexus treats '_' and ' ' as equivalent, labels may be quoted, and a
|
|
5063
5341
|
// matrix often capitalizes taxon names differently from the tree -- so
|
|
@@ -5168,6 +5446,19 @@
|
|
|
5168
5446
|
}
|
|
5169
5447
|
block = resolved;
|
|
5170
5448
|
}
|
|
5449
|
+
// '*' is NOT absence: it is a residue, the stop codon of a
|
|
5450
|
+
// translated alignment. What counts as absence is what the block
|
|
5451
|
+
// DECLARED, plus '.', which is the matchchar and gap convention.
|
|
5452
|
+
for (let j = 0; j < block.length; ++j) {
|
|
5453
|
+
let c = block.charAt(j).toLowerCase();
|
|
5454
|
+
if (c !== missingChar.toLowerCase() && c !== gapChar.toLowerCase() && c !== '.') {
|
|
5455
|
+
seqIdsWithResidues[id] = true;
|
|
5456
|
+
break;
|
|
5457
|
+
}
|
|
5458
|
+
}
|
|
5459
|
+
// ... and only THEN normalize: afterwards '?' has become X or N
|
|
5460
|
+
// and a row of nothing but missing data would read as residues.
|
|
5461
|
+
block = normalizeMatrixResidues(block, datatype);
|
|
5171
5462
|
seqs[id] = {
|
|
5172
5463
|
value: seqs[id] ? (seqs[id].value + block) : block,
|
|
5173
5464
|
type: datatype
|
|
@@ -5200,7 +5491,9 @@
|
|
|
5200
5491
|
}
|
|
5201
5492
|
let seqsByKey = Object.create(null);
|
|
5202
5493
|
for (let id in seqs) {
|
|
5203
|
-
|
|
5494
|
+
if (seqIdsWithResidues[id]) {
|
|
5495
|
+
seqsByKey[joinKey(id)] = seqs[id];
|
|
5496
|
+
}
|
|
5204
5497
|
}
|
|
5205
5498
|
let externals = forester.getAllExternalNodes(phy);
|
|
5206
5499
|
let annotationNs = null;
|
|
@@ -5331,6 +5624,9 @@
|
|
|
5331
5624
|
// scope the rows to THIS matrix block, so a later block
|
|
5332
5625
|
// cannot cross-contaminate an earlier one
|
|
5333
5626
|
seqs = Object.create(null);
|
|
5627
|
+
seqIdsWithResidues = Object.create(null);
|
|
5628
|
+
missingChar = '?';
|
|
5629
|
+
gapChar = '-';
|
|
5334
5630
|
} else if (inTreesBlock) {
|
|
5335
5631
|
if (lc.startsWith('title')) {
|
|
5336
5632
|
let tm = TITLE_RE.exec(line);
|
|
@@ -5522,6 +5818,14 @@
|
|
|
5522
5818
|
if (mm) {
|
|
5523
5819
|
matchchar = mm[1];
|
|
5524
5820
|
}
|
|
5821
|
+
let miss = MISSING_RE.exec(dlc);
|
|
5822
|
+
if (miss) {
|
|
5823
|
+
missingChar = miss[1];
|
|
5824
|
+
}
|
|
5825
|
+
let gp = GAP_RE.exec(dlc);
|
|
5826
|
+
if (gp) {
|
|
5827
|
+
gapChar = gp[1];
|
|
5828
|
+
}
|
|
5525
5829
|
if (dlc === 'matrix' || dlc.startsWith('matrix ')) {
|
|
5526
5830
|
inMatrix = true;
|
|
5527
5831
|
let after = line.substring(6).trim();
|
|
@@ -6087,9 +6391,90 @@
|
|
|
6087
6391
|
* @param writeConfidences - to write confidence values in brackets
|
|
6088
6392
|
* @returns {*} - a New Hampshire (Newick) formatted string.
|
|
6089
6393
|
*/
|
|
6394
|
+
// The label a node is written under, in New Hampshire and in Nexus alike:
|
|
6395
|
+
// name, then taxonomy (code / scientific / common), then the sequence's
|
|
6396
|
+
// name / symbol / gene name, then its ACCESSION -- each step tried in turn,
|
|
6397
|
+
// so a taxonomy element that is present but empty does not stop the search.
|
|
6398
|
+
// An EXTERNAL node left with nothing gets a 'node<N>' placeholder, N being
|
|
6399
|
+
// its 1-based position in tip order; an empty label would not parse back
|
|
6400
|
+
// out of TaxLabels, and in Newick it names nothing at all. An INTERNAL node
|
|
6401
|
+
// does NOT get one: a placeholder there would invent a name for an
|
|
6402
|
+
// ancestor, and an unlabeled internal node is perfectly ordinary.
|
|
6403
|
+
//
|
|
6404
|
+
// ONE chain for both writers. A Nexus file whose TaxLabels, matrix rows and
|
|
6405
|
+
// trees block disagree about a taxon cannot be joined back up, and a tree
|
|
6406
|
+
// saved as Newick and as Nexus should name its tips the same way -- ours
|
|
6407
|
+
// did not, until 2026-09-23: toNexus applied this chain and toNewHampshire
|
|
6408
|
+
// wrote node.name and nothing else, so a nameless tip was HUMAN in one file
|
|
6409
|
+
// and empty in the other. It is the desktop's chain
|
|
6410
|
+
// (PhylogenyNode.toNewHampshire), adopted on Christian's word, and the two
|
|
6411
|
+
// programs' output is compared byte for byte in the tests.
|
|
6412
|
+
function nhNodeLabel(node, placeholder) {
|
|
6413
|
+
let s = node.name || '';
|
|
6414
|
+
if (!s && node.taxonomies && node.taxonomies.length > 0) {
|
|
6415
|
+
let t = node.taxonomies[0];
|
|
6416
|
+
s = t.code || t.scientific_name || t.common_name || '';
|
|
6417
|
+
}
|
|
6418
|
+
if (!s && node.sequences && node.sequences.length > 0) {
|
|
6419
|
+
let q = node.sequences[0];
|
|
6420
|
+
s = q.name || q.symbol || q.gene_name || '';
|
|
6421
|
+
}
|
|
6422
|
+
if (!s && node.sequences && node.sequences.length > 0) {
|
|
6423
|
+
let a = node.sequences[0].accession;
|
|
6424
|
+
s = (a && a.value) ? a.value : '';
|
|
6425
|
+
}
|
|
6426
|
+
if (!s && placeholder) {
|
|
6427
|
+
s = placeholder;
|
|
6428
|
+
}
|
|
6429
|
+
return s;
|
|
6430
|
+
}
|
|
6431
|
+
|
|
6432
|
+
// A placeholder label for every external node, by tip index, in the order
|
|
6433
|
+
// the tips are written (getAllExternalNodes collects them in pre-order from
|
|
6434
|
+
// the far side, so reversing it gives left-to-right). Used only for a node
|
|
6435
|
+
// that nothing else names.
|
|
6436
|
+
//
|
|
6437
|
+
// A tip may literally be CALLED "node2". Minting that same token for a
|
|
6438
|
+
// different tip gives two taxa one label: illegal Nexus, and our own reader
|
|
6439
|
+
// takes the repeated row for an interleaved continuation and hands both
|
|
6440
|
+
// tips the two sequences joined together. So the labels the tree already
|
|
6441
|
+
// produces are collected first and stepped over.
|
|
6442
|
+
function tipPlaceholders(phy) {
|
|
6443
|
+
let m = new Map();
|
|
6444
|
+
// An EMPTY tree has no tips to number. Its single top node looks like
|
|
6445
|
+
// an external node -- no children -- and numbering it would turn the
|
|
6446
|
+
// empty string, which parseNewHampshire accepts and toNewHampshire has
|
|
6447
|
+
// always written back as the empty string, into "node1;". (The desktop
|
|
6448
|
+
// throws on an empty string rather than parsing it, so there is no
|
|
6449
|
+
// joint behaviour to match here, only ours to keep.)
|
|
6450
|
+
if (!phy.children || phy.children.length !== 1 || !phy.children[0].children) {
|
|
6451
|
+
return m;
|
|
6452
|
+
}
|
|
6453
|
+
let ext = forester.getAllExternalNodes(phy).reverse();
|
|
6454
|
+
let taken = Object.create(null);
|
|
6455
|
+
ext.forEach(function (n) {
|
|
6456
|
+
let label = nhNodeLabel(n, null);
|
|
6457
|
+
if (label.length > 0) {
|
|
6458
|
+
taken[label] = true;
|
|
6459
|
+
}
|
|
6460
|
+
});
|
|
6461
|
+
let i = 1;
|
|
6462
|
+
let spare = ext.length + 1;
|
|
6463
|
+
ext.forEach(function (n) {
|
|
6464
|
+
let candidate = 'node' + (i++);
|
|
6465
|
+
while (taken[candidate]) {
|
|
6466
|
+
candidate = 'node' + (spare++);
|
|
6467
|
+
}
|
|
6468
|
+
taken[candidate] = true;
|
|
6469
|
+
m.set(n, candidate);
|
|
6470
|
+
});
|
|
6471
|
+
return m;
|
|
6472
|
+
}
|
|
6473
|
+
|
|
6090
6474
|
forester.toNewHampshire = function (phy, decPointsMax, replaceChars, writeConfidences) {
|
|
6091
6475
|
void replaceChars; // retired: see the note above; labels are always quoted now
|
|
6092
6476
|
let nh = "";
|
|
6477
|
+
let tips = tipPlaceholders(phy);
|
|
6093
6478
|
if (phy.children && phy.children.length === 1) {
|
|
6094
6479
|
toNewHampshireHelper(phy.children[0], true);
|
|
6095
6480
|
}
|
|
@@ -6107,8 +6492,9 @@
|
|
|
6107
6492
|
}
|
|
6108
6493
|
nh += ")";
|
|
6109
6494
|
}
|
|
6110
|
-
|
|
6111
|
-
|
|
6495
|
+
let label = nhNodeLabel(node, tips.get(node));
|
|
6496
|
+
if (label.length > 0) {
|
|
6497
|
+
nh += sanitizeLabelForNH(label);
|
|
6112
6498
|
}
|
|
6113
6499
|
if (node.branch_length !== undefined && node.branch_length !== null) {
|
|
6114
6500
|
if (decPointsMax && decPointsMax > 0) {
|
|
@@ -6120,10 +6506,21 @@
|
|
|
6120
6506
|
// the support slot holds support: a MAD value (madRoot) never goes
|
|
6121
6507
|
// there -- it would read as support, and would crowd out the
|
|
6122
6508
|
// bootstrap on a branch carrying both. phyloXML keeps it, typed.
|
|
6509
|
+
//
|
|
6510
|
+
// Newick has ONE slot and phyloXML allows many, so a node carrying
|
|
6511
|
+
// both a bootstrap and a posterior has to lose one of them. It
|
|
6512
|
+
// writes the FIRST that is not MAD, which is what the desktop's
|
|
6513
|
+
// BranchData.getSupportConfidence returns and therefore what its
|
|
6514
|
+
// writer emits -- verified by running it: a node with
|
|
6515
|
+
// bootstrap=95 and posterior=0.99 comes out as "ab:0.3[95]".
|
|
6516
|
+
// This used to require EXACTLY one and so wrote nothing at all,
|
|
6517
|
+
// which lost the bootstrap too, on real files: the repo's own
|
|
6518
|
+
// phyloWithConfidences.xml has a node carrying bootstrap and
|
|
6519
|
+
// likelihood together.
|
|
6123
6520
|
let support = writeConfidences && node.confidences
|
|
6124
6521
|
? node.confidences.filter(function (c) { return c.type !== forester.MAD_CONFIDENCE_TYPE; })
|
|
6125
6522
|
: [];
|
|
6126
|
-
if (support.length
|
|
6523
|
+
if (support.length > 0 && support[0].value !== undefined && support[0].value !== null) {
|
|
6127
6524
|
if (decPointsMax && decPointsMax > 0) {
|
|
6128
6525
|
nh += "[" + forester.roundNumber(support[0].value, decPointsMax) + "]";
|
|
6129
6526
|
} else {
|
|
@@ -6137,6 +6534,70 @@
|
|
|
6137
6534
|
|
|
6138
6535
|
};
|
|
6139
6536
|
|
|
6537
|
+
// A node's molecular sequence for the Nexus matrix: the first one carrying
|
|
6538
|
+
// residues, or null. is_aligned is deliberately NOT consulted -- the
|
|
6539
|
+
// desktop's writer does not have that flag and decides on the lengths
|
|
6540
|
+
// instead, and equal lengths are what a character matrix actually
|
|
6541
|
+
// requires. (A JS node may hold several sequences where a desktop node
|
|
6542
|
+
// holds one; the first with residues is the one written.)
|
|
6543
|
+
function molSeqOfNode(node) {
|
|
6544
|
+
if (!node.sequences) {
|
|
6545
|
+
return null;
|
|
6546
|
+
}
|
|
6547
|
+
for (let j = 0; j < node.sequences.length; ++j) {
|
|
6548
|
+
let q = node.sequences[j];
|
|
6549
|
+
if (q.mol_seq && q.mol_seq.value) {
|
|
6550
|
+
return q.mol_seq.value;
|
|
6551
|
+
}
|
|
6552
|
+
}
|
|
6553
|
+
return null;
|
|
6554
|
+
}
|
|
6555
|
+
|
|
6556
|
+
// ForesterUtil.guessMolecularSequenceType, ported verbatim so that the two
|
|
6557
|
+
// programs declare the same DataType for the same residues: the letters a
|
|
6558
|
+
// nucleotide sequence cannot contain, then T for DNA and U for RNA, and
|
|
6559
|
+
// null when the sequence says nothing either way (all gaps, or A/C/G
|
|
6560
|
+
// alone). Note that it reads the residues and ignores any DECLARED type,
|
|
6561
|
+
// which is the desktop's rule and therefore ours -- and the right one:
|
|
6562
|
+
// bunya_glyco.xml declares type="protein" over 121 sequences of pure ACGT.
|
|
6563
|
+
//
|
|
6564
|
+
// F, P and V were added 2026-09-23 on Christian's word, jointly with the
|
|
6565
|
+
// desktop, because the test was missing most of the protein-exclusive
|
|
6566
|
+
// alphabet: under forester's own alphabets that is BDEFHILOPQVXZ and only
|
|
6567
|
+
// DEHILQ were tested. A protein built solely from nucleotide letters
|
|
6568
|
+
// therefore guessed DNA -- MKATSWNP has exactly one protein-exclusive
|
|
6569
|
+
// residue and it was P -- and a matrix wrongly declared DNA comes back
|
|
6570
|
+
// with every non-nucleotide residue replaced by N. Measured against
|
|
6571
|
+
// UniProt residue frequencies, the chance a protein carries none of the
|
|
6572
|
+
// tested letters falls from 12.5% to 3.3% at length 5 and from 1.6% to
|
|
6573
|
+
// 0.11% at length 10; a real alignment of a hundred columns was never at
|
|
6574
|
+
// risk either way. O, X and Z are protein-exclusive too and deliberately
|
|
6575
|
+
// left out: rare enough to buy almost nothing, and every letter added is
|
|
6576
|
+
// one both programs must add.
|
|
6577
|
+
//
|
|
6578
|
+
// V (and B, D, H) are nucleotide ambiguity codes in full IUPAC but not in
|
|
6579
|
+
// forester's DNA alphabet, which maps them to N, so they cannot survive in
|
|
6580
|
+
// a DNA sequence here and testing them for protein is consistent.
|
|
6581
|
+
//
|
|
6582
|
+
// NEITHER PROGRAM RETUNES THIS ALONE. A letter added on one side types the
|
|
6583
|
+
// same file two ways, which is worse than a blind spot they share;
|
|
6584
|
+
// testNexusMatrixDatatype pins the alphabet letter for letter.
|
|
6585
|
+
function guessMolSeqType(v) {
|
|
6586
|
+
let s = v.toUpperCase();
|
|
6587
|
+
if (s.indexOf('L') >= 0 || s.indexOf('I') >= 0 || s.indexOf('E') >= 0
|
|
6588
|
+
|| s.indexOf('H') >= 0 || s.indexOf('D') >= 0 || s.indexOf('Q') >= 0
|
|
6589
|
+
|| s.indexOf('F') >= 0 || s.indexOf('P') >= 0 || s.indexOf('V') >= 0) {
|
|
6590
|
+
return 'Protein';
|
|
6591
|
+
}
|
|
6592
|
+
if (s.indexOf('T') >= 0) {
|
|
6593
|
+
return 'DNA';
|
|
6594
|
+
}
|
|
6595
|
+
if (s.indexOf('U') >= 0) {
|
|
6596
|
+
return 'RNA';
|
|
6597
|
+
}
|
|
6598
|
+
return null;
|
|
6599
|
+
}
|
|
6600
|
+
|
|
6140
6601
|
// Writes a phylogeny as a Nexus-formatted string, ported from the
|
|
6141
6602
|
// desktop's PhylogenyWriter: a TAXA block (Dimensions, TaxLabels) and a
|
|
6142
6603
|
// TREES block (the tree under its name, [&R]/[&U] rootedness, the same
|
|
@@ -6151,95 +6612,148 @@
|
|
|
6151
6612
|
// all three go through sanitizeLabelForNH -- the same helper
|
|
6152
6613
|
// toNewHampshire writes the tree with.
|
|
6153
6614
|
//
|
|
6154
|
-
//
|
|
6155
|
-
//
|
|
6156
|
-
//
|
|
6157
|
-
//
|
|
6158
|
-
//
|
|
6159
|
-
|
|
6160
|
-
//
|
|
6161
|
-
//
|
|
6162
|
-
function
|
|
6163
|
-
|
|
6164
|
-
|
|
6165
|
-
|
|
6166
|
-
|
|
6167
|
-
|
|
6168
|
-
s = t.code || t.scientific_name || t.common_name || '';
|
|
6169
|
-
} else if (node.sequences && node.sequences.length > 0) {
|
|
6170
|
-
let q = node.sequences[0];
|
|
6171
|
-
s = q.name || q.symbol || q.gene_name || '';
|
|
6172
|
-
}
|
|
6173
|
-
if (!s) {
|
|
6174
|
-
s = 'node' + (i + 1); // an empty TaxLabels token would not parse back
|
|
6175
|
-
}
|
|
6176
|
-
return s;
|
|
6615
|
+
// The chain lives in nhNodeLabel, shared with toNewHampshire, and it
|
|
6616
|
+
// returns the label UNQUOTED because every use here quotes it through
|
|
6617
|
+
// sanitizeLabelForNH -- the same helper the tree string goes through.
|
|
6618
|
+
//
|
|
6619
|
+
// No tip is renamed on the way past any more. This used to assign the
|
|
6620
|
+
// computed label to node.name for nameless tips so that toNewHampshire,
|
|
6621
|
+
// which knew only node.name, would write the same token; the caller's
|
|
6622
|
+
// tree was mutated and restored around the call. Now both writers ask
|
|
6623
|
+
// the same function, so there is nothing to patch up.
|
|
6624
|
+
let ext = forester.getAllExternalNodes(phy).reverse();
|
|
6625
|
+
let tips = tipPlaceholders(phy);
|
|
6626
|
+
|
|
6627
|
+
function nexusLabel(node) {
|
|
6628
|
+
return nhNodeLabel(node, tips.get(node));
|
|
6177
6629
|
}
|
|
6178
6630
|
|
|
6179
|
-
let ext = forester.getAllExternalNodes(phy).reverse();
|
|
6180
|
-
// a nameless tip gets its taxa-block label in the TREE as well --
|
|
6181
|
-
// TaxLabels, the Matrix and the Newick must agree on every taxon or
|
|
6182
|
-
// nothing can join them back up (restored before returning, so the
|
|
6183
|
-
// caller's tree is never mutated)
|
|
6184
|
-
let renamed = [];
|
|
6185
|
-
ext.forEach(function (node, i) {
|
|
6186
|
-
if (!node.name) {
|
|
6187
|
-
node.name = nexusLabel(node, i);
|
|
6188
|
-
renamed.push(node);
|
|
6189
|
-
}
|
|
6190
|
-
});
|
|
6191
6631
|
let s = '#NEXUS\n';
|
|
6192
6632
|
s += 'Begin Taxa;\n';
|
|
6193
6633
|
s += ' Dimensions NTax=' + ext.length + ';\n';
|
|
6194
6634
|
s += ' TaxLabels';
|
|
6195
|
-
ext.forEach(function (node
|
|
6196
|
-
s += ' ' + sanitizeLabelForNH(nexusLabel(node
|
|
6635
|
+
ext.forEach(function (node) {
|
|
6636
|
+
s += ' ' + sanitizeLabelForNH(nexusLabel(node));
|
|
6197
6637
|
});
|
|
6198
6638
|
s += ';\n';
|
|
6199
6639
|
s += 'End;\n';
|
|
6200
6640
|
|
|
6201
|
-
|
|
6202
|
-
|
|
6203
|
-
|
|
6641
|
+
// Every tip's label and, where there is one, its molecular sequence.
|
|
6642
|
+
// Written to the desktop's rules (PhylogenyWriter.writeNexusCharactersBlock),
|
|
6643
|
+
// because a Nexus file is a joint artifact: whichever program wrote it,
|
|
6644
|
+
// the other has to read the same bytes back.
|
|
6645
|
+
let labels = ext.map(function (node) {
|
|
6646
|
+
return sanitizeLabelForNH(nexusLabel(node));
|
|
6647
|
+
});
|
|
6648
|
+
let withSeq = [];
|
|
6204
6649
|
ext.forEach(function (node, i) {
|
|
6205
|
-
|
|
6206
|
-
|
|
6650
|
+
let v = molSeqOfNode(node);
|
|
6651
|
+
if (v !== null) {
|
|
6652
|
+
withSeq.push({i: i, value: v});
|
|
6653
|
+
}
|
|
6654
|
+
});
|
|
6655
|
+
if (withSeq.length > 0) {
|
|
6656
|
+
// A matrix is keyed on the taxon label, so two tips sharing one
|
|
6657
|
+
// cannot be told apart: our reader takes the second row for an
|
|
6658
|
+
// interleaved continuation and hands BOTH tips the two sequences
|
|
6659
|
+
// joined together. The Taxa and Trees blocks have always written
|
|
6660
|
+
// such a tree -- invalid Nexus, but only cosmetically; a matrix
|
|
6661
|
+
// would make it corrupting, so it is not written.
|
|
6662
|
+
let seen = Object.create(null);
|
|
6663
|
+
let duplicate = null;
|
|
6664
|
+
for (let k = 0; k < labels.length; ++k) {
|
|
6665
|
+
if (seen[labels[k]]) {
|
|
6666
|
+
duplicate = labels[k];
|
|
6667
|
+
break;
|
|
6668
|
+
}
|
|
6669
|
+
seen[labels[k]] = true;
|
|
6670
|
+
}
|
|
6671
|
+
if (duplicate !== null) {
|
|
6672
|
+
s += '[ Molecular sequences were not written: two or more tips share the taxon label '
|
|
6673
|
+
+ duplicate + ', and a character matrix keyed on an ambiguous label cannot be'
|
|
6674
|
+
+ ' read back. ]\n';
|
|
6675
|
+
withSeq = [];
|
|
6207
6676
|
}
|
|
6208
|
-
|
|
6209
|
-
|
|
6210
|
-
|
|
6211
|
-
|
|
6212
|
-
|
|
6213
|
-
|
|
6214
|
-
|
|
6677
|
+
}
|
|
6678
|
+
if (withSeq.length > 0) {
|
|
6679
|
+
let nchar = withSeq[0].value.length;
|
|
6680
|
+
let ragged = -1;
|
|
6681
|
+
for (let k = 1; k < withSeq.length; ++k) {
|
|
6682
|
+
if (withSeq[k].value.length !== nchar) {
|
|
6683
|
+
ragged = withSeq[k].value.length;
|
|
6684
|
+
break;
|
|
6685
|
+
}
|
|
6686
|
+
}
|
|
6687
|
+
if (ragged >= 0) {
|
|
6688
|
+
// A Nexus matrix is rectangular, so sequences of unequal length
|
|
6689
|
+
// cannot be one: padding them would state an alignment that
|
|
6690
|
+
// does not exist. Say so in the file rather than leaving the
|
|
6691
|
+
// reader to wonder where the data went.
|
|
6692
|
+
s += '[ Molecular sequences were not written: they are of unequal length ('
|
|
6693
|
+
+ nchar + ' vs ' + ragged + '), so they are not an alignment and cannot'
|
|
6694
|
+
+ ' form a Nexus character matrix. ]\n';
|
|
6695
|
+
} else {
|
|
6696
|
+
// The datatype is a property of the whole MATRIX, so every
|
|
6697
|
+
// sequence decides it, not the first one that guesses non-null.
|
|
6698
|
+
// guessMolSeqType looks for residues only protein has, so a
|
|
6699
|
+
// short protein made of nucleotide letters guesses DNA -- and a
|
|
6700
|
+
// matrix wrongly declared DNA is read back with every
|
|
6701
|
+
// non-nucleotide residue replaced by N (MKATSWNP came back
|
|
6702
|
+
// MKATSWNN). Protein therefore wins any disagreement: calling a
|
|
6703
|
+
// nucleotide alignment protein leaves the residues readable,
|
|
6704
|
+
// the reverse destroys them. Gaps and missing symbols are
|
|
6705
|
+
// stripped first so they cannot sway the guess.
|
|
6706
|
+
let datatype = 'Protein';
|
|
6707
|
+
let sawAa = false;
|
|
6708
|
+
let sawNt = false;
|
|
6709
|
+
let isRna = false;
|
|
6710
|
+
for (let k = 0; k < withSeq.length; ++k) {
|
|
6711
|
+
let bare = withSeq[k].value.replace(/[-.?*]/g, '');
|
|
6712
|
+
if (bare.length < 1) {
|
|
6713
|
+
continue;
|
|
6215
6714
|
}
|
|
6216
|
-
|
|
6715
|
+
let t = guessMolSeqType(bare);
|
|
6716
|
+
if (t === 'DNA') {
|
|
6717
|
+
sawNt = true;
|
|
6718
|
+
} else if (t === 'RNA') {
|
|
6719
|
+
sawNt = true;
|
|
6720
|
+
isRna = true;
|
|
6721
|
+
} else if (t !== null) {
|
|
6722
|
+
sawAa = true;
|
|
6723
|
+
}
|
|
6724
|
+
}
|
|
6725
|
+
if (sawNt && !sawAa) {
|
|
6726
|
+
datatype = isRna ? 'RNA' : 'DNA';
|
|
6217
6727
|
}
|
|
6728
|
+
let width = 0;
|
|
6729
|
+
labels.forEach(function (l) {
|
|
6730
|
+
width = Math.max(width, l.length);
|
|
6731
|
+
});
|
|
6732
|
+
++width;
|
|
6733
|
+
let missing = '?'.repeat(nchar);
|
|
6734
|
+
let byTip = Object.create(null);
|
|
6735
|
+
withSeq.forEach(function (r) {
|
|
6736
|
+
byTip[r.i] = r.value;
|
|
6737
|
+
});
|
|
6738
|
+
s += 'Begin Characters;\n';
|
|
6739
|
+
// NChar ONLY: the Nexus standard allows NTax in a CHARACTERS
|
|
6740
|
+
// block's DIMENSIONS solely alongside NEWTAXA (the taxa are the
|
|
6741
|
+
// TAXA block's), and strict readers -- jebl, and so AliView --
|
|
6742
|
+
// reject the file over it
|
|
6743
|
+
s += ' Dimensions NChar=' + nchar + ';\n';
|
|
6744
|
+
s += ' Format DataType=' + datatype + ' Interleave=No Gap=- Missing=?;\n';
|
|
6745
|
+
s += ' Matrix\n';
|
|
6746
|
+
// A row per TAXON, not per sequence: a tip carrying none gets a
|
|
6747
|
+
// row of the missing symbol, so the matrix covers every taxon
|
|
6748
|
+
// the Taxa block declares. parseNexus reads such a row back as
|
|
6749
|
+
// absence of data, never as a sequence of question marks.
|
|
6750
|
+
labels.forEach(function (label, i) {
|
|
6751
|
+
s += ' ' + label + ' '.repeat(width - label.length) + ' '
|
|
6752
|
+
+ (byTip[i] === undefined ? missing : byTip[i]) + '\n';
|
|
6753
|
+
});
|
|
6754
|
+
s += ' ;\n';
|
|
6755
|
+
s += 'End;\n';
|
|
6218
6756
|
}
|
|
6219
|
-
});
|
|
6220
|
-
if (rows.length > 0) {
|
|
6221
|
-
if (!datatype) {
|
|
6222
|
-
// no declared type (e.g. the tree came from Newick plus a
|
|
6223
|
-
// fasta): judge on the residues themselves
|
|
6224
|
-
datatype = forester.msaIsNucleotide(rows[0].value) ? 'dna' : 'protein';
|
|
6225
|
-
}
|
|
6226
|
-
let width = 0;
|
|
6227
|
-
rows.forEach(function (r) {
|
|
6228
|
-
width = Math.max(width, r.label.length);
|
|
6229
|
-
});
|
|
6230
|
-
s += 'Begin Characters;\n';
|
|
6231
|
-
// NChar ONLY: the Nexus standard allows NTax in a CHARACTERS
|
|
6232
|
-
// block's DIMENSIONS solely alongside NEWTAXA (the taxa are the
|
|
6233
|
-
// TAXA block's), and strict readers -- jebl, and so AliView --
|
|
6234
|
-
// reject the file over it
|
|
6235
|
-
s += ' Dimensions NChar=' + nchar + ';\n';
|
|
6236
|
-
s += ' Format DataType=' + datatype + ' Missing=? Gap=-;\n';
|
|
6237
|
-
s += ' Matrix\n';
|
|
6238
|
-
rows.forEach(function (r) {
|
|
6239
|
-
s += ' ' + r.label + ' '.repeat(width - r.label.length + 1) + r.value + '\n';
|
|
6240
|
-
});
|
|
6241
|
-
s += ' ;\n';
|
|
6242
|
-
s += 'End;\n';
|
|
6243
6757
|
}
|
|
6244
6758
|
|
|
6245
6759
|
s += 'Begin Trees;\n';
|
|
@@ -6249,9 +6763,6 @@
|
|
|
6249
6763
|
s += ' Tree ' + (treeName ? sanitizeLabelForNH(treeName) : 'tree1') + '=';
|
|
6250
6764
|
s += (phy.rooted === false) ? '[&U]' : '[&R]';
|
|
6251
6765
|
let nh = forester.toNewHampshire(phy, decPointsMax, true, writeConfidences);
|
|
6252
|
-
renamed.forEach(function (node) {
|
|
6253
|
-
delete node.name;
|
|
6254
|
-
});
|
|
6255
6766
|
if (nh.length === 0) {
|
|
6256
6767
|
// an empty tree would otherwise write "Tree tree1=[&R]" with no
|
|
6257
6768
|
// tree and no terminating ';' -- a syntactically invalid file
|