Archive MaleCNS research sources, provenance, bibliography and immutable data inventory
This commit is contained in:
@@ -0,0 +1,3 @@
|
||||
50a7718770c57220f160ba4f431ab89e annotations/body-annotations-male-cns-v1.0-minconf-0.5.feather
|
||||
3d842b12fe5c49eefade528d7dd24a1f annotations/body-neurotransmitters-male-cns-v1.0.feather
|
||||
f30e9dcca25cfd021bf1e7b3d975599e connectivity/connectome-weights-male-cns-v1.0-minconf-0.5.feather
|
||||
@@ -0,0 +1,3 @@
|
||||
2177e246113e4cfbf1e7772ec37c6da1955ff22e8063d0b1f833101f99a9a3b2 annotations/body-annotations-male-cns-v1.0-minconf-0.5.feather
|
||||
95c9289220663abeb3409f3ad9e5a7f8a53f8093f5139d15502cd08da8879621 annotations/body-neurotransmitters-male-cns-v1.0.feather
|
||||
e35da783d1c686b2b58b3b87cd6a403ae43bfcfba8bff28e08ef752c1a56afc1 connectivity/connectome-weights-male-cns-v1.0-minconf-0.5.feather
|
||||
+96
@@ -0,0 +1,96 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly RAW_ROOT="${1:-/data/digital-drosophila/datasets/male-cns-v1.0/raw}"
|
||||
readonly BASE_URL="https://storage.googleapis.com/flyem-male-cns/v1.0/connectome-data/flat-connectome"
|
||||
readonly LOG_DIR="${RAW_ROOT}/_logs"
|
||||
readonly MANIFEST_DIR="${RAW_ROOT}/_manifests"
|
||||
|
||||
mkdir -p "${RAW_ROOT}/annotations" "${RAW_ROOT}/connectivity" "$LOG_DIR" "$MANIFEST_DIR"
|
||||
|
||||
download_one() {
|
||||
local subdir="$1"
|
||||
local filename="$2"
|
||||
local expected_md5="$3"
|
||||
local destination="${RAW_ROOT}/${subdir}/${filename}"
|
||||
local attempt
|
||||
|
||||
if [[ -f "$destination" ]] && printf '%s %s\n' "$expected_md5" "$destination" | md5sum --check --status; then
|
||||
printf '[OK] already verified: %s/%s\n' "$subdir" "$filename"
|
||||
return 0
|
||||
fi
|
||||
|
||||
for attempt in 1 2 3; do
|
||||
if [[ -f "$destination" && ! -f "${destination}.aria2" ]]; then
|
||||
mv -- "$destination" "${destination}.corrupt-$(date +%Y%m%dT%H%M%S)"
|
||||
fi
|
||||
|
||||
printf '[DOWNLOAD] attempt %d: %s/%s\n' "$attempt" "$subdir" "$filename"
|
||||
aria2c \
|
||||
--continue=true \
|
||||
--max-tries=0 \
|
||||
--retry-wait=10 \
|
||||
--connect-timeout=30 \
|
||||
--timeout=60 \
|
||||
--max-connection-per-server=8 \
|
||||
--split=8 \
|
||||
--min-split-size=16M \
|
||||
--file-allocation=falloc \
|
||||
--auto-file-renaming=false \
|
||||
--allow-overwrite=false \
|
||||
--check-integrity=true \
|
||||
--console-log-level=notice \
|
||||
--summary-interval=60 \
|
||||
--dir="${RAW_ROOT}/${subdir}" \
|
||||
--out="$filename" \
|
||||
"${BASE_URL}/${filename}" \
|
||||
>"${LOG_DIR}/${filename}.log" 2>&1
|
||||
|
||||
if printf '%s %s\n' "$expected_md5" "$destination" | md5sum --check --status; then
|
||||
printf '[VERIFIED] %s/%s\n' "$subdir" "$filename"
|
||||
return 0
|
||||
fi
|
||||
|
||||
printf '[CHECKSUM FAILED] %s/%s\n' "$subdir" "$filename" >&2
|
||||
[[ -f "$destination" ]] && mv -- "$destination" "${destination}.corrupt-$(date +%Y%m%dT%H%M%S)"
|
||||
[[ -f "${destination}.aria2" ]] && mv -- "${destination}.aria2" "${destination}.aria2.corrupt-$(date +%Y%m%dT%H%M%S)"
|
||||
done
|
||||
|
||||
printf '[FAILED] checksum mismatch after 3 complete downloads: %s/%s\n' "$subdir" "$filename" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
jobs=(
|
||||
"annotations|body-annotations-male-cns-v1.0-minconf-0.5.feather|50a7718770c57220f160ba4f431ab89e"
|
||||
"annotations|body-neurotransmitters-male-cns-v1.0.feather|3d842b12fe5c49eefade528d7dd24a1f"
|
||||
"connectivity|connectome-weights-male-cns-v1.0-minconf-0.5.feather|f30e9dcca25cfd021bf1e7b3d975599e"
|
||||
)
|
||||
|
||||
pids=()
|
||||
for job in "${jobs[@]}"; do
|
||||
IFS='|' read -r subdir filename expected_md5 <<<"$job"
|
||||
download_one "$subdir" "$filename" "$expected_md5" &
|
||||
pids+=("$!")
|
||||
done
|
||||
|
||||
status=0
|
||||
for pid in "${pids[@]}"; do
|
||||
wait "$pid" || status=1
|
||||
done
|
||||
|
||||
if (( status != 0 )); then
|
||||
printf '[FAILED] one or more files could not be downloaded and verified\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
cd "$RAW_ROOT"
|
||||
md5sum --check "${MANIFEST_DIR}/checksums.md5"
|
||||
sha256sum \
|
||||
annotations/*.feather \
|
||||
connectivity/*.feather \
|
||||
>"${MANIFEST_DIR}/checksums.sha256"
|
||||
|
||||
find annotations connectivity -maxdepth 1 -type f -name '*.feather' -printf '%P\t%s bytes\n' \
|
||||
| sort >"${MANIFEST_DIR}/files.tsv"
|
||||
|
||||
printf '[COMPLETE] all MaleCNS v1.0 phase-1 files passed official MD5 verification\n'
|
||||
@@ -0,0 +1,3 @@
|
||||
body-annotations-male-cns-v1.0-minconf-0.5.feather 14483314 bytes
|
||||
body-neurotransmitters-male-cns-v1.0.feather 43282834 bytes
|
||||
connectome-weights-male-cns-v1.0-minconf-0.5.feather 1051241946 bytes
|
||||
|
+73
@@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env bash
|
||||
set -Eeuo pipefail
|
||||
|
||||
readonly RAW_ROOT="${1:-/data/digital-drosophila/datasets/male-cns-v1.0/raw}"
|
||||
readonly SKELETON_DIR="${RAW_ROOT}/skeletons"
|
||||
readonly MANIFEST_DIR="${RAW_ROOT}/_manifests/skeletons"
|
||||
readonly LOG_DIR="${RAW_ROOT}/_logs/skeletons"
|
||||
readonly CONCURRENCY="${MALECNS_SWC_CONCURRENCY:-32}"
|
||||
|
||||
mkdir -p "$SKELETON_DIR" "$MANIFEST_DIR" "$LOG_DIR"
|
||||
|
||||
for required in remote-files.tsv checksums.md5 download.aria2 remote-summary.json; do
|
||||
if [[ ! -s "${MANIFEST_DIR}/${required}" ]]; then
|
||||
printf 'missing manifest: %s\n' "${MANIFEST_DIR}/${required}" >&2
|
||||
exit 2
|
||||
fi
|
||||
done
|
||||
|
||||
expected_count="$(jq -r '.swc_file_count' "${MANIFEST_DIR}/remote-summary.json")"
|
||||
expected_bytes="$(jq -r '.total_bytes' "${MANIFEST_DIR}/remote-summary.json")"
|
||||
printf '[REMOTE] files=%s bytes=%s\n' "$expected_count" "$expected_bytes"
|
||||
printf '[DOWNLOAD] concurrency=%s destination=%s\n' "$CONCURRENCY" "$SKELETON_DIR"
|
||||
|
||||
aria2c \
|
||||
--input-file="${MANIFEST_DIR}/download.aria2" \
|
||||
--continue=true \
|
||||
--always-resume=true \
|
||||
--max-resume-failure-tries=20 \
|
||||
--max-tries=0 \
|
||||
--retry-wait=5 \
|
||||
--connect-timeout=30 \
|
||||
--timeout=60 \
|
||||
--max-concurrent-downloads="$CONCURRENCY" \
|
||||
--max-connection-per-server=1 \
|
||||
--split=1 \
|
||||
--file-allocation=none \
|
||||
--auto-file-renaming=false \
|
||||
--allow-overwrite=true \
|
||||
--check-integrity=true \
|
||||
--disk-cache=64M \
|
||||
--console-log-level=notice \
|
||||
--summary-interval=60 \
|
||||
>"${LOG_DIR}/aria2.log" 2>&1
|
||||
|
||||
cd "$SKELETON_DIR"
|
||||
|
||||
actual_count="$(find . -maxdepth 1 -type f -name '*.swc' -printf '%f\n' | wc -l)"
|
||||
actual_bytes="$(find . -maxdepth 1 -type f -name '*.swc' -printf '%s\n' | awk '{sum += $1} END {print sum + 0}')"
|
||||
empty_count="$(find . -maxdepth 1 -type f -name '*.swc' -size 0 -printf '%f\n' | wc -l)"
|
||||
partial_count="$(find . -maxdepth 1 -type f -name '*.aria2' -printf '%f\n' | wc -l)"
|
||||
|
||||
find . -maxdepth 1 -type f -name '*.swc' -printf '%f\n' | sort >"${MANIFEST_DIR}/local-files.txt"
|
||||
cut -f1 "${MANIFEST_DIR}/remote-files.tsv" | sort >"${MANIFEST_DIR}/expected-files.txt"
|
||||
comm -23 "${MANIFEST_DIR}/expected-files.txt" "${MANIFEST_DIR}/local-files.txt" >"${MANIFEST_DIR}/missing-files.txt"
|
||||
comm -13 "${MANIFEST_DIR}/expected-files.txt" "${MANIFEST_DIR}/local-files.txt" >"${MANIFEST_DIR}/unexpected-files.txt"
|
||||
missing_count="$(wc -l <"${MANIFEST_DIR}/missing-files.txt")"
|
||||
unexpected_count="$(wc -l <"${MANIFEST_DIR}/unexpected-files.txt")"
|
||||
|
||||
printf '[LOCAL] files=%s bytes=%s empty=%s partial=%s missing=%s unexpected=%s\n' \
|
||||
"$actual_count" "$actual_bytes" "$empty_count" "$partial_count" "$missing_count" "$unexpected_count"
|
||||
|
||||
if [[ "$actual_count" != "$expected_count" || "$actual_bytes" != "$expected_bytes" || \
|
||||
"$empty_count" != 0 || "$partial_count" != 0 || "$missing_count" != 0 || "$unexpected_count" != 0 ]]; then
|
||||
printf '[FAILED] local inventory does not match the remote manifest\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! md5sum --check --quiet "${MANIFEST_DIR}/checksums.md5" >"${LOG_DIR}/md5-verification.log" 2>&1; then
|
||||
printf '[FAILED] one or more SWC files failed the official GCS MD5 check\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf '[COMPLETE] all SWC files match count, size, names, and official MD5 checksums\n'
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Enumerate the public MaleCNS v1.0 SWC objects and build download manifests."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import time
|
||||
import urllib.parse
|
||||
import urllib.request
|
||||
|
||||
|
||||
BUCKET = "flyem-male-cns"
|
||||
PREFIX = "v1.0/segmentation/skeletons-malecns/skeletons-swc/"
|
||||
API_URL = f"https://storage.googleapis.com/storage/v1/b/{BUCKET}/o"
|
||||
OBJECT_BASE_URL = f"https://storage.googleapis.com/{BUCKET}/"
|
||||
|
||||
|
||||
def fetch_json(url: str, attempts: int = 10) -> dict:
|
||||
for attempt in range(1, attempts + 1):
|
||||
try:
|
||||
request = urllib.request.Request(url, headers={"User-Agent": "malecns-swc-enumerator/1.0"})
|
||||
with urllib.request.urlopen(request, timeout=60) as response:
|
||||
return json.load(response)
|
||||
except Exception as exc: # noqa: BLE001 - retries cover transient HTTP/TLS failures
|
||||
if attempt == attempts:
|
||||
raise
|
||||
delay = min(60, 2 ** (attempt - 1))
|
||||
print(f"listing attempt {attempt} failed: {exc}; retrying in {delay}s", file=sys.stderr)
|
||||
time.sleep(delay)
|
||||
raise RuntimeError("unreachable")
|
||||
|
||||
|
||||
def atomic_write(path: str, content: str) -> None:
|
||||
temporary = f"{path}.tmp.{os.getpid()}"
|
||||
with open(temporary, "w", encoding="utf-8", newline="\n") as handle:
|
||||
handle.write(content)
|
||||
handle.flush()
|
||||
os.fsync(handle.fileno())
|
||||
os.replace(temporary, path)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("output_dir")
|
||||
parser.add_argument("--download-dir", required=True)
|
||||
args = parser.parse_args()
|
||||
|
||||
os.makedirs(args.output_dir, exist_ok=True)
|
||||
objects: list[tuple[str, int, str, str]] = []
|
||||
page_token: str | None = None
|
||||
page_count = 0
|
||||
|
||||
while True:
|
||||
query = {
|
||||
"prefix": PREFIX,
|
||||
"maxResults": "1000",
|
||||
"fields": "items(name,size,md5Hash),nextPageToken",
|
||||
}
|
||||
if page_token:
|
||||
query["pageToken"] = page_token
|
||||
payload = fetch_json(f"{API_URL}?{urllib.parse.urlencode(query)}")
|
||||
page_count += 1
|
||||
|
||||
for item in payload.get("items", []):
|
||||
name = item["name"]
|
||||
if not name.endswith(".swc"):
|
||||
continue
|
||||
relative_name = name[len(PREFIX) :]
|
||||
if not relative_name or "/" in relative_name:
|
||||
raise RuntimeError(f"unexpected nested SWC object: {name}")
|
||||
md5_hex = base64.b64decode(item["md5Hash"]).hex()
|
||||
url = OBJECT_BASE_URL + urllib.parse.quote(name, safe="/")
|
||||
objects.append((relative_name, int(item["size"]), md5_hex, url))
|
||||
|
||||
page_token = payload.get("nextPageToken")
|
||||
if page_count % 25 == 0:
|
||||
print(f"listed {page_count} pages, {len(objects)} SWC files", file=sys.stderr)
|
||||
if not page_token:
|
||||
break
|
||||
|
||||
objects.sort(key=lambda row: row[0])
|
||||
names = [row[0] for row in objects]
|
||||
if len(names) != len(set(names)):
|
||||
raise RuntimeError("duplicate SWC basenames in remote listing")
|
||||
|
||||
total_bytes = sum(row[1] for row in objects)
|
||||
listing_digest = hashlib.sha256(
|
||||
"".join(f"{name}\t{size}\t{md5_hex}\n" for name, size, md5_hex, _ in objects).encode()
|
||||
).hexdigest()
|
||||
|
||||
files_tsv = "".join(
|
||||
f"{name}\t{size}\t{md5_hex}\t{url}\n" for name, size, md5_hex, url in objects
|
||||
)
|
||||
checksums_md5 = "".join(f"{md5_hex} {name}\n" for name, _, md5_hex, _ in objects)
|
||||
aria2_input = "".join(
|
||||
f"{url}\n dir={args.download_dir}\n out={name}\n checksum=md5={md5_hex}\n"
|
||||
for name, _, md5_hex, url in objects
|
||||
)
|
||||
summary = {
|
||||
"bucket": BUCKET,
|
||||
"prefix": PREFIX,
|
||||
"swc_file_count": len(objects),
|
||||
"total_bytes": total_bytes,
|
||||
"listing_pages": page_count,
|
||||
"listing_sha256": listing_digest,
|
||||
"generated_at_unix": int(time.time()),
|
||||
}
|
||||
|
||||
atomic_write(os.path.join(args.output_dir, "remote-files.tsv"), files_tsv)
|
||||
atomic_write(os.path.join(args.output_dir, "checksums.md5"), checksums_md5)
|
||||
atomic_write(os.path.join(args.output_dir, "download.aria2"), aria2_input)
|
||||
atomic_write(os.path.join(args.output_dir, "remote-summary.json"), json.dumps(summary, indent=2) + "\n")
|
||||
print(json.dumps(summary, sort_keys=True))
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"bucket": "flyem-male-cns",
|
||||
"prefix": "v1.0/segmentation/skeletons-malecns/skeletons-swc/",
|
||||
"swc_file_count": 211573,
|
||||
"total_bytes": 9299975999,
|
||||
"listing_pages": 212,
|
||||
"listing_sha256": "8d81e5cf21daac97518319ed79baad52431968be6b4057f419112bc05f848942",
|
||||
"generated_at_unix": 1789756376
|
||||
}
|
||||
Reference in New Issue
Block a user