Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
d0ed7ac
fix(merge relic): fixed error where old code did not support debug mo…
AustinHouston Jun 18, 2026
f81a4c3
feat(separate real and dt): they are now on seperate yamls, some chan…
AustinHouston Jun 18, 2026
88a46a1
tests
AustinHouston Jun 18, 2026
92e0bf4
rename(host and port): match gui and run_servers.py
AustinHouston Jun 18, 2026
e1ec3c1
fix
AustinHouston Jun 18, 2026
2fccd81
important(yamls): configs updated to match current convention. check …
AustinHouston Jun 18, 2026
61f298f
feat(option to register folder data on tiled start): useful for preac…
AustinHouston Jun 18, 2026
54f4640
chore
AustinHouston Jun 19, 2026
3c69214
refactor(gui): terminal moved to right
AustinHouston Jun 19, 2026
5dbfc53
fix(tiled bug)
AustinHouston Jun 19, 2026
2b9a7a7
chore
AustinHouston Jun 23, 2026
63ab027
feat(electron diffraction digital twin): for the summer school, based…
AustinHouston Jun 23, 2026
48e7845
chore(colab compatibility)
AustinHouston Jun 23, 2026
606dcd7
chore(hack prep)
AustinHouston Jun 23, 2026
f041404
chore(adjust dt summer school)
AustinHouston Jun 23, 2026
30341cc
fix: updated dependencies because `pyqt5` doesn't have wheels for mac…
DomPTech Jun 29, 2026
fec06d8
fix: add missing parameter for file-based logging in `run_servers.py`
DomPTech Jun 29, 2026
0bac6db
feat(AI-use-CLaude-4.8: medium)(conscious-use: yes): jeol-> implement…
Jun 30, 2026
237d358
chore(test): delete old test causing failure
AustinHouston Jun 30, 2026
c0aaec3
fix(jeol): decode snapshot_rawdata bytes into a 2D image
Jun 30, 2026
73c0219
chore
AustinHouston Jun 30, 2026
e97cf0c
chore
AustinHouston Jun 30, 2026
3ed189e
choreee
AustinHouston Jun 30, 2026
0f4fc24
Merge pull request #123 from pycroscopy/dev-ACH
AustinHouston Jun 30, 2026
8066af5
Merge branch 'main' into main
AustinHouston Jun 30, 2026
58405af
Merge pull request #125 from DomPTech/main
AustinHouston Jun 30, 2026
eeb5b5c
Add PyJEM wheel and reorganize vendor stubs into versioned folders
Jun 30, 2026
d575899
add: _connect_hardware in jeol.py
Jun 30, 2026
b1a19de
refactor: align JEOL config + device properties to main's instrument …
Jun 30, 2026
93c89ef
Merge branch 'main' into jeol-cm
Jun 30, 2026
0a8302f
fix: hardcoded local path(eg: sqlite:///C:/Users/ahoust17/Desktop/) i…
Jun 30, 2026
3ebf932
refactor and add: remove _persist and fake ReplicaAdornedImageJeol cl…
Jul 2, 2026
d3e48db
Merge pull request #126 from pycroscopy/jeol-cm
utkarshp1161 Jul 4, 2026
6896ec5
Fix: Clean up old github-pages artifacts before deployment
utkarshp1161 Jul 4, 2026
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
6 changes: 5 additions & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,11 +37,15 @@ jobs:
cd docs
myst build --html
touch _build/html/.nojekyll
- name: Delete old GitHub Pages artifact
uses: geekyeggo/delete-artifact@v2
with:
name: github-pages
failOnError: false
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: 'docs/_build/html'
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

13 changes: 5 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,15 @@ See - docs/dev_guide.md

### Core installation (simulation mode)

```bash
pip install --find-links ./stubs -e .
```

or with `uv`:

```bash
uv sync
```

This installs `asyncroscopy` and all core dependencies. AutoScript is not required—the
framework will fall back to simulated acquisition automatically.
This installs `asyncroscopy` and all core dependencies. The vendor wheels are
local, version-pinned, and resolved via `[tool.uv.sources]` in `pyproject.toml`
(AutoScript under `stubs/AutoScript_v_1.17/`, PyJEM under
`stubs/PyJEM_v_1.3.0.3564/`). AutoScript/PyJEM hardware is not required—the
framework falls back to simulated acquisition automatically.

### Hardware installation (Thermo Fisher AutoScript)

Expand Down
Binary file modified asyncroscopy/.DS_Store
Binary file not shown.
126 changes: 0 additions & 126 deletions asyncroscopy/JeolMicroscope.py

This file was deleted.

60 changes: 50 additions & 10 deletions asyncroscopy/data/data.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
DEFAULT_ACQUISITION_DIR = "outputs/tiled_acquisitions"
ONE_NODE_PER_FILE_WALKER = "tiled.client.register:one_node_per_item"
REGISTER_TIMEOUT_SECONDS = 120
REGISTER_SAVE_PATH_TIMEOUT_SECONDS = 3600
REGISTER_POLL_SECONDS = 0.25


Expand Down Expand Up @@ -131,21 +132,26 @@ def start_tiled_server(self, timeout=30) -> str:
self._tiled_server_status = "running; files register manually"
return self.get_config()

catalog = str(Path(self._save_path).expanduser() / ".asyncroscopy_tiled_catalog.db")
if _is_windows_drive_path(catalog):
catalog = catalog.replace("\\", "/")
save_path = PureWindowsPath(self._save_path) if _is_windows_drive_path(self._save_path) else Path(self._save_path).expanduser()
catalog = save_path / ".asyncroscopy_tiled_catalog.db"
catalog_database = _catalog_database_uri(catalog)

try:
_ensure_directory(self._save_path)
command = [self._tiled_executable(), "catalog", "init", "--if-not-exists", catalog]
command = [*self._tiled_command(), "catalog", "init", "--if-not-exists", catalog_database]
subprocess.run(command, check=True, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
except subprocess.CalledProcessError as exc:
self._tiled_server = "no"
output = (exc.stdout or "").strip()
self._tiled_server_status = f"{exc}; output: {output}" if output else str(exc)
return self.get_config()
except Exception as exc:
self._tiled_server = "no"
self._tiled_server_status = str(exc)
return self.get_config()

command = [
self._tiled_executable(), "serve", "catalog", catalog,
*self._tiled_command(), "serve", "catalog", catalog_database,
"--read", self._save_path, "--public", "--api-key", self._api_key,
"--host", self._host, "--port", str(self._port),
]
Expand Down Expand Up @@ -205,6 +211,35 @@ async def register_with_tiled_client() -> None:
self._tiled_server_status = "running; registered path"
return key

@command(dtype_out=str)
def register_save_path(self) -> str:
"""Register the configured save directory with Tiled once."""
save_path = str(Path(self._save_path).expanduser())

async def register_directory_with_tiled_client() -> None:
client = from_uri(self._uri(), api_key=self._api_key)
await register(client, save_path, walkers=[ONE_NODE_PER_FILE_WALKER], key_from_filename=identity)

try:
asyncio.run(asyncio.wait_for(register_directory_with_tiled_client(), REGISTER_SAVE_PATH_TIMEOUT_SECONDS))
except Exception as exc:
message = (
f"Save path registration failed: {exc}\n\n"
f"Data save path:\n {save_path}\n\n"
f"Tiled server serving:\n {self._tiled_serve_path or '(external server; path not managed by DATA)'}"
)
self._tiled_server_status = message
raise RuntimeError(message) from exc

result = {
"registered_path": save_path,
"tiled_server": self._tiled_server,
"tiled_server_status": "running; registered save path",
"tiled_server_serving": self._tiled_serve_path,
}
self._tiled_server_status = result["tiled_server_status"]
return json.dumps(result)

def _uri(self) -> str:
return f"http://{self._host}:{self._port}"

Expand Down Expand Up @@ -248,14 +283,19 @@ def _parse_uri(uri: str) -> tuple[str, int]:
return host or "10.46.217.241", int(port or 9091)

@staticmethod
def _tiled_executable() -> str:
candidate = Path(sys.executable).with_name("tiled")
return str(candidate) if candidate.exists() else "tiled"
def _tiled_command() -> list[str]:
return [sys.executable, "-m", "tiled"]


def _is_windows_drive_path(path: str | Path | PureWindowsPath) -> bool:
text = str(path)
return isinstance(path, PureWindowsPath) or (len(text) >= 3 and text[1] == ":" and text[0].isalpha() and text[2] in {"\\", "/"})
windows_path = PureWindowsPath(path)
return bool(windows_path.drive)


def _catalog_database_uri(path: str | Path | PureWindowsPath) -> str:
if _is_windows_drive_path(path):
return f"sqlite:///{PureWindowsPath(path).as_posix()}"
return str(Path(path).expanduser())


def _ensure_directory(path: str | Path) -> None:
Expand Down
59 changes: 55 additions & 4 deletions asyncroscopy/data/data_writer.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import h5py
import numpy as np
import tifffile

DEFAULT_ACQUISITION_DIR = "outputs/tiled_acquisitions"

Expand All @@ -35,10 +36,55 @@ def acquisition_filename(
return directory / f"{acquisition_type}_{detector}_{stamp}.{extension.lower().lstrip('.')}"


def save_acquisition(device, data_server, acquisition_type: str, detectors, data, dataset_name: str = "image") -> str:
"""Save one acquisition to one HDF5 file and return its DATA/Tiled key."""
def _write_tiff(source, path: Path, attrs: dict | None = None) -> None:
"""Write one image to ``path`` as TIFF.

AutoScript adorned images save themselves natively (embedding their own
metadata); anything else is a raw array (JEOL/twin) written via tifffile,
with ``attrs`` (e.g. JEOL's get_detectorsetting() dict) json-encoded into the
TIFF ImageDescription tag so the provenance survives the format.
"""
if hasattr(source, "save"): # adorned images from AutoScript have a save method
source.save(str(path))
else:
array = source.data if hasattr(source, "data") and not isinstance(source, np.ndarray) else source
tifffile.imwrite(str(path), np.asarray(array), description=json.dumps(attrs) if attrs else None)


def save_acquisition(
device,
data_server,
acquisition_type: str,
detectors,
data,
dataset_name: str = "image",
dataset_attrs: dict | list[dict] | None = None,
file_attrs: dict | None = None,
output_format: str = ".h5",
) -> str:
"""Save one acquisition and return its DATA/Tiled key.

``.h5`` writes all detectors into one stacked file; ``.tiff`` writes one
file per detector sharing a timestamp (TIFF cannot stack), registering each.
"""
if output_format not in (".h5", ".tiff"):
raise ValueError(f"Unsupported output_format {output_format!r}; expected '.h5' or '.tiff'")
detector_list = list(detectors) if isinstance(detectors, (list, tuple)) else [detectors]
data_list = list(data) if isinstance(data, (list, tuple)) else [data]
attrs_list = dataset_attrs if isinstance(dataset_attrs, list) else [dataset_attrs] * len(data_list)

if output_format == ".tiff":
save_dir = data_server.save_path if data_server is not None else DEFAULT_ACQUISITION_DIR
directory = Path(save_dir).expanduser()
directory.mkdir(parents=True, exist_ok=True)
stem = f"{acquisition_type}_{datetime.now().strftime('%Y%m%dT%H%M%S%f')}"
for index, (source, detector) in enumerate(zip(data_list, detector_list)):
path = directory / f"{stem}_{detector}.tiff"
_write_tiff(source, path, attrs_list[index])
if data_server is not None:
data_server.register_path(str(path))
return stem

has_labeled_datasets = len(detector_list) > 1 or len(data_list) > 1
detector_label = "_".join([str(detector) for detector in detector_list])
path = acquisition_filename(device, acquisition_type, detector_label, data_server)
Expand All @@ -50,9 +96,11 @@ def save_acquisition(device, data_server, acquisition_type: str, detectors, data
name = f"image/{detector}"
else:
name = f"{dataset_name}/{detector}" if has_labeled_datasets else dataset_name
datasets.append({"name": name, "source": source, "attrs": {"acquisition_type": acquisition_type, "detector": detector}})
attrs = {"acquisition_type": acquisition_type, "detector": detector}
attrs.update(attrs_list[index] or {})
datasets.append({"name": name, "source": source, "attrs": attrs})

save_acquisition_hdf5(path, datasets)
save_acquisition_hdf5(path, datasets, file_attrs=file_attrs)
return data_server.register_path(str(path)) if data_server is not None else str(path)


Expand Down Expand Up @@ -86,3 +134,6 @@ def save_acquisition_hdf5(path: str | Path, datasets: list[dict], file_attrs: di
if key in dset.attrs:
key = f"{key}_{len(dset.attrs)}"
dset.attrs[key] = elem.text.strip()
elif isinstance(metadata, dict):
for key, value in metadata.items():
dset.attrs[key] = value if isinstance(value, (str, int, float, bool, np.number)) else json.dumps(value)
Loading
Loading