worldarray 0.3.0__tar.gz → 0.3.1__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.
- {worldarray-0.3.0 → worldarray-0.3.1}/CHANGELOG.md +15 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/PKG-INFO +13 -4
- {worldarray-0.3.0 → worldarray-0.3.1}/README.md +12 -3
- {worldarray-0.3.0 → worldarray-0.3.1}/docs/spec.md +7 -5
- {worldarray-0.3.0 → worldarray-0.3.1}/pyproject.toml +1 -1
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/__init__.py +1 -1
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/array.py +20 -3
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/resample.py +12 -4
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/units.py +3 -1
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/test_grid.py +1 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/test_read.py +22 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/test_write.py +1 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/.github/workflows/ci.yml +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/.github/workflows/docs.yml +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/.github/workflows/publish.yml +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/.gitignore +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/LICENSE +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/docs/changelog.md +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/docs/index.md +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/mkdocs.yml +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/context.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/geometry.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/grid.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/levels.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/placement.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/retry.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/store.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/src/worldarray/write.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/conftest.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/fixtures/metadata.json +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/fixtures/write.json +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/test_access.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/test_geometry.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/test_metadata.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/tests/test_resample.py +0 -0
- {worldarray-0.3.0 → worldarray-0.3.1}/uv.lock +0 -0
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.1 (2026-10-22)
|
|
4
|
+
|
|
5
|
+
- `source_voxel_size` reports the stored level's voxel size under `on_mismatch="relabel"`,
|
|
6
|
+
as the spec says; it reported the relabelled size (cellmap-flow's report). Reads,
|
|
7
|
+
`voxel_size`, `translation` and `corner` are unchanged.
|
|
8
|
+
- `box` of an array opened with a resample target is the extent of the target voxels
|
|
9
|
+
`shape` counts, as the spec's `[corner, corner + shape * voxel_size)` says and as
|
|
10
|
+
`Grid.from_meta(meta, voxel_size=t).box` already was; it was the stored data's extent, so
|
|
11
|
+
`read(box)` could return fewer voxels than `shape` (cellmap-flow's 7 voxels of 8 nm at
|
|
12
|
+
24 nm: box ended at 56 nm, now 72). No voxel moves.
|
|
13
|
+
- The unit table knows kilometres (`kilometer`, `kilometre`, `km`, 1e3 m), so they
|
|
14
|
+
convert, abbreviate to `km` and are written as OME's `kilometer` (chunkmirage's ask).
|
|
15
|
+
- README: the placement rule gains the corner formula, a worked example, why the centre
|
|
16
|
+
is the model, and how the read rule follows from it.
|
|
17
|
+
|
|
3
18
|
## 0.3.0 (2026-10-22)
|
|
4
19
|
|
|
5
20
|
Grids and blocks, built to spec draft 3 (Behaviours 17 to 19). Nothing on the read or
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: worldarray
|
|
3
|
-
Version: 0.3.
|
|
3
|
+
Version: 0.3.1
|
|
4
4
|
Summary: Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only.
|
|
5
5
|
Project-URL: Homepage, https://janeliascicomp.github.io/worldarray/
|
|
6
6
|
Project-URL: Documentation, https://janeliascicomp.github.io/worldarray/
|
|
@@ -165,9 +165,18 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
165
165
|
## The placement rule
|
|
166
166
|
|
|
167
167
|
A translation is where the centre of voxel 0 is. Index `i` sits at
|
|
168
|
-
`translation + i * voxel_size
|
|
169
|
-
|
|
170
|
-
|
|
168
|
+
`translation + i * voxel_size` and covers half a voxel either side, so the array's corner
|
|
169
|
+
is `corner = translation - voxel_size / 2`. With 8 nm voxels and the corner at 0, the
|
|
170
|
+
translation is 4: voxel 0 covers 0 to 8 nm around its centre at 4, voxel 1 covers 8 to
|
|
171
|
+
16 nm around 12. That is how OME-NGFF defines it and how Neuroglancer draws it.
|
|
172
|
+
|
|
173
|
+
The centre is the model because a voxel is a sample taken at a point. Everything that
|
|
174
|
+
compares or resamples grids (picking a level, reading one volume onto another's grid,
|
|
175
|
+
interpolating) works on those points, and the box a voxel covers follows from its point.
|
|
176
|
+
The read rule follows too: a voxel is read iff its centre lies in the box, so boxes that
|
|
177
|
+
adjoin never share a voxel or drop one.
|
|
178
|
+
|
|
179
|
+
Dialects that store the corner are moved half a voxel on reading:
|
|
171
180
|
|
|
172
181
|
| Stored | Means | Read as `translation` |
|
|
173
182
|
|---|---|---|
|
|
@@ -106,9 +106,18 @@ s1 = wa.open_pyramid("out.zarr", mode="r+").add_level("s1", factors=2) # place
|
|
|
106
106
|
## The placement rule
|
|
107
107
|
|
|
108
108
|
A translation is where the centre of voxel 0 is. Index `i` sits at
|
|
109
|
-
`translation + i * voxel_size
|
|
110
|
-
|
|
111
|
-
|
|
109
|
+
`translation + i * voxel_size` and covers half a voxel either side, so the array's corner
|
|
110
|
+
is `corner = translation - voxel_size / 2`. With 8 nm voxels and the corner at 0, the
|
|
111
|
+
translation is 4: voxel 0 covers 0 to 8 nm around its centre at 4, voxel 1 covers 8 to
|
|
112
|
+
16 nm around 12. That is how OME-NGFF defines it and how Neuroglancer draws it.
|
|
113
|
+
|
|
114
|
+
The centre is the model because a voxel is a sample taken at a point. Everything that
|
|
115
|
+
compares or resamples grids (picking a level, reading one volume onto another's grid,
|
|
116
|
+
interpolating) works on those points, and the box a voxel covers follows from its point.
|
|
117
|
+
The read rule follows too: a voxel is read iff its centre lies in the box, so boxes that
|
|
118
|
+
adjoin never share a voxel or drop one.
|
|
119
|
+
|
|
120
|
+
Dialects that store the corner are moved half a voxel on reading:
|
|
112
121
|
|
|
113
122
|
| Stored | Means | Read as `translation` |
|
|
114
123
|
|---|---|---|
|
|
@@ -149,7 +149,8 @@ vol.meta # ArrayMeta in the store's own units
|
|
|
149
149
|
vol.grid # ArrayMeta in the requested units, every axis
|
|
150
150
|
vol.shape, vol.dtype, vol.axes, vol.spatial, vol.level_name, vol.levels, vol.remote
|
|
151
151
|
# shape is the stored shape, or with a resample target the target voxels covering the data
|
|
152
|
-
vol.source_voxel_size, vol.resampled # the stored level's voxel size
|
|
152
|
+
vol.source_voxel_size, vol.resampled # the stored level's voxel size, also under resample and relabel; whether reads
|
|
153
|
+
# resample to a target
|
|
153
154
|
vol.list_chunks() # the chunk indices that exist in storage, by listing keys (zarr v2/v3, N5, unsharded precomputed)
|
|
154
155
|
vol.path # what the caller passed (a group, say); vol.level_path is the array actually opened
|
|
155
156
|
vol.voxel_size # tuple[float] in `units`, spatial axes, the array's order (zyx for a volume)
|
|
@@ -212,7 +213,8 @@ window and a spatial box at once); anything with `.begin` and `.end`, or `.offse
|
|
|
212
213
|
`shift`, `snap(voxel_size, translation, mode="grow" | "shrink" | "closest")` to a grid's
|
|
213
214
|
voxel corners, `to_index` (the centre rule) and `Box.from_index`; `vol.box` is the array's
|
|
214
215
|
own extent on the spatial axes, `[corner, corner + shape * voxel_size)`, what funlib calls
|
|
215
|
-
the array's roi
|
|
216
|
+
the array's roi; with a resample target, the target voxels `shape` counts, so `read(vol.box)`
|
|
217
|
+
returns `shape` and equals `Grid.from_meta(meta, voxel_size=t).box`. So `vol.read(vol.box.intersect(roi).snap(vol.voxel_size, vol.translation))`
|
|
216
218
|
is the migration of a clipped, snapped Roi read, without funlib.
|
|
217
219
|
|
|
218
220
|
There is no `offset` attribute anywhere in this API, because that word means the corner in
|
|
@@ -393,7 +395,7 @@ this document and that one differ, fix one of them.
|
|
|
393
395
|
3. **Units are kept as stored and converted only on the axes a caller asks to convert**
|
|
394
396
|
(`ArrayMeta.in_units(space=, time=)`, `in_nm()`, the `units` argument), through one table:
|
|
395
397
|
for length, American and British spellings, `nm`, `um`, `µm` (U+00B5), `μm` (U+03BC),
|
|
396
|
-
`mm`, `cm`, `m`, `angstrom`/`Å`, `pm`; for time, `s`/`second`, `ms`, `us`/`µs`, `ns`,
|
|
398
|
+
`mm`, `cm`, `m`, `km`, `angstrom`/`Å`, `pm`; for time, `s`/`second`, `ms`, `us`/`µs`, `ns`,
|
|
397
399
|
`minute`, `hour`, `day`. Any unit string on any axis is accepted and never rejected until
|
|
398
400
|
a conversion is asked for (`"s"`, `"m"`, `"degrees_north"`, `""`). A spatial axis with no
|
|
399
401
|
unit is taken as nanometres, with a **warning once per array** (every repo assumed this
|
|
@@ -549,8 +551,8 @@ this document and that one differ, fix one of them.
|
|
|
549
551
|
reads that level voxel for voxel and reports `voxel_size` as the requested size with
|
|
550
552
|
**voxel `i` staying voxel `i`**: every world quantity, the translation and the corner
|
|
551
553
|
included, scales by the ratio of the sizes (cellmap-flow's rule, which chunkmirage's
|
|
552
|
-
server relies on), so a box of `m` requested voxels returns `m` stored voxels of the
|
|
553
|
-
level; `"keep"` reads the chosen level at its own voxel size (a level pick, nothing
|
|
554
|
+
server relies on), so a box of `m` requested voxels returns `m` stored voxels of the level,
|
|
555
|
+
and `source_voxel_size` still reports the level's own size; `"keep"` reads the chosen level at its own voxel size (a level pick, nothing
|
|
554
556
|
more). `relabel` and `nearest` are different things and both exist: one is a grid
|
|
555
557
|
policy, the other a resampling kernel. `level` also takes a position, `0` or `"first"`
|
|
556
558
|
for the first listed level. The pyramid an array belongs to is found through
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "worldarray"
|
|
3
|
-
version = "0.3.
|
|
3
|
+
version = "0.3.1"
|
|
4
4
|
description = "Read zarr, N5 and Neuroglancer precomputed volumes in world coordinates through tensorstore: correct voxel placement, pyramid levels, padded region reads, resampling. numpy + tensorstore only."
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { file = "LICENSE" }
|
|
@@ -139,6 +139,8 @@ class WorldArray:
|
|
|
139
139
|
self._meta = self._rescaled_to_base(base_voxel_size, base_units)
|
|
140
140
|
space, time = _unit_targets(units)
|
|
141
141
|
self._grid = self._meta.in_units(space=space, time=time, strict=strict_units)
|
|
142
|
+
# the stored level's spatial voxel size, before any relabel renames the grid
|
|
143
|
+
self._stored_voxel_size = tuple(self._grid.voxel_size[i] for i in self._grid.spatial)
|
|
142
144
|
|
|
143
145
|
self._target: tuple[float, ...] | None = None
|
|
144
146
|
if voxel_size is not None:
|
|
@@ -299,8 +301,9 @@ class WorldArray:
|
|
|
299
301
|
|
|
300
302
|
@property
|
|
301
303
|
def source_voxel_size(self) -> tuple[float, ...]:
|
|
302
|
-
"""The stored level's voxel size on the spatial axes, in ``units
|
|
303
|
-
|
|
304
|
+
"""The stored level's voxel size on the spatial axes, in ``units``: what the level
|
|
305
|
+
is, whether reads resample it or ``on_mismatch="relabel"`` renames its grid."""
|
|
306
|
+
return self._stored_voxel_size
|
|
304
307
|
|
|
305
308
|
@property
|
|
306
309
|
def resampled(self) -> bool:
|
|
@@ -337,8 +340,22 @@ class WorldArray:
|
|
|
337
340
|
def box(self) -> Box:
|
|
338
341
|
"""The array's extent on the spatial axes as a world box, ``[corner, corner + shape *
|
|
339
342
|
voxel_size)``: what funlib calls the array's roi. ``Box.from_index`` gives the same
|
|
340
|
-
over every axis.
|
|
343
|
+
over every axis. With a resample target it is the extent of the target voxels that
|
|
344
|
+
``shape`` counts, which may reach past the stored data by less than one target voxel,
|
|
345
|
+
so ``read(box)`` returns ``shape``."""
|
|
341
346
|
sp = self._grid.spatial
|
|
347
|
+
if self._target is not None:
|
|
348
|
+
begin, end = [], []
|
|
349
|
+
for k, axis in enumerate(sp):
|
|
350
|
+
first, last = _resample.target_range(
|
|
351
|
+
self._grid.shape[axis],
|
|
352
|
+
self._grid.voxel_size[axis],
|
|
353
|
+
self._target[k],
|
|
354
|
+
self._origins()[k],
|
|
355
|
+
)
|
|
356
|
+
begin.append(self.anchor[k] + first * self._target[k])
|
|
357
|
+
end.append(self.anchor[k] + last * self._target[k])
|
|
358
|
+
return Box(tuple(begin), tuple(end))
|
|
342
359
|
return Box.from_index(
|
|
343
360
|
(0,) * len(sp),
|
|
344
361
|
tuple(self._grid.shape[i] for i in sp),
|
|
@@ -153,14 +153,22 @@ def apply_axis(block: np.ndarray, axis: int, plan: AxisPlan) -> np.ndarray:
|
|
|
153
153
|
raise ValueError(plan.kind)
|
|
154
154
|
|
|
155
155
|
|
|
156
|
+
def target_range(n: int, s: float, t: float, origin: float = 0.0) -> tuple[int, int]:
|
|
157
|
+
"""``[first, last)``: the target voxels of size ``t`` that overlap ``n`` source voxels of
|
|
158
|
+
size ``s``, indexed on a target grid whose voxel 0 corner sits ``origin`` source voxels
|
|
159
|
+
from the source corner."""
|
|
160
|
+
ratio = t / s
|
|
161
|
+
first = int(np.floor(snap_integral(-origin / ratio)))
|
|
162
|
+
last = int(np.ceil(snap_integral((n - origin) / ratio)))
|
|
163
|
+
return first, max(last, first)
|
|
164
|
+
|
|
165
|
+
|
|
156
166
|
def target_shape(n: int, s: float, t: float, origin: float = 0.0) -> int:
|
|
157
167
|
"""How many target voxels of size ``t`` cover ``n`` source voxels of size ``s``, counting
|
|
158
168
|
from a target grid whose voxel 0 corner sits ``origin`` source voxels from the source
|
|
159
169
|
corner: the first and last target voxels that overlap the data, inclusive."""
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
last = np.ceil(snap_integral((n - origin) / ratio))
|
|
163
|
-
return int(max(last - first, 0))
|
|
170
|
+
first, last = target_range(n, s, t, origin)
|
|
171
|
+
return last - first
|
|
164
172
|
|
|
165
173
|
|
|
166
174
|
def resample(
|
|
@@ -19,6 +19,7 @@ LENGTH_M = {
|
|
|
19
19
|
"millimeter": 1e-3, "millimetre": 1e-3, "mm": 1e-3,
|
|
20
20
|
"centimeter": 1e-2, "centimetre": 1e-2, "cm": 1e-2,
|
|
21
21
|
"meter": 1.0, "metre": 1.0, "m": 1.0,
|
|
22
|
+
"kilometer": 1e3, "kilometre": 1e3, "km": 1e3,
|
|
22
23
|
} # fmt: skip
|
|
23
24
|
TIME_S = {
|
|
24
25
|
"nanosecond": 1e-9, "ns": 1e-9,
|
|
@@ -81,7 +82,7 @@ def factor(
|
|
|
81
82
|
# entry through the table above, so "nanometre", "nm" and "Nanometers" agree.
|
|
82
83
|
_SHORT = {
|
|
83
84
|
"picometer": "pm", "angstrom": "Å", "nanometer": "nm", "micrometer": "µm",
|
|
84
|
-
"millimeter": "mm", "centimeter": "cm", "meter": "m",
|
|
85
|
+
"millimeter": "mm", "centimeter": "cm", "meter": "m", "kilometer": "km",
|
|
85
86
|
"nanosecond": "ns", "microsecond": "µs", "millisecond": "ms", "second": "s",
|
|
86
87
|
"minute": "min", "hour": "h", "day": "d",
|
|
87
88
|
} # fmt: skip
|
|
@@ -103,6 +104,7 @@ def abbreviation(unit: str) -> str:
|
|
|
103
104
|
# The American spelling is OME's.
|
|
104
105
|
_OME_NAMES = (
|
|
105
106
|
"picometer", "angstrom", "nanometer", "micrometer", "millimeter", "centimeter", "meter",
|
|
107
|
+
"kilometer",
|
|
106
108
|
"nanosecond", "microsecond", "millisecond", "second", "minute", "hour", "day",
|
|
107
109
|
) # fmt: skip
|
|
108
110
|
_OME_BY_SIZE = {_lookup(name): name for name in _OME_NAMES}
|
|
@@ -299,6 +299,7 @@ SHORT_FORMS = {
|
|
|
299
299
|
"mm": ("millimeter", "millimetre", "mm"),
|
|
300
300
|
"cm": ("centimeter", "centimetre", "cm"),
|
|
301
301
|
"m": ("meter", "metre", "m"),
|
|
302
|
+
"km": ("kilometer", "kilometre", "km"),
|
|
302
303
|
"ns": ("nanosecond", "ns"),
|
|
303
304
|
"µs": ("microsecond", "us", "µs", "μs"),
|
|
304
305
|
"ms": ("millisecond", "ms"),
|
|
@@ -550,6 +550,28 @@ def test_relabel_rescales_everything_by_the_ratio(tmp_path):
|
|
|
550
550
|
and vol.translation == (20.0, 20.0, 20.0)
|
|
551
551
|
and vol.corner == (16.0, 16.0, 16.0)
|
|
552
552
|
)
|
|
553
|
+
assert vol.source_voxel_size == (4.0, 4.0, 4.0) # what the level is (cellmap-flow's report)
|
|
554
|
+
resampled = wa.open(str(group / "s0"), voxel_size=(8, 8, 8), on_mismatch="resample")
|
|
555
|
+
assert resampled.source_voxel_size == (4.0, 4.0, 4.0)
|
|
556
|
+
assert resampled.voxel_size == (8.0, 8.0, 8.0)
|
|
557
|
+
|
|
558
|
+
|
|
559
|
+
def test_a_resampled_box_is_the_target_voxels_shape_counts(tmp_path):
|
|
560
|
+
"""cellmap-flow's 7 voxels of 8 nm read at 24 nm: 3 target voxels, box 0 to 72, not 56."""
|
|
561
|
+
group = tmp_path / "rs.zarr"
|
|
562
|
+
write_zarr2_group(group, ome_multiscales("zyx", [("s0", (8, 8, 8), (4, 4, 4))]))
|
|
563
|
+
write_zarr2(group / "s0", pattern((7, 7, 7)))
|
|
564
|
+
vol = wa.open(str(group / "s0"), voxel_size=(24, 24, 24), on_mismatch="resample")
|
|
565
|
+
assert vol.shape == (3, 3, 3) and vol.box == wa.Box((0.0,) * 3, (72.0,) * 3)
|
|
566
|
+
assert vol.read(vol.box).shape == vol.shape
|
|
567
|
+
g = wa.Grid.from_meta(wa.open(str(group / "s0")).grid, voxel_size=(24, 24, 24))
|
|
568
|
+
assert g.box == vol.box # the planned grid and the opened array agree
|
|
569
|
+
# target corners at 4 + 24 k: the voxels from -20 cover the data from 0
|
|
570
|
+
anchored = wa.open(
|
|
571
|
+
str(group / "s0"), voxel_size=(24, 24, 24), on_mismatch="resample", anchor=(4, 4, 4)
|
|
572
|
+
)
|
|
573
|
+
assert anchored.shape == (4, 4, 4) and anchored.box == wa.Box((-20.0,) * 3, (76.0,) * 3)
|
|
574
|
+
assert anchored.read(anchored.box).shape == anchored.shape
|
|
553
575
|
|
|
554
576
|
|
|
555
577
|
def test_post_transform_runs_after_resampling_and_before_padding(tmp_path):
|
|
@@ -57,6 +57,7 @@ def test_units_are_written_in_ome_names_and_unknown_ones_as_given():
|
|
|
57
57
|
assert wa.units.ome_name(spelling) == "nanometer"
|
|
58
58
|
assert wa.units.ome_name("um") == wa.units.ome_name("\u00b5m") == "micrometer"
|
|
59
59
|
assert wa.units.ome_name("s") == "second"
|
|
60
|
+
assert wa.units.ome_name("km") == wa.units.ome_name("Kilometres") == "kilometer"
|
|
60
61
|
with pytest.raises(ValueError, match="unknown unit"):
|
|
61
62
|
wa.units.ome_name("pixel")
|
|
62
63
|
doc = wa.ome_multiscales(("z", "y", "x"), "pixel", [("s0", (1, 1, 1), (0.5,) * 3)])
|
|
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
|
|
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
|