Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7c6cb9e
Update TCubeLaserControl.jl documentation for closed-loop calibration…
AliKNS Sep 29, 2026
3c54051
Update DAQmx source to track the main branch
AliKNS Sep 29, 2026
258c42e
TCube laser: closed-loop (power) mode, DiodeLaser interface, hardware…
AliKNS Sep 29, 2026
db046c2
TCube closed loop: ramp upward setpoints; verified to 40 mW on the 64…
AliKNS Sep 29, 2026
40260f7
Record the Kinesis handover power-cycle and the ramp's USB-bound speed
AliKNS Sep 29, 2026
9689d22
TCube closed loop: single-write setpoints by default, ramp kept as fa…
AliKNS Sep 29, 2026
7c93add
642 nm rig ceiling: HL6366DG rating, path transmission 0.93, 70 mW / …
AliKNS Sep 29, 2026
10f1aba
CLAUDE.md: DiodeLaser, the two regulation modes, SimDiodeLaser and th…
AliKNS Sep 29, 2026
e17155a
Merge main (0.2.4 and the 0.3rc1 fold, #69) into laser-642-power-mode
kalidke Sep 29, 2026
e07069f
TCubeLaser: max_current has no default in closed loop; test light_on'…
kalidke Sep 29, 2026
f89e5f7
Keep 0.2.4 working: mode defaults to ConstantCurrent, setpower forwar…
kalidke Sep 29, 2026
d6344b8
Fold #66's changelog into Unreleased as a non-breaking 0.2.5 change; …
kalidke Sep 29, 2026
4a1439e
Pass max_current in the two remaining closed-loop test constructions
kalidke Sep 29, 2026
b4c16da
TCubeLaser.initialize leaves the output off and lowers an open-loop c…
kalidke Sep 29, 2026
e769cce
Closed-loop light_on and setoutputpower! re-check the controller befo…
kalidke Sep 29, 2026
2360e1c
Move the ramp to per-laser PhotodiodeLoop fields, ramp from what the …
kalidke Sep 29, 2026
c962c3c
Diode laser panel: the output toggle's label follows the driver, not …
kalidke Sep 29, 2026
eabb0f8
TCube docs: placeholder serials, cut the rig diary from CALIBRATION.m…
kalidke Sep 29, 2026
e36d3e0
Power-mode clamp re-check catches a one-step pot drift; clamp recorde…
kalidke Sep 29, 2026
891193a
SimDiodeLaser: constructed exactly as TCubeLaser
kalidke Sep 29, 2026
4720e1b
TCubeLaser: one cleanup helper for a failure while the diode may be l…
kalidke Sep 29, 2026
6e4f535
TCubeLaser: default properties back to 0.2.4's; drop the power_unit c…
kalidke Sep 29, 2026
c0231af
Open loop: warn below the pot's floor instead of failing; lowering by…
kalidke Sep 29, 2026
29ee845
Open-loop and closed-loop sends follow what the driver knows; correct…
kalidke Sep 29, 2026
44b4fc8
Share the diode-laser keyword rules; reword the SimDiodeLaser claim; …
kalidke Sep 29, 2026
c17d729
Merge branch-model-main (8342359, #69's head): CLAUDE.md states 0033'…
kalidke Sep 29, 2026
fe5bb10
Open loop never newly fails at the pot; setcurrent! reads the output …
kalidke Sep 29, 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
235 changes: 235 additions & 0 deletions CHANGELOG.md

Large diffs are not rendered by default.

7 changes: 5 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ What CI actually runs, deliberately thin (`.github/workflows/CI.yml`):
Batch fixups into one push rather than pushing each review round separately;
every push to an open pull request starts a fresh run.

Tests use simulated devices only (`SimCamera`, `SimStage3d`/`SimStage2d`/`SimStage1d`, `SimLight`) - no hardware required. GLMakie needs a display: run under `xvfb-run -a` on a headless Linux box (CI does this). Test sets: "Simulated Camera", "Simulated Stage", "Simulated Light Source", "Export State".
Tests use simulated devices only (`SimCamera`, `SimStage3d`/`SimStage2d`/`SimStage1d`, `SimLight`, `SimDiodeLaser`) plus a fake Kinesis SDK (`test/tcube_fake_sdk.jl`) that the real `TCubeLaser` driver runs against - no hardware required. GLMakie needs a display: run under `xvfb-run -a` on a headless Linux box (CI does this). Test sets: "Simulated Camera", "Simulated Stage", "Simulated Light Source", "Export State".

## Architecture

Expand All @@ -64,7 +64,8 @@ MicroscopeControl.jl uses a **three-layer architecture** leveraging Julia's mult
│ Abstract types + contracts │ Concrete device drivers │
│ - CameraInterface │ - SimulatedCamera, DCAM4 │
│ - StageInterface │ - SimulatedStage, PI, MCL │
│ - LightSourceInterface │ - SimulatedLight, TCube │
│ - LightSourceInterface │ - SimulatedLight, TCube, │
│ (+ DiodeLaser) │ SimDiodeLaser │
│ - DAQInterface │ - NIDAQcard │
│ - SLMInterface │ - OK_XEM (FPGA) │
│ - AttenuatorInterface │ - LCC1620 │
Expand Down Expand Up @@ -101,6 +102,8 @@ Interfaces define method signatures with throwing `error("<name> not implemented

**LightSource**: `setpower`, `light_on`, `light_off`

**DiodeLaser** (`<: LightSource`; `TCubeLaser{M}`, `SimDiodeLaser{M}` with `M` = `ConstantCurrent` or `ConstantPhotocurrent`, fixed at construction, `mode=` required): `setcurrent!` (mA, `ConstantCurrent` only), `setoutputpower!` (mW at the laser output, `ConstantPhotocurrent` only), `setlevel!` (0..1 of the declared range, both), `measured_current`, `measured_photocurrent`, `indicated_output_power`, `loop_status`, `regulation_mode`, `supported_modes`. `setpower` throws on a `DiodeLaser`. Mode-shared methods are written against the bare `TCubeLaser`, mode-specific ones against `TCubeLaser{ConstantCurrent}` / `{ConstantPhotocurrent}`; never a `where M` method on a generic `test/contract.jl` checks. `subtypes` is one level deep, so the contract test and API map walk to the leaves (`device_types`). Closed-loop calibration and the 642 nm rig's measured facts: `src/hardware_implementations/tcube_laser/CALIBRATION.md`.

**DAQ**: `showdevices`, `showchannels`, `createtask`, `setvoltage`, `readvoltage`, `deletetask`

**Attenuator**: `setdrivevoltage`, `getdrivevoltage`, `settransmission`, `gettransmission`, `set_calibration!`
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,8 @@ MicroscopeControl.jl is organized to ensure scalability and easy integration of
- Simulated stage for testing

### Light Sources
- Thorlabs TCube laser diode controller
- Thorlabs TCube laser diode controller (TLD001), open loop (constant current) or closed loop on the monitor photodiode (see `src/hardware_implementations/tcube_laser/CALIBRATION.md`)
- Simulated laser diode (`SimDiodeLaser`) for testing either mode
- CrystaLaser 561nm
- Vortran 488nm laser
- Simulated light source for testing
Expand Down
79 changes: 58 additions & 21 deletions skills/mc-api-map/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,29 @@ not necessarily the one you are about to call. The case that exposed this was
sth)`, the map listed that and nothing else, and the 1-arg `export_state(laser)` every
lifecycle loop calls fell through to the throwing `AbstractInstrument` stub.
**[fixed in v0.2.3]** — `export_state(::TCubeLaser)` exists and
`which(export_state, Tuple{TCubeLaser})` lands on it. The 2-arg method is still
there as a deprecated forwarder that warns and delegates (removal scheduled for
a future 0.3.0), so the map may still list *that* signature: adding the 1-arg
method was the fix, not deleting the other one. The blind spot
is a property of the generator rather than of that driver, and an installed map
generated against an older pinned tag still shows the old signature. `light_on` is a
live example (below). When the listed signature is not the one you are calling, check
the exact tuple with `hasmethod`/`which`.
`which(export_state, Tuple{TCubeLaser})` lands on it. v0.2.3 kept the 2-arg method
as a deprecated forwarder; **[guarantee]** 0.2.5 keeps it (deprecated, removed
at the next breaking release), so `hasmethod(export_state, Tuple{TCubeLaser,Any})`
is still true and an installed map lists both signatures. The blind spot is a property of the generator
rather than of that driver, and an installed map generated against an older pinned
tag still shows the old signature. A current example: the generator's inherited
check asks about the 1-arg tuple only, so a 2-arg generic whose only method is on an
abstract intermediate (`setpower(::DiodeLaser, ::Float64)`, the deprecated forwarder in open loop and refusal in closed loop, and
`setlevel!(::DiodeLaser, ::Float64)`, the shared implementation) does not appear in a
`TCubeLaser` or `SimDiodeLaser` section at all. When the listed signature is not the
one you are calling, check the exact tuple with `hasmethod`/`which`.

**[guarantee]** Parametric devices are listed once, under the bare name
(`### TCubeLaser`), and a method written against one instantiation is printed with
it: `setcurrent!(TCubeLaser{ConstantCurrent}, Float64)`,
`setoutputpower!(TCubeLaser{ConstantPhotocurrent}, Float64)`. A method written
against the bare type (`light_on(TCubeLaser)`) serves both modes. The generator
walks the type tree to its non-abstract leaves, so drivers beneath the abstract
intermediate `DiodeLaser` are listed under `## LightSource`.
**[limitation]** `InteractiveUtils.subtypes` is one level deep: downstream code that
enumerates lights with `subtypes(LightSource)` gets `DiodeLaser` in place of
`TCubeLaser` and `SimDiodeLaser`, silently. Walk to the leaves
(`isabstracttype(S) ? recurse : keep`), as the map generator and `test/contract.jl` do.

**[limitation]** `MLSLM` and `Triggerscope4` sit outside the `AbstractInstrument` hierarchy (`SLM`
and `TRIG` are `abstract type ... end` with no supertype). A lifecycle name missing
Expand All @@ -129,20 +144,27 @@ where dispatch lands and whether that is the concrete type:
```julia
using MicroscopeControl

# true for every LightSource, because the interface stub exists
# false from 0.2.5: the 2-arg light_on stub was removed (up to v0.2.x it was true
# for every LightSource and landed on a throwing stub)
hasmethod(light_on, Tuple{TCubeLaser,Float64})
# -> true
# -> false

# where the call actually goes
which(light_on, Tuple{TCubeLaser,Float64}).sig
# -> Tuple{typeof(light_on), LightSource, Float64} (the throwing stub: no
# driver implements 2-arg light_on)
# resolves, but lands on the DiodeLaser-level method: on ConstantCurrent a deprecated
# forwarder to setcurrent!, on ConstantPhotocurrent a refusal naming setoutputpower! / setlevel!
which(setpower, Tuple{TCubeLaser{ConstantCurrent},Float64}).sig
# -> Tuple{typeof(setpower), DiodeLaser, Float64}

# mode-specific: exists only on the instantiation for that mode
hasmethod(setcurrent!, Tuple{TCubeLaser{ConstantPhotocurrent},Float64})
# -> true, but it is the throwing DiodeLaser stub; check which(...).sig
which(setcurrent!, Tuple{TCubeLaser{ConstantCurrent},Float64}).sig
# -> Tuple{typeof(setcurrent!), TCubeLaser{ConstantCurrent}, Float64}

which(export_state, Tuple{TCubeLaser}).sig
# -> Tuple{typeof(export_state), TCubeLaser} (real driver code from v0.2.3;
# up to v0.2.2, the throwing stub.
# Tuple{TCubeLaser,Any} still resolves,
# to the deprecated forwarder)
# Tuple{TCubeLaser,Any} is the
# deprecated forwarder, kept in 0.2.5)

which(getdata, Tuple{SimCamera}).sig
# -> Tuple{typeof(getdata), SimCamera} (real driver code)
Expand All @@ -156,13 +178,22 @@ in the upstream repo uses:

```julia
has_specific(f, T, args...) =
hasmethod(f, Tuple{T, args...}) && which(f, Tuple{T, args...}).sig.parameters[2] === T
hasmethod(f, Tuple{T, args...}) &&
Base.unwrap_unionall(which(f, Tuple{T, args...}).sig).parameters[2] === T

has_specific(initialize, SimCamera) # true
has_specific(initialize, ThorCamCSCCamera) # false
has_specific(setpower, SimLight, Float64) # true
has_specific(setpower, TCubeLaser, Float64) # false: lands on the DiodeLaser method
has_specific(light_on, TCubeLaser) # true: mode-shared, written on the bare type
has_specific(setcurrent!, TCubeLaser{ConstantCurrent}, Float64) # true: mode-specific
```

The `unwrap_unionall` matters from 0.2.5: a `where`-method has a `UnionAll`
signature whose `.parameters` throws. Pass the bare `TCubeLaser` for a mode-shared
method and the instantiation for a mode-specific one; `has_specific(setcurrent!,
TCubeLaser, Float64)` is false because no method is written on the bare type.

For `gui` the right question is "does dispatch avoid the `AbstractInstrument`
stub", since the interface-level `gui` is the intended shared implementation:
`which(gui, Tuple{T}).sig.parameters[2] !== AbstractInstrument`.
Expand All @@ -178,10 +209,16 @@ per device.

## Arity traps the map makes visible

- **[limitation]** `light_on` is declared `light_on(::LightSource, ipower::Float64)` at the interface
and implemented as `light_on(::T)` by all five drivers. The 2-arg call throws for
every light source. The map lists `light_on(TCubeLaser)` etc. as device-specific;
a 2-arg form never appears.
- **[guarantee]** `light_on` takes the light only. Up to v0.2.4 the interface
declared `light_on(::LightSource, ipower::Float64)`, which no driver implemented
and which threw for every light; 0.2.5 replaced it with a 1-arg stub, so the 2-arg
call is now a plain `MethodError`. Set the level first, then `light_on`.
- **[guarantee]** `setpower` on a `DiodeLaser` (`TCubeLaser`, `SimDiodeLaser`)
resolves to the `DiodeLaser` method: on a `ConstantCurrent` laser it forwards to
`setcurrent!` (mA) with a deprecation warning; on a `ConstantPhotocurrent` laser it
throws and names `setoutputpower!` (mW at the laser output) and `setlevel!`
(unit-free `0..1`). It is unchanged on
the other lights. None of this shows in the map (the 2-arg blind spot above).
- `move` takes `Float64` positions. `move(stage, 1, 2, 3)` with integers is a
`MethodError`, not a stub error.
- The SmarAct `MCS2Stage` has `move(MCS2Stage, Float64, Float64[, Float64])` in
Expand Down
43 changes: 43 additions & 0 deletions skills/mc-api-map/references/gui-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,4 +75,47 @@ panel is observably read-only. The fix is scoped to the shared light panel;
it is not a guarantee about the others, and `gui(::DAQ)` still calls
`showdevices`/`showchannels` at construction.

From 0.2.5 this panel serves only the lights that are not a `DiodeLaser`
(`CrystaLaser`, `VortranLaser`, `DaqTrLight`, `SimLight`); a `DiodeLaser` has its
own, below.

## `gui(::DiodeLaser)` (0.2.5)

Dispatches on `regulation_mode(laser)` to `current_panel` (`ConstantCurrent`:
slider in mA over `min_current .. effective_max_current(laser)`, calling
`setcurrent!`) or `power_panel` (`ConstantPhotocurrent`: slider in mW **at the
laser output** over `properties.min_power .. max_power`, calling
`setoutputpower!`, with a basis line naming `wa_calibration`, the TIA range and
the TEC state; the range is printed as `[lo mW - hi mW]`). Enumerated from
`lightsource_interface/diode_laser_gui.jl`; `TCubeLaser` and `SimDiodeLaser`
carry every field.

| Field | Read by | Notes |
|---|---|---|
| `unique_id::String` | header label and window title | both panels |
| `properties.is_on` | initial state of the on/off toggle | both panels; the toggle, not the slider's bottom, is "off" |
| `properties.min_power`, `max_power` | slider range and textbox bounds | `power_panel` only; mW at the laser output |
| `min_current` | slider floor | `current_panel` only |
| `effective_max_current(laser)` (a method) | slider ceiling | `current_panel` only; falls back to `max_current`, `TCubeLaser` takes the smallest of `max_current`, `controller_max_current`, `max_setcurrent` |
| `threshold_current` | header text (`"unknown"` when `NaN`) | `current_panel`; also `loop_status`'s `below_threshold` |
| `drive_current` | slider start and "commanded" line | `current_panel` only |
| `pd.wa_calibration`, `pd.tia_range`, `pd.tec_stabilised` | basis line; `wa_calibration` also for the readout's indicated power | `power_panel` only |
| `pd.output_power_requested`, `pd.photocurrent_requested` | slider start and "commanded" line | `power_panel` only |

`properties.power` is **not** read by either panel.

Methods called, each only on a user action: `setcurrent!`/`setoutputpower!` (slider
or textbox), `light_on`/`light_off` (toggle), `loop_status` (the Read button, or
the Poll toggle at 2 Hz, which stops when turned off or the window closes).

`[guarantee]` Opening either panel issues **no command and no read**: the readout
shows "not read yet" until Read is pressed or Poll (off at construction) is
turned on. On a `SimDiodeLaser` this is checkable: `laser.log` gains nothing when
the panel opens. `[guarantee]` The textbox accepts only a number within the
slider's range, the same bounds the driver enforces; anything else is not
applied and the border turns red. An entry moves the slider, so it issues exactly
one command. The "commanded" line is refreshed from the device's fields only after
the command returns, and a refusal is shown in the panel as `refused: <message>`,
not thrown out of the callback.

`gui(::TRIG)` exists for `Triggerscope4`; `MLSLM` has no `gui` at all.
Loading
Loading