linked-rolls 0.62.0 → 0.63.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 CHANGED
@@ -22,12 +22,15 @@ single undo step.
22
22
 
23
23
  ```ts
24
24
  import { produce } from 'immer'
25
- import { asJsonLd, connectVersions, createVersion, EditionView, importJsonLd, readFromStanfordAton } from 'linked-rolls'
25
+ import { alignCopy, asJsonLd, connectVersions, createVersion, EditionView, importJsonLd, readFromStanfordAton } from 'linked-rolls'
26
26
 
27
27
  let edition = importJsonLd(json)
28
28
 
29
- // Add a copy read from a SUPRA analysis file, with a version of its own
30
- edition = produce(edition, createVersion(readFromStanfordAton(aton)))
29
+ // Add a copy read from a SUPRA analysis file, with a version of its own,
30
+ // and align it with the reference copy, whose millimetres are the axis
31
+ const copy = readFromStanfordAton(aton)
32
+ edition = produce(edition, createVersion(copy))
33
+ edition = produce(edition, alignCopy(copy.id))
31
34
 
32
35
  // Collate that version against another one
33
36
  const view = new EditionView(edition)
@@ -36,6 +39,14 @@ edition = produce(edition, connectVersions(view, childId, parentId))
36
39
  const document = asJsonLd(edition)
37
40
  ```
38
41
 
42
+ Within the library every place along the roll is a place on the edition's
43
+ axis, the millimetres of its reference copy. A document holds each copy's
44
+ features at the copy's own places, as they were read, together with the
45
+ alignment that carries them onto the axis; the import and the export map
46
+ between the two. What the alignments say about the paper of the copies,
47
+ how far each has stretched and how the papers of the systems relate, is
48
+ worked out by `paperOf`.
49
+
39
50
  Documents written by earlier releases are brought up to date on import.
40
51
  To check a document against the schema:
41
52
 
@@ -1,4 +1,5 @@
1
- import { speedScalesIn } from "./ownPaper.js";
1
+ import { paperOfVersion } from "./ownPaper.js";
2
+ import { alignmentProblems } from "./paper.js";
2
3
  import { idOf } from "../model/Assumption.js";
3
4
  import { isCommand, pairsAmong, placementsOf } from "../model/Symbol.js";
4
5
  import { keyOf } from "../systems/TrackerBar.js";
@@ -91,17 +92,22 @@ const carriersOffTheirMeaning = (view, version, commands) => {
91
92
  .map(symbol => ({ version, symbol: symbol.id, problem: 'carrier-on-another-track' }));
92
93
  };
93
94
  /**
94
- * Where the copies of the version's own system disagree about the scale
95
- * that put them on the edition's shared axis.
95
+ * Where a copy of the version's own system strays from the others in
96
+ * its paper by more than paper does.
96
97
  *
97
- * That scale is what takes a place back to the paper the version's roll
98
- * ran on, so a performance needs one number. Copies disagreeing about it
99
- * is evidence about the copies, and averaging it away would hide both
100
- * the disagreement and the fact that the playback rests on a guess.
98
+ * The paper of the system is what takes a place back to the paper the
99
+ * version's roll ran on, so a performance needs one figure. A copy far
100
+ * off it is evidence about the copies, perhaps one cut for another
101
+ * speed, and averaging it in would hide both that and the fact that the
102
+ * playback rests on a guess.
101
103
  */
