Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
67 commits
Select commit Hold shift + click to select a range
300730f
resolve conflicts
wright-stuff Jun 3, 2025
f33c579
Merge branch 'main' of https://github.com/yaq-project/yaqd-pi
wright-stuff Jun 3, 2025
b0e7430
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 5, 2025
9452401
roi, logging works
wright-stuff Jun 5, 2025
24865d9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 5, 2025
df7ff3e
refactor (minor)
ddkohler Jun 6, 2025
e9f7068
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 6, 2025
aadb31f
wip playing with api
wright-stuff Jun 6, 2025
cc41344
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 6, 2025
49aa86c
measure working
wright-stuff Jun 9, 2025
eb4dd4f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2025
f9b606e
refactor properties
ddkohler Jun 9, 2025
0795f84
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2025
2ee63dc
call commit_parameters when safe
ddkohler Jun 9, 2025
056c92d
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2025
9c4b289
remove frame_processing_method
wright-stuff Jun 9, 2025
bfa56d5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 9, 2025
2c12e48
try to fix ci
ddkohler Jun 10, 2025
53f57d4
Update _pi_proem.py
ddkohler Jun 10, 2025
57341bc
mypy
ddkohler Jun 10, 2025
43c6791
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 10, 2025
2f6cace
Update _pi_proem.py
ddkohler Jun 10, 2025
8067a93
Merge branch 'fs-local' of https://github.com/wright-stuff/yaqd-pi in…
ddkohler Jun 10, 2025
7a74ba2
Update _pi_proem.py
ddkohler Jun 10, 2025
1104f2e
roi setter
ddkohler Jun 10, 2025
b66041c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 10, 2025
3ebeac0
Update _pi_proem.py
wright-stuff Jun 10, 2025
942406e
clean up messages, properties
wright-stuff Jun 10, 2025
bee5b8a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 10, 2025
68e9212
mypy
wright-stuff Jun 10, 2025
d1a9712
updated avpr
wright-stuff Jun 10, 2025
3d2c101
remove roi from state
wright-stuff Jun 10, 2025
2fcfa36
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 10, 2025
ad52b2a
fixes from testing
wright-stuff Jun 10, 2025
b4179c0
Merge branch 'fs-local' of https://github.com/wright-stuff/yaqd-pi in…
wright-stuff Jun 10, 2025
ea279f7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 10, 2025
88604f2
Update CHANGELOG.md
wright-stuff Jun 10, 2025
c8430e6
Merge branch 'fs-local' of https://github.com/wright-stuff/yaqd-pi in…
wright-stuff Jun 10, 2025
5ebcbf2
clean up
wright-stuff Jun 10, 2025
22dd8fa
Update temp.py
wright-stuff Jun 10, 2025
d5cb36d
refactor spectrometer params, remove spectral mapping
ddkohler Jun 11, 2025
f4466bd
initial testing
wright-stuff Jun 11, 2025
649ef5c
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 11, 2025
aade34d
Update _pi_proem.py
wright-stuff Jun 12, 2025
4e0be27
Update pyproject.toml
wright-stuff Jun 12, 2025
8f97e53
check mypy with correct import
wright-stuff Jun 12, 2025
93a623e
Merge pull request #16 from wright-stuff/from-fs
ddkohler Jun 12, 2025
84ab555
check types for emulated camera
ddkohler Jun 12, 2025
584ab4f
partial notes on wavelength assignments
ddkohler Jun 12, 2025
67f60ed
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 12, 2025
350823a
mypy
ddkohler Jun 12, 2025
d9de299
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 12, 2025
56506c6
gui first version
wright-stuff Jun 12, 2025
dfdaf3e
gui functional
wright-stuff Jun 12, 2025
3606516
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 12, 2025
2a23886
Update .pre-commit-config.yaml
ddkohler Jun 13, 2025
23e0880
Update .pre-commit-config.yaml
ddkohler Jun 13, 2025
f4fb2f0
Update .pre-commit-config.yaml
ddkohler Jun 13, 2025
e190d06
Update pyproject.toml
ddkohler Jun 13, 2025
8487102
Update gui.py
ddkohler Jun 13, 2025
aa554e7
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 13, 2025
f12011f
Update gui.py
wright-stuff Jun 13, 2025
442cd4f
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 13, 2025
f7c4757
update Readme
ddkohler Jun 14, 2025
8a9a8af
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 14, 2025
51db584
Merge pull request #19 from wright-stuff/from-fs
ddkohler Jun 16, 2025
4f3e1f3
update from fs-table testing (#22)
wright-stuff Sep 26, 2025
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
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ repos:
rev: v1.14.1
hooks:
- id: mypy
exclude: ^docs/conf.py
exclude: ^(docs/conf.py|scripts/)

- repo: https://github.com/yaq-project/yaq-traits
rev: v2023.6.0
Expand Down
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/).

