Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 33 additions & 27 deletions apps/cadecon/src/components/import/FileDropZone.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createSignal, Show, type JSX } from 'solid-js';
import { parseNpy, parseNpz, processNpyResult } from '@calab/io';
import { parseNpy, parseNpz, parseMat, processNpyResult } from '@calab/io';
import type { NpzResult } from '@calab/core';
import {
rawFile,
setRawFile,
Expand All @@ -19,12 +20,36 @@ export function FileDropZone(): JSX.Element {
return mb >= 1 ? `${mb.toFixed(1)} MB` : `${(bytes / 1024).toFixed(1)} KB`;
};

// Shared handling for multi-array containers (.npz and .mat): pick the single
// 2D array automatically, or hand off to the array selector when ambiguous.
const handleMultiArrayResult = (result: NpzResult, ext: string) => {
const twoDArrayNames = result.arrayNames.filter(
(name) => result.arrays[name].shape.length === 2,
);

if (twoDArrayNames.length === 0) {
setImportError(
`No 2D arrays found in .${ext} file. CaDecon requires a 2D array (cells x timepoints).`,
);
return;
}

if (twoDArrayNames.length === 1) {
// Auto-select the only 2D array
const processed = processNpyResult(result.arrays[twoDArrayNames[0]]);
setParsedData(processed);
} else {
// Multiple 2D arrays: let user select
setNpzArrays(result);
}
};

const handleFile = async (file: File) => {
const ext = file.name.split('.').pop()?.toLowerCase();

if (ext !== 'npy' && ext !== 'npz') {
if (ext !== 'npy' && ext !== 'npz' && ext !== 'mat') {
setImportError(
`Unsupported file format: .${ext ?? 'unknown'}. Please use .npy or .npz files.`,
`Unsupported file format: .${ext ?? 'unknown'}. Please use .npy, .npz, or .mat files.`,
);
return;
}
Expand All @@ -37,28 +62,9 @@ export function FileDropZone(): JSX.Element {
const buffer = await file.arrayBuffer();

if (ext === 'npz') {
const npzResult = parseNpz(buffer);
// Filter to only 2D numeric arrays
const twoDArrayNames = npzResult.arrayNames.filter((name) => {
const arr = npzResult.arrays[name];
return arr.shape.length === 2;
});

if (twoDArrayNames.length === 0) {
setImportError(
'No 2D arrays found in .npz file. CaDecon requires a 2D array (cells x timepoints).',
);
return;
}

if (twoDArrayNames.length === 1) {
// Auto-select the only 2D array
const processed = processNpyResult(npzResult.arrays[twoDArrayNames[0]]);
setParsedData(processed);
} else {
// Multiple 2D arrays: let user select
setNpzArrays(npzResult);
}
handleMultiArrayResult(parseNpz(buffer), 'npz');
} else if (ext === 'mat') {
handleMultiArrayResult(parseMat(buffer), 'mat');
} else {
// .npy file
const result = parseNpy(buffer);
Expand Down Expand Up @@ -121,13 +127,13 @@ export function FileDropZone(): JSX.Element {
</svg>
</div>
<p class="drop-zone__text">
Drop a <strong>.npy</strong> or <strong>.npz</strong> file here
Drop a <strong>.npy</strong>, <strong>.npz</strong>, or <strong>.mat</strong> file here
</p>
<p class="drop-zone__subtext">or click to browse</p>
<input
ref={inputRef}
type="file"
accept=".npy,.npz"
accept=".npy,.npz,.mat"
style="display:none"
onChange={handleInputChange}
/>
Expand Down
4 changes: 2 additions & 2 deletions apps/cadecon/src/components/import/NpzArraySelector.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ export function NpzArraySelector(): JSX.Element {
<div class="card">
<h3 class="card__title">Select Array</h3>
<p class="text-secondary">
This .npz file contains {twoDArrays().length} arrays with 2D data. Select the one
containing your calcium traces:
This file contains {twoDArrays().length} arrays with 2D data. Select the one containing
your calcium traces:
</p>
<div class="npz-array-list">
<For each={twoDArrays()}>
Expand Down
47 changes: 47 additions & 0 deletions packages/io/src/__fixtures__/gen-mat-fixtures.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
#!/usr/bin/env python3
"""Regenerate the real MATLAB .mat fixtures used by mat-parser.test.ts.

These fixtures are written by scipy (a genuine MATLAB Level-5 writer), which is
the whole point: hand-built .mat bytes can only encode our *assumptions* about
the format, including wrong ones. A real writer catches format-reality bugs --
e.g. that compressed data elements are NOT padded to an 8-byte boundary, which
a synthetic builder happily got wrong.

Keep the arrays tiny (fixtures are committed to the repo) and use integer values
so the TypeScript test can assert exact round-trip values.

Usage:
python gen-mat-fixtures.py # requires numpy + scipy

Produces, next to this script:
traces_v6.mat uncompressed (savemat do_compression=False, ~= MATLAB -v6)
traces_v7.mat zlib-compressed (do_compression=True, ~= MATLAB -v7)
traces_multi.mat compressed, multiple variables (traces + fps + tvec)
"""

import os

import numpy as np
import scipy.io as sio

HERE = os.path.dirname(os.path.abspath(__file__))

# 3 cells x 5 timepoints, row-major logical layout [[1..5],[6..10],[11..15]].
TRACES = np.arange(1, 16, dtype=np.float64).reshape(3, 5)


def main() -> None:
sio.savemat(os.path.join(HERE, "traces_v6.mat"), {"traces": TRACES}, do_compression=False)
sio.savemat(os.path.join(HERE, "traces_v7.mat"), {"traces": TRACES}, do_compression=True)
sio.savemat(
os.path.join(HERE, "traces_multi.mat"),
{"traces": TRACES, "fps": 30.0, "tvec": np.arange(5.0)},
do_compression=True,
)
for name in ("traces_v6.mat", "traces_v7.mat", "traces_multi.mat"):
path = os.path.join(HERE, name)
print(f"wrote {name}: {os.path.getsize(path)} bytes")


if __name__ == "__main__":
main()
Binary file added packages/io/src/__fixtures__/traces_multi.mat
Binary file not shown.
Binary file added packages/io/src/__fixtures__/traces_v6.mat
Binary file not shown.
Binary file added packages/io/src/__fixtures__/traces_v7.mat
Binary file not shown.
Loading
Loading