102
- const paperDisagreed = (view, version) => speedScalesIn(view, version).length > 1
103
- ? [{ version: version.id, symbol: version.id, problem: 'copies-disagree-on-the-paper' }]
104
- : [];
104
+ const paperDisagreed = (view, version) => {
105
+ const own = new Set(paperOfVersion(view, version)?.copies ?? []);
106
+ return alignmentProblems(view.edition)
107
+ .some(({ copy, problem }) => problem === 'paper-beyond-its-spread' && own.has(copy))
108
+ ? [{ version: version.id, symbol: version.id, problem: 'copies-disagree-on-the-paper' }]
109
+ : [];
110
+ };
105
111
  /**
106
112
  * A version's strikes that take nothing out of its text: each deleted
107
113
  * symbol has to be one the parent hands down, or one the version put
@@ -1,10 +1,8 @@
1
1
  import type { EditionView } from "../view/EditionView.js";
2
2
  import type { Version } from "../model/Version.js";
3
- import { RollCopy } from "../model/RollCopy.js";
4
- /** The copies of the version's own system that carry any of its symbols. */
5
- export declare const copiesOwning: (view: EditionView, version: Readonly<Version>) => Readonly<RollCopy>[];
6
- /** The scales of the version's own copies that are not their own paper stretch. */
7
- export declare const speedScalesIn: (view: EditionView, version: Readonly<Version>) => number[];
3
+ import { PaperOfSystem } from "./paper.js";
4
+ /** What the alignments say about the paper of the version's own system, where they say anything. */
5
+ export declare const paperOfVersion: (view: EditionView, version: Readonly<Version>) => PaperOfSystem | undefined;
8
6
  /**
9
7
  * How a place on the edition's shared axis relates to the paper of
10
8
  * this version: place × factor = millimetres of its own paper.
@@ -12,15 +10,12 @@ export declare const speedScalesIn: (view: EditionView, version: Readonly<Versio
12
10
  * Copies cut for different systems are scaled onto one axis so that
13
11
  * they can be collated at all, which leaves a version of another
14
12
  * system carrying places in the axis copy's millimetres. A
15
- * performance needs the paper the roll actually ran on, and the
16
- * factor is the inverse of the scale `alignCopy` recorded.
13
+ * performance needs the paper the roll actually ran on, unstretched,
14
+ * and that is what the alignments of all the copies give together
15
+ * (`paperOf`): the ratio of the version's system to the reference
16
+ * copy's, over how far the reference copy's paper has stretched.
17
17
  *
18
- * It is read only from the copies of the version's own system, since
19
- * under the shared axis a green version's notes are carried by red
20
- * copies too and those say nothing about green paper. A copy whose
21
- * scale is put down to its own paper having stretched is left out as
22
- * well: that is a fact about the one exemplar, not about the speed
23
- * the system's rolls were cut at. Where what remains disagrees,
24
- * `constraintProblems` reports it rather than averaging it away.
18
+ * Nothing where no copy of the version's system measures its paper,
19
+ * which leaves the performance on the axis.
25
20
  */
26
21
  export declare const toOwnPaperOf: (view: EditionView, version: Readonly<Version>) => number | undefined;
