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
48 changes: 48 additions & 0 deletions docs/programs.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ Arbitrary tools for viewing, analyzing, or working with instamatic data.
- [instamatic.defocus_helper](#instamaticdefocus_helper) (`instamatic.gui.defocus_button:main`)
- [instamatic.find_crystals](#instamaticfind_crystals) (`instamatic.processing.find_crystals:main_entry`)
- [instamatic.find_crystals_ilastik](#instamaticfind_crystals_ilastik) (`instamatic.processing.find_crystals_ilastik:main_entry`)
- [instamatic.grid_finder](#instamaticgridfinder) (`instamatic.grid.finder:main`)
- [instamatic.grid_monitor](#instamaticgridmonitor) (`instamatic.grid.monitor:main`)
- [instamatic.learn](#instamaticlearn) (`scripts.learn:main_entry`)

**Server**
Expand Down Expand Up @@ -546,6 +548,52 @@ d <path>`, `--output <path>`, `--output_name <path>`
: Generate `MapScaleInd.yaml` for `predicrystal` from config.


## instamatic.grid_finder

Determine the geometry and orientation of the copper grid using various methods.

**Usage:**
```bash
instamatic.grid_finder [-h] [FILEPATH] manual
instamatic.grid_finder [-h] [FILEPATH] auto [--idx IDX] [--arms ARMS]
[--order ORDER] [--offset OFFSET]
```
**Positional arguments:**

`FILEPATH`
: Path to the file where grid geometry is or is to be stored, typically `grid.yaml`.

**Optional arguments:**

`-h`, `--help`
: Show this help message and exit

`--idx`
: Target window index to find (default: next available window)

`--arms`
: Number of directions the sweep arms will go to find edge

`--order`
: For each order above 1, sweep also in previous orders midpoints

`--offset`
: Rotate the arms by this many degrees before first order search


## instamatic.grid_monitor

GUI program to monitor a grid.yaml file and plot any updates live.

**Usage:**
```bash
instamatic.grid_monitor [-h] [FILEPATH]
```
**Positional arguments:**

`FILEPATH`
: Path to the file where grid geometry is stored, typically `grid.yaml`.

## instamatic.learn

Predict whether a crystal is of good or bad quality by its diffraction pattern.
Expand Down
2 changes: 2 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,8 @@ publishing = [
"instamatic.defocus_helper" = "instamatic.gui.defocus_button:main"
"instamatic.find_crystals" = "instamatic.processing.find_crystals:main_entry"
"instamatic.find_crystals_ilastik" = "instamatic.processing.find_crystals_ilastik:main_entry"
"instamatic.grid_finder" = "instamatic.grid.finder:main"
"instamatic.grid_monitor" = "instamatic.grid.monitor:main"
"instamatic.learn" = "scripts.learn:main_entry"
# server
"instamatic.temserver" = "instamatic.server.tem_server:main"
Expand Down
22 changes: 22 additions & 0 deletions src/instamatic/grid/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from __future__ import annotations

from typing import Optional, Union

import numpy as np

Intercepts = dict[int, np.ndarray]


def cross2d(a: np.ndarray, b: np.ndarray) -> float:
"""A scalar 2d cross product between two arrays of length 2."""
return (a[0] * b[1] - a[1] * b[0]).item()


def versor(
*,
deg: Optional[Union[float, np.ndarray]] = None,
rad: Optional[Union[float, np.ndarray]] = None,
) -> np.ndarray:
"""A versor in the direction of angle expressed in radians or degrees."""
radians = np.deg2rad(deg) if rad is None else rad
return np.array([np.cos(radians), np.sin(radians)], dtype=float)
99 changes: 99 additions & 0 deletions src/instamatic/grid/artist.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
from __future__ import annotations

from typing import Optional

import numpy as np
from matplotlib import pyplot as plt
from matplotlib.axes import Axes
from matplotlib.figure import Figure
from matplotlib.patches import Polygon
from matplotlib.ticker import FuncFormatter

from instamatic._typing import float_nm
from instamatic.grid import Intercepts
from instamatic.grid.grid import PeriodicConvexPolygonGrid


def plot_grid(
grid: PeriodicConvexPolygonGrid,
*,
intercepts: Optional[Intercepts] = None,
limit_x: Optional[float_nm] = None,
limit_y: Optional[float_nm] = None,
ax: Optional[Axes] = None,
show_indices: bool = True,
show_intercepts: bool = True,
figsize: tuple[float, float] = (5, 5),
dpi: int = 100,
) -> tuple[Figure, Axes]:
"""Draw geometry with windows based on intercepts dict or limit_x/y."""

fig, ax = (ax.figure, ax) if ax else plt.subplots(figsize=figsize, dpi=dpi)

fig.patch.set_facecolor('black')
ax.set_facecolor('black')
ax.set_aspect('equal', adjustable='box')
ax.tick_params(colors='white', direction='out')
for spine in ax.spines.values():
spine.set_color('white')
ax.set_xlabel('x / um', color='white')
ax.set_ylabel('y / um', color='white')
ax.xaxis.set_major_formatter(FuncFormatter(lambda x, _: f'{x * 1e-3:g}'))
ax.yaxis.set_major_formatter(FuncFormatter(lambda y, _: f'{y * 1e-3:g}'))
ax.grid(True, which='major', color='white', linewidth=0.8, alpha=0.25, zorder=0)
ax.set_axisbelow(True)
patch_kw = {'facecolor': 'white', 'edgecolor': 'white', 'closed': True, 'zorder': 1}
text_kw = {'color': 'black', 'ha': 'center', 'va': 'center', 'fontsize': 10, 'zorder': 2}

indices = sorted(intercepts) if intercepts else list(range(25))
if limit_x is not None and limit_y is not None:
indices = grid.windows_in_limits(x=limit_x, y=limit_y)

cmap = plt.colormaps.get_cmap('tab10')
for idx in indices:
try:
window = grid.window(idx)
except ValueError: # negative/invalid Ulam index
color = '#808080'
else:
color = cmap(idx % 10)
corners = np.asarray(window.corners, dtype=float)
ax.add_patch(Polygon(corners, **patch_kw))
if show_indices:
cx, cy = map(float, window.center)
ax.text(cx, cy, str(idx), **text_kw)

if show_intercepts and intercepts and idx in intercepts:
xys = np.asarray(intercepts[idx], dtype=float)
ax.plot(
xys[:, 0],
xys[:, 1],
marker='x',
linestyle='',
color=color,
markersize=6,
zorder=5,
)

ax.relim()
ax.autoscale_view()

x0, x1 = ax.get_xlim()
y0, y1 = ax.get_ylim()
cx, cy = 0.5 * (x0 + x1), 0.5 * (y0 + y1)
r = 0.5 * max(x1 - x0, y1 - y0)
ax.set_xlim(cx - r, cx + r)
ax.set_ylim(cy - r, cy + r)

ax.set_autoscale_on(False) # Freeze limits so lines don't affect view
ax.axhline(0, color='white', linewidth=1.0, alpha=0.6, zorder=0)
ax.axvline(0, color='white', linewidth=1.0, alpha=0.6, zorder=0)

if limit_x is not None:
ax.axvline(-limit_x, color='red', linewidth=1.0, zorder=4)
ax.axvline(limit_x, color='red', linewidth=1.0, zorder=4)
if limit_y is not None:
ax.axhline(-limit_y, color='red', linewidth=1.0, zorder=4)
ax.axhline(limit_y, color='red', linewidth=1.0, zorder=4)

return fig, ax
Loading
Loading