## [Unreleased]

### Fixed
- daemon logging now works as intended (was hidded due to interference with dependency logging settings)
- `ReadoutCounts` works as intended for multiple frame collections

### Changed
- property methods are written dynamically
- `processing_method` is not longer available; mean processing is always used
- channel name `img` -> `mean`

### Added
- yaq properties for selected parameters
- initial release
35 changes: 33 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,37 @@ This package contains the following daemon(s):

- https://yaq.fyi/daemons/pi-proem

## maintainers
# yaqd-pi-proem: Spectrometer configuration

- [Jason Scheeler](https://github.com/jscheeler1)
Wavelength mappings are calculated using grating equation. The diffracted angle $\beta$, incidence angle $\alpha$, and wavelength $\lambda$ are related by:
$$ \frac{m \lambda}{d} = \sin \beta - \sin \alpha, $$
where $m$ is the diffraction order and $d$ is the grating groove spacing.
_Note that with a transmissive grating, the incidence angle is internal to the grating and will be affected by refraction._

A cone of diffracted rays are captured with a lens and Fourier mapped on the camera.
The lens and camera are aligned such that the normal ray hits the center of the camera.
The diffracted rays angles are related to the camera pixel position by:
$$ \delta x = f \tan \left( \beta - \beta_0 \right), $$

$$ \implies \beta = \beta_0 + \tan^{-1} \frac{\delta x}{f} $$
where $f$ is the focal length of the imaging optic, and $\beta_0$ the angle of the color that propagates along the optic axis of lens.
$\delta x$ is the position along the plane of the camera, relative to the $\beta_0$ ray.

Using both equations, we can relate imaging position to wavelength:
$$ \beta_0 + \tan^{-1} \frac{\delta x}{f} = \sin^{-1}\left(\frac{m\lambda}{d} - \sin \alpha \right) $$
or
$$ \lambda(\delta x; f, m, d, \alpha, \beta_0) = \frac{d}{m} \sin \left[ \beta_0 + \tan^{-1} \left(\frac{\delta x}{f}\right) \right] + \sin\alpha $$
To set these parameters, confer the configuration file schema.

## An example calibration routine

TODO
To perform a single-point calibration, send in a known wavelength and find the position on the camera.

A more convenient approximation measures displacement from the nominal ray.
Suppose the lens is aligned so that a normal ray hits the center of the camera.
We can then describe deviations from the camera center in terms of the normal ray:
$$ \delta x = f \tan \left(\beta - \beta_0 \right) $$
Where $\beta_0$ is the special normal ray (that in turn is related to a special color).
Since angles are small, we can use the paraxial approximation:
$$ \delta x \approx f \frac{\beta - \beta_0}{}$$
35 changes: 21 additions & 14 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,18 @@
requires = ["flit_core >=2,<4"]
build-backend = "flit_core.buildapi"

[tool.flit.metadata]
module = "yaqd_pi"
dist-name = "yaqd-pi"
author = "yaq developers"
home-page = "https://yaq.fyi"
description-file = "README.md"
[project]
name = "yaqd-pi"
authors = [
{name="Jason Scheeler"},
{name="Dan Kohler"}
]
readme = "README.md"
requires-python = ">=3.7"
requires = [
dependencies = [
"yaqd-core>=2021.2.0",
"instrumental<=0.7",
"scipy"
"instrumental-lib[cameras.picam]",
"numpy",
]
classifiers = [
"Development Status :: 2 - Pre-Alpha",
Expand All @@ -25,17 +26,23 @@ classifiers = [
"Programming Language :: Python :: 3.12",
"Topic :: Scientific/Engineering",
]
dynamic = ["version", "description"]

[project.scripts]
yaqd-pi-proem = "yaqd_pi._pi_proem:PiProem.main"

[tool.flit.metadata.urls]
[project.urls]
Homepage = "https://yaq.fyi"
Source = "https://github.com/yaq-project/yaqd-pi"
Issues = "https://github.com/yaq-project/yaqd-pi/issues"

[tool.flit.metadata.requires-extra]
[tool.optional-dependencies]
dev = ["black", "pre-commit"]
gui = ["yaqc", "matplotlib", "click"]

[tool.flit.scripts]
yaqd-pi-proem = "yaqd_pi._pi_proem:PiProem.main"

[[tool.mypy.overrides]]
module = ["instrumental.drivers.*"]
ignore_missing_imports = true

[tool.black]
line-length = 99
Expand Down
98 changes: 98 additions & 0 deletions scripts/gui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
from matplotlib.widgets import Slider, CheckButtons
from matplotlib.colors import Normalize, LogNorm
import matplotlib.pyplot as plt
import yaqc
import numpy as np
import click
import logging


log_level = logging.INFO
norm = Normalize


@click.command()
@click.option("--host", default="127.0.0.1", help="host of yaqd-pi-proem. defaults to 127.0.0.1")
@click.argument("port", type=int)
def main(port: int, host):
logger = logging.getLogger("GUI")
logger.setLevel(log_level)
logger.addHandler(logging.StreamHandler())

cam = yaqc.Client(port=port, host=host)

logger.info(cam)
x = cam.get_mappings()["x_index"]
y = cam.get_mappings()["y_index"]

fig, (ax, opt1, opt2, opt3) = plt.subplots(
nrows=4, height_ratios=[10, 1, 1, 1], gridspec_kw={"hspace": 0.1}
)

try:
y0 = cam.get_measured()["mean"]
except KeyError:
y0 = np.zeros((x * y).shape)
art = ax.matshow(y0, cmap="viridis")
fig.colorbar(art, ax=ax)

integration = Slider(
opt1, "integration time (ms)", 33, 1e3, valstep=1, valinit=cam.get_exposure_time()
)
acquisition = Slider(
opt2, "acquisitions (2^x)", 0, 8, valinit=int(np.log2(cam.get_readout_count())), valstep=1
)
measure_button = CheckButtons(opt3, labels=["call measure"], label_props=dict(fontsize=[20]))

state = {"current": 0, "next": 0}
title = "ID {}"

def update_plot(data):
if ax.get_title() != title.format(data["measurement_id"]):
try:
ax.set_title(f"ID {data['measurement_id']}")
if data["measurement_id"]:
art.set_data(data["mean"])
art.set_norm(norm())
except Exception as e:
logger.error("", exc_info=e, stack_info=True)
return
fig.canvas.draw_idle()

def submit(measure=False):
try:
if "call measure" in measure_button.get_checked_labels() or measure:
if state["current"] >= state["next"]:
state["next"] = cam.measure()
measured = cam.get_measured()
state["current"] = measured["measurement_id"]
if state["current"]:
update_plot(measured)
except Exception as e:
logger.error(state, exc_info=e)
if e == ConnectionError or e == ConnectionRefusedError:
pass

timer = fig.canvas.new_timer(interval=200)

@timer.add_callback
def update():
submit()

def update_integration_time(arg):
print(f"updating to {arg}")
cam.set_exposure_time(arg)

def update_acquisition(arg):
cam.set_readout_count(2**arg)

integration.on_changed(update_integration_time)
acquisition.on_changed(update_acquisition)

plt.tight_layout()
timer.start()
plt.show()


if __name__ == "__main__":
main()
74 changes: 74 additions & 0 deletions scripts/spectrometer_mapping.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# mypy: ignore-errors

# scratchwork for implementing spectrometer mapping
import numpy as np
from scipy.interpolate import interp1d

spec = dict(
gpmm=200,
grating_aoi_deg=5,
focal_length=75,
grating_refractive_index=1.6,
spectral_range=[0.36, 0.841],
)
mm_per_pixel = 0.016

groove_spacing = 1e3 / spec["gpmm"] # um
aoi = np.radians(spec["grating_aoi_deg"])
ws = np.linspace(spec["spectral_range"][0], spec["spectral_range"][1], 2048) # um
f = spec["focal_length"] # mm
n = spec["grating_refractive_index"]
# calculate diffraction angles
aods = np.arcsin(n * np.sin(aoi) - (ws / groove_spacing))
# convert angles to spatial positions
xs = f * np.tan(aods) # mm
# position (relative to 0 order transmission) = f * np.tan(np.arcsin(c1-lambda/a))
# position (relative to camera center) = f * np.tan(np.arcsin(c1-lambda/a)) + c2
# np.atan((x-c2) / f) = np.asin(c1-lambda/a)
# np.sin(np.atan((x-c2)/f)) = c1-lambda/a
# lambda = LHS
# reference position from minimum value
xs_rel = np.abs(xs - xs.min()) # mm
# map wavelengths onto detector by interpolating
spec_divided = xs_rel / mm_per_pixel
# inversion of grating equation--input position, output color
g = interp1d(spec_divided, ws)
num_pixels = np.round(spec_divided[0], 0)
pixels = np.arange(num_pixels)
out = g(pixels) * 1000

# out = out[
# self._mappings["x_index"][0][:]
# ] # keep horizontal mappings equal size so wt5 file doesn't get confused
# return np.round(out, 2) # account for physical orientation of camera


# from yaqd-wright-ingaas
def _gen_mappings(self):
"""Get map."""
# translate inputs into appropriate internal units
spec_inclusion_angle_rad = np.radians(self._config["inclusion_angle"])
spec_focal_length_tilt_rad = np.radians(self._config["focal_length_tilt"])
pixel_width_mm = 0.050 # 50 microns
# create array
i_pixel = np.arange(256) # 256 pixels
# calculate terms
x = np.arcsin(
(1e-6 * self._config["order"] * self._config["grooves_per_mm"] * self.spec_position)
/ (2 * np.cos(spec_inclusion_angle_rad / 2.0))
)
A = np.sin(x - spec_inclusion_angle_rad / 2)
B = np.sin(
(spec_inclusion_angle_rad)
+ x
- (spec_inclusion_angle_rad / 2)
- np.arctan(
(
pixel_width_mm * (i_pixel - self._config["calibration_pixel"])
+ self._config["focal_length"] * spec_focal_length_tilt_rad
)
/ (self._config["focal_length"] * np.cos(spec_focal_length_tilt_rad))
)
)
out = ((A + B) * 1e6) / (self._config["order"] * self._config["grooves_per_mm"])
return out
40 changes: 40 additions & 0 deletions scripts/temp.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
"""
simple imports to test functionality of instrumental-lib
this was used to inform on daemon methods like _measure
"""

from instrumental.drivers.cameras.picam import PicamEnums, PicamCamera, list_instruments, PicamError # type: ignore

deviceArray = list_instruments()
proem: PicamCamera = deviceArray[0].create()


def measure(n_frames, exposure_time_ms):
proem.params.ReadoutCount.set_value(n_frames)
proem.params.ExposureTime.set_value(exposure_time_ms) # ms
proem.commit_parameters()
readouts = [] # readouts[readout][readout_frame][frame roi]
running = True
proem._dev.StartAcquisition()
while running:
try:
# wait is blocking, so we might as well allow errors
available_data, status = proem._dev.WaitForAcquisitionUpdate(10)
except PicamError as e:
if e.code == PicamEnums.Error.TimeOutOccurred:
print(e)
else:
print(e)
proem._dev.StopAcquisition()
raise e
else:
running = status.running
print(status.running, available_data.readout_count)
if available_data.readout_count > 0:
readouts.extend(proem._extract_available_data(available_data, copy=True))
return readouts


# print(proem._dev.AreParametersCommitted())
# proem.commit_parameters()
# print(proem._dev.AreParametersCommitted())
Loading