@@ -1,22 +1,10 @@
1
- import { idsOf } from "../model/Assumption.js";
2
- import { isPaperStretch } from "../model/RollCopy.js";
3
- import { systemIdOf } from "../systems/TrackerBar.js";
4
- /** The copies of the version's own system that carry any of its symbols. */
5
- export const copiesOwning = (view, version) => {
6
- const system = systemIdOf(version.system);
7
- const carrying = new Set(view.snapshot(version.id)
8
- .flatMap(symbol => idsOf(symbol.carriers))
9
- .flatMap(id => {
10
- const copy = view.copyOf(id);
11
- return copy ? [copy.id] : [];
12
- }));
13
- return view.edition.copies.filter(copy => carrying.has(copy.id) && systemIdOf(copy.production?.system) === system);
1
+ import { defaultTrackerBar, trackerBarOf } from "../systems/index.js";
2
+ import { paperOf } from "./paper.js";
3
+ /** What the alignments say about the paper of the version's own system, where they say anything. */
4
+ export const paperOfVersion = (view, version) => {
5
+ const system = (trackerBarOf(version.system) ?? defaultTrackerBar).id;
6
+ return paperOf(view.edition)?.systems.find(paper => paper.system === system);
14
7
  };
15
- /** The scales of the version's own copies that are not their own paper stretch. */
16
- export const speedScalesIn = (view, version) => [...new Set(copiesOwning(view, version)
17
- .filter(copy => !copy.conditions.some(isPaperStretch))
18
- .map(copy => copy.measurements.scale)
19
- .filter((scale) => scale !== undefined && scale > 0))];
20
8
  /**
21
9
  * How a place on the edition's shared axis relates to the paper of
22
10
  * this version: place × factor = millimetres of its own paper.
@@ -24,18 +12,12 @@ export const speedScalesIn = (view, version) => [...new Set(copiesOwning(view, v
24
12
  * Copies cut for different systems are scaled onto one axis so that
25
13
  * they can be collated at all, which leaves a version of another
26
14
  * system carrying places in the axis copy's millimetres. A
27
- * performance needs the paper the roll actually ran on, and the
28
- * factor is the inverse of the scale `alignCopy` recorded.
15
+ * performance needs the paper the roll actually ran on, unstretched,
16
+ * and that is what the alignments of all the copies give together
17
+ * (`paperOf`): the ratio of the version's system to the reference
18
+ * copy's, over how far the reference copy's paper has stretched.
29
19
  *
30
- * It is read only from the copies of the version's own system, since
31
- * under the shared axis a green version's notes are carried by red
32
- * copies too and those say nothing about green paper. A copy whose
33
- * scale is put down to its own paper having stretched is left out as
34
- * well: that is a fact about the one exemplar, not about the speed
35
- * the system's rolls were cut at. Where what remains disagrees,
36
- * `constraintProblems` reports it rather than averaging it away.
20
+ * Nothing where no copy of the version's system measures its paper,
21
+ * which leaves the performance on the axis.
37
22
  */
38
- export const toOwnPaperOf = (view, version) => {
39
- const scales = speedScalesIn(view, version);
40
- return scales.length === 1 ? 1 / scales[0] : undefined;
41
- };
23
+ export const toOwnPaperOf = (view, version) => paperOfVersion(view, version)?.toOwnPaper;
@@ -0,0 +1,129 @@
1
+ import { Edition } from "../model/Edition.js";
2
+ import { Strain } from "../model/RollCopy.js";
3
+ import { Percent } from "../model/Quantity.js";
4
+ /**
5
+ * How far the paper of one copy is taken to have stretched or shrunk
6
+ * along the roll, as a standard deviation, where the edition has too few
7
+ * copies of one system to say: the three red copies of Welte 225 spread
8
+ * by 0.19 %. It includes the scanner's error along the roll, which the
9
+ * alignment cannot tell from the paper's.
10
+ */
11
+ export declare const PAPER_SPREAD: Percent;
12
+ /**
13
+ * How far a paper speed a label or the literature states is taken to be
14
+ * from the speed a copy was cut at: the literature gives the red roll's
15
+ * three metres a minute as 2.9 as well (Bärtsch 2020), which is 3.4 %.
16
+ */
17
+ export declare const SPEED_TOLERANCE = 0.035;
18
+ /** What the alignments say about one copy's paper. */
19
+ export interface PaperOfCopy {
20
+ copy: string;
21
+ /** The system the copy was cut for, by the id of its bar. */
22
+ system: string;
23
+ /** How far its paper has stretched along the roll since it was perforated, and how surely that is known. */
24
+ along: Strain;
25
+ /** Whether a strain measured on the copy went into it, rather than the spread of paper alone. */
26
+ measured: boolean;
27
+ }
28
+ /** What the alignments say about the paper the copies of one system were cut on. */
29
+ export interface PaperOfSystem {
30
+ /** The system, by the id of its bar. */
31
+ system: string;
32
+ /**
33
+ * The length of this system's paper, unstretched, for a length of the
34
+ * reference system's: one for the reference copy's own system, about
35
+ * 0.77 for a Licensee or green re-cut of a red roll. It is the ratio
36
+ * of the paper speeds the two were cut for.
37
+ */
38
+ ratio: number;
39
+ /** The standard uncertainty of the ratio. */
40
+ ratioError: number;
41
+ /**
42
+ * A place on the axis times this is millimetres of this system's
43
+ * paper, unstretched: what a performance of one of its versions runs
44
+ * on (`RollProperties.toOwnPaper`).
45
+ */
46
+ toOwnPaper: number;
47
+ /** The standard uncertainty of `toOwnPaper`. */
48
+ toOwnPaperError: number;
49
+ /** The copies of the system it rests on. */
50
+ copies: string[];
51
+ }
52
+ /** What the alignments of an edition say about the paper of its copies, and how surely. */
53
+ export interface PaperReading {
54
+ /** The copy whose millimetres are the axis. */
55
+ reference: string;
56
+ /**
57
+ * How far one copy's paper is taken to stray from its system's, and
58
+ * how many degrees of freedom the copies give it: none where it is
59
+ * `PAPER_SPREAD` for want of copies.
60
+ */
61
+ spread: {
62
+ value: Percent;
63
+ degreesOfFreedom: number;
64
+ };
65
+ copies: PaperOfCopy[];
66
+ systems: PaperOfSystem[];
67
+ }
68
+ /**
69
+ * What the alignments of the edition say about the paper of its copies:
70
+ * how far each copy's paper has stretched along the roll, and how long
71
+ * the paper of each system is against the reference copy's.
72
+ *
73
+ * An alignment's scale is one number with three causes. A copy cut for
74
+ * another paper speed is shorter or longer by the ratio of the speeds,
75
+ * which the system's copies share; its paper has stretched or shrunk as
76
+ * it alone has, and so has the reference copy's. In true strains, the
77
+ * logarithms of the ratios of the lengths, the causes add:
78
+ *
79
+ * ln scale = strain_reference − strain_copy − ln ratio_system
80
+ *
81
+ * The alignments give the left side to a few parts in a hundred thousand.
82
+ * The right side has a strain for every copy and a ratio for every
83
+ * system, so it is found by least squares with each strain taken to lie
84
+ * about zero by the spread of paper (`PAPER_SPREAD`, or what the copies
85
+ * show where there are enough of one system), or about a strain
86
+ * measured on the copy by that measurement's uncertainty. Where a system
87
+ * has one copy only its ratio takes the whole scale and the copy's strain
88
+ * stays at zero, known no better than paper varies; where the copies of
89
+ * the reference system are several, the axis comes out at their mean
90
+ * paper and not at the reference copy's.
91
+ *
92
+ * Copies whose places are not places on the paper, such as a roll
93
+ * reader's, say nothing about the paper and are left out. Nothing is
94
+ * said where the edition has no reference copy.
95
+ */
96
+ export declare const paperOf: (edition: Pick<Edition, 'copies' | 'referenceCopy'>) => PaperReading | undefined;
97
+ /** Where the alignments of the copies, or what they say about the paper, want looking into. */
98
+ export type AlignmentProblem = {
99
+ copy: string;
100
+ problem:
101
+ /** The copy has features but no alignment, and is not the reference copy. */
102
+ 'not-aligned'
103
+ /** It was aligned against a copy that is not the reference copy now. */
104
+ | 'aligned-against-another-copy'
105
+ /**
106
+ * The speed stated for it and the reference copy's stated or
107
+ * nominal one give another ratio of the papers than the
108
+ * alignments, by more than a stated speed may be off.
109
+ */
110
+ | 'speed-disagrees-with-paper'
111
+ /**
112
+ * Its paper strays from its system's by more than paper does:
113
+ * it may have been cut for another speed, or read wrongly.
114
+ */
115
+ | 'paper-beyond-its-spread';
116
+ /**
117
+ * How far: for a speed, the logarithm of the ratio the speeds give
118
+ * over the one the alignments give; for a paper, the copy's strain in
119
+ * per cent.
120
+ */
121
+ by?: number;
122
+ };
123
+ /**
124
+ * Where the alignments of the edition's copies, or what they say about
125
+ * the paper, want looking into. None of it is recomputed, so it is cheap
126
+ * to ask; whether an alignment would come out otherwise if found again
127
+ * is `alignmentFor`'s to say.
128
+ */
129
+ export declare const alignmentProblems: (edition: Pick<Edition, 'copies' | 'referenceCopy'>) => AlignmentProblem[];
@@ -0,0 +1,260 @@
1
+ import { referenceCopyOf } from "../model/Edition.js";
2
+ import { barOf, featuresOf, paperStretchOf } from "../model/RollCopy.js";
3
+ import { certaintyOf, isAsserted } from "../model/Assumption.js";
4
+ import { inMetersPerMinute, percent } from "../model/Quantity.js";
5
+ /**
6
+ * How far the paper of one copy is taken to have stretched or shrunk
7
+ * along the roll, as a standard deviation, where the edition has too few
8
+ * copies of one system to say: the three red copies of Welte 225 spread
9
+ * by 0.19 %. It includes the scanner's error along the roll, which the
10
+ * alignment cannot tell from the paper's.
11
+ */
12
+ export const PAPER_SPREAD = percent(0.2);
13
+ /**
14
+ * How far a paper speed a label or the literature states is taken to be
15
+ * from the speed a copy was cut at: the literature gives the red roll's
16
+ * three metres a minute as 2.9 as well (Bärtsch 2020), which is 3.4 %.
17
+ */
18
+ export const SPEED_TOLERANCE = 0.035;
19
+ /** The kinds of source that give places on the paper, so that the length between two of them is the paper's. */
20
+ const measuringLength = new Set(['roll', 'scan', 'analysis']);
21
+ /**
22
+ * Whether the copy's places are places on its paper, so that its length
23
+ * between two notes is a length of paper: it is the reference copy or
24
+ * aligned with it, and was read off the roll or an image of it. A copy
25
+ * that names no source counts as read so. A roll reader measures time,
26
+ * and places worked out from it are only as long as the speed they were
27
+ * worked out with.
28
+ */
29
+ const measuresPaper = (copy, reference) => (copy.id === reference.id || copy.measurements.alignment !== undefined)
30
+ && featuresOf(copy).length > 0
31
+ && (!copy.readFrom || measuringLength.has(copy.readFrom.kind));
32
+ /**
33
+ * A strain in per cent as the fit takes it: the logarithm of the ratio of
34
+ * the lengths, the true strain, which adds where the causes of a scale
35
+ * multiply. For the tenths of a per cent paper does it is the same.
36
+ */
37
+ const logarithmicOf = (value) => Math.log1p(value / 100);
38
+ /** A true strain back in per cent of the length. */
39
+ const percentOf = (logarithmic) => percent(Math.expm1(logarithmic) * 100);
40
+ /** The strain measured on the copy along the roll, where one is and the edition holds it. */
41
+ const measuredAlong = (copy) => {
42
+ const stretch = paperStretchOf(copy);
43
+ return stretch && isAsserted(certaintyOf(stretch)) ? stretch.along : undefined;
44
+ };
45
+ /** How closely an alignment's scale is known, as a share of it, at no less than a part in a million. */
46
+ const scaleShareError = (copy) => {
47
+ const alignment = copy.measurements.alignment;
48
+ if (!alignment)
49
+ return 0;
50
+ const error = alignment.scaleError ?? 0;
51
+ return Math.max(Number.isFinite(error) ? error / alignment.scale : 0, 1e-6);
52
+ };
53
+ /**
54
+ * The spread of the copies' papers about their systems', as far as
55
+ * copies of one system can show it: each copy's length against the axis,
56
+ * `-ln scale`, differs from another's of its system by their strains
57
+ * alone, the ratio of the system being common to them.
58
+ */
59
+ const spreadIn = (lengths) => {
60
+ const groups = [...lengths.values()];
61
+ const degreesOfFreedom = groups.reduce((total, group) => total + group.length - 1, 0);
62
+ if (degreesOfFreedom < 2)
63
+ return { value: logarithmicOf(PAPER_SPREAD), degreesOfFreedom: 0 };
64
+ const squares = groups.reduce((total, group) => {
65
+ const mean = group.reduce((sum, length) => sum + length, 0) / group.length;
66
+ return total + group.reduce((sum, length) => sum + (length - mean) ** 2, 0);
67
+ }, 0);
68
+ return { value: Math.sqrt(squares / degreesOfFreedom), degreesOfFreedom };
69
+ };
70
+ /** The inverse of a symmetric positive definite matrix, by Gauss and Jordan. */
71
+ const inverse = (matrix) => {
72
+ const n = matrix.length;
73
+ const work = matrix.map((row, i) => [...row, ...Array.from({ length: n }, (_, j) => (i === j ? 1 : 0))]);
74
+ for (let column = 0; column < n; column++) {
75
+ const pivot = work.reduce((best, row, i) => i >= column && Math.abs(row[column]) > Math.abs(work[best][column]) ? i : best, column);
76
+ [work[column], work[pivot]] = [work[pivot], work[column]];
77
+ const lead = work[column][column];
78
+ work[column] = work[column].map(value => value / lead);
79
+ work.forEach((row, i) => {
80
+ if (i === column)
81
+ return;
82
+ const factor = row[column];
83
+ work[i] = row.map((value, j) => value - factor * work[column][j]);
84
+ });
85
+ }
86
+ return work.map(row => row.slice(n));
87
+ };
88
+ /**
89
+ * What the alignments of the edition say about the paper of its copies:
90
+ * how far each copy's paper has stretched along the roll, and how long
91
+ * the paper of each system is against the reference copy's.
92
+ *
93
+ * An alignment's scale is one number with three causes. A copy cut for
94
+ * another paper speed is shorter or longer by the ratio of the speeds,
95
+ * which the system's copies share; its paper has stretched or shrunk as
96
+ * it alone has, and so has the reference copy's. In true strains, the
97
+ * logarithms of the ratios of the lengths, the causes add:
98
+ *
99
+ * ln scale = strain_reference − strain_copy − ln ratio_system
100
+ *
101
+ * The alignments give the left side to a few parts in a hundred thousand.
102
+ * The right side has a strain for every copy and a ratio for every
103
+ * system, so it is found by least squares with each strain taken to lie
104
+ * about zero by the spread of paper (`PAPER_SPREAD`, or what the copies
105
+ * show where there are enough of one system), or about a strain
106
+ * measured on the copy by that measurement's uncertainty. Where a system
107
+ * has one copy only its ratio takes the whole scale and the copy's strain
108
+ * stays at zero, known no better than paper varies; where the copies of
109
+ * the reference system are several, the axis comes out at their mean
110
+ * paper and not at the reference copy's.
111
+ *
112
+ * Copies whose places are not places on the paper, such as a roll
113
+ * reader's, say nothing about the paper and are left out. Nothing is
114
+ * said where the edition has no reference copy.
115
+ */
116
+ export const paperOf = (edition) => {
117
+ const reference = referenceCopyOf(edition);
118
+ if (!reference)
119
+ return undefined;
120
+ const copies = edition.copies.filter(copy => measuresPaper(copy, reference));
121
+ if (!copies.some(copy => copy.id === reference.id))
122
+ return undefined;
123
+ const referenceSystem = barOf(reference).id;
124
+ const systemOf = (copy) => barOf(copy).id;
125
+ const systems = [...new Set(copies.map(systemOf))].filter(system => system !== referenceSystem);
126
+ const lengthOf = (copy) => copy.id === reference.id ? 0 : -Math.log(copy.measurements.alignment.scale);
127
+ const lengths = Map.groupBy(copies, systemOf);
128
+ const spread = spreadIn(new Map([...lengths].map(([system, group]) => [system, group.map(lengthOf)])));
129
+ // Unknowns: the strain of each copy, then the log ratio of each system but the reference's.
130
+ const strainAt = new Map(copies.map((copy, i) => [copy.id, i]));
131
+ const ratioAt = new Map(systems.map((system, i) => [system, copies.length + i]));
132
+ const referenceAt = strainAt.get(reference.id);
133
+ const aligned = copies
134
+ .filter(copy => copy.id !== reference.id)
135
+ .map(copy => {
136
+ const ratio = ratioAt.get(systemOf(copy));
137
+ return {
138
+ coefficients: new Map([
139
+ [referenceAt, 1],
140
+ [strainAt.get(copy.id), -1],
141
+ ...(ratio !== undefined ? [[ratio, -1]] : [])
142
+ ]),
143
+ value: Math.log(copy.measurements.alignment.scale),
144
+ variance: scaleShareError(copy) ** 2
145
+ };
146
+ });
147
+ const measured = new Map(copies.flatMap(copy => {
148
+ const strain = measuredAlong(copy);
149
+ return strain ? [[copy.id, strain]] : [];
150
+ }));
151
+ const priors = copies.map(copy => {
152
+ const strain = measured.get(copy.id);
153
+ const uncertainty = strain?.uncertainty !== undefined
154
+ ? strain.uncertainty / (100 + strain.value)
155
+ : spread.value;
156
+ return {
157
+ coefficients: new Map([[strainAt.get(copy.id), 1]]),
158
+ value: strain ? logarithmicOf(strain.value) : 0,
159
+ variance: uncertainty ** 2
160
+ };
161
+ });
162
+ const size = copies.length + systems.length;
163
+ const normal = Array.from({ length: size }, () => Array.from({ length: size }, () => 0));
164
+ const right = Array.from({ length: size }, () => 0);
165
+ [...aligned, ...priors].forEach(({ coefficients, value, variance }) => {
166
+ const weight = 1 / variance;
167
+ coefficients.forEach((a, i) => {
168
+ right[i] += weight * a * value;
169
+ coefficients.forEach((b, j) => { normal[i][j] += weight * a * b; });
170
+ });
171
+ });
172
+ const covariance = inverse(normal);
173
+ const solution = covariance.map(row => row.reduce((total, value, j) => total + value * right[j], 0));
174
+ const strainReference = solution[referenceAt];
175
+ const varianceReference = covariance[referenceAt][referenceAt];
176
+ const systemReading = (system) => {
177
+ const at = ratioAt.get(system);
178
+ const logRatio = at === undefined ? 0 : solution[at];
179
+ const varianceRatio = at === undefined ? 0 : covariance[at][at];
180
+ const covarianceWithReference = at === undefined ? 0 : covariance[at][referenceAt];
181
+ const ratio = Math.exp(logRatio);
182
+ const toOwnPaper = Math.exp(logRatio - strainReference);
183
+ return {
184
+ system,
185
+ ratio,
186
+ ratioError: ratio * Math.sqrt(varianceRatio),
187
+ toOwnPaper,
188
+ toOwnPaperError: toOwnPaper * Math.sqrt(Math.max(0, varianceRatio + varianceReference - 2 * covarianceWithReference)),
189
+ copies: copies.filter(copy => systemOf(copy) === system).map(copy => copy.id)
190
+ };
191
+ };
192
+ return {
193
+ reference: reference.id,
194
+ spread: { value: percentOf(spread.value), degreesOfFreedom: spread.degreesOfFreedom },
195
+ copies: copies.map(copy => {
196
+ const at = strainAt.get(copy.id);
197
+ return {
198
+ copy: copy.id,
199
+ system: systemOf(copy),
200
+ along: {
201
+ value: percentOf(solution[at]),
202
+ uncertainty: percent(Math.exp(solution[at]) * Math.sqrt(covariance[at][at]) * 100),
203
+ unit: 'percent'
204
+ },
205
+ measured: measured.has(copy.id)
206
+ };
207
+ }),
208
+ systems: [referenceSystem, ...systems].map(systemReading)
209
+ };
210
+ };
211
+ /** A paper speed the edition holds true or likely, in metres a minute. */
212
+ const statedSpeed = (copy) => {
213
+ const speed = copy.production?.speed;
214
+ return speed && isAsserted(certaintyOf(speed)) ? inMetersPerMinute(speed) : undefined;
215
+ };
216
+ /** The speed the reference copy was cut at: as stated for it, or as its system runs. */
217
+ const referenceSpeed = (reference) => {
218
+ const system = barOf(reference).paperSpeed;
219
+ return statedSpeed(reference) ?? (system && inMetersPerMinute(system));
220
+ };
221
+ /**
222
+ * Where the alignments of the edition's copies, or what they say about
223
+ * the paper, want looking into. None of it is recomputed, so it is cheap
224
+ * to ask; whether an alignment would come out otherwise if found again
225
+ * is `alignmentFor`'s to say.
226
+ */
227
+ export const alignmentProblems = (edition) => {
228
+ const reference = referenceCopyOf(edition);
229
+ if (!reference)
230
+ return [];
231
+ const unaligned = edition.copies
232
+ .filter(copy => copy.id !== reference.id && !copy.measurements.alignment && featuresOf(copy).length > 0)
233
+ .map(copy => ({ copy: copy.id, problem: 'not-aligned' }));
234
+ const elsewhere = edition.copies
235
+ .filter(copy => {
236
+ const against = copy.measurements.alignment?.against;
237
+ return against !== undefined && against !== reference.id;
238
+ })
239
+ .map(copy => ({ copy: copy.id, problem: 'aligned-against-another-copy' }));
240
+ const reading = paperOf(edition);
241
+ if (!reading)
242
+ return [...unaligned, ...elsewhere];
243
+ const beyond = reading.copies
244
+ .filter(paper => Math.abs(paper.along.value) > 3 * PAPER_SPREAD)
245
+ .map(paper => ({ copy: paper.copy, problem: 'paper-beyond-its-spread', by: paper.along.value }));
246
+ const speedOfReference = referenceSpeed(reference);
247
+ const speeds = speedOfReference === undefined ? [] : reading.copies.flatMap(paper => {
248
+ const copy = edition.copies.find(c => c.id === paper.copy);
249
+ const speed = copy.id === reference.id ? undefined : statedSpeed(copy);
250
+ const system = reading.systems.find(s => s.system === paper.system);
251
+ if (speed === undefined)
252
+ return [];
253
+ const by = Math.log((speed / speedOfReference) / system.ratio);
254
+ const allowed = SPEED_TOLERANCE + 3 * system.ratioError / system.ratio;
255
+ return Math.abs(by) > allowed
256
+ ? [{ copy: copy.id, problem: 'speed-disagrees-with-paper', by }]
257
+ : [];
258
+ });
259
+ return [...unaligned, ...elsewhere, ...beyond, ...speeds];
260
+ };
@@ -1,14 +1,34 @@
1
1
  import { FeatureOrPatch } from "../model/Feature.js";
2
- import { RollCopy, Shift } from "../model/RollCopy.js";
2
+ import { Alignment, RollCopy } from "../model/RollCopy.js";
3
3
  import { TrackerBar } from "../systems/TrackerBar.js";
4
4
  import { Millimeters } from "../model/Quantity.js";
5
- export declare const applyShift: (shift: Shift, copy: RollCopy) => void;
6
- /** Scales the copy's features away from the beginning of the roll, and records the factor. */
7
- export declare const applyScale: (factor: number, copy: RollCopy) => void;
8
- /** Takes the shift off the copy's features again, as far as one was applied. */
9
- export declare const revertShift: (copy: RollCopy) => void;
10
- /** Takes the scale off the copy's features again, as far as one was applied. */
11
- export declare const revertScale: (copy: RollCopy) => void;
5
+ /** A place on the copy's own paper, on the edition's axis. */
6
+ export declare const toAxis: (alignment: Alignment | undefined) => (place: Millimeters) => Millimeters;
7
+ /** A place on the edition's axis, on the copy's own paper. */
8
+ export declare const fromAxis: (alignment: Alignment | undefined) => (place: Millimeters) => Millimeters;
9
+ /**
10
+ * Carries the copy's features and tears from its own places onto the
11
+ * axis, and records how, in place of any alignment it had: that one is
12
+ * taken off first.
13
+ */
14
+ export declare const applyAlignment: (alignment: Alignment, copy: RollCopy) => void;
15
+ /** Takes the copy's features and tears back to its own places, as far as it was aligned. */
16
+ export declare const revertAlignment: (copy: RollCopy) => void;
17
+ /**
18
+ * The copy with its features and tears at its own places, as a document
19
+ * holds them, and its alignment kept to say how they go onto the axis.
20
+ * The copy given is left as it is. A copy that is not aligned is the
21
+ * very same copy.
22
+ */
23
+ export declare const atOwnPlaces: (copy: RollCopy) => RollCopy;
24
+ /**
25
+ * Puts the features and tears of a copy read from a document, which
26
+ * holds them at the copy's own places, onto the axis. The copy given is
27
+ * changed.
28
+ */
29
+ export declare const putOnAxis: (copy: RollCopy) => void;
30
+ /** The copy's features at its own places, as it was read, whatever alignment it has. */
31
+ export declare const ownFeaturesOf: (copy: RollCopy) => FeatureOrPatch[];
12
32
  /**
13
33
  * Takes the extension a pneumatic reader adds off the ends of the
14
34
  * copy's chains of holes, and records how much was taken.
@@ -59,6 +79,16 @@ export declare const tooShortToShorten: (extension: Millimeters, copy: RollCopy,
59
79
  export declare const shortenChains: (extension: Millimeters, copy: RollCopy, leaving?: ReadonlySet<string>) => void;
60
80
  /** Puts the reader's extension back on the chains it was taken off, as far as one was taken off. */
61
81
  export declare const revertShortening: (copy: RollCopy) => void;
82
+ /**
83
+ * The method `alignFeatures` follows, as an alignment records it. Its
84
+ * revision changes when the method does and at no other release, so
85
+ * that an alignment found by an earlier method can be told from one
86
+ * that would be found again as it stands.
87
+ */
88
+ export declare const ALIGNMENT_METHOD: {
89
+ readonly software: 'linked-rolls alignFeatures';
90
+ readonly version: '1';
91
+ };
62
92
  /**
63
93
  * How a copy's places are carried onto another copy's:
64
94
  * `x_other = (x + shift) · scale`.
@@ -71,6 +101,14 @@ export interface AlignmentResult {
71
101
  matched: number;
72
102
  /** How far the counterparts still lie apart, as a root mean square in the other copy's millimetres. */
73
103
  residual: Millimeters;
104
+ /**
105
+ * The standard error of the scale, as for a line of least squares
106
+ * through the matches: the residual over the spread of the matched
107
+ * notes along the copy, and over the root of their number. The
108
+ * robust line is fitted otherwise, but on Welte 225 this comes
109
+ * within a few per cent of what resampling the matches gives.
110
+ */
111
+ scaleError: number;
74
112
  }
75
113
  /**
76
114
  * Finds the shift and scale that carry the places of `rollA` onto those