> The scan server's Jython scripts are at `/home/controls/share/scan/` (resolves to
> `/media/ssd1/beamlines/share/scan/`). Phoebus source at `/home/controls/src/phoebus/`.
---
## 1. Executive Summary
@@ -327,52 +334,122 @@ This is used in the automated alignment sequence (alignment.py) when the chi axi
## 5. Data Flow in the IOC-Integrated Version
### 5.1 Scan Execution Sequence
### 5.1 The `WriteDataToPV` Jython Script — The Critical Bridge
The scan server does **not** directly write to FittingIOC PVs. Instead, it executes Jython scripts via `Script` commands. The `WriteDataToPV` script at `/home/controls/share/scan/writedatatopv.py` is the bridge:
context.write(self.pv,data,False)# False = no completion wait
```
**Key behavior:**`context.getData(device)` retrieves **all logged samples for that device** accumulated since the scan started. So at step N, it returns an array of N values (all motor positions logged so far, or all detector values). The script writes this **entire accumulated array** to the target PV.
This means:
-`Data:X` receives `[chi_0]`, then `[chi_0, chi_1]`, then `[chi_0, chi_1, chi_2]`, etc.
- For scalar detectors: `Data:Y` receives the same accumulated pattern
- For 2D detectors: `Data:XY` receives the **latest waveform** (context.getData returns the most recent logged waveform, not an accumulated array of waveforms)
### 5.2 Scan Execution Sequence (Corrected)
The `Alignment.createScan()` method (in `ScanTools/align/__init__.py`) constructs this scan sequence:
```python
# From Alignment.createScan() (lines 168-186)
x_pv=f'{self.fitting_pv}Data:X'
ifself.detector==resolveAlias('xy'):
y_pv=f'{self.fitting_pv}Data:XY'# route to Data:XY PV
elifself.detector==resolveAlias('xprofile'):
y_pv=f'{self.fitting_pv}Data:XPROFILE'# route to Data:XPROFILE PV
elifself.detector==resolveAlias('yprofile'):
y_pv=f'{self.fitting_pv}Data:YPROFILE'# route to Data:YPROFILE PV
else:
y_pv=f'{self.fitting_pv}Data:Y'# standard scalar path
loop_body=[
wait,# Wait for pcharge criteria
Log(log_devs),# Log all devices to scan data
Script('WriteDataToPV',[self.motor,x_pv]),# Write motor positions → Data:X
Alignment.scan() returns → data table from scan_client.getData()
│
▼
AlignmentIOC reads fit results:
Fit:Pos = optimal chi position
Fit:Amp, Fit:Wid, Fit:Base = Gaussian parameters
State = OK/Error
Alignment.align() checks detector type:
if detector in ['xy','xprofile','yprofile']:
x = y = None # skip adjustData — IOC has accumulated Data:Y
else:
(x, y) = adjustData(data) # extract from scan log, normalize
│
▼
If MoveToCenter: caput(BL4B:Mot:chis, Fit:Pos)
Alignment.fit(x, y):
if x is not None and y is not None:
caput(Data:X, x) # overwrite with potentially filtered data
caput(Data:Y, y) # overwrite with normalized data
# For 2D detectors: x=y=None, so skip — IOC has our data already
caput(Method, self.method, wait=True) # triggers final fit with completion
position = caget(Fit:Pos)
│
▼
If MoveToCenter: Alignment.setDevice(motor, position)
```
### 5.2 What Gets Accumulated in Data:Y
@@ -409,23 +486,43 @@ The most recent commit on issue/4850 is marked "glitched," indicating incomplete
-**09cd8b8** ("add perform_fitXdist_for_chis()"): Simplified chi_scan.py, removed the standalone `perform_fitXdist_for_chis()` function
-**e1ccfb0** ("glitched"): Changed `if True:` to `if FitXdist:` in `extract_XProfile_pos()`, added `else: plt.cla()` branches
### 6.2 Syntax Issue at Line 390
### 6.2 `chifit` Undefined Variable in Scantools chi_scan.py
**Critical bug.** In the scantools `issue/4850` copy of `chi_scan.py` (at `/home/controls/common/scantools/issue/4850/ioc/fit/chi_scan.py`), the `extract_XY_pos()` function references `chifit` at lines 149 and 164, but this variable is never defined within that function:
In the commented-out code block, line 390 has a malformed comment:
This should be `##` not `#3`. This is in dead code (doubly-commented) so it has no runtime effect, but indicates hasty editing.
### 6.3 `Data:Y` Accumulation Semantics
The bl4b repo copy has `if FitXdist:` guarding this code, which means it only runs when FitXdist=True. But `chifit` is still referenced in the return statement. In the bl4b copy, the `FitImage` code path sets `chifit` when it runs, and there's an `else: chifit=0.0` fallback — but this only works if the `if FitImage:` block precedes the `if FitXdist:` block.
**Potential discrepancy:** When the scan server writes `Data:Y` directly (the normal, non-chi case), the `write()` handler replaces `Data:Y` with the incoming value and computes `Data:Err = sqrt(value)`. But when a waveform PV (XPROFILE/XY/YPROFILE) triggers `trigger_data_to_y_and_err()`, the function *appends* to the existing `Data:Y`.
**Impact:** When using `extract_XY_pos()` with the scantools version, this will raise a `NameError` at runtime if `FitImage=False` (which it is by default). The bl4b version partially fixes this but the code paths are fragile.
This means:
- If the scan server also writes `Data:Y` (as it normally does for the detector counts), the two paths would conflict — the scan server would overwrite the accumulated SigmaX values
- The current code handles this by having `Data:Y`'s write handler call `callbackPV` in `finally`, which signals completion. If the detector PV writes happen *before*`Data:Y` writes, the accumulated values could be overwritten
### 6.3 Two Copies of chi_scan.py — Divergence Risk
There are **two copies** of `chi_scan.py`:
1.`/home/controls/common/scantools/issue/4850/ioc/fit/chi_scan.py` — the scantools repo version (199 lines)
2.`/home/controls/bl4b/applications/bl4b-ScanSupport/iocBoot/bl4b-Fit/chi_scan.py` — the bl4b repo version (454 lines)
These have already diverged. The bl4b version has more code (includes `FitImage` mode logic, `FitXdist` conditional), while the scantools version has `perform_fitXdist_for_chis()` (which references undefined `curve_fit`, `ChiRange`, `ChiValue`). The bl4b `st.cmd` uses the local copy, not the symlinked scantools version. **Future changes must be coordinated across both repos.**
### 6.4 `Data:Y` Accumulation — Resolved by Design
**This is likely the source of one of the observed discrepancies.** The scan configuration needs to ensure that only the waveform detector PV is logged (not the normal scalar detector), or the `Data:Y` write handler needs to be aware of chi-scan mode and not overwrite.
**Previously hypothesized as a problem, now confirmed as intentionally correct:**
The `Alignment.createScan()` method routes detector writes to different PVs based on detector type. When `detector='xprofile'`, the `WriteDataToPV` script writes to `Data:XPROFILE` (not `Data:Y`). The FittingIOC's `trigger_data_to_y_and_err()` then internally appends sigma values to `Data:Y`.
Since the scan server never writes to `Data:Y` for 2D/1D detectors, there is no conflict. However:
- The `WriteDataToPV` script calls `context.write(Data:XPROFILE, data, False)` with `False` (no completion wait). The scan server then separately executes `Wait(Data:XPROFILE, None)` to wait for the FittingIOC's `callbackPV()`.
- The FittingIOC's `write('Data:XPROFILE')` handler does a **second `caget()`** to fetch the real waveform data from the detector PV. This is because `context.write()` may not deliver the full waveform — it writes whatever `context.getData()` returned from the scan log, which may be a summary.
**Remaining concern:** The `context.getData(detector)` call in `WriteDataToPV` retrieves accumulated scan log data. For waveform PVs logged via `Log()`, the scan server stores the full array in its Derby database. But the FittingIOC ignores the incoming value and re-fetches via `caget()` anyway. This double-fetch is intentional but inefficient — the comment says "get vector from PV b/c scan server wont."
### 6.4 Module-Level Flags Are Static
@@ -442,7 +539,16 @@ These are set at module import time and cannot be changed via PVs at runtime. Th
- The flags must be edited in chi_scan.py and the IOC restarted
- A future enhancement should expose these as PVs or derive them from the scan configuration
### 6.5 Diagnostic Image Size Mismatch
### 6.5 `perform_fitXdist_for_chis()` Has Undefined References
The scantools version of `chi_scan.py` includes `perform_fitXdist_for_chis()` (lines 166-198) which is intended to fit the accumulated chi-vs-sigma data. This function references:
-`curve_fit` — imported as `scipy.optimize.curve_fit` in the bl4b version but **commented out** (`##from scipy.optimize import curve_fit`) in the scantools version
-`ChiRange` — a module-level constant that was commented out
-`ChiValue` — a loop variable from the original Spyder script that doesn't exist in the module
This function is not currently called from `FittingIOC.py`. The post-loop fit is instead handled by `trigger_fit()` which uses the `fittings` module. The `perform_fitXdist_for_chis()` function appears to be dead code left from an incomplete attempt to do custom post-loop fitting.
### 6.6 Diagnostic Image Size Mismatch
The `Diag:XY` PV is sized at 640×480 (307,200 elements), but `get_img_array_from_fig_iobuf()` returns whatever size the matplotlib figure canvas produces. The `reshape(w, h)` uses the canvas dimensions, which may not be 640×480 unless the figure is explicitly sized. If the figure size doesn't match, the PV write could fail or produce garbled output.
@@ -583,7 +689,107 @@ The integration strategy should follow the same decomposition:
---
## 10. File Inventory
## 10. Scan Server Infrastructure
### 10.1 Scan Server Process
The scan server is a Java application running on the instrument control system:
```
java -jar service-scan-server-4.6.4.jar \
-settings /home/controls/css/phoebus.ini \
-settings /home/controls/bl4b/css/phoebus.ini \
-config /home/controls/bl4b/scan_config.xml
```
It exposes an HTTP REST API (used by PyScanClient) and executes scan command sequences (Loop, Set, Wait, Log, Script, etc.). The scan server maintains an internal Derby database for logged data.
### 10.2 `scan_config.xml` — Scan Server Configuration
The configuration at `/home/controls/bl4b/scan_config.xml` defines:
-**79 named PVs** with optional aliases — these are the PVs the scan server knows about
**Important:**`Data:X`, `Data:Y`, `Data:XY`, `Data:XPROFILE` are **not** listed in scan_config.xml. They are accessed via the `WriteDataToPV` Jython script, which uses `context.write()` with full PV names passed as arguments.
### 10.3 Jython Script Commands
The scan server can execute Jython classes that implement `org.csstudio.scan.command.ScanScript`. Key scripts:
| `post_scan.scn` | `/home/controls/bl4b/scan/post_scan.scn` | Post-scan: set run control stop |
### Spyder Scripts (reference originals)
| File | Path | Integration Status |
@@ -622,19 +848,21 @@ The integration strategy should follow the same decomposition:
---
## 11. Tasks for Future Agents
## 12. Tasks for Future Agents
### Immediate (Complete CHI Scan Integration)
1.**Resolve `Data:Y` conflict** — determine whether the scan server also writes `Data:Y` when a waveform detector is selected, and if so, prevent overwriting the accumulated sigma values. May need to conditionally skip the normal `Data:Y` write path when in chi-scan mode.
1.**Fix `chifit` undefined variable** — in both copies of `chi_scan.py`, the `extract_XY_pos()` function references `chifit` before it is defined when `FitImage=False`. The bl4b version partially guards this with `if FitXdist:` but still returns `chifit` which may be unset. Add `chifit = 0.0` initialization at the top of `extract_XY_pos()`.
2.**Unify the two chi_scan.py copies** — the bl4b and scantools repos have diverged. Decide which is canonical and ensure the other is a symlink or identical copy. The `st.cmd` PYTHONPATH currently uses the local bl4b copy.
2.**Verify fit function compatibility** — confirm that the `fittings` module's "Gauss+const" or "Gauss+slope" methods produce equivalent results to the Spyder script's `Gauss_ConstantBkg` for fitting sigma-vs-chi curves.
3.**Fix `perform_fitXdist_for_chis()` or remove it** — the scantools version has this function but it references undefined variables (`curve_fit`, `ChiRange`, `ChiValue`). Either fix the imports and integrate it into the workflow, or remove it as dead code.
3.**Add runtime mode selection** — expose `FitXdist`/`FitImage`/`FitYdist`/`FitIdist` as PVs or derive from configuration, so operators don't need to edit code and restart the IOC.
4.**Verify fit function compatibility** — confirm that the `fittings` module's "Gauss+const" or "Gauss+slope" methods produce equivalent results to the Spyder script's `Gauss_ConstantBkg` for fitting sigma-vs-chi curves.
4.**Fix `Diag:XY` sizing** — ensure the matplotlib figure is sized to match the 640×480 PV allocation, or make the PV size dynamic.
5.**Fix `Diag:XY` sizing** — ensure the matplotlib figure is sized to match the 640×480 PV allocation, or make the PV size dynamic.
5.**Test with real beam data** — run a chi scan from the alignment OPI and compare results to a simultaneous Spyder execution. Verify:
6.**Test with real beam data** — run a chi scan from the alignment OPI and compare results to a simultaneous Spyder execution. Verify:
- Same sigma values at each chi position
- Same fitted chi optimum
- Correct error bars
@@ -658,7 +886,7 @@ The integration strategy should follow the same decomposition:
---
## 12. PV Reference
## 13. PV Reference
### Motor PVs
@@ -711,7 +939,7 @@ The integration strategy should follow the same decomposition: