seqeyes-python 0.2.7__tar.gz → 0.2.9__tar.gz
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.
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/PKG-INFO +1 -1
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/pyproject.toml +1 -1
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/src/seqeyes/__init__.py +1 -1
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/src/seqeyes/resources/pulseq-bundle.js +148 -25
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/.gitignore +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/README.md +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/examples/demo.ipynb +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/examples/demo.py +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/src/seqeyes/_plot.py +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/src/seqeyes/_renderer.py +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/src/seqeyes/resources/viewer.html +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/conftest.py +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/debug_full.html +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/debug_minimal.html +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/debug_viewer.html +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/debug_viewer.py +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/integration_test.py +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/test_plot.py +0 -0
- {seqeyes_python-0.2.7 → seqeyes_python-0.2.9}/tests/test_renderer.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: seqeyes-python
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.9
|
|
4
4
|
Summary: Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq.
|
|
5
5
|
Project-URL: Homepage, https://github.com/bughht/seqeyes_plugin
|
|
6
6
|
Project-URL: Repository, https://github.com/bughht/seqeyes_plugin
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "seqeyes-python"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.9"
|
|
8
8
|
description = "Interactive Pulseq MRI sequence viewer for Jupyter — lightweight, beautiful, replaces seq.plot() in pypulseq."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -53,7 +53,7 @@ var Pulseq = (() => {
|
|
|
53
53
|
});
|
|
54
54
|
|
|
55
55
|
// package.json
|
|
56
|
-
var version = "0.2.
|
|
56
|
+
var version = "0.2.9";
|
|
57
57
|
|
|
58
58
|
// src/pulseq/decompressor.ts
|
|
59
59
|
function decompressShape(compressed, numSamples) {
|
|
@@ -1782,11 +1782,16 @@ var Pulseq = (() => {
|
|
|
1782
1782
|
}
|
|
1783
1783
|
|
|
1784
1784
|
// src/pulseq/kspace.ts
|
|
1785
|
+
var TRAJECTORY_TIME_ACCURACY_SEC = 1e-10;
|
|
1786
|
+
var GRADIENT_ENDPOINT_TOLERANCE_SEC = 1e-12;
|
|
1787
|
+
function canonicalTrajectoryTime(timeSec) {
|
|
1788
|
+
return TRAJECTORY_TIME_ACCURACY_SEC * Math.round(timeSec / TRAJECTORY_TIME_ACCURACY_SEC);
|
|
1789
|
+
}
|
|
1785
1790
|
function calculateKspace(blocks, gradientRaster, totalDuration, trajectoryDelay = 0, _options) {
|
|
1786
1791
|
if (!blocks.length || !gradientRaster || gradientRaster <= 0) return null;
|
|
1787
1792
|
const GR = gradientRaster;
|
|
1788
1793
|
const RF = _options?.rfRaster && _options.rfRaster > 0 ? _options.rfRaster : 1e-6;
|
|
1789
|
-
const tacc =
|
|
1794
|
+
const tacc = TRAJECTORY_TIME_ACCURACY_SEC;
|
|
1790
1795
|
const gradientSupport = _options?.gradientSupport ?? "endpoints";
|
|
1791
1796
|
const excT = [], refT = [];
|
|
1792
1797
|
const gradTimes = [];
|
|
@@ -1854,7 +1859,7 @@ var Pulseq = (() => {
|
|
|
1854
1859
|
let cum = 0;
|
|
1855
1860
|
for (const b of blocks) {
|
|
1856
1861
|
cum += b.duration;
|
|
1857
|
-
edges.push(cum);
|
|
1862
|
+
edges.push(canonicalTrajectoryTime(cum));
|
|
1858
1863
|
}
|
|
1859
1864
|
for (let i = 0; i < N; i++) {
|
|
1860
1865
|
const t = grid[i];
|
|
@@ -1955,7 +1960,10 @@ var Pulseq = (() => {
|
|
|
1955
1960
|
if (!g || g.type === "none") return 0;
|
|
1956
1961
|
const tp = g.timePoints, wf = g.waveform;
|
|
1957
1962
|
if (!tp || tp.length < 2) return 0;
|
|
1958
|
-
|
|
1963
|
+
const first = tp[0], last = tp[tp.length - 1];
|
|
1964
|
+
if (t < first - GRADIENT_ENDPOINT_TOLERANCE_SEC || t > last + GRADIENT_ENDPOINT_TOLERANCE_SEC) return 0;
|
|
1965
|
+
if (t <= first + GRADIENT_ENDPOINT_TOLERANCE_SEC) return wf[0];
|
|
1966
|
+
if (t >= last - GRADIENT_ENDPOINT_TOLERANCE_SEC) return wf[wf.length - 1];
|
|
1959
1967
|
let lo = 0, hi = tp.length - 1;
|
|
1960
1968
|
while (hi - lo > 1) {
|
|
1961
1969
|
const m = lo + hi >> 1;
|
|
@@ -2044,11 +2052,34 @@ var Pulseq = (() => {
|
|
|
2044
2052
|
}
|
|
2045
2053
|
add(tSec, value) {
|
|
2046
2054
|
if (!Number.isFinite(tSec) || !Number.isFinite(value)) return;
|
|
2055
|
+
this.addToBucket(this.bucketIndex(tSec), tSec, value);
|
|
2056
|
+
}
|
|
2057
|
+
/** Fill a known constant interval without visiting every source-raster sample. */
|
|
2058
|
+
addConstantRange(startSec, endSec, value) {
|
|
2059
|
+
if (!Number.isFinite(startSec) || !Number.isFinite(endSec) || !Number.isFinite(value)) return;
|
|
2060
|
+
const start = Math.max(this.startSec, Math.min(startSec, endSec));
|
|
2061
|
+
const end = Math.min(this.endSec, Math.max(startSec, endSec));
|
|
2062
|
+
if (end < start) return;
|
|
2063
|
+
const firstIndex = this.bucketIndex(start);
|
|
2064
|
+
const lastIndex = this.bucketIndex(end);
|
|
2065
|
+
for (let index = firstIndex; index <= lastIndex; index++) {
|
|
2066
|
+
const bucketStart = this.span > 0 ? this.startSec + this.span * index / this.bucketCount : start;
|
|
2067
|
+
const bucketEnd = this.span > 0 ? this.startSec + this.span * (index + 1) / this.bucketCount : end;
|
|
2068
|
+
const left = Math.max(start, bucketStart);
|
|
2069
|
+
const right = Math.min(end, bucketEnd);
|
|
2070
|
+
if (right < left) continue;
|
|
2071
|
+
this.addToBucket(index, left, value);
|
|
2072
|
+
this.addToBucket(index, right, value);
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
bucketIndex(tSec) {
|
|
2047
2076
|
const normalized = this.span > 0 ? (tSec - this.startSec) / this.span : 0;
|
|
2048
|
-
|
|
2077
|
+
return Math.max(0, Math.min(
|
|
2049
2078
|
this.bucketCount - 1,
|
|
2050
2079
|
Math.floor(normalized * this.bucketCount)
|
|
2051
2080
|
));
|
|
2081
|
+
}
|
|
2082
|
+
addToBucket(index, tSec, value) {
|
|
2052
2083
|
const bucket = this.buckets[index];
|
|
2053
2084
|
if (!bucket) {
|
|
2054
2085
|
this.buckets[index] = {
|
|
@@ -2198,19 +2229,45 @@ var Pulseq = (() => {
|
|
|
2198
2229
|
if (x.t.length !== y.t.length || x.t.length !== z.t.length) {
|
|
2199
2230
|
warnings.push(`Internal warning: per-axis M1 output sizes disagree (${x.t.length}, ${y.t.length}, ${z.t.length}). Plot may be inconsistent.`);
|
|
2200
2231
|
}
|
|
2232
|
+
const output = referenceMode === "rfCenter" ? compactRfCenteredSamples(x.t, x.m1, y.m1, z.m1) : { t: x.t, x: x.m1, y: y.m1, z: z.m1 };
|
|
2201
2233
|
return {
|
|
2202
2234
|
valid: true,
|
|
2203
2235
|
ok: true,
|
|
2204
2236
|
referenceMode,
|
|
2205
|
-
tSec: new Float64Array(
|
|
2206
|
-
m1x: new Float64Array(x
|
|
2207
|
-
m1y: new Float64Array(y
|
|
2208
|
-
m1z: new Float64Array(z
|
|
2237
|
+
tSec: new Float64Array(output.t),
|
|
2238
|
+
m1x: new Float64Array(output.x),
|
|
2239
|
+
m1y: new Float64Array(output.y),
|
|
2240
|
+
m1z: new Float64Array(output.z),
|
|
2209
2241
|
warnings,
|
|
2210
2242
|
excitationTimesSec: new Float64Array(excitationTimes),
|
|
2211
2243
|
refocusingTimesSec: new Float64Array(refocusingTimes)
|
|
2212
2244
|
};
|
|
2213
2245
|
}
|
|
2246
|
+
function compactRfCenteredSamples(time, x, y, z) {
|
|
2247
|
+
const count = Math.min(time.length, x.length, y.length, z.length);
|
|
2248
|
+
if (count <= 2) return {
|
|
2249
|
+
t: time.slice(0, count),
|
|
2250
|
+
x: x.slice(0, count),
|
|
2251
|
+
y: y.slice(0, count),
|
|
2252
|
+
z: z.slice(0, count)
|
|
2253
|
+
};
|
|
2254
|
+
const out = { t: [time[0]], x: [x[0]], y: [y[0]], z: [z[0]] };
|
|
2255
|
+
const sameVector = (left, right) => x[left] === x[right] && y[left] === y[right] && z[left] === z[right];
|
|
2256
|
+
for (let index = 1; index < count - 1; index++) {
|
|
2257
|
+
const duplicateTime = time[index] <= time[index - 1] + TIME_EPS2 || time[index + 1] <= time[index] + TIME_EPS2;
|
|
2258
|
+
const insidePlateau = sameVector(index - 1, index) && sameVector(index, index + 1);
|
|
2259
|
+
if (!duplicateTime && insidePlateau) continue;
|
|
2260
|
+
out.t.push(time[index]);
|
|
2261
|
+
out.x.push(x[index]);
|
|
2262
|
+
out.y.push(y[index]);
|
|
2263
|
+
out.z.push(z[index]);
|
|
2264
|
+
}
|
|
2265
|
+
out.t.push(time[count - 1]);
|
|
2266
|
+
out.x.push(x[count - 1]);
|
|
2267
|
+
out.y.push(y[count - 1]);
|
|
2268
|
+
out.z.push(z[count - 1]);
|
|
2269
|
+
return out;
|
|
2270
|
+
}
|
|
2214
2271
|
function appendM1AdvisoryWarnings(rfEvents, tMin, warnings) {
|
|
2215
2272
|
let recentExcCount = 0;
|
|
2216
2273
|
let lastExcT = -1e9;
|
|
@@ -2280,21 +2337,21 @@ var Pulseq = (() => {
|
|
|
2280
2337
|
createDecodedGradientSampler(blocks, "gy"),
|
|
2281
2338
|
createDecodedGradientSampler(blocks, "gz")
|
|
2282
2339
|
];
|
|
2283
|
-
const
|
|
2284
|
-
const
|
|
2340
|
+
const effectiveM0 = [0, 0, 0];
|
|
2341
|
+
const effectiveM1 = [0, 0, 0];
|
|
2285
2342
|
let sign = 1;
|
|
2286
2343
|
let tReset = excitationTimes.length ? excitationTimes[0] : range.first;
|
|
2287
2344
|
if (range.first < tReset) tReset = range.first;
|
|
2288
2345
|
let currentT = tReset;
|
|
2289
|
-
const reported = (axis, tSec) => referenceMode === "observationTime" ?
|
|
2346
|
+
const reported = (axis, tSec) => referenceMode === "observationTime" ? effectiveM1[axis] - (tSec - tReset) * effectiveM0[axis] : effectiveM1[axis];
|
|
2290
2347
|
const advanceTo = (targetT) => {
|
|
2291
2348
|
if (!(targetT > currentT + TIME_EPS2)) return;
|
|
2292
2349
|
for (let axis = 0; axis < 3; axis++) {
|
|
2293
2350
|
const ga = samplers[axis](currentT);
|
|
2294
2351
|
const gb = samplers[axis](targetT);
|
|
2295
2352
|
const integrated = integrateLinearSegment(currentT, targetT, tReset, ga, gb);
|
|
2296
|
-
|
|
2297
|
-
|
|
2353
|
+
effectiveM0[axis] += sign * integrated[0];
|
|
2354
|
+
effectiveM1[axis] += sign * integrated[1];
|
|
2298
2355
|
}
|
|
2299
2356
|
currentT = targetT;
|
|
2300
2357
|
};
|
|
@@ -2305,19 +2362,45 @@ var Pulseq = (() => {
|
|
|
2305
2362
|
const regularLast = range.first + Math.max(0, regularCount - 1) * gradientRaster;
|
|
2306
2363
|
const hasFinalSample = regularLast < range.last - TIME_EPS2;
|
|
2307
2364
|
const totalSamples = regularCount + (hasFinalSample ? 1 : 0);
|
|
2365
|
+
const sampleTimeAt = (index) => index < regularCount ? range.first + index * gradientRaster : range.last;
|
|
2366
|
+
const gradientFreeIntervals = referenceMode === "rfCenter" ? collectGradientFreeIntervals(blocks) : [];
|
|
2367
|
+
let gradientFreeIndex = 0;
|
|
2308
2368
|
let eventIndex = 0;
|
|
2309
2369
|
let sampleIndex = 0;
|
|
2310
2370
|
while (eventIndex < events.length || sampleIndex < totalSamples) {
|
|
2311
2371
|
const eventTime = eventIndex < events.length ? events[eventIndex].tSec : Number.POSITIVE_INFINITY;
|
|
2312
|
-
const sampleTime = sampleIndex < totalSamples ? sampleIndex
|
|
2372
|
+
const sampleTime = sampleIndex < totalSamples ? sampleTimeAt(sampleIndex) : Number.POSITIVE_INFINITY;
|
|
2373
|
+
while (gradientFreeIndex < gradientFreeIntervals.length && gradientFreeIntervals[gradientFreeIndex].end <= currentT + TIME_EPS2) {
|
|
2374
|
+
gradientFreeIndex++;
|
|
2375
|
+
}
|
|
2376
|
+
const gap = gradientFreeIntervals[gradientFreeIndex];
|
|
2377
|
+
const ordinaryTarget = Math.min(eventTime, sampleTime);
|
|
2378
|
+
if (gap && currentT < gap.start - TIME_EPS2 && gap.start < ordinaryTarget - TIME_EPS2) {
|
|
2379
|
+
advanceTo(gap.start);
|
|
2380
|
+
addReported(gap.start);
|
|
2381
|
+
continue;
|
|
2382
|
+
}
|
|
2383
|
+
if (gap && currentT >= gap.start - TIME_EPS2 && currentT < gap.end - TIME_EPS2) {
|
|
2384
|
+
const jumpTarget = Math.min(gap.end, eventTime, endSec);
|
|
2385
|
+
if (jumpTarget > currentT + TIME_EPS2) {
|
|
2386
|
+
for (let axis = 0; axis < 3; axis++) {
|
|
2387
|
+
builders[axis].addConstantRange(currentT, jumpTarget, effectiveM1[axis]);
|
|
2388
|
+
}
|
|
2389
|
+
currentT = jumpTarget;
|
|
2390
|
+
while (sampleIndex < totalSamples && sampleTimeAt(sampleIndex) <= currentT + TIME_EPS2) {
|
|
2391
|
+
sampleIndex++;
|
|
2392
|
+
}
|
|
2393
|
+
continue;
|
|
2394
|
+
}
|
|
2395
|
+
}
|
|
2313
2396
|
if (eventTime <= sampleTime) {
|
|
2314
2397
|
advanceTo(eventTime);
|
|
2315
2398
|
if (events[eventIndex].kind === "reset") {
|
|
2316
2399
|
sign = 1;
|
|
2317
2400
|
tReset = eventTime;
|
|
2318
2401
|
currentT = eventTime;
|
|
2319
|
-
|
|
2320
|
-
|
|
2402
|
+
effectiveM0.fill(0);
|
|
2403
|
+
effectiveM1.fill(0);
|
|
2321
2404
|
for (const builder of builders) builder.add(eventTime, 0);
|
|
2322
2405
|
} else {
|
|
2323
2406
|
addReported(eventTime);
|
|
@@ -2378,6 +2461,46 @@ var Pulseq = (() => {
|
|
|
2378
2461
|
}
|
|
2379
2462
|
return series;
|
|
2380
2463
|
}
|
|
2464
|
+
function collectGradientFreeIntervals(blocks) {
|
|
2465
|
+
const gaps = [];
|
|
2466
|
+
const appendGap = (start, end) => {
|
|
2467
|
+
if (!(end > start + TIME_EPS2)) return;
|
|
2468
|
+
const previous = gaps[gaps.length - 1];
|
|
2469
|
+
if (previous && start <= previous.end + TIME_EPS2) {
|
|
2470
|
+
previous.end = Math.max(previous.end, end);
|
|
2471
|
+
} else {
|
|
2472
|
+
gaps.push({ start, end });
|
|
2473
|
+
}
|
|
2474
|
+
};
|
|
2475
|
+
for (const block of blocks) {
|
|
2476
|
+
const blockStart = block.startTime;
|
|
2477
|
+
const blockEnd = block.startTime + block.duration;
|
|
2478
|
+
if (!(blockEnd > blockStart + TIME_EPS2)) continue;
|
|
2479
|
+
const active = [];
|
|
2480
|
+
for (const gradient of [block.gx, block.gy, block.gz]) {
|
|
2481
|
+
if (!gradient?.timePoints.length || !gradient.waveform.length || gradient.type === "none") continue;
|
|
2482
|
+
let nonzero = false;
|
|
2483
|
+
for (const value of gradient.waveform) {
|
|
2484
|
+
if (value !== 0) {
|
|
2485
|
+
nonzero = true;
|
|
2486
|
+
break;
|
|
2487
|
+
}
|
|
2488
|
+
}
|
|
2489
|
+
if (!nonzero) continue;
|
|
2490
|
+
const first = Math.max(blockStart, gradient.timePoints[0]);
|
|
2491
|
+
const last = Math.min(blockEnd, gradient.timePoints[gradient.timePoints.length - 1]);
|
|
2492
|
+
if (last > first + TIME_EPS2) active.push({ start: first, end: last });
|
|
2493
|
+
}
|
|
2494
|
+
active.sort((left, right) => left.start - right.start);
|
|
2495
|
+
let cursor = blockStart;
|
|
2496
|
+
for (const interval of active) {
|
|
2497
|
+
if (interval.start > cursor + TIME_EPS2) appendGap(cursor, interval.start);
|
|
2498
|
+
cursor = Math.max(cursor, interval.end);
|
|
2499
|
+
}
|
|
2500
|
+
if (cursor < blockEnd - TIME_EPS2) appendGap(cursor, blockEnd);
|
|
2501
|
+
}
|
|
2502
|
+
return gaps;
|
|
2503
|
+
}
|
|
2381
2504
|
function appendGradientPoint(series, t, value) {
|
|
2382
2505
|
if (!Number.isFinite(t) || !Number.isFinite(value)) return;
|
|
2383
2506
|
const last = series.time.length - 1;
|
|
@@ -2441,8 +2564,8 @@ var Pulseq = (() => {
|
|
|
2441
2564
|
let tReset = excitationTimes.length ? excitationTimes[0] : tMin;
|
|
2442
2565
|
if (samples.length && samples[0] < tReset) tReset = samples[0];
|
|
2443
2566
|
let currentT = tReset;
|
|
2444
|
-
let
|
|
2445
|
-
let
|
|
2567
|
+
let effectiveM0 = 0;
|
|
2568
|
+
let effectiveM1 = 0;
|
|
2446
2569
|
let gradientIndex = -1;
|
|
2447
2570
|
const seekGradient = (t) => {
|
|
2448
2571
|
while (gradientIndex + 1 < gradient.time.length && gradient.time[gradientIndex + 1] <= t + TIME_EPS2) {
|
|
@@ -2464,8 +2587,8 @@ var Pulseq = (() => {
|
|
|
2464
2587
|
return gradient.value[gradientIndex] + alpha * (gradient.value[gradientIndex + 1] - gradient.value[gradientIndex]);
|
|
2465
2588
|
};
|
|
2466
2589
|
const reportedM1At = (t) => {
|
|
2467
|
-
if (referenceMode === "observationTime") return
|
|
2468
|
-
return
|
|
2590
|
+
if (referenceMode === "observationTime") return effectiveM1 - (t - tReset) * effectiveM0;
|
|
2591
|
+
return effectiveM1;
|
|
2469
2592
|
};
|
|
2470
2593
|
const advanceTo = (targetT) => {
|
|
2471
2594
|
if (!(targetT > currentT + TIME_EPS2)) return;
|
|
@@ -2476,8 +2599,8 @@ var Pulseq = (() => {
|
|
|
2476
2599
|
const ga = sampleGradient(currentT);
|
|
2477
2600
|
const gb = sampleGradient(nextT);
|
|
2478
2601
|
const [m0Seg, m1Seg] = integrateLinearSegment(currentT, nextT, tReset, ga, gb);
|
|
2479
|
-
|
|
2480
|
-
|
|
2602
|
+
effectiveM0 += sign * m0Seg;
|
|
2603
|
+
effectiveM1 += sign * m1Seg;
|
|
2481
2604
|
currentT = nextT;
|
|
2482
2605
|
}
|
|
2483
2606
|
};
|
|
@@ -2499,8 +2622,8 @@ var Pulseq = (() => {
|
|
|
2499
2622
|
sign = 1;
|
|
2500
2623
|
tReset = nextEvtT;
|
|
2501
2624
|
currentT = nextEvtT;
|
|
2502
|
-
|
|
2503
|
-
|
|
2625
|
+
effectiveM0 = 0;
|
|
2626
|
+
effectiveM1 = 0;
|
|
2504
2627
|
} else {
|
|
2505
2628
|
outT.push(nextEvtT);
|
|
2506
2629
|
outM1.push(reportedM1At(nextEvtT));
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|