Compare commits

..

51 Commits

Author SHA1 Message Date
x12sa
38a325b12f final cleanup config files smaract
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m29s
CI for csaxs_bec / test (push) Successful in 1m30s
2026-01-27 18:24:07 +01:00
x12sa
5d2de8a021 mod filter trans to check CCM in use 2026-01-27 18:24:07 +01:00
x12sa
68e7222668 updated dev config 2026-01-27 18:24:07 +01:00
x12sa
4c8e56422c moved axis mapping to config file 2026-01-27 18:24:07 +01:00
x12sa
de2f58f170 mods after discussing at beamline 2026-01-27 18:24:07 +01:00
x12sa
cba891e5f1 added to user template 2026-01-27 18:24:07 +01:00
x12sa
4f37175439 added sastt config file 2026-01-27 18:24:07 +01:00
x12sa
86b8730bc1 changed strategy for config file structure 2026-01-27 18:24:07 +01:00
x12sa
6aefbcb2a4 fixes during commissioning 2026-01-27 18:24:07 +01:00
x12sa
a54c3e05a0 first fixes commissioning with hardware 2026-01-27 18:24:07 +01:00
x12sa
17102d455a Fix Smaract wait for connection 2026-01-27 18:24:07 +01:00
x01dc
4f7746453e added readback method for fil_trans. untested. 2026-01-27 18:23:39 +01:00
x01dc
634ffe16b5 initial version of fil_trans 2026-01-27 18:23:39 +01:00
x01dc
b37e430e58 initial version of fil_trans 2026-01-27 18:23:39 +01:00
9ceb5fc38b more attenuation data 2026-01-27 18:23:39 +01:00
b217264f07 attenuation data 2026-01-27 18:23:39 +01:00
x01dc
3a774f92ae csaxs master class added and init smar positions 2026-01-27 18:23:39 +01:00
x01dc
49b4ce7d1d init function first version, untested 2026-01-27 18:23:39 +01:00
x01dc
bcaa093a6a update smaract test config for all devices 2026-01-27 18:23:39 +01:00
x12sa
8cccb8e5aa initial config for smaract ES hutch 2026-01-27 18:23:39 +01:00
153e6a89a4 test: add test for controller to call destroy controller.off
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m32s
2026-01-26 07:45:01 +01:00
628e31da60 fix(controller): Ensure that destroy calls controller.off 2026-01-26 07:45:01 +01:00
6647140d43 w
Some checks failed
CI for csaxs_bec / test (pull_request) Successful in 1m30s
CI for csaxs_bec / test (push) Has been cancelled
2026-01-23 15:25:52 +01:00
b48b27114d cleanup
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m27s
CI for csaxs_bec / test (push) Successful in 1m33s
2026-01-23 15:16:18 +01:00
11c887b078 feat(debug-tools): add debug tools and adjust logic from beamline tests
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m27s
CI for csaxs_bec / test (push) Successful in 1m30s
2026-01-23 15:00:35 +01:00
bfcecd73c2 refactor(mcs-ddg): cleanup and fix mcs and ddg from beamline tests 2026-01-23 13:29:43 +01:00
146b10eb85 tests: fix tests for ddg and mcs integrations 2026-01-23 13:29:43 +01:00
48ad1b334c docs: Add documentation to MCS and DDG modules 2026-01-23 13:29:43 +01:00
188e23df48 fix: Fix MCS card and DDG implementation after testing with hardware at cSAXS 2026-01-23 13:29:43 +01:00
14c56939bf fix(ddg): adapt DDG, remove mcs.readytoread 2026-01-23 13:29:43 +01:00
ef0c31c8dc refactor(mcs-card): adjust mcs card to only have mca channels. 2026-01-23 13:29:43 +01:00
2cf2f4b4e4 test(controller): Fix test for controller wait_for_connection
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m16s
CI for csaxs_bec / test (push) Successful in 1m26s
2026-01-16 10:52:24 +01:00
1a9a0beb86 fix(controller): Ensure wait_for_connection calls controller.on()
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m13s
CI for csaxs_bec / test (pull_request) Successful in 1m19s
2026-01-15 18:09:34 +01:00
9f9aef348a some more docs
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m16s
CI for csaxs_bec / test (push) Successful in 1m18s
2026-01-08 12:36:14 +01:00
x01dc
f7a313b37f added file selection in gui docs
Some checks failed
CI for csaxs_bec / test (push) Has been cancelled
2026-01-08 12:06:47 +01:00
7326c471f8 test(falcon): fix test for improved patched_device method in ophyd_devices
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m19s
CI for csaxs_bec / test (push) Successful in 1m18s
2026-01-07 11:17:50 +01:00
4b95ebace3 test(falcon): fix test after falcon refactoring 2026-01-07 11:17:50 +01:00
c1dee287b8 refactor(falcon): Migrate Falcon integration to PsiDeviceBase 2026-01-07 11:17:50 +01:00
dd3b0144b9 feat(pilatus): deprecate pilatus integration 2026-01-07 11:17:50 +01:00
149af32ab1 fix: rate limit warning log in live mode
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m30s
CI for csaxs_bec / test (push) Successful in 1m32s
2026-01-06 13:22:43 +01:00
x01dc
47f0b66791 mod gui tools for pdf viewer
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m29s
CI for csaxs_bec / test (push) Successful in 1m28s
2026-01-06 12:49:40 +01:00
2c0fced9b7 FZP layout 60 nm
All checks were successful
CI for csaxs_bec / test (push) Successful in 1m32s
2026-01-06 12:00:56 +01:00
d99b44b619 refactor(configs):Migrate deviceClass for EpicsMotorEX to EpicsUserMotorVME
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m30s
CI for csaxs_bec / test (push) Successful in 1m30s
2025-12-11 07:52:52 +01:00
x12sa
dbab981ac2 add micfoc and change some motor settings 2025-12-11 07:52:52 +01:00
x12sa
bdd7f1767f new yaml file sastt for sastt setup 2025-12-11 07:52:52 +01:00
0f41648053 test(rt-flomni): fix tests for rt-flomni
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m25s
CI for csaxs_bec / test (push) Successful in 1m35s
2025-12-09 16:51:24 +01:00
ec45bb4c33 fix(controller): deprecate get_device_manager() in favor of dm 2025-12-09 16:34:53 +01:00
ac8177a132 fix(controller): add controller.on to wait_for_connection for devices with socket controllers 2025-12-09 16:34:53 +01:00
36e8d87411 refactor(controller): refactor set_device_enable method from controller to set_device_read_write 2025-12-09 16:34:53 +01:00
f56a834db5 fix(controller): fix controller init for all controller instances, fix formatting 2025-12-09 16:34:53 +01:00
90d2c99c4a fix: remove deprecated bl_check
All checks were successful
CI for csaxs_bec / test (pull_request) Successful in 1m15s
CI for csaxs_bec / test (push) Successful in 1m22s
2025-12-05 17:16:13 +01:00
71 changed files with 10405 additions and 2800 deletions

View File

@@ -0,0 +1,41 @@
# import builtins
# import datetime
# import os
# import subprocess
# import time
# from pathlib import Path
# import numpy as np
from bec_lib import bec_logger
# from bec_lib.alarm_handler import AlarmBase
# from bec_lib.pdf_writer import PDFWriter
from typeguard import typechecked
from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSInitSmaractStages
from csaxs_bec.bec_ipython_client.plugins.cSAXS.smaract import cSAXSSmaract
from csaxs_bec.bec_ipython_client.plugins.omny.omny_general_tools import OMNYTools
from csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission import cSAXSFilterTransmission
class cSAXSError(Exception):
pass
class cSAXS(
cSAXSInitSmaractStages,
cSAXSSmaract,
cSAXSFilterTransmission,
):
def __init__(self, client):
self.client = client
self.device_manager = client.device_manager
self.OMNYTools = OMNYTools(self.client)
super().__init__(client=client)
# this is the csaxs master file that imports all routines from csaxs
# can be imported in the bec client by
# run in bec from folder /sls/x12sa/config/bec/production/csaxs_bec
# from csaxs_bec.bec_ipython_client.plugins.cSAXS.cSAXS import cSAXS
# csaxs = cSAXS(bec)
#
# then all commands can be accessed by for example
# csaxs._cSAXS_smaract_stages_.....

View File

@@ -0,0 +1,601 @@
4000.00 10.5509
4012.90 10.6470
4025.83 10.7440
4038.81 10.8418
4051.83 10.9406
4064.90 11.0404
4078.00 11.1410
4091.15 11.2426
4104.34 11.3451
4117.57 11.4487
4130.85 11.5532
4144.17 11.6587
4157.53 11.7652
4170.93 11.8726
4184.38 11.9811
4197.87 12.0906
4211.41 12.2011
4224.98 12.3126
4238.60 12.4252
4252.27 12.5389
4265.98 12.6536
4279.73 12.7694
4293.53 12.8863
4307.37 13.0042
4321.26 13.1234
4335.19 13.2436
4349.17 13.3650
4363.19 13.4875
4377.26 13.6111
4391.37 13.7360
4405.53 13.8620
4419.73 13.9892
4433.98 14.1175
4448.28 14.2470
4462.62 14.3779
4477.01 14.5099
4491.44 14.6432
4505.92 14.7777
4520.45 14.9135
4535.02 15.0507
4549.65 15.1891
4564.31 15.3289
4579.03 15.4699
4593.79 15.6123
4608.60 15.7560
4623.46 15.9010
4638.37 16.0473
4653.32 16.1950
4668.33 16.3442
4683.38 16.4948
4698.48 16.6468
4713.62 16.8002
4728.82 16.9551
4744.07 17.1114
4759.36 17.2693
4774.71 17.4287
4790.10 17.5895
4805.54 17.7519
4821.04 17.9157
4836.58 18.0811
4852.17 18.2481
4867.82 18.4166
4883.51 18.5866
4899.26 18.7583
4915.05 18.9317
4930.90 19.1069
4946.80 19.2836
4962.75 19.4619
4978.75 19.6419
4994.80 19.8237
5010.90 20.0072
5027.06 20.1924
5043.26 20.3793
5059.52 20.5680
5075.84 20.7585
5092.20 20.9507
5108.62 21.1448
5125.09 21.3406
5141.61 21.5384
5158.19 21.7383
5174.82 21.9400
5191.50 22.1436
5208.24 22.3491
5225.03 22.5565
5241.88 22.7659
5258.78 22.9772
5275.73 23.1905
5292.74 23.4057
5309.81 23.6231
5326.93 23.8425
5344.10 24.0641
5361.33 24.2876
5378.62 24.5133
5395.96 24.7411
5413.35 24.9712
5430.81 25.2034
5448.32 25.4377
5465.88 25.6742
5483.50 25.9131
5501.18 26.1546
5518.92 26.3982
5536.71 26.6441
5554.56 26.8923
5572.47 27.1429
5590.44 27.3957
5608.46 27.6508
5626.54 27.9084
5644.68 28.1683
5662.88 28.4308
5681.14 28.6958
5699.46 28.9633
5717.83 29.2334
5736.27 29.5059
5754.76 29.7811
5773.31 30.0590
5791.93 30.3395
5810.60 30.6226
5829.33 30.9083
5848.13 31.1968
5866.98 31.4883
5885.90 31.7823
5904.88 32.0791
5923.91 32.3787
5943.01 32.6814
5962.17 32.9873
5981.40 33.2960
6000.68 33.6076
6020.03 33.9221
6039.44 34.2394
6058.91 34.5595
6078.44 34.8826
6098.04 35.2086
6117.70 35.5378
6137.42 35.8704
6157.21 36.2065
6177.06 36.5457
6196.98 36.8881
6216.96 37.2338
6237.00 37.5828
6257.11 37.9350
6277.28 38.2906
6297.52 38.6496
6317.82 39.0118
6338.19 39.3777
6358.63 39.7471
6379.13 40.1200
6399.69 40.4964
6420.33 40.8763
6441.03 41.2599
6461.79 41.6472
6482.63 42.0382
6503.53 42.4328
6524.49 42.8311
6545.53 43.2333
6566.63 43.6396
6587.80 44.0496
6609.04 44.4635
6630.35 44.8813
6651.73 45.3031
6673.17 45.7289
6694.69 46.1588
6716.27 46.5926
6737.93 47.0306
6759.65 47.4730
6781.44 47.9197
6803.31 48.3706
6825.24 48.8258
6847.25 49.2852
6869.32 49.7491
6891.47 50.2174
6913.69 50.6902
6935.98 51.1675
6958.34 51.6491
6980.77 52.1356
7003.28 52.6268
7025.86 53.1226
7048.51 53.6231
7071.24 54.1282
7094.03 54.6384
7116.91 55.1537
7139.85 55.6736
7162.87 56.1985
7185.96 56.7283
7209.13 57.2635
7232.38 57.8038
7255.69 58.3491
7279.09 58.8996
7302.55 59.4554
7326.10 60.0165
7349.72 60.5832
7373.41 61.1551
7397.19 61.7324
7421.03 62.3153
7444.96 62.9039
7468.96 63.4982
7493.04 64.0981
7517.20 64.7037
7541.44 65.3149
7565.75 65.9323
7590.14 66.5556
7614.62 67.1849
7639.17 67.8201
7663.79 68.4612
7688.50 69.1087
7713.29 69.7625
7738.16 70.4224
7763.11 71.0884
7788.14 71.7608
7813.25 72.4400
7838.44 73.1259
7863.71 73.8182
7889.06 74.5170
7914.50 75.2224
7940.01 75.9348
7965.61 76.6538
7991.29 77.3796
8017.06 78.1123
8042.91 78.8520
8068.84 79.5991
8094.85 80.3534
8120.95 81.1149
8147.13 81.8836
8173.40 82.6595
8199.75 83.4434
8226.19 84.2349
8252.71 85.0338
8279.32 85.8404
8306.01 86.6546
8332.79 87.4766
8359.65 88.3065
8386.60 89.1443
8413.64 89.9900
8440.77 90.8436
8467.98 91.7062
8495.29 92.5774
8522.67 93.4567
8550.15 94.3443
8577.72 95.2403
8605.37 96.1451
8633.12 97.0584
8660.95 97.9803
8688.87 98.9109
8716.89 99.8503
8744.99 100.799
8773.19 101.757
8801.47 102.724
8829.85 103.700
8858.32 104.686
8886.88 105.681
8915.53 106.687
8944.27 107.701
8973.11 108.726
9002.04 109.760
9031.06 110.804
9060.18 111.859
9089.39 112.924
9118.69 113.998
9148.09 115.083
9177.59 116.179
9207.18 117.285
9236.86 118.401
9266.64 119.529
9296.52 120.666
9326.49 121.816
9356.56 122.976
9386.72 124.148
9416.99 125.330
9447.35 126.524
9477.81 127.730
9508.37 128.946
9539.02 130.174
9569.78 131.414
9600.63 132.666
9631.58 133.931
9662.63 135.209
9693.79 136.498
9725.04 137.800
9756.39 139.115
9787.85 140.441
9819.41 141.780
9851.07 143.131
9882.83 144.496
9914.69 145.873
9946.65 147.265
9978.72 148.671
10010.9 150.090
10043.2 151.522
10075.5 152.968
10108.0 154.429
10140.6 155.904
10173.3 157.393
10206.1 158.896
10239.0 160.413
10272.0 161.946
10305.2 163.493
10338.4 165.055
10371.7 166.632
10405.1 168.224
10438.7 169.832
10472.3 171.455
10506.1 173.093
10540.0 174.747
10574.0 176.417
10608.1 178.104
10642.3 179.808
10676.6 181.527
10711.0 183.263
10745.5 185.015
10780.2 186.785
10814.9 188.572
10849.8 190.376
10884.8 192.197
10919.9 194.035
10955.1 195.892
10990.4 197.767
11025.8 199.659
11061.4 201.570
11097.0 203.498
11132.8 205.447
11168.7 207.414
11204.7 209.400
11240.8 211.405
11277.1 213.429
11313.4 215.474
11349.9 217.538
11386.5 219.622
11423.2 221.725
11460.0 223.849
11497.0 225.994
11534.1 228.159
11571.2 230.344
11608.6 232.550
11646.0 234.778
11683.5 237.028
11721.2 239.300
11759.0 241.593
11796.9 243.908
11834.9 246.246
11873.1 248.607
11911.4 250.991
11949.8 253.398
11988.3 255.827
12026.9 258.280
12065.7 260.757
12104.6 263.257
12143.7 265.781
12182.8 268.329
12222.1 270.902
12261.5 273.502
12301.0 276.126
12340.7 278.776
12380.5 281.450
12420.4 284.150
12460.4 286.877
12500.6 289.629
12540.9 292.408
12581.3 295.212
12621.9 298.045
12662.6 300.906
12703.4 303.794
12744.4 306.709
12785.5 309.653
12826.7 312.624
12868.0 315.625
12909.5 318.655
12951.1 321.713
12992.9 324.800
13034.8 327.917
13076.8 331.067
13119.0 334.246
13161.3 337.456
13203.7 340.696
13246.3 343.968
13289.0 347.271
13331.8 350.607
13374.8 353.973
13417.9 357.371
13461.2 360.802
13504.6 364.269
13548.1 367.769
13591.8 371.301
13635.6 374.867
13679.6 378.467
13723.7 382.103
13767.9 385.773
13812.3 389.478
13856.9 393.217
13901.5 396.994
13946.4 400.809
13991.3 404.660
14036.4 408.548
14081.7 412.472
14127.1 416.434
14172.6 420.436
14218.3 424.475
14264.2 428.552
14310.2 432.668
14356.3 436.824
14402.6 441.022
14449.0 445.260
14495.6 449.539
14542.3 453.858
14589.2 458.217
14636.2 462.619
14683.4 467.064
14730.8 471.549
14778.3 476.076
14825.9 480.648
14873.7 485.268
14921.7 489.931
14969.8 494.637
15018.0 499.389
15066.5 504.191
15115.0 509.040
15163.8 513.935
15212.7 518.876
15261.7 523.864
15310.9 528.901
15360.3 533.988
15409.8 539.123
15459.5 544.306
15509.3 549.539
15559.3 554.823
15609.5 560.159
15659.8 565.546
15710.3 570.984
15761.0 576.473
15811.8 582.014
15862.7 587.611
15913.9 593.262
15965.2 598.964
16016.7 604.719
16068.3 610.531
16120.1 616.402
16172.1 622.326
16224.2 628.306
16276.5 634.342
16329.0 640.437
16381.7 646.596
16434.5 652.811
16487.5 659.084
16540.6 665.416
16593.9 671.811
16647.4 678.268
16701.1 684.786
16755.0 691.364
16809.0 698.004
16863.2 704.710
16917.5 711.482
16972.1 718.318
17026.8 725.217
17081.7 732.182
17136.8 739.214
17192.0 746.315
17247.4 753.482
17303.1 760.717
17358.8 768.018
17414.8 775.391
17470.9 782.836
17527.3 790.351
17583.8 797.934
17640.5 805.587
17697.4 813.318
17754.4 821.128
17811.6 829.008
17869.1 836.961
17926.7 844.987
17984.5 853.094
18042.5 861.278
18100.6 869.538
18159.0 877.874
18217.5 886.286
18276.3 894.784
18335.2 903.364
18394.3 912.023
18453.6 920.761
18513.1 929.582
18572.8 938.489
18632.7 947.484
18692.8 956.561
18753.0 965.723
18813.5 974.967
18874.1 984.303
18935.0 993.729
18996.0 1003.24
19057.3 1012.84
19118.7 1022.53
19180.4 1032.31
19242.2 1042.19
19304.2 1052.16
19366.5 1062.22
19428.9 1072.37
19491.6 1082.63
19554.4 1092.98
19617.4 1103.42
19680.7 1113.96
19744.1 1124.59
19807.8 1135.38
19871.7 1146.29
19935.7 1157.29
20000.0 1168.40
20081.3 1182.56
20162.8 1196.82
20244.8 1211.23
20327.0 1225.81
20409.6 1240.55
20492.5 1255.47
20575.8 1270.57
20659.4 1285.83
20743.3 1301.26
20827.6 1316.88
20912.2 1332.67
20997.2 1348.64
21082.5 1364.79
21168.1 1381.13
21254.1 1397.65
21340.5 1414.36
21427.2 1431.26
21514.3 1448.35
21601.7 1465.63
21689.4 1483.11
21777.6 1500.78
21866.0 1518.66
21954.9 1536.73
22044.1 1555.01
22133.6 1573.50
22223.6 1592.19
22313.9 1611.09
22404.5 1630.19
22495.5 1649.51
22586.9 1669.05
22678.7 1688.80
22770.8 1708.77
22863.4 1728.96
22956.3 1749.38
23049.5 1770.02
23143.2 1790.88
23237.2 1811.97
23331.6 1833.30
23426.4 1854.85
23521.6 1876.64
23617.1 1898.66
23713.1 1920.93
23809.4 1943.44
23906.2 1966.19
24003.3 1989.19
24100.8 2012.43
24198.7 2035.93
24297.1 2059.67
24395.8 2083.67
24494.9 2107.92
24594.4 2132.44
24694.3 2157.20
24794.7 2182.24
24895.4 2207.53
24996.6 2233.09
25098.1 2258.92
25200.1 2285.02
25302.5 2311.40
25405.3 2338.05
25508.5 2364.98
25612.1 2392.19
25716.2 2419.68
25820.7 2447.45
25925.6 2475.50
26030.9 2503.85
26136.7 2532.50
26242.9 2561.42
26349.5 2590.64
26456.5 2620.16
26564.0 2649.97
26672.0 2680.08
26780.3 2710.49
26889.1 2741.21
26998.4 2772.24
27108.1 2803.57
27218.2 2835.21
27328.8 2867.17
27439.8 2899.44
27551.3 2932.02
27663.2 2964.91
27775.6 2998.14
27888.5 3031.69
28001.8 3065.56
28115.6 3099.75
28229.8 3134.27
28344.5 3169.12
28459.6 3204.30
28575.3 3239.80
28691.4 3275.66
28807.9 3311.85
28925.0 3348.37
29042.5 3385.23
29160.5 3422.44
29279.0 3459.98
29397.9 3497.87
29517.4 3536.09
29637.3 3574.69
29757.7 3613.62
29878.6 3652.90
30000.0 3692.52

View File

@@ -0,0 +1,713 @@
cr Density=7.19, Angle=90.deg
Photon Energy (eV), Atten Length (microns)
4000.00 7.36948
4012.90 7.43487
4025.83 7.50083
4038.81 7.56737
4051.83 7.63450
4064.90 7.70224
4078.00 7.77057
4091.15 7.83951
4104.34 7.90905
4117.57 7.97916
4130.85 8.04989
4144.17 8.12124
4157.53 8.19322
4170.93 8.26584
4184.38 8.33903
4197.87 8.41287
4211.41 8.48736
4224.98 8.56251
4238.60 8.63834
4252.27 8.71478
4265.98 8.79189
4279.73 8.86969
4293.53 8.94819
4307.37 9.02737
4321.26 9.10723
4335.19 9.18779
4349.17 9.26908
4363.19 9.35108
4377.26 9.43379
4391.37 9.51723
4405.53 9.60141
4419.73 9.68634
4433.98 9.77201
4448.28 9.85842
4462.62 9.94550
4477.01 10.0334
4491.44 10.1220
4505.92 10.2114
4520.45 10.3016
4535.02 10.3923
4549.65 10.4839
4564.31 10.5763
4579.03 10.6695
4593.79 10.7635
4608.60 10.8584
4623.46 10.9541
4638.37 11.0507
4653.32 11.1481
4668.33 11.2464
4683.38 11.3454
4698.48 11.4454
4713.62 11.5462
4728.82 11.6479
4744.07 11.7504
4759.36 11.8538
4774.71 11.9581
4790.10 12.0633
4805.54 12.1694
4821.04 12.2764
4836.58 12.3843
4852.17 12.4932
4867.82 12.6031
4883.51 12.7139
4899.26 12.8256
4915.05 12.9381
4930.90 13.0516
4946.80 13.1662
4962.75 13.2817
4978.75 13.3982
4994.80 13.5157
5010.90 13.6342
5027.06 13.7537
5043.26 13.8743
5059.52 13.9959
5075.84 14.1184
5092.20 14.2419
5108.62 14.3665
5125.09 14.4922
5141.61 14.6190
5158.19 14.7468
5174.82 14.8758
5191.50 15.0059
5208.24 15.1371
5225.03 15.2694
5241.88 15.4027
5258.78 15.5371
5275.73 15.6728
5292.74 15.8096
5309.81 15.9475
5326.93 16.0865
5344.10 16.2268
5361.33 16.3682
5378.62 16.5109
5395.96 16.6547
5413.35 16.7997
5430.81 16.9460
5448.32 17.0935
5465.88 17.2423
5483.50 17.3923
5501.18 17.5434
5518.92 17.6959
5536.71 17.8497
5554.56 18.0048
5572.47 18.1612
5590.44 18.3188
5608.46 18.4778
5626.54 18.6382
5644.68 18.7999
5662.88 18.9630
5681.14 19.1272
5699.46 19.2929
5717.83 19.4600
5736.27 19.6286
5754.76 19.7984
5773.31 19.9696
5791.93 20.1423
5810.60 20.3164
5829.33 20.4921
5848.13 20.6691
5866.98 20.8476
5885.90 21.0276
5904.88 21.2091
5923.91 21.3922
5943.01 21.5768
5962.17 21.7631
5981.40 21.9510
5985.00 21.9863
5985.10 21.9873
5985.20 21.9883
5985.30 21.9892
5985.40 21.9902
5985.50 21.9912
5985.60 21.9922
5985.70 21.9932
5985.80 21.9941
5985.90 21.9951
5986.00 21.9961
5986.10 21.9971
5986.20 21.9981
5986.30 21.9990
5986.40 22.0000
5986.50 22.0010
5986.60 22.0020
5986.70 22.0030
5986.80 22.0040
5986.90 22.0049
5987.00 22.0059
5987.10 22.0069
5987.20 22.0079
5987.30 22.0088
5987.40 22.0098
5987.50 22.0108
5987.60 22.0118
5987.70 22.0128
5987.80 22.0138
5987.90 22.0147
5988.00 22.0157
5988.10 22.0167
5988.20 22.0177
5988.30 22.0187
5988.40 22.0196
5988.50 22.0206
5988.60 22.0216
5988.70 22.0226
5988.80 22.0236
5988.90 22.0245
5989.00 22.0255
5989.10 22.0265
5989.11 20.7400
5989.12 18.3877
5989.13 16.3020
5989.14 14.4528
5989.15 13.6083
5989.16 12.0645
5989.17 10.6957
5989.18 9.48212
5989.19 8.40620
5989.20 7.91492
5989.30 2.84297
5989.40 2.67688
5989.50 2.67699
5989.60 2.67710
5989.70 2.67720
5989.80 2.67732
5989.90 2.67742
5990.00 2.67753
5990.10 2.67764
5990.20 2.67775
5990.30 2.67786
5990.40 2.67797
5990.50 2.67808
5990.60 2.67819
5990.70 2.67829
5990.80 2.67841
5990.90 2.67851
5991.00 2.67862
5991.10 2.67874
5991.20 2.67884
5991.30 2.67895
5991.40 2.67906
5991.50 2.67917
5991.60 2.67928
5991.70 2.67939
5991.80 2.67950
5991.90 2.67961
5992.00 2.67972
5992.10 2.67983
5992.20 2.67993
5992.30 2.68004
5992.40 2.68015
5992.50 2.68026
5992.60 2.68037
5992.70 2.68048
5992.80 2.68059
5992.90 2.68070
5993.00 2.68081
5993.10 2.68092
5993.20 2.68103
5993.30 2.68114
5993.40 2.68124
5993.50 2.68136
5993.60 2.68146
5993.70 2.68157
5993.80 2.68168
5993.90 2.68179
5994.00 2.68190
5994.10 2.68201
5994.20 2.68212
5994.30 2.68223
5994.40 2.68234
5994.50 2.68245
5994.60 2.68256
5994.70 2.68267
5994.80 2.68277
5994.90 2.68289
5995.00 2.68299
6000.68 2.68920
6020.03 2.71041
6039.44 2.73179
6058.91 2.75333
6078.44 2.77504
6098.04 2.79692
6117.70 2.81898
6137.42 2.84127
6157.21 2.86379
6177.06 2.88648
6196.98 2.90936
6216.96 2.93243
6237.00 2.95574
6257.11 2.97929
6277.28 3.00304
6297.52 3.02698
6317.82 3.05110
6338.19 3.07550
6358.63 3.10015
6379.13 3.12500
6399.69 3.15005
6420.33 3.17530
6441.03 3.20082
6461.79 3.22661
6482.63 3.25261
6503.53 3.27882
6524.49 3.30523
6545.53 3.33194
6566.63 3.35893
6587.80 3.38613
6609.04 3.41355
6630.35 3.44119
6651.73 3.46915
6673.17 3.49738
6694.69 3.52584
6716.27 3.55453
6737.93 3.58346
6759.65 3.61269
6781.44 3.64220
6803.31 3.67196
6825.24 3.70197
6847.25 3.73221
6869.32 3.76280
6891.47 3.79370
6913.69 3.82485
6935.98 3.85626
6958.34 3.88793
6980.77 3.91995
7003.28 3.95230
7025.86 3.98491
7048.51 4.01779
7071.24 4.05094
7094.03 4.08445
7116.91 4.11828
7139.85 4.15239
7162.87 4.18677
7185.96 4.22145
7209.13 4.25650
7232.38 4.29188
7255.69 4.32756
7279.09 4.36353
7302.55 4.39981
7326.10 4.43648
7349.72 4.47351
7373.41 4.51084
7397.19 4.54849
7421.03 4.58645
7444.96 4.62485
7468.96 4.66363
7493.04 4.70273
7517.20 4.74216
7541.44 4.78192
7565.75 4.82210
7590.14 4.86264
7614.62 4.90353
7639.17 4.94475
7663.79 4.98633
7688.50 5.02838
7713.29 5.07085
7738.16 5.11367
7763.11 5.15684
7788.14 5.20039
7813.25 5.24440
7838.44 5.28883
7863.71 5.33362
7889.06 5.37880
7914.50 5.42435
7940.01 5.47042
7965.61 5.51690
7991.29 5.56378
8017.06 5.61105
8042.91 5.65874
8068.84 5.70693
8094.85 5.75557
8120.95 5.80462
8147.13 5.85410
8173.40 5.90399
8199.75 5.95444
8226.19 6.00537
8252.71 6.05673
8279.32 6.10854
8306.01 6.16078
8332.79 6.21357
8359.65 6.26684
8386.60 6.32058
8413.64 6.37477
8440.77 6.42942
8467.98 6.48472
8495.29 6.54056
8522.67 6.59686
8550.15 6.65365
8577.72 6.71093
8605.37 6.76883
8633.12 6.82725
8660.95 6.88617
8688.87 6.94559
8716.89 7.00554
8744.99 7.06612
8773.19 7.12724
8801.47 7.18888
8829.85 7.25106
8858.32 7.31379
8886.88 7.37722
8915.53 7.44124
8944.27 7.50581
8973.11 7.57095
9002.04 7.63665
9031.06 7.70305
9060.18 7.77005
9089.39 7.83764
9118.69 7.90581
9148.09 7.97457
9177.59 8.04414
9207.18 8.11433
9236.86 8.18516
9266.64 8.25659
9296.52 8.32864
9326.49 8.40145
9356.56 8.47492
9386.72 8.54902
9416.99 8.62377
9447.35 8.69918
9477.81 8.77535
9508.37 8.85219
9539.02 8.92970
9569.78 9.00788
9600.63 9.08677
9631.58 9.16655
9662.63 9.24704
9693.79 9.32825
9725.04 9.41018
9756.39 9.49281
9787.85 9.57645
9819.41 9.66083
9851.07 9.74598
9882.83 9.83186
9914.69 9.91849
9946.65 10.0060
9978.72 10.0943
10010.9 10.1834
10043.2 10.2733
10075.5 10.3640
10108.0 10.4556
10140.6 10.5481
10173.3 10.6413
10206.1 10.7354
10239.0 10.8303
10272.0 10.9263
10305.2 11.0231
10338.4 11.1208
10371.7 11.2194
10405.1 11.3188
10438.7 11.4194
10472.3 11.5208
10506.1 11.6230
10540.0 11.7262
10574.0 11.8304
10608.1 11.9356
10642.3 12.0418
10676.6 12.1490
10711.0 12.2571
10745.5 12.3661
10780.2 12.4764
10814.9 12.5876
10849.8 12.6998
10884.8 12.8130
10919.9 12.9272
10955.1 13.0427
10990.4 13.1592
11025.8 13.2768
11061.4 13.3954
11097.0 13.5150
11132.8 13.6360
11168.7 13.7580
11204.7 13.8811
11240.8 14.0053
11277.1 14.1307
11313.4 14.2574
11349.9 14.3852
11386.5 14.5142
11423.2 14.6443
11460.0 14.7757
11497.0 14.9083
11534.1 15.0422
11571.2 15.1773
11608.6 15.3135
11646.0 15.4511
11683.5 15.5901
11721.2 15.7303
11759.0 15.8718
11796.9 16.0146
11834.9 16.1587
11873.1 16.3043
11911.4 16.4513
11949.8 16.5995
11988.3 16.7491
12026.9 16.9000
12065.7 17.0525
12104.6 17.2064
12143.7 17.3617
12182.8 17.5184
12222.1 17.6765
12261.5 17.8363
12301.0 17.9976
12340.7 18.1603
12380.5 18.3245
12420.4 18.4901
12460.4 18.6575
12500.6 18.8264
12540.9 18.9968
12581.3 19.1688
12621.9 19.3424
12662.6 19.5177
12703.4 19.6947
12744.4 19.8733
12785.5 20.0535
12826.7 20.2353
12868.0 20.4190
12909.5 20.6044
12951.1 20.7915
12992.9 20.9802
13034.8 21.1708
13076.8 21.3633
13119.0 21.5576
13161.3 21.7536
13203.7 21.9515
13246.3 22.1512
13289.0 22.3528
13331.8 22.5564
13374.8 22.7618
13417.9 22.9690
13461.2 23.1782
13504.6 23.3896
13548.1 23.6029
13591.8 23.8181
13635.6 24.0353
13679.6 24.2545
13723.7 24.4759
13767.9 24.6994
13812.3 24.9248
13856.9 25.1523
13901.5 25.3820
13946.4 25.6141
13991.3 25.8482
14036.4 26.0845
14081.7 26.3230
14127.1 26.5638
14172.6 26.8069
14218.3 27.0522
14264.2 27.2998
14310.2 27.5497
14356.3 27.8019
14402.6 28.0567
14449.0 28.3139
14495.6 28.5734
14542.3 28.8353
14589.2 29.0996
14636.2 29.3665
14683.4 29.6359
14730.8 29.9077
14778.3 30.1820
14825.9 30.4590
14873.7 30.7388
14921.7 31.0211
14969.8 31.3061
15018.0 31.5936
15066.5 31.8840
15115.0 32.1772
15163.8 32.4731
15212.7 32.7717
15261.7 33.0731
15310.9 33.3774
15360.3 33.6847
15409.8 33.9948
15459.5 34.3078
15509.3 34.6237
15559.3 34.9426
15609.5 35.2647
15659.8 35.5897
15710.3 35.9178
15761.0 36.2488
15811.8 36.5830
15862.7 36.9205
15913.9 37.2612
15965.2 37.6049
16016.7 37.9518
16068.3 38.3020
16120.1 38.6557
16172.1 39.0126
16224.2 39.3728
16276.5 39.7363
16329.0 40.1034
16381.7 40.4742
16434.5 40.8483
16487.5 41.2259
16540.6 41.6069
16593.9 41.9918
16647.4 42.3803
16701.1 42.7724
16755.0 43.1681
16809.0 43.5676
16863.2 43.9708
16917.5 44.3781
16972.1 44.7890
17026.8 45.2038
17081.7 45.6225
17136.8 46.0452
17192.0 46.4720
17247.4 46.9028
17303.1 47.3376
17358.8 47.7763
17414.8 48.2193
17470.9 48.6665
17527.3 49.1180
17583.8 49.5735
17640.5 50.0333
17697.4 50.4976
17754.4 50.9666
17811.6 51.4398
17869.1 51.9174
17926.7 52.3995
17984.5 52.8862
18042.5 53.3776
18100.6 53.8735
18159.0 54.3740
18217.5 54.8791
18276.3 55.3893
18335.2 55.9043
18394.3 56.4241
18453.6 56.9488
18513.1 57.4783
18572.8 58.0130
18632.7 58.5529
18692.8 59.0978
18753.0 59.6479
18813.5 60.2029
18874.1 60.7633
18935.0 61.3292
18996.0 61.9003
19057.3 62.4767
19118.7 63.0584
19180.4 63.6458
19242.2 64.2390
19304.2 64.8376
19366.5 65.4417
19428.9 66.0514
19491.6 66.6670
19554.4 67.2887
19617.4 67.9160
19680.7 68.5491
19744.1 69.1881
19807.8 69.8335
19871.7 70.4851
19935.7 71.1427
20000.0 71.8064
20081.3 72.6515
20162.8 73.5071
20244.8 74.3726
20327.0 75.2484
20409.6 76.1342
20492.5 77.0315
20575.8 77.9393
20659.4 78.8575
20743.3 79.7867
20827.6 80.7275
20912.2 81.6791
20997.2 82.6420
21082.5 83.6161
21168.1 84.6024
21254.1 85.6003
21340.5 86.6097
21427.2 87.6312
21514.3 88.6652
21601.7 89.7114
21689.4 90.7699
21777.6 91.8408
21866.0 92.9252
21954.9 94.0222
22044.1 95.1321
22133.6 96.2552
22223.6 97.3920
22313.9 98.5423
22404.5 99.7058
22495.5 100.883
22586.9 102.075
22678.7 103.281
22770.8 104.501
22863.4 105.736
22956.3 106.986
23049.5 108.250
23143.2 109.529
23237.2 110.824
23331.6 112.134
23426.4 113.459
23521.6 114.800
23617.1 116.157
23713.1 117.531
23809.4 118.921
23906.2 120.327
24003.3 121.750
24100.8 123.190
24198.7 124.647
24297.1 126.121
24395.8 127.612
24494.9 129.122
24594.4 130.649
24694.3 132.194
24794.7 133.758
24895.4 135.340
24996.6 136.941
25098.1 138.561
25200.1 140.200
25302.5 141.859
25405.3 143.537
25508.5 145.235
25612.1 146.954
25716.2 148.693
25820.7 150.452
25925.6 152.232
26030.9 154.033
26136.7 155.856
26242.9 157.700
26349.5 159.566
26456.5 161.454
26564.0 163.364
26672.0 165.297
26780.3 167.252
26889.1 169.231
26998.4 171.233
27108.1 173.258
27218.2 175.308
27328.8 177.381
27439.8 179.479
27551.3 181.602
27663.2 183.749
27775.6 185.923
27888.5 188.122
28001.8 190.346
28115.6 192.597
28229.8 194.874
28344.5 197.178
28459.6 199.509
28575.3 201.867
28691.4 204.253
28807.9 206.667
28925.0 209.110
29042.5 211.580
29160.5 214.080
29279.0 216.610
29397.9 219.169
29517.4 221.758
29637.3 224.377
29757.7 227.027
29878.6 229.707
30000.0 232.418

View File

@@ -0,0 +1,636 @@
4000.00 3.25746
4016.15 3.29307
4032.37 3.32906
4048.65 3.36546
4065.00 3.40227
4081.41 3.43947
4097.89 3.47709
4114.44 3.51514
4131.06 3.55362
4147.74 3.59251
4164.48 3.63183
4181.30 3.67160
4198.18 3.71182
4215.14 3.75247
4232.16 3.79357
4249.25 3.83514
4266.40 3.87716
4283.63 3.91965
4300.93 3.96259
4318.29 4.00604
4335.73 4.04997
4353.24 4.09438
4370.82 4.13928
4388.47 4.18468
4406.19 4.23059
4423.98 4.27699
4441.84 4.32391
4459.78 4.37138
4477.79 4.41938
4495.87 4.46791
4514.02 4.51696
4532.25 4.56663
4550.55 4.61685
4568.93 4.66763
4587.37 4.71896
4605.90 4.77084
4624.50 4.82328
4643.17 4.87629
4661.92 4.92989
4680.74 4.98413
4699.64 5.03898
4718.62 5.09443
4737.67 5.15049
4756.80 5.20723
4776.01 5.26461
4795.30 5.32262
4814.66 5.38126
4834.10 5.44057
4853.62 5.50053
4873.22 5.56116
4892.90 5.62244
4912.65 5.68447
4932.49 5.74717
4952.41 5.81058
4972.41 5.87468
4992.48 5.93953
5012.64 6.00510
5032.88 6.07139
5053.21 6.13842
5073.61 6.20623
5094.10 6.27479
5114.67 6.34412
5135.32 6.41420
5156.06 6.48511
5176.88 6.55680
5197.78 6.62929
5218.77 6.70258
5239.84 6.77683
5261.00 6.85190
5282.24 6.92781
5303.57 7.00455
5324.99 7.08208
5346.49 7.16047
5368.08 7.23972
5389.76 7.31986
5411.52 7.40104
5433.37 7.48313
5455.31 7.56613
5477.34 7.65005
5499.46 7.73501
5521.66 7.82091
5543.96 7.90777
5566.34 7.99558
5588.82 8.08432
5611.39 8.17404
5634.05 8.26476
5656.80 8.35649
5679.64 8.44953
5702.57 8.54360
5725.60 8.63871
5748.72 8.73487
5771.93 8.83200
5795.24 8.93022
5818.64 9.02952
5842.13 9.12998
5865.72 9.23186
5889.41 9.33488
5913.19 9.43905
5937.07 9.54436
5961.04 9.65080
5985.11 9.75840
6009.28 9.86722
6033.54 9.97725
6057.91 10.0886
6082.37 10.2012
6106.93 10.3150
6131.59 10.4301
6156.35 10.5469
6181.21 10.6649
6206.17 10.7843
6231.23 10.9050
6256.39 11.0271
6281.65 11.1506
6307.01 11.2754
6332.48 11.4017
6358.05 11.5295
6383.73 11.6588
6409.50 11.7895
6435.38 11.9216
6461.37 12.0555
6487.46 12.1909
6513.66 12.3278
6539.96 12.4663
6566.37 12.6064
6592.88 12.7481
6619.50 12.8914
6646.23 13.0364
6673.07 13.1831
6700.01 13.3316
6727.07 13.4816
6754.23 13.6334
6781.50 13.7871
6808.89 13.9425
6836.38 14.0996
6863.99 14.2586
6891.70 14.4197
6919.53 14.5825
6947.47 14.7472
6975.53 14.9137
7003.69 15.0824
7031.97 15.2530
7060.37 15.4255
7088.88 15.6001
7117.50 15.7768
7146.24 15.9555
7175.10 16.1362
7204.07 16.3191
7233.16 16.5043
7262.37 16.6916
7291.69 16.8810
7321.13 17.0726
7350.70 17.2667
7380.38 17.4630
7410.18 17.6615
7440.10 17.8624
7470.14 18.0658
7500.31 18.2715
7530.59 18.4796
7561.00 18.6902
7591.53 18.9034
7622.19 19.1191
7652.96 19.3372
7683.87 19.5580
7714.89 19.7815
7746.05 20.0076
7777.32 20.2363
7808.73 20.4678
7840.26 20.7023
7871.92 20.9395
7903.70 21.1793
7935.62 21.4222
7967.66 21.6681
7999.84 21.9168
8032.14 22.1684
8064.57 22.4232
8097.14 22.6812
8129.83 22.9422
8162.66 23.2061
8195.62 23.4734
8228.71 23.7441
8261.94 24.0178
8295.30 24.2947
8328.80 24.5752
8362.43 24.8594
8396.20 25.1467
8430.10 25.4374
8464.14 25.7318
8498.32 26.0300
8532.63 26.3316
8567.09 26.6368
8601.68 26.9458
8636.41 27.2589
8671.29 27.5756
8706.30 27.8960
8741.46 28.2205
8776.75 28.5491
8812.19 28.8816
8847.78 29.2179
8883.50 29.5581
8919.37 29.9023
8955.00 30.2467
8956.00 30.2564
8957.00 30.2661
8958.00 30.2758
8958.99 30.2855
8959.99 30.2952
8960.99 30.3049
8961.99 30.3146
8962.99 30.3243
8963.99 30.3340
8964.99 30.3438
8965.99 30.3535
8966.98 30.3632
8967.98 30.3729
8968.98 30.3826
8969.98 30.3924
8970.98 30.4021
8971.98 30.4118
8972.98 30.4216
8973.98 30.4313
8974.98 30.4411
8975.98 30.4508
8976.98 30.4605
8977.98 30.4703
8978.00 30.4705
8978.01 30.4706
8978.02 30.4706
8978.03 30.4708
8978.04 30.4709
8978.05 30.4710
8978.06 30.4710
8978.07 30.4711
8978.08 30.4712
8978.09 30.4714
8978.10 30.4715
8978.11 30.4715
8978.12 30.4716
8978.13 30.4717
8978.14 30.4718
8978.15 30.4720
8978.16 30.4720
8978.17 30.4721
8978.18 30.4722
8978.19 30.4723
8978.20 30.4724
8978.21 30.4725
8978.22 30.4726
8978.23 30.4727
8978.24 30.4728
8978.25 30.4729
8978.26 30.4730
8978.27 30.4731
8978.28 30.4732
8978.29 30.4733
8978.30 30.4734
8978.31 30.4735
8978.32 30.4736
8978.33 30.4737
8978.34 30.4738
8978.35 30.4739
8978.36 30.4740
8978.37 30.4741
8978.38 30.4741
8978.39 30.4743
8978.40 30.4744
8978.41 30.4745
8978.42 30.4746
8978.43 30.4746
8978.44 30.4747
8978.45 30.4749
8978.46 30.4750
8978.47 30.4751
8978.48 30.4751
8978.49 30.4752
8978.50 30.4753
8978.51 30.4755
8978.52 30.4756
8978.53 30.4756
8978.54 30.4757
8978.55 30.4758
8978.56 30.4759
8978.57 30.4761
8978.58 30.4761
8978.59 30.4762
8978.60 30.4763
8978.61 30.4764
8978.62 30.4765
8978.63 30.4766
8978.64 30.4767
8978.65 30.4768
8978.66 30.4769
8978.67 30.4770
8978.68 30.4771
8978.69 30.4772
8978.70 30.4773
8978.71 30.4774
8978.72 30.4775
8978.73 30.4776
8978.74 30.4777
8978.75 30.4778
8978.76 30.4779
8978.77 30.4780
8978.78 30.4781
8978.79 30.4782
8978.80 30.4782
8978.81 25.6979
8978.82 23.5962
8978.83 21.6663
8978.84 19.8939
8978.85 18.2664
8978.86 15.3995
8978.87 14.1393
8978.88 12.9822
8978.89 11.9197
8978.90 10.9441
8978.91 10.0483
8978.92 9.22574
8978.93 7.77706
8978.94 7.14036
8978.95 6.55577
8978.96 6.01902
8978.97 5.52620
8978.98 4.65827
8978.99 4.27683
8979.00 3.92662
8979.99 3.92768
8980.99 3.92876
8981.99 3.92983
8982.99 3.93091
8983.99 3.93198
8984.99 3.93306
8985.99 3.93414
8987.00 3.93522
8988.00 3.93630
8989.00 3.93738
8990.00 3.93846
8991.55 3.94013
9027.86 3.97936
9064.31 4.01898
9100.91 4.05900
9137.66 4.09942
9174.56 4.14035
9211.61 4.18176
9248.80 4.22359
9286.15 4.26582
9323.65 4.30865
9361.29 4.35198
9399.09 4.39576
9437.05 4.43996
9475.15 4.48477
9513.41 4.53010
9551.83 4.57590
9590.40 4.62215
9629.12 4.66903
9668.00 4.71646
9707.04 4.76436
9746.24 4.81275
9785.59 4.86176
9825.11 4.91132
9864.78 4.96138
9904.61 5.01195
9944.61 5.06327
9984.76 5.11518
10025.1 5.16764
10065.6 5.22063
10106.2 5.27433
10147.0 5.32865
10188.0 5.38352
10229.1 5.43896
10270.4 5.49516
10311.9 5.55200
10353.5 5.60942
10395.3 5.66744
10437.3 5.72624
10479.5 5.78571
10521.8 5.84579
10564.3 5.90650
10606.9 5.96803
10649.8 6.03027
10692.8 6.09315
10735.9 6.15668
10779.3 6.22107
10822.8 6.28618
10866.5 6.35197
10910.4 6.41844
10954.5 6.48583
10998.7 6.55397
11043.1 6.62282
11087.7 6.69240
11132.5 6.76293
11177.4 6.83425
11222.5 6.90631
11267.9 6.97914
11313.4 7.05296
11359.0 7.12760
11404.9 7.20304
11451.0 7.27925
11497.2 7.35650
11543.6 7.43459
11590.2 7.51351
11637.0 7.59326
11684.0 7.67411
11731.2 7.75584
11778.6 7.83845
11826.1 7.92193
11873.9 8.00656
11921.8 8.09211
11970.0 8.17857
12018.3 8.26597
12066.8 8.35453
12115.6 8.44406
12164.5 8.53453
12213.6 8.62599
12262.9 8.71873
12312.4 8.81246
12362.2 8.90721
12412.1 9.00296
12462.2 9.10001
12512.5 9.19810
12563.0 9.29726
12613.8 9.39747
12664.7 9.49907
12715.8 9.60177
12767.2 9.70556
12818.7 9.81049
12870.5 9.91679
12922.5 10.0243
12974.7 10.1329
13027.0 10.2427
13079.6 10.3541
13132.5 10.4666
13185.5 10.5804
13238.7 10.6954
13292.2 10.8119
13345.9 10.9297
13399.8 11.0488
13453.9 11.1692
13508.2 11.2912
13562.7 11.4146
13617.5 11.5393
13672.5 11.6654
13727.7 11.7931
13783.1 11.9222
13838.8 12.0527
13894.7 12.1847
13950.8 12.3184
14007.1 12.4536
14063.7 12.5903
14120.4 12.7285
14177.5 12.8685
14234.7 13.0101
14292.2 13.1532
14349.9 13.2979
14407.8 13.4445
14466.0 13.5928
14524.4 13.7426
14583.1 13.8942
14642.0 14.0477
14701.1 14.2028
14760.5 14.3597
14820.1 14.5184
14879.9 14.6791
14940.0 14.8416
15000.3 15.0059
15060.9 15.1721
15121.7 15.3405
15182.8 15.5106
15244.1 15.6827
15305.6 15.8567
15367.4 16.0330
15429.5 16.2112
15491.8 16.3915
15554.3 16.5738
15617.1 16.7584
15680.2 16.9451
15743.5 17.1338
15807.1 17.3247
15870.9 17.5180
15935.0 17.7135
15999.3 17.9111
16063.9 18.1110
16128.8 18.3135
16193.9 18.5181
16259.3 18.7251
16325.0 18.9345
16390.9 19.1466
16457.1 19.3610
16523.5 19.5778
16590.3 19.7971
16657.3 20.0192
16724.5 20.2438
16792.0 20.4708
16859.9 20.7006
16927.9 20.9332
16996.3 21.1684
17064.9 21.4062
17133.8 21.6469
17203.0 21.8905
17272.5 22.1368
17342.2 22.3859
17412.2 22.6380
17482.6 22.8931
17553.1 23.1511
17624.0 23.4120
17695.2 23.6760
17766.6 23.9434
17838.4 24.2137
17910.4 24.4870
17982.7 24.7637
18055.3 25.0436
18128.3 25.3268
18201.5 25.6131
18275.0 25.9029
18348.7 26.1961
18422.8 26.4927
18497.2 26.7925
18571.9 27.0961
18646.9 27.4033
18722.2 27.7139
18797.8 28.0281
18873.7 28.3461
18949.9 28.6679
19026.4 28.9934
19103.3 29.3226
19180.4 29.6557
19257.8 29.9929
19335.6 30.3339
19413.7 30.6787
19492.1 31.0276
19570.8 31.3807
19649.8 31.7378
19729.2 32.0989
19808.8 32.4645
19888.8 32.8345
19969.1 33.2087
20049.8 33.5872
20130.7 33.9702
20212.0 34.3579
20293.6 34.7499
20375.6 35.1465
20457.8 35.5478
20540.4 35.9540
20623.4 36.3648
20706.7 36.7803
20790.3 37.2008
20874.2 37.6263
20958.5 38.0566
21043.1 38.4918
21128.1 38.9324
21213.4 39.3782
21299.1 39.8291
21385.1 40.2851
21471.4 40.7466
21558.1 41.2136
21645.2 41.6859
21732.6 42.1637
21820.3 42.6473
21908.5 43.1366
21996.9 43.6316
22085.7 44.1322
22174.9 44.6389
22264.5 45.1516
22354.4 45.6702
22444.6 46.1946
22535.3 46.7256
22626.3 47.2627
22717.6 47.8060
22809.4 48.3556
22901.5 48.9118
22993.9 49.4746
23086.8 50.0438
23180.0 50.6196
23273.6 51.2024
23367.6 51.7921
23461.9 52.3884
23556.7 52.9916
23651.8 53.6023
23747.3 54.2201
23843.2 54.8451
23939.5 55.4771
24036.1 56.1170
24133.2 56.7643
24230.6 57.4191
24328.5 58.0813
24426.7 58.7517
24525.3 59.4300
24624.4 60.1159
24723.8 60.8098
24823.6 61.5120
24923.9 62.2226
25024.5 62.9412
25125.6 63.6682
25227.0 64.4041
25328.9 65.1487
25431.2 65.9019
25533.9 66.6635
25637.0 67.4347
25740.5 68.2147
25844.4 69.0037
25948.8 69.8017
26053.6 70.6097
26158.8 71.4271
26264.4 72.2539
26370.4 73.0902
26476.9 73.9366
26583.8 74.7929
26691.2 75.6589
26799.0 76.5350
26907.2 77.4217
27015.8 78.3188
27124.9 79.2263
27234.4 80.1439
27344.4 81.0731
27454.8 82.0129
27565.7 82.9636
27677.0 83.9250
27788.7 84.8986
27901.0 85.8832
28013.6 86.8793
28126.7 87.8868
28240.3 88.9064
28354.3 89.9379
28468.8 90.9811
28583.8 92.0364
28699.2 93.1048
28815.1 94.1855
28931.4 95.2785
29048.3 96.3842
29165.6 97.5036
29283.3 98.6356
29401.6 99.7807
29520.3 100.939
29639.5 102.111
29759.2 103.297
29879.4 104.497
30000.0 105.710

View File

@@ -0,0 +1,601 @@
4000.00 75.3048
4012.90 76.0470
4025.83 76.7964
4038.81 77.5538
4051.83 78.3243
4064.90 79.1027
4078.00 79.8886
4091.15 80.6822
4104.34 81.4839
4117.57 82.2946
4130.85 83.1132
4144.17 83.9400
4157.53 84.7749
4170.93 85.6183
4184.38 86.4702
4197.87 87.3304
4211.41 88.1993
4224.98 89.0767
4238.60 89.9631
4252.27 90.8593
4265.98 91.7644
4279.73 92.6785
4293.53 93.6018
4307.37 94.5342
4321.26 95.4763
4335.19 96.4277
4349.17 97.3888
4363.19 98.3592
4377.26 99.3393
4391.37 100.330
4405.53 101.330
4419.73 102.341
4433.98 103.361
4448.28 104.392
4462.62 105.433
4477.01 106.486
4491.44 107.549
4505.92 108.622
4520.45 109.706
4535.02 110.802
4549.65 111.909
4564.31 113.026
4579.03 114.155
4593.79 115.296
4608.60 116.449
4623.46 117.613
4638.37 118.789
4653.32 119.976
4668.33 121.176
4683.38 122.388
4698.48 123.612
4713.62 124.848
4728.82 126.097
4744.07 127.358
4759.36 128.632
4774.71 129.920
4790.10 131.220
4805.54 132.533
4821.04 133.859
4836.58 135.200
4852.17 136.554
4867.82 137.921
4883.51 139.302
4899.26 140.697
4915.05 142.107
4930.90 143.531
4946.80 144.969
4962.75 146.421
4978.75 147.888
4994.80 149.371
5010.90 150.869
5027.06 152.382
5043.26 153.910
5059.52 155.454
5075.84 157.014
5092.20 158.590
5108.62 160.181
5125.09 161.789
5141.61 163.412
5158.19 165.053
5174.82 166.709
5191.50 168.382
5208.24 170.072
5225.03 171.779
5241.88 173.505
5258.78 175.249
5275.73 177.010
5292.74 178.788
5309.81 180.584
5326.93 182.400
5344.10 184.235
5361.33 186.087
5378.62 187.958
5395.96 189.848
5413.35 191.759
5430.81 193.689
5448.32 195.638
5465.88 197.606
5483.50 199.595
5501.18 201.604
5518.92 203.634
5536.71 205.683
5554.56 207.753
5572.47 209.844
5590.44 211.955
5608.46 214.088
5626.54 216.241
5644.68 218.416
5662.88 220.614
5681.14 222.837
5699.46 225.082
5717.83 227.350
5736.27 229.639
5754.76 231.953
5773.31 234.292
5791.93 236.655
5810.60 239.040
5829.33 241.449
5848.13 243.884
5866.98 246.346
5885.90 248.831
5904.88 251.341
5923.91 253.876
5943.01 256.437
5962.17 259.024
5981.40 261.636
6000.68 264.273
6020.03 266.939
6039.44 269.632
6058.91 272.352
6078.44 275.100
6098.04 277.874
6117.70 280.677
6137.42 283.510
6157.21 286.375
6177.06 289.268
6196.98 292.190
6216.96 295.141
6237.00 298.122
6257.11 301.134
6277.28 304.176
6297.52 307.249
6317.82 310.352
6338.19 313.487
6358.63 316.655
6379.13 319.855
6399.69 323.086
6420.33 326.349
6441.03 329.647
6461.79 332.980
6482.63 336.345
6503.53 339.744
6524.49 343.176
6545.53 346.645
6566.63 350.150
6587.80 353.689
6609.04 357.264
6630.35 360.873
6651.73 364.522
6673.17 368.208
6694.69 371.931
6716.27 375.690
6737.93 379.487
6759.65 383.324
6781.44 387.200
6803.31 391.114
6825.24 395.068
6847.25 399.060
6869.32 403.095
6891.47 407.171
6913.69 411.288
6935.98 415.446
6958.34 419.644
6980.77 423.888
7003.28 428.176
7025.86 432.507
7048.51 436.880
7071.24 441.297
7094.03 445.759
7116.91 450.268
7139.85 454.820
7162.87 459.417
7185.96 464.060
7209.13 468.753
7232.38 473.493
7255.69 478.280
7279.09 483.113
7302.55 487.996
7326.10 492.927
7349.72 497.909
7373.41 502.939
7397.19 508.019
7421.03 513.150
7444.96 518.336
7468.96 523.575
7493.04 528.866
7517.20 534.209
7541.44 539.604
7565.75 545.056
7590.14 550.562
7614.62 556.123
7639.17 561.737
7663.79 567.406
7688.50 573.135
7713.29 578.922
7738.16 584.765
7763.11 590.665
7788.14 596.622
7813.25 602.644
7838.44 608.728
7863.71 614.870
7889.06 621.071
7914.50 627.333
7940.01 633.662
7965.61 640.054
7991.29 646.507
8017.06 653.027
8042.91 659.613
8068.84 666.267
8094.85 672.988
8120.95 679.774
8147.13 686.627
8173.40 693.545
8199.75 700.536
8226.19 707.596
8252.71 714.725
8279.32 721.924
8306.01 729.191
8332.79 736.537
8359.65 743.956
8386.60 751.447
8413.64 759.010
8440.77 766.645
8467.98 774.359
8495.29 782.151
8522.67 790.016
8550.15 797.956
8577.72 805.973
8605.37 814.075
8633.12 822.254
8660.95 830.511
8688.87 838.848
8716.89 847.264
8744.99 855.773
8773.19 864.364
8801.47 873.036
8829.85 881.791
8858.32 890.632
8886.88 899.564
8915.53 908.583
8944.27 917.687
8973.11 926.879
9002.04 936.156
9031.06 945.529
9060.18 954.991
9089.39 964.544
9118.69 974.186
9148.09 983.918
9177.59 993.754
9207.18 1003.68
9236.86 1013.71
9266.64 1023.83
9296.52 1034.04
9326.49 1044.35
9356.56 1054.77
9386.72 1065.27
9416.99 1075.88
9447.35 1086.59
9477.81 1097.41
9508.37 1108.33
9539.02 1119.35
9569.78 1130.47
9600.63 1141.70
9631.58 1153.05
9662.63 1164.51
9693.79 1176.07
9725.04 1187.74
9756.39 1199.52
9787.85 1211.41
9819.41 1223.40
9851.07 1235.52
9882.83 1247.73
9914.69 1260.07
9946.65 1272.53
9978.72 1285.10
10010.9 1297.80
10043.2 1310.62
10075.5 1323.56
10108.0 1336.63
10140.6 1349.82
10173.3 1363.12
10206.1 1376.55
10239.0 1390.10
10272.0 1403.79
10305.2 1417.60
10338.4 1431.54
10371.7 1445.60
10405.1 1459.79
10438.7 1474.12
10472.3 1488.58
10506.1 1503.17
10540.0 1517.88
10574.0 1532.73
10608.1 1547.73
10642.3 1562.86
10676.6 1578.12
10711.0 1593.52
10745.5 1609.05
10780.2 1624.73
10814.9 1640.56
10849.8 1656.52
10884.8 1672.62
10919.9 1688.86
10955.1 1705.26
10990.4 1721.80
11025.8 1738.48
11061.4 1755.31
11097.0 1772.28
11132.8 1789.42
11168.7 1806.71
11204.7 1824.15
11240.8 1841.73
11277.1 1859.47
11313.4 1877.37
11349.9 1895.43
11386.5 1913.64
11423.2 1932.00
11460.0 1950.51
11497.0 1969.20
11534.1 1988.04
11571.2 2007.04
11608.6 2026.20
11646.0 2045.52
11683.5 2065.01
11721.2 2084.67
11759.0 2104.49
11796.9 2124.47
11834.9 2144.61
11873.1 2164.94
11911.4 2185.44
11949.8 2206.10
11988.3 2226.93
12026.9 2247.93
12065.7 2269.10
12104.6 2290.45
12143.7 2311.97
12182.8 2333.66
12222.1 2355.52
12261.5 2377.58
12301.0 2399.82
12340.7 2422.23
12380.5 2444.81
12420.4 2467.57
12460.4 2490.52
12500.6 2513.65
12540.9 2536.95
12581.3 2560.43
12621.9 2584.10
12662.6 2607.97
12703.4 2632.01
12744.4 2656.23
12785.5 2680.65
12826.7 2705.24
12868.0 2730.02
12909.5 2754.99
12951.1 2780.15
12992.9 2805.48
13034.8 2831.01
13076.8 2856.75
13119.0 2882.68
13161.3 2908.79
13203.7 2935.09
13246.3 2961.58
13289.0 2988.27
13331.8 3015.16
13374.8 3042.22
13417.9 3069.48
13461.2 3096.93
13504.6 3124.59
13548.1 3152.44
13591.8 3180.48
13635.6 3208.71
13679.6 3237.14
13723.7 3265.77
13767.9 3294.58
13812.3 3323.59
13856.9 3352.79
13901.5 3382.19
13946.4 3411.80
13991.3 3441.60
14036.4 3471.59
14081.7 3501.78
14127.1 3532.17
14172.6 3562.75
14218.3 3593.52
14264.2 3624.49
14310.2 3655.65
14356.3 3687.00
14402.6 3718.57
14449.0 3750.32
14495.6 3782.27
14542.3 3814.41
14589.2 3846.74
14636.2 3879.25
14683.4 3911.97
14730.8 3944.86
14778.3 3977.94
14825.9 4011.22
14873.7 4044.71
14921.7 4078.38
14969.8 4112.24
15018.0 4146.37
15066.5 4180.85
15115.0 4215.54
15163.8 4250.41
15212.7 4285.47
15261.7 4320.73
15310.9 4356.18
15360.3 4391.83
15409.8 4427.67
15459.5 4463.69
15509.3 4499.91
15559.3 4536.31
15609.5 4572.91
15659.8 4609.69
15710.3 4646.67
15761.0 4683.81
15811.8 4721.14
15862.7 4758.65
15913.9 4796.35
15965.2 4834.22
16016.7 4872.26
16068.3 4910.49
16120.1 4948.89
16172.1 4987.46
16224.2 5026.21
16276.5 5065.12
16329.0 5104.21
16381.7 5143.49
16434.5 5182.92
16487.5 5222.51
16540.6 5262.27
16593.9 5302.21
16647.4 5342.29
16701.1 5382.53
16755.0 5422.93
16809.0 5463.49
16863.2 5504.19
16917.5 5545.04
16972.1 5586.05
17026.8 5627.20
17081.7 5668.50
17136.8 5709.94
17192.0 5751.52
17247.4 5793.24
17303.1 5835.11
17358.8 5877.10
17414.8 5919.21
17470.9 5961.44
17527.3 6003.81
17583.8 6046.29
17640.5 6088.90
17697.4 6131.63
17754.4 6174.50
17811.6 6217.47
17869.1 6260.56
17926.7 6303.76
17984.5 6347.06
18042.5 6390.45
18100.6 6433.93
18159.0 6477.52
18217.5 6521.21
18276.3 6565.00
18335.2 6608.85
18394.3 6652.80
18453.6 6696.84
18513.1 6740.98
18572.8 6785.17
18632.7 6829.43
18692.8 6873.77
18753.0 6918.19
18813.5 6962.68
18874.1 7007.21
18935.0 7051.79
18996.0 7096.45
19057.3 7141.15
19118.7 7185.91
19180.4 7230.70
19242.2 7275.52
19304.2 7320.39
19366.5 7365.30
19428.9 7410.25
19491.6 7455.20
19554.4 7500.17
19617.4 7545.17
19680.7 7590.19
19744.1 7635.24
19807.8 7676.69
19871.7 7716.04
19935.7 7755.41
20000.0 7794.78
20081.3 7845.79
20162.8 7905.30
20244.8 7965.51
20327.0 8025.78
20409.6 8086.09
20492.5 8146.39
20575.8 8206.73
20659.4 8267.08
20743.3 8327.46
20827.6 8387.78
20912.2 8448.09
20997.2 8508.41
21082.5 8568.71
21168.1 8628.96
21254.1 8689.19
21340.5 8749.37
21427.2 8809.53
21514.3 8869.58
21601.7 8929.59
21689.4 8989.56
21777.6 9049.44
21866.0 9109.23
21954.9 9168.94
22044.1 9228.57
22133.6 9288.12
22223.6 9347.52
22313.9 9406.82
22404.5 9466.02
22495.5 9525.10
22586.9 9584.02
22678.7 9642.81
22770.8 9701.49
22863.4 9760.01
22956.3 9818.35
23049.5 9876.55
23143.2 9934.59
23237.2 9992.46
23331.6 10050.1
23426.4 10107.6
23521.6 10165.0
23617.1 10222.1
23713.1 10279.0
23809.4 10335.7
23906.2 10392.3
24003.3 10448.6
24100.8 10504.6
24198.7 10560.5
24297.1 10616.1
24395.8 10671.5
24494.9 10726.7
24594.4 10781.6
24694.3 10836.3
24794.7 10890.7
24895.4 10944.8
24996.6 10998.7
25098.1 11052.4
25200.1 11105.7
25302.5 11158.8
25405.3 11211.6
25508.5 11264.2
25612.1 11316.4
25716.2 11368.4
25820.7 11420.0
25925.6 11471.4
26030.9 11522.5
26136.7 11573.3
26242.9 11623.8
26349.5 11673.9
26456.5 11723.8
26564.0 11773.3
26672.0 11822.5
26780.3 11871.4
26889.1 11920.0
26998.4 11968.2
27108.1 12016.1
27218.2 12063.7
27328.8 12111.0
27439.8 12157.9
27551.3 12204.5
27663.2 12250.7
27775.6 12296.6
27888.5 12342.2
28001.8 12387.4
28115.6 12432.3
28229.8 12476.8
28344.5 12520.9
28459.6 12564.8
28575.3 12608.3
28691.4 12651.4
28807.9 12694.1
28925.0 12736.5
29042.5 12778.6
29160.5 12820.3
29279.0 12861.6
29397.9 12902.6
29517.4 12943.3
29637.3 12983.5
29757.7 13023.4
29878.6 13062.9
30000.0 13102.1

View File

@@ -0,0 +1,654 @@
4000.00 4.18514
4032.00 4.27608
4064.00 4.36835
4096.00 4.46188
4128.00 4.55672
4160.00 4.65282
4192.00 4.75023
4224.00 4.84893
4256.00 4.94900
4288.00 5.05041
4320.00 5.15311
4352.00 5.25711
4384.00 5.36245
4416.00 5.46917
4448.00 5.57723
4480.00 5.68671
4512.00 5.79754
4544.00 5.90984
4576.00 6.02351
4608.00 6.13856
4640.00 6.25497
4672.00 6.37280
4704.00 6.49212
4736.00 6.61284
4768.00 6.73496
4800.00 6.85847
4832.00 6.98349
4864.00 7.10998
4896.00 7.23792
4928.00 7.36743
4960.00 7.49840
4992.00 7.63088
5024.00 7.76488
5056.00 7.90034
5088.00 8.03728
5120.00 8.17572
5152.00 8.31572
5184.00 8.45730
5216.00 8.60040
5248.00 8.74518
5280.00 8.89151
5312.00 9.03937
5344.00 9.18870
5376.00 9.33957
5408.00 9.49208
5440.00 9.64620
5472.00 9.80192
5504.00 9.95941
5536.00 10.1185
5568.00 10.2793
5600.00 10.4416
5632.00 10.6056
5664.00 10.7712
5696.00 10.9384
5728.00 11.1073
5760.00 11.2779
5792.00 11.4503
5824.00 11.6244
5856.00 11.8001
5888.00 11.9775
5920.00 12.1565
5952.00 12.3374
5984.00 12.5200
6016.00 12.7044
6048.00 12.8905
6080.00 13.0784
6112.00 13.2679
6144.00 13.4594
6176.00 13.6526
6208.00 13.8477
6240.00 14.0445
6272.00 14.2430
6304.00 14.4433
6336.00 14.6454
6368.00 14.8494
6400.00 15.0552
6432.00 15.2628
6464.00 15.4728
6496.00 15.6846
6528.00 15.8982
6560.00 16.1135
6592.00 16.3306
6624.00 16.5496
6656.00 16.7704
6688.00 16.9930
6720.00 17.2175
6752.00 17.4439
6784.00 17.6725
6816.00 17.9030
6848.00 18.1353
6880.00 18.3699
6912.00 18.6065
6944.00 18.8451
6976.00 19.0856
7008.00 19.3280
7040.00 19.5724
7072.00 19.8187
7104.00 20.0672
7136.00 20.3177
7168.00 20.5701
7200.00 20.8246
7232.00 21.0813
7264.00 21.3400
7296.00 21.6007
7328.00 21.8635
7360.00 22.1284
7392.00 22.3954
7424.00 22.6644
7456.00 22.9356
7488.00 23.2089
7520.00 23.4843
7552.00 23.7618
7584.00 24.0416
7616.00 24.3234
7648.00 24.6073
7680.00 24.8934
7712.00 25.1819
7744.00 25.4725
7776.00 25.7652
7808.00 26.0601
7840.00 26.3574
7872.00 26.6568
7904.00 26.9584
7936.00 27.2623
7968.00 27.5685
8000.00 27.8769
8032.00 28.1875
8064.00 28.5004
8096.00 28.8157
8128.00 29.1332
8160.00 29.4529
8192.00 29.7750
8224.00 30.0995
8256.00 30.4263
8288.00 30.7553
8320.00 31.0867
8352.00 31.4206
8384.00 31.7567
8416.00 32.0952
8448.00 32.4360
8480.00 32.7795
8512.00 33.1254
8544.00 33.4736
8576.00 33.8241
8608.00 34.1772
8640.00 34.5327
8672.00 34.8906
8704.00 35.2508
8736.00 35.6136
8768.00 35.9790
8800.00 36.3468
8832.00 36.7170
8864.00 37.0896
8896.00 37.4651
8928.00 37.8431
8960.00 38.2235
8992.00 38.6063
9024.00 38.9918
9056.00 39.3799
9088.00 39.7704
9120.00 40.1635
9152.00 40.5591
9184.00 40.9576
9216.00 41.3587
9248.00 41.7624
9280.00 42.1685
9312.00 42.5773
9344.00 42.9889
9376.00 43.4031
9408.00 43.8199
9440.00 44.2392
9472.00 44.6616
9504.00 45.0866
9536.00 45.5144
9568.00 45.9447
9600.00 46.3776
9632.00 46.8134
9664.00 47.2520
9696.00 47.6933
9728.00 48.1371
9760.00 48.5836
9792.00 49.0336
9824.00 49.4863
9856.00 49.9416
9888.00 50.3996
9920.00 50.8604
9952.00 51.3245
9984.00 51.7913
10016.0 52.2608
10048.0 52.7331
10080.0 53.2082
10112.0 53.6867
10144.0 54.1679
10176.0 54.6518
10208.0 55.1386
10240.0 55.6281
10272.0 56.1213
10304.0 56.6173
10336.0 57.1161
10368.0 57.6177
10400.0 58.1222
10432.0 58.6302
10464.0 59.1412
10496.0 59.6551
10528.0 60.1718
10560.0 60.6913
10592.0 61.2144
10624.0 61.7407
10656.0 62.2698
10688.0 62.8019
10720.0 63.3370
10752.0 63.8753
10784.0 64.4172
10816.0 64.9622
10848.0 65.5099
10880.0 66.0608
10912.0 66.6146
10944.0 67.1722
10976.0 67.7332
11008.0 68.2969
11040.0 68.8638
11072.0 69.4338
11090.0 69.7556
11090.4 69.7628
11090.8 69.7700
11091.2 69.7772
11091.6 69.7843
11092.0 69.7914
11092.4 69.7986
11092.8 69.8058
11093.2 69.8130
11093.6 69.8202
11094.0 69.8273
11094.4 69.8345
11094.8 69.8417
11095.2 69.8489
11095.6 69.8561
11096.0 69.8632
11096.4 69.8704
11096.8 69.8777
11097.2 69.8849
11097.6 69.8921
11098.0 69.8992
11098.4 69.9064
11098.8 69.9136
11099.2 69.9209
11099.6 69.9280
11100.0 69.9352
11100.4 69.9424
11100.8 69.9496
11101.2 69.9569
11101.6 69.9640
11102.0 69.9712
11102.4 69.9784
11102.8 69.9857
11103.0 69.9893
11103.1 26.8271
11103.2 9.23223
11103.3 9.23243
11103.6 9.23305
11104.0 9.23389
11104.4 9.23473
11104.8 9.23557
11105.2 9.23639
11105.6 9.23724
11106.0 9.23807
11106.4 9.23891
11106.8 9.23974
11107.2 9.24058
11107.6 9.24142
11108.0 9.24226
11108.4 9.24310
11108.8 9.24392
11109.2 9.24476
11109.6 9.24560
11110.0 9.24644
11136.0 9.30092
11168.0 9.36821
11200.0 9.43583
11232.0 9.50371
11264.0 9.57189
11296.0 9.64038
11328.0 9.70916
11360.0 9.77823
11392.0 9.84760
11424.0 9.91726
11456.0 9.98722
11488.0 10.0578
11520.0 10.1286
11552.0 10.1998
11584.0 10.2712
11616.0 10.3430
11648.0 10.4151
11680.0 10.4878
11712.0 10.5608
11744.0 10.6341
11776.0 10.7077
11808.0 10.7816
11840.0 10.8559
11872.0 10.9307
11904.0 11.0059
11936.0 11.0814
11968.0 11.1572
12000.0 11.2333
12032.0 11.3098
12064.0 11.3868
12096.0 11.4641
12128.0 11.5417
12160.0 11.6197
12192.0 11.6980
12224.0 11.7767
12256.0 11.8560
12288.0 11.9356
12320.0 12.0155
12352.0 12.0958
12384.0 12.1763
12416.0 12.2573
12448.0 12.3388
12480.0 12.4206
12512.0 12.5028
12544.0 12.5853
12576.0 12.6681
12608.0 12.7513
12640.0 12.8351
12672.0 12.9192
12704.0 13.0037
12736.0 13.0885
12768.0 13.1737
12800.0 13.2592
12832.0 13.3452
12864.0 13.4316
12896.0 13.5184
12928.0 13.6056
12960.0 13.6931
12992.0 13.7809
13024.0 13.8691
13056.0 13.9580
13088.0 14.0472
13120.0 14.1368
13152.0 14.2267
13184.0 14.3170
13216.0 14.4076
13248.0 14.4987
13280.0 14.5903
13312.0 14.6823
13344.0 14.7746
13376.0 14.8673
13408.0 14.9603
13440.0 15.0537
13472.0 15.1477
13504.0 15.2421
13536.0 15.3369
13568.0 15.4321
13600.0 15.5276
13632.0 15.6235
13664.0 15.7198
13696.0 15.8166
13728.0 15.9139
13760.0 16.0115
13792.0 16.1095
13824.0 16.2079
13856.0 16.3066
13888.0 16.4057
13920.0 16.5056
13952.0 16.6057
13984.0 16.7063
14016.0 16.8072
14048.0 16.9086
14080.0 17.0102
14112.0 17.1123
14144.0 17.2151
14176.0 17.3182
14208.0 17.4217
14240.0 17.5256
14272.0 17.6298
14304.0 17.7345
14336.0 17.8395
14368.0 17.9452
14400.0 18.0513
14432.0 18.1578
14464.0 18.2648
14496.0 18.3721
14528.0 18.4797
14560.0 18.5878
14592.0 18.6965
14624.0 18.8056
14656.0 18.9151
14688.0 19.0250
14720.0 19.1353
14752.0 19.2461
14784.0 19.3572
14816.0 19.4687
14848.0 19.5810
14880.0 19.6936
14912.0 19.8067
14944.0 19.9201
14976.0 20.0340
15008.0 20.1483
15040.0 20.2629
15072.0 20.3782
15104.0 20.4940
15136.0 20.6102
15168.0 20.7268
15200.0 20.8439
15232.0 20.9613
15264.0 21.0791
15296.0 21.1974
15328.0 21.3164
15360.0 21.4357
15392.0 21.5556
15424.0 21.6758
15456.0 21.7964
15488.0 21.9174
15520.0 22.0389
15552.0 22.1609
15584.0 22.2836
15616.0 22.4066
15648.0 22.5301
15680.0 22.6540
15712.0 22.7783
15744.0 22.9031
15776.0 23.0283
15808.0 23.1540
15840.0 23.2804
15872.0 23.4071
15904.0 23.5343
15936.0 23.6620
15968.0 23.7900
16000.0 23.9185
16032.0 24.0474
16064.0 24.1769
16096.0 24.3070
16128.0 24.4375
16160.0 24.5685
16192.0 24.6998
16224.0 24.8317
16256.0 24.9640
16288.0 25.0967
16320.0 25.2300
16352.0 25.3639
16384.0 25.4983
16416.0 25.6331
16448.0 25.7683
16480.0 25.9041
16512.0 26.0402
16544.0 26.1768
16576.0 26.3140
16608.0 26.4518
16640.0 26.5901
16672.0 26.7288
16704.0 26.8680
16736.0 27.0076
16768.0 27.1477
16800.0 27.2883
16832.0 27.4292
16864.0 27.5710
16896.0 27.7132
16928.0 27.8559
16960.0 27.9990
16992.0 28.1426
17024.0 28.2867
17056.0 28.4312
17088.0 28.5762
17120.0 28.7218
17152.0 28.8680
17184.0 29.0147
17216.0 29.1619
17248.0 29.3096
17280.0 29.4577
17312.0 29.6063
17344.0 29.7553
17376.0 29.9049
17408.0 30.0551
17440.0 30.2058
17472.0 30.3571
17504.0 30.5087
17536.0 30.6609
17568.0 30.8135
17600.0 30.9667
17632.0 31.1203
17664.0 31.2744
17696.0 31.4293
17728.0 31.5847
17760.0 31.7406
17792.0 31.8969
17824.0 32.0539
17856.0 32.2112
17888.0 32.3690
17920.0 32.5274
17952.0 32.6862
17984.0 32.8457
18016.0 33.0058
18048.0 33.1663
18080.0 33.3274
18112.0 33.4889
18144.0 33.6510
18176.0 33.8135
18208.0 33.9765
18240.0 34.1400
18272.0 34.3043
18304.0 34.4691
18336.0 34.6345
18368.0 34.8003
18400.0 34.9667
18432.0 35.1335
18464.0 35.3009
18496.0 35.4688
18528.0 35.6371
18560.0 35.8062
18592.0 35.9758
18624.0 36.1461
18656.0 36.3168
18688.0 36.4880
18720.0 36.6597
18752.0 36.8320
18784.0 37.0047
18816.0 37.1780
18848.0 37.3518
18880.0 37.5264
18912.0 37.7016
18944.0 37.8772
18976.0 38.0533
19008.0 38.2300
19040.0 38.4072
19072.0 38.5849
19104.0 38.7631
19136.0 38.9418
19168.0 39.1213
19200.0 39.3014
19232.0 39.4821
19264.0 39.6633
19296.0 39.8449
19328.0 40.0272
19360.0 40.2099
19392.0 40.3932
19424.0 40.5770
19456.0 40.7613
19488.0 40.9464
19520.0 41.1321
19552.0 41.3183
19584.0 41.5051
19616.0 41.6923
19648.0 41.8801
19680.0 42.0685
19712.0 42.2573
19744.0 42.4468
19776.0 42.6368
19808.0 42.8276
19840.0 43.0190
19872.0 43.2109
19904.0 43.4034
19936.0 43.5964
19968.0 43.7899
20000.0 43.9840
20081.3 44.4794
20162.8 44.9811
20244.8 45.4884
20327.0 46.0015
20409.6 46.5203
20492.5 47.0458
20575.8 47.5773
20659.4 48.1147
20743.3 48.6582
20827.6 49.2087
20912.2 49.7653
20997.2 50.3283
21082.5 50.8976
21168.1 51.4742
21254.1 52.0573
21340.5 52.6470
21427.2 53.2434
21514.3 53.8472
21601.7 54.4579
21689.4 55.0755
21777.6 55.7001
21866.0 56.3329
21954.9 56.9726
22044.1 57.6197
22133.6 58.2743
22223.6 58.9369
22313.9 59.6071
22404.5 60.2848
22495.5 60.9705
22586.9 61.6647
22678.7 62.3667
22770.8 63.0767
22863.4 63.7950
22956.3 64.5220
23049.5 65.2573
23143.2 66.0009
23237.2 66.7533
23331.6 67.5151
23426.4 68.2854
23521.6 69.0646
23617.1 69.8528
23713.1 70.6508
23809.4 71.4578
23906.2 72.2739
24003.3 73.0998
24100.8 73.9355
24198.7 74.7810
24297.1 75.6360
24395.8 76.5011
24494.9 77.3767
24594.4 78.2624
24694.3 79.1580
24794.7 80.0644
24895.4 80.9815
24996.6 81.9092
25098.1 82.8474
25200.1 83.7969
25302.5 84.7579
25405.3 85.7297
25508.5 86.7128
25612.1 87.7077
25716.2 88.7142
25820.7 89.7324
25925.6 90.7621
26030.9 91.8045
26136.7 92.8592
26242.9 93.9258
26349.5 95.0048
26456.5 96.0966
26564.0 97.2014
26672.0 98.3188
26780.3 99.4487
26889.1 100.593
26998.4 101.750
27108.1 102.920
27218.2 104.104
27328.8 105.303
27439.8 106.515
27551.3 107.741
27663.2 108.981
27775.6 110.237
27888.5 111.507
28001.8 112.791
28115.6 114.091
28229.8 115.406
28344.5 116.736
28459.6 118.082
28575.3 119.443
28691.4 120.821
28807.9 122.214
28925.0 123.624
29042.5 125.050
29160.5 126.493
29279.0 127.953
29397.9 129.430
29517.4 130.924
29637.3 132.436
29757.7 133.965
29878.6 135.512
30000.0 137.076

View File

@@ -0,0 +1,601 @@
4000.00 9.62277
4032.00 9.83842
4064.00 10.0573
4096.00 10.2794
4128.00 10.5051
4160.00 10.7339
4192.00 10.9660
4224.00 11.2014
4256.00 11.4403
4288.00 11.6826
4320.00 11.9284
4352.00 12.1776
4384.00 12.4304
4416.00 12.6865
4448.00 12.9464
4480.00 13.2098
4512.00 13.4767
4544.00 13.7474
4576.00 14.0215
4608.00 14.2994
4640.00 14.5809
4672.00 14.8664
4704.00 15.1555
4736.00 15.4484
4768.00 15.7450
4800.00 16.0453
4832.00 16.3495
4864.00 16.6575
4896.00 16.9695
4928.00 17.2853
4960.00 17.6050
4992.00 17.9287
5024.00 18.2562
5056.00 18.5883
5088.00 18.9244
5120.00 19.2644
5152.00 19.6086
5184.00 19.9567
5216.00 20.3089
5248.00 20.6651
5280.00 21.0254
5312.00 21.3903
5344.00 21.7594
5376.00 22.1326
5408.00 22.5101
5440.00 22.8918
5472.00 23.2781
5504.00 23.6687
5536.00 24.0635
5568.00 24.4631
5600.00 24.8670
5632.00 25.2752
5664.00 25.6881
5696.00 26.1054
5728.00 26.5272
5760.00 26.9536
5792.00 27.3845
5824.00 27.8199
5856.00 28.2600
5888.00 28.7046
5920.00 29.1541
5952.00 29.6085
5984.00 30.0675
6016.00 30.5313
6048.00 31.0001
6080.00 31.4734
6112.00 31.9515
6144.00 32.4345
6176.00 32.9223
6208.00 33.4148
6240.00 33.9124
6272.00 34.4149
6304.00 34.9221
6336.00 35.4342
6368.00 35.9524
6400.00 36.4762
6432.00 37.0050
6464.00 37.5379
6496.00 38.0757
6528.00 38.6185
6560.00 39.1667
6592.00 39.7202
6624.00 40.2787
6656.00 40.8428
6688.00 41.4125
6720.00 41.9874
6752.00 42.5676
6784.00 43.1532
6816.00 43.7441
6848.00 44.3403
6880.00 44.9420
6912.00 45.5490
6944.00 46.1614
6976.00 46.7793
7008.00 47.4032
7040.00 48.0324
7072.00 48.6671
7104.00 49.3076
7136.00 49.9537
7168.00 50.6054
7200.00 51.2625
7232.00 51.9256
7264.00 52.5944
7296.00 53.2688
7328.00 53.9491
7360.00 54.6357
7392.00 55.3279
7424.00 56.0259
7456.00 56.7298
7488.00 57.4395
7520.00 58.1551
7552.00 58.8764
7584.00 59.6041
7616.00 60.3376
7648.00 61.0770
7680.00 61.8224
7712.00 62.5745
7744.00 63.3325
7776.00 64.0965
7808.00 64.8666
7840.00 65.6432
7872.00 66.4257
7904.00 67.2144
7936.00 68.0094
7968.00 68.8110
8000.00 69.6188
8032.00 70.4328
8064.00 71.2532
8096.00 72.0802
8128.00 72.9134
8160.00 73.7529
8192.00 74.5991
8224.00 75.4518
8256.00 76.3108
8288.00 77.1762
8320.00 78.0482
8352.00 78.9273
8384.00 79.8130
8416.00 80.7051
8448.00 81.6036
8480.00 82.5091
8512.00 83.4214
8544.00 84.3400
8576.00 85.2654
8608.00 86.1981
8640.00 87.1375
8672.00 88.0838
8704.00 89.0367
8736.00 89.9967
8768.00 90.9639
8800.00 91.9378
8832.00 92.9185
8864.00 93.9060
8896.00 94.9009
8928.00 95.9025
8960.00 96.9111
8992.00 97.9265
9024.00 98.9496
9056.00 99.9804
9088.00 101.018
9120.00 102.063
9152.00 103.115
9184.00 104.174
9216.00 105.240
9248.00 106.314
9280.00 107.395
9312.00 108.483
9344.00 109.578
9376.00 110.681
9408.00 111.791
9440.00 112.909
9472.00 114.034
9504.00 115.168
9536.00 116.308
9568.00 117.457
9600.00 118.612
9632.00 119.776
9664.00 120.947
9696.00 122.125
9728.00 123.311
9760.00 124.505
9792.00 125.707
9824.00 126.917
9856.00 128.134
9888.00 129.358
9920.00 130.591
9952.00 131.832
9984.00 133.080
10016.0 134.336
10048.0 135.601
10080.0 136.872
10112.0 138.154
10144.0 139.442
10176.0 140.739
10208.0 142.044
10240.0 143.356
10272.0 144.677
10304.0 146.006
10336.0 147.343
10368.0 148.687
10400.0 150.040
10432.0 151.402
10464.0 152.773
10496.0 154.152
10528.0 155.538
10560.0 156.933
10592.0 158.336
10624.0 159.748
10656.0 161.168
10688.0 162.596
10720.0 164.032
10752.0 165.477
10784.0 166.932
10816.0 168.395
10848.0 169.867
10880.0 171.346
10912.0 172.835
10944.0 174.332
10976.0 175.837
11008.0 177.351
11040.0 178.873
11072.0 180.404
11104.0 181.944
11136.0 183.494
11168.0 185.052
11200.0 186.620
11232.0 188.195
11264.0 189.780
11296.0 191.374
11328.0 192.976
11360.0 194.588
11392.0 196.209
11424.0 197.838
11456.0 199.475
11488.0 201.123
11520.0 202.780
11552.0 204.446
11584.0 206.121
11616.0 207.805
11648.0 209.497
11680.0 211.201
11712.0 212.913
11744.0 214.634
11776.0 216.365
11808.0 218.104
11840.0 219.853
11872.0 221.612
11904.0 223.381
11936.0 225.158
11968.0 226.944
12000.0 228.740
12032.0 230.545
12064.0 232.361
12096.0 234.186
12128.0 236.020
12160.0 237.864
12192.0 239.716
12224.0 241.579
12256.0 243.451
12288.0 245.333
12320.0 247.226
12352.0 249.127
12384.0 251.038
12416.0 252.958
12448.0 254.890
12480.0 256.832
12512.0 258.783
12544.0 260.744
12576.0 262.715
12608.0 264.695
12640.0 266.686
12672.0 268.687
12704.0 270.697
12736.0 272.718
12768.0 274.748
12800.0 276.788
12832.0 278.839
12864.0 280.900
12896.0 282.971
12928.0 285.053
12960.0 287.144
12992.0 289.246
13024.0 291.357
13056.0 293.479
13088.0 295.612
13120.0 297.754
13152.0 299.907
13184.0 302.069
13216.0 304.242
13248.0 306.425
13280.0 308.621
13312.0 310.827
13344.0 313.044
13376.0 315.270
13408.0 317.508
13440.0 319.755
13472.0 322.013
13504.0 324.282
13536.0 326.561
13568.0 328.850
13600.0 331.150
13632.0 333.460
13664.0 335.781
13696.0 338.114
13728.0 340.458
13760.0 342.813
13792.0 345.178
13824.0 347.554
13856.0 349.940
13888.0 352.337
13920.0 354.747
13952.0 357.167
13984.0 359.599
14016.0 362.041
14048.0 364.494
14080.0 366.957
14112.0 369.432
14144.0 371.917
14176.0 374.414
14208.0 376.922
14240.0 379.440
14272.0 381.970
14304.0 384.509
14336.0 387.061
14368.0 389.625
14400.0 392.202
14432.0 394.789
14464.0 397.388
14496.0 399.998
14528.0 402.618
14560.0 405.251
14592.0 407.895
14624.0 410.550
14656.0 413.216
14688.0 415.894
14720.0 418.584
14752.0 421.285
14784.0 423.996
14816.0 426.719
14848.0 429.456
14880.0 432.204
14912.0 434.963
14944.0 437.734
14976.0 440.516
15008.0 443.310
15040.0 446.117
15072.0 448.937
15104.0 451.769
15136.0 454.612
15168.0 457.468
15200.0 460.335
15232.0 463.213
15264.0 466.102
15296.0 469.003
15328.0 471.919
15360.0 474.845
15392.0 477.785
15424.0 480.734
15456.0 483.697
15488.0 486.670
15520.0 489.657
15552.0 492.654
15584.0 495.668
15616.0 498.692
15648.0 501.728
15680.0 504.777
15712.0 507.838
15744.0 510.910
15776.0 513.994
15808.0 517.091
15840.0 520.202
15872.0 523.325
15904.0 526.460
15936.0 529.607
15968.0 532.766
16000.0 535.937
16032.0 539.120
16064.0 542.317
16096.0 545.528
16128.0 548.751
16160.0 551.986
16192.0 555.233
16224.0 558.493
16256.0 561.765
16288.0 565.049
16320.0 568.346
16352.0 571.658
16384.0 574.983
16416.0 578.320
16448.0 581.669
16480.0 585.031
16512.0 588.405
16544.0 591.791
16576.0 595.191
16608.0 598.605
16640.0 602.032
16672.0 605.472
16704.0 608.925
16736.0 612.389
16768.0 615.866
16800.0 619.357
16832.0 622.859
16864.0 626.377
16896.0 629.907
16928.0 633.450
16960.0 637.007
16992.0 640.575
17024.0 644.157
17056.0 647.751
17088.0 651.359
17120.0 654.980
17152.0 658.617
17184.0 662.267
17216.0 665.931
17248.0 669.607
17280.0 673.296
17312.0 676.998
17344.0 680.712
17376.0 684.440
17408.0 688.184
17440.0 691.941
17472.0 695.713
17504.0 699.495
17536.0 703.292
17568.0 707.102
17600.0 710.925
17632.0 714.761
17664.0 718.610
17696.0 722.475
17728.0 726.355
17760.0 730.248
17792.0 734.153
17824.0 738.073
17856.0 742.005
17888.0 745.950
17920.0 749.909
17952.0 753.881
17984.0 757.869
18016.0 761.873
18048.0 765.889
18080.0 769.919
18112.0 773.963
18144.0 778.019
18176.0 782.090
18208.0 786.173
18240.0 790.271
18272.0 794.384
18304.0 798.512
18336.0 802.655
18368.0 806.810
18400.0 810.978
18432.0 815.161
18464.0 819.358
18496.0 823.568
18528.0 827.792
18560.0 832.031
18592.0 836.287
18624.0 840.558
18656.0 844.842
18688.0 849.140
18720.0 853.452
18752.0 857.777
18784.0 862.116
18816.0 866.467
18848.0 870.834
18880.0 875.220
18912.0 879.619
18944.0 884.030
18976.0 888.457
19008.0 892.897
19040.0 897.352
19072.0 901.820
19104.0 906.300
19136.0 910.796
19168.0 915.308
19200.0 919.837
19232.0 924.381
19264.0 928.937
19296.0 933.507
19328.0 938.093
19360.0 942.690
19392.0 947.303
19424.0 951.930
19456.0 956.571
19488.0 961.231
19520.0 965.908
19552.0 970.597
19584.0 975.302
19616.0 980.021
19648.0 984.753
19680.0 989.501
19712.0 994.261
19744.0 999.036
19776.0 1003.83
19808.0 1008.64
19840.0 1013.46
19872.0 1018.30
19904.0 1023.16
19936.0 1028.03
19968.0 1032.91
20000.0 1037.81
20081.3 1050.33
20162.8 1063.00
20244.8 1075.81
20327.0 1088.77
20409.6 1101.88
20492.5 1115.15
20575.8 1128.57
20659.4 1142.15
20743.3 1155.88
20827.6 1169.78
20912.2 1183.84
20997.2 1198.06
21082.5 1212.45
21168.1 1227.01
21254.1 1241.73
21340.5 1256.62
21427.2 1271.69
21514.3 1286.92
21601.7 1302.34
21689.4 1317.93
21777.6 1333.69
21866.0 1349.65
21954.9 1365.78
22044.1 1382.10
22133.6 1398.61
22223.6 1415.31
22313.9 1432.20
22404.5 1449.27
22495.5 1466.55
22586.9 1484.02
22678.7 1501.69
22770.8 1519.56
22863.4 1537.63
22956.3 1555.91
23049.5 1574.40
23143.2 1593.09
23237.2 1611.99
23331.6 1631.11
23426.4 1650.44
23521.6 1669.98
23617.1 1689.75
23713.1 1709.74
23809.4 1729.95
23906.2 1750.38
24003.3 1771.04
24100.8 1791.94
24198.7 1813.06
24297.1 1834.42
24395.8 1856.01
24494.9 1877.85
24594.4 1899.93
24694.3 1922.25
24794.7 1944.81
24895.4 1967.62
24996.6 1990.69
25098.1 2014.00
25200.1 2037.56
25302.5 2061.38
25405.3 2085.46
25508.5 2109.80
25612.1 2134.41
25716.2 2159.29
25820.7 2184.43
25925.6 2209.84
26030.9 2235.52
26136.7 2261.48
26242.9 2287.71
26349.5 2314.22
26456.5 2341.01
26564.0 2368.09
26672.0 2395.46
26780.3 2423.11
26889.1 2451.06
26998.4 2479.30
27108.1 2507.82
27218.2 2536.65
27328.8 2565.78
27439.8 2595.22
27551.3 2624.97
27663.2 2655.01
27775.6 2685.37
27888.5 2716.04
28001.8 2747.03
28115.6 2778.33
28229.8 2809.95
28344.5 2841.89
28459.6 2874.14
28575.3 2906.73
28691.4 2939.65
28807.9 2972.90
28925.0 3006.49
29042.5 3040.40
29160.5 3074.66
29279.0 3109.27
29397.9 3144.20
29517.4 3179.48
29637.3 3215.11
29757.7 3251.08
29878.6 3287.40
30000.0 3324.06

View File

@@ -0,0 +1,689 @@
4000.00 14.6403
4012.90 14.7694
4025.83 14.8996
4038.81 15.0310
4051.83 15.1635
4064.90 15.2972
4078.00 15.4320
4091.15 15.5680
4104.34 15.7050
4117.57 15.8433
4130.85 15.9828
4144.17 16.1237
4157.53 16.2662
4170.93 16.4100
4184.38 16.5550
4197.87 16.7012
4211.41 16.8485
4224.98 16.9972
4238.60 17.1472
4252.27 17.2985
4265.98 17.4509
4279.73 17.6047
4293.53 17.7599
4307.37 17.9166
4321.26 18.0750
4335.19 18.2347
4349.17 18.3959
4363.19 18.5586
4377.26 18.7226
4391.37 18.8881
4405.53 19.0551
4419.73 19.2235
4433.98 19.3933
4448.28 19.5646
4462.62 19.7375
4477.01 19.9119
4491.44 20.0878
4505.92 20.2653
4520.45 20.4443
4535.02 20.6251
4549.65 20.8077
4564.31 20.9919
4579.03 21.1778
4593.79 21.3654
4608.60 21.5546
4623.46 21.7455
4638.37 21.9381
4653.32 22.1324
4668.33 22.3285
4683.38 22.5262
4698.48 22.7258
4713.62 22.9271
4728.82 23.1303
4744.07 23.3353
4759.36 23.5420
4774.71 23.7507
4790.10 23.9613
4805.54 24.1737
4821.04 24.3880
4836.58 24.6042
4852.17 24.8225
4867.82 25.0427
4883.51 25.2649
4899.26 25.4891
4915.05 25.7153
4930.90 25.9436
4946.80 26.1739
4960.00 26.3662
4960.20 26.3691
4960.40 26.3720
4960.60 26.3750
4960.80 26.3779
4961.00 26.3808
4961.20 26.3837
4961.40 26.3866
4961.60 26.3896
4961.80 26.3925
4962.00 26.3954
4962.20 26.3983
4962.40 26.4012
4962.60 26.4041
4962.80 26.4071
4963.00 26.4100
4963.20 26.4129
4963.40 26.4158
4963.60 26.4188
4963.80 26.4217
4964.00 26.4246
4964.20 26.4275
4964.40 26.4304
4964.60 26.4334
4964.80 26.4363
4965.00 26.4392
4965.20 26.4421
4965.40 26.4451
4965.60 26.4480
4965.80 26.4509
4966.00 26.4539
4966.20 26.4568
4966.21 26.4570
4966.22 26.4571
4966.23 26.4573
4966.24 26.4574
4966.25 26.4576
4966.26 26.4577
4966.27 26.4579
4966.28 26.4580
4966.29 26.4581
4966.30 26.4583
4966.31 23.8848
4966.32 21.5614
4966.33 19.4639
4966.34 16.6939
4966.35 15.0697
4966.36 13.6035
4966.37 12.2799
4966.38 11.0851
4966.39 10.0065
4966.40 9.03278
4966.41 8.15382
4966.42 7.36037
4966.43 6.31256
4966.44 5.69825
4966.45 5.14372
4966.46 4.64315
4966.47 4.19129
4966.48 3.78339
4966.49 3.41519
4966.50 3.08282
4966.51 3.08283
4966.52 3.08285
4966.53 3.08287
4966.54 3.08288
4966.55 3.08290
4966.56 3.08291
4966.57 3.08293
4966.58 3.08294
4966.59 3.08296
4966.60 3.08297
4966.80 3.08327
4967.00 3.08358
4967.20 3.08388
4967.40 3.08419
4967.60 3.08449
4967.80 3.08479
4968.00 3.08510
4968.20 3.08540
4968.40 3.08570
4968.60 3.08601
4968.80 3.08631
4969.00 3.08662
4969.20 3.08692
4969.40 3.08722
4969.60 3.08753
4969.80 3.08783
4970.00 3.08814
4978.75 3.10144
4994.80 3.12596
5010.90 3.15067
5027.06 3.17557
5043.26 3.20067
5059.52 3.22597
5075.84 3.25146
5092.20 3.27716
5108.62 3.30319
5125.09 3.32942
5141.61 3.35585
5158.19 3.38250
5174.82 3.40948
5191.50 3.43668
5208.24 3.46410
5225.03 3.49173
5241.88 3.51971
5258.78 3.54792
5275.73 3.57634
5292.74 3.60500
5309.81 3.63399
5326.93 3.66323
5344.10 3.69270
5361.33 3.72241
5378.62 3.75246
5395.96 3.78276
5413.35 3.81331
5430.81 3.84410
5448.32 3.87526
5465.88 3.90668
5483.50 3.93837
5501.18 3.97031
5518.92 4.00261
5536.71 4.03521
5554.56 4.06807
5572.47 4.10120
5590.44 4.13469
5608.46 4.16849
5626.54 4.20255
5644.68 4.23691
5662.88 4.27164
5681.14 4.30669
5699.46 4.34202
5717.83 4.37766
5736.27 4.41366
5754.76 4.44998
5773.31 4.48660
5791.93 4.52352
5810.60 4.56084
5829.33 4.59850
5848.13 4.63647
5866.98 4.67476
5885.90 4.71346
5904.88 4.75253
5923.91 4.79192
5943.01 4.83164
5962.17 4.87178
5981.40 4.91229
6000.68 4.95315
6020.03 4.99434
6039.44 5.03595
6058.91 5.07795
6078.44 5.12030
6098.04 5.16301
6117.70 5.20616
6137.42 5.24972
6157.21 5.29365
6177.06 5.33794
6196.98 5.38268
6216.96 5.42785
6237.00 5.47339
6257.11 5.51932
6277.28 5.56570
6297.52 5.61256
6317.82 5.65980
6338.19 5.70744
6358.63 5.75555
6379.13 5.80417
6399.69 5.85320
6420.33 5.90263
6441.03 5.95254
6461.79 6.00294
6482.63 6.05376
6503.53 6.10501
6524.49 6.15675
6545.53 6.20899
6566.63 6.26170
6587.80 6.31484
6609.04 6.36848
6630.35 6.42265
6651.73 6.47729
6673.17 6.53239
6694.69 6.58802
6716.27 6.64423
6737.93 6.70093
6759.65 6.75810
6781.44 6.81583
6803.31 6.87416
6825.24 6.93301
6847.25 6.99235
6869.32 7.05223
6891.47 7.11270
6913.69 7.17368
6935.98 7.23521
6958.34 7.29728
6980.77 7.35997
7003.28 7.42321
7025.86 7.48699
7048.51 7.55137
7071.24 7.61644
7094.03 7.68207
7116.91 7.74827
7139.85 7.81507
7162.87 7.88255
7185.96 7.95061
7209.13 8.01927
7232.38 8.08853
7255.69 8.15854
7279.09 8.22916
7302.55 8.30039
7326.10 8.37226
7349.72 8.44491
7373.41 8.51819
7397.19 8.59211
7421.03 8.66669
7444.96 8.74202
7468.96 8.81801
7493.04 8.89466
7517.20 8.97199
7541.44 9.05018
7565.75 9.12905
7590.14 9.20861
7614.62 9.28887
7639.17 9.36994
7663.79 9.45172
7688.50 9.53421
7713.29 9.61744
7738.16 9.70154
7763.11 9.78637
7788.14 9.87195
7813.25 9.95827
7838.44 10.0456
7863.71 10.1336
7889.06 10.2224
7914.50 10.3120
7940.01 10.4025
7965.61 10.4937
7991.29 10.5858
8017.06 10.6786
8042.91 10.7726
8068.84 10.8674
8094.85 10.9630
8120.95 11.0595
8147.13 11.1570
8173.40 11.2553
8199.75 11.3545
8226.19 11.4546
8252.71 11.5557
8279.32 11.6578
8306.01 11.7607
8332.79 11.8646
8359.65 11.9695
8386.60 12.0754
8413.64 12.1822
8440.77 12.2900
8467.98 12.3988
8495.29 12.5087
8522.67 12.6195
8550.15 12.7312
8577.72 12.8441
8605.37 12.9580
8633.12 13.0730
8660.95 13.1889
8688.87 13.3061
8716.89 13.4244
8744.99 13.5438
8773.19 13.6643
8801.47 13.7858
8829.85 13.9084
8858.32 14.0320
8886.88 14.1568
8915.53 14.2830
8944.27 14.4105
8973.11 14.5392
9002.04 14.6689
9031.06 14.7999
9060.18 14.9320
9089.39 15.0654
9118.69 15.1999
9148.09 15.3357
9177.59 15.4728
9207.18 15.6112
9236.86 15.7509
9266.64 15.8917
9296.52 16.0339
9326.49 16.1773
9356.56 16.3221
9386.72 16.4684
9416.99 16.6162
9447.35 16.7655
9477.81 16.9160
9508.37 17.0680
9539.02 17.2213
9569.78 17.3760
9600.63 17.5322
9631.58 17.6897
9662.63 17.8488
9693.79 18.0092
9725.04 18.1712
9756.39 18.3347
9787.85 18.4999
9819.41 18.6666
9851.07 18.8348
9882.83 19.0044
9914.69 19.1758
9946.65 19.3488
9978.72 19.5234
10010.9 19.6996
10043.2 19.8773
10075.5 20.0569
10108.0 20.2382
10140.6 20.4211
10173.3 20.6057
10206.1 20.7920
10239.0 20.9800
10272.0 21.1700
10305.2 21.3616
10338.4 21.5550
10371.7 21.7500
10405.1 21.9471
10438.7 22.1461
10472.3 22.3469
10506.1 22.5495
10540.0 22.7540
10574.0 22.9605
10608.1 23.1689
10642.3 23.3792
10676.6 23.5915
10711.0 23.8057
10745.5 24.0220
10780.2 24.2404
10814.9 24.4608
10849.8 24.6833
10884.8 24.9077
10919.9 25.1343
10955.1 25.3632
10990.4 25.5942
11025.8 25.8273
11061.4 26.0625
11097.0 26.3000
11132.8 26.5398
11168.7 26.7818
11204.7 27.0260
11240.8 27.2724
11277.1 27.5212
11313.4 27.7726
11349.9 28.0262
11386.5 28.2822
11423.2 28.5405
11460.0 28.8013
11497.0 29.0646
11534.1 29.3304
11571.2 29.5985
11608.6 29.8691
11646.0 30.1424
11683.5 30.4183
11721.2 30.6966
11759.0 30.9775
11796.9 31.2610
11834.9 31.5474
11873.1 31.8366
11911.4 32.1285
11949.8 32.4231
11988.3 32.7203
12026.9 33.0205
12065.7 33.3236
12104.6 33.6295
12143.7 33.9381
12182.8 34.2496
12222.1 34.5641
12261.5 34.8817
12301.0 35.2022
12340.7 35.5256
12380.5 35.8520
12420.4 36.1816
12460.4 36.5144
12500.6 36.8503
12540.9 37.1892
12581.3 37.5312
12621.9 37.8768
12662.6 38.2256
12703.4 38.5776
12744.4 38.9329
12785.5 39.2915
12826.7 39.6536
12868.0 40.0192
12909.5 40.3881
12951.1 40.7605
12992.9 41.1363
13034.8 41.5159
13076.8 41.8991
13119.0 42.2859
13161.3 42.6762
13203.7 43.0702
13246.3 43.4679
13289.0 43.8695
13331.8 44.2748
13374.8 44.6838
13417.9 45.0965
13461.2 45.5134
13504.6 45.9344
13548.1 46.3592
13591.8 46.7880
13635.6 47.2206
13679.6 47.6577
13723.7 48.0990
13767.9 48.5443
13812.3 48.9937
13856.9 49.4473
13901.5 49.9055
13946.4 50.3679
13991.3 50.8346
14036.4 51.3056
14081.7 51.7811
14127.1 52.2613
14172.6 52.7460
14218.3 53.2352
14264.2 53.7291
14310.2 54.2274
14356.3 54.7308
14402.6 55.2389
14449.0 55.7517
14495.6 56.2693
14542.3 56.7917
14589.2 57.3192
14636.2 57.8518
14683.4 58.3893
14730.8 58.9317
14778.3 59.4792
14825.9 60.0323
14873.7 60.5908
14921.7 61.1543
14969.8 61.7231
15018.0 62.2972
15066.5 62.8770
15115.0 63.4622
15163.8 64.0528
15212.7 64.6489
15261.7 65.2506
15310.9 65.8585
15360.3 66.4721
15409.8 67.0915
15459.5 67.7166
15509.3 68.3476
15559.3 68.9847
15609.5 69.6277
15659.8 70.2767
15710.3 70.9319
15761.0 71.5930
15811.8 72.2610
15862.7 72.9353
15913.9 73.6160
15965.2 74.3028
16016.7 74.9961
16068.3 75.6963
16120.1 76.4033
16172.1 77.1167
16224.2 77.8368
16276.5 78.5635
16329.0 79.2975
16381.7 80.0384
16434.5 80.7861
16487.5 81.5408
16540.6 82.3025
16593.9 83.0723
16647.4 83.8492
16701.1 84.6333
16755.0 85.4248
16809.0 86.2237
16863.2 87.0304
16917.5 87.8445
16972.1 88.6662
17026.8 89.4956
17081.7 90.3328
17136.8 91.1786
17192.0 92.0322
17247.4 92.8938
17303.1 93.7636
17358.8 94.6414
17414.8 95.5282
17470.9 96.4233
17527.3 97.3269
17583.8 98.2387
17640.5 99.1590
17697.4 100.088
17754.4 101.027
17811.6 101.973
17869.1 102.929
17926.7 103.893
17984.5 104.868
18042.5 105.852
18100.6 106.844
18159.0 107.846
18217.5 108.858
18276.3 109.879
18335.2 110.911
18394.3 111.951
18453.6 113.002
18513.1 114.062
18572.8 115.133
18632.7 116.214
18692.8 117.305
18753.0 118.406
18813.5 119.518
18874.1 120.640
18935.0 121.773
18996.0 122.917
19057.3 124.071
19118.7 125.236
19180.4 126.413
19242.2 127.601
19304.2 128.800
19366.5 130.010
19428.9 131.232
19491.6 132.465
19554.4 133.710
19617.4 134.967
19680.7 136.235
19744.1 137.516
19807.8 138.809
19871.7 140.115
19935.7 141.432
20000.0 142.762
20081.3 144.456
20162.8 146.170
20244.8 147.905
20327.0 149.660
20409.6 151.436
20492.5 153.234
20575.8 155.052
20659.4 156.892
20743.3 158.755
20827.6 160.641
20912.2 162.549
20997.2 164.479
21082.5 166.432
21168.1 168.409
21254.1 170.409
21340.5 172.433
21427.2 174.481
21514.3 176.554
21601.7 178.652
21689.4 180.774
21777.6 182.922
21866.0 185.095
21954.9 187.294
22044.1 189.520
22133.6 191.772
22223.6 194.051
22313.9 196.357
22404.5 198.690
22495.5 201.051
22586.9 203.441
22678.7 205.858
22770.8 208.305
22863.4 210.781
22956.3 213.287
23049.5 215.822
23143.2 218.387
23237.2 220.984
23331.6 223.611
23426.4 226.269
23521.6 228.958
23617.1 231.679
23713.1 234.433
23809.4 237.220
23906.2 240.038
24003.3 242.892
24100.8 245.779
24198.7 248.701
24297.1 251.656
24395.8 254.648
24494.9 257.675
24594.4 260.737
24694.3 263.835
24794.7 266.971
24895.4 270.144
24996.6 273.355
25098.1 276.603
25200.1 279.890
25302.5 283.216
25405.3 286.580
25508.5 289.984
25612.1 293.430
25716.2 296.915
25820.7 300.442
25925.6 304.009
26030.9 307.621
26136.7 311.275
26242.9 314.971
26349.5 318.710
26456.5 322.494
26564.0 326.323
26672.0 330.195
26780.3 334.113
26889.1 338.079
26998.4 342.092
27108.1 346.150
27218.2 350.256
27328.8 354.412
27439.8 358.616
27551.3 362.869
27663.2 367.171
27775.6 371.526
27888.5 375.930
28001.8 380.385
28115.6 384.893
28229.8 389.455
28344.5 394.070
28459.6 398.738
28575.3 403.460
28691.4 408.239
28807.9 413.071
28925.0 417.960
29042.5 422.906
29160.5 427.911
29279.0 432.974
29397.9 438.094
29517.4 443.275
29637.3 448.518
29757.7 453.821
29878.6 459.184
30000.0 464.610

View File

@@ -0,0 +1,901 @@
4000.00 1.79125
4012.90 1.80643
4025.83 1.82173
4038.81 1.83717
4051.83 1.85277
4064.90 1.86850
4078.00 1.88436
4091.15 1.90036
4104.34 1.91650
4117.57 1.93280
4130.85 1.94923
4144.17 1.96581
4157.53 1.98253
4170.93 1.99939
4184.38 2.01642
4197.87 2.03360
4211.41 2.05093
4224.98 2.06840
4238.60 2.08602
4252.27 2.10383
4265.98 2.12178
4279.73 2.13989
4293.53 2.15815
4307.37 2.17657
4321.26 2.19517
4335.19 2.21393
4349.17 2.23286
4363.19 2.25194
4377.26 2.27119
4391.37 2.29064
4405.53 2.31025
4419.73 2.33003
4433.98 2.34998
4448.28 2.37011
4462.62 2.39043
4477.01 2.41092
4491.44 2.43159
4505.92 2.45244
4520.45 2.47347
4535.02 2.49471
4549.65 2.51612
4564.31 2.53773
4579.03 2.55951
4593.79 2.58150
4608.60 2.60369
4623.46 2.62608
4638.37 2.64865
4653.32 2.67143
4668.33 2.69441
4683.38 2.71761
4698.48 2.74101
4713.62 2.76461
4728.82 2.78842
4744.07 2.81244
4759.36 2.83669
4774.71 2.86115
4790.10 2.88582
4805.54 2.91070
4821.04 2.93581
4836.58 2.96115
4852.17 2.98672
4867.82 3.01250
4883.51 3.03851
4899.26 3.06475
4915.05 3.09123
4930.90 3.11795
4946.80 3.14490
4962.75 3.17208
4978.75 3.19950
4994.80 3.22719
5010.90 3.25511
5027.06 3.28328
5043.26 3.31169
5059.52 3.34036
5075.84 3.36930
5092.20 3.39849
5108.62 3.42794
5125.09 3.45764
5141.61 3.48761
5158.19 3.51787
5174.82 3.54839
5191.50 3.57918
5208.24 3.61023
5225.03 3.64156
5241.88 3.67318
5258.78 3.70507
5275.73 3.73723
5292.74 3.76968
5309.81 3.80242
5326.93 3.83547
5344.10 3.86881
5361.33 3.90244
5378.62 3.93635
5395.96 3.97058
5413.35 4.00513
5430.81 4.03998
5448.32 4.07513
5465.88 4.11059
5483.50 4.14637
5501.18 4.18249
5518.92 4.21891
5536.71 4.25566
5554.56 4.29272
5572.47 4.33013
5590.44 4.36787
5608.46 4.40593
5626.54 4.44433
5644.68 4.48307
5662.88 4.52218
5681.14 4.56165
5699.46 4.60148
5717.83 4.64165
5736.27 4.68217
5754.76 4.72305
5773.31 4.76428
5791.93 4.80589
5810.60 4.84784
5829.33 4.89016
5848.13 4.93287
5866.98 4.97599
5885.90 5.01947
5904.88 5.06333
5923.91 5.10757
5943.01 5.15223
5962.17 5.19730
5981.40 5.24277
6000.68 5.28863
6020.03 5.33489
6039.44 5.38159
6058.91 5.42870
6078.44 5.47622
6098.04 5.52416
6117.70 5.57253
6137.42 5.62133
6157.21 5.67057
6177.06 5.72025
6196.98 5.77036
6216.96 5.82091
6237.00 5.87193
6257.11 5.92342
6277.28 5.97537
6297.52 6.02778
6317.82 6.08064
6338.19 6.13398
6358.63 6.18781
6379.13 6.24212
6399.69 6.29691
6420.33 6.35217
6441.03 6.40791
6461.79 6.46416
6482.63 6.52088
6503.53 6.57811
6524.49 6.63584
6545.53 6.69413
6566.63 6.75299
6587.80 6.81235
6609.04 6.87223
6630.35 6.93264
6651.73 6.99358
6673.17 7.05504
6694.69 7.11704
6716.27 7.17959
6737.93 7.24269
6759.65 7.30638
6781.44 7.37065
6803.31 7.43548
6825.24 7.50090
6847.25 7.56688
6869.32 7.63346
6891.47 7.70065
6913.69 7.76843
6935.98 7.83682
6958.34 7.90580
6980.77 7.97541
7003.28 8.04564
7025.86 8.11651
7048.51 8.18798
7071.24 8.26009
7094.03 8.33285
7116.91 8.40627
7139.85 8.48033
7162.87 8.55504
7185.96 8.63041
7209.13 8.70650
7232.38 8.78327
7255.69 8.86073
7279.09 8.93886
7302.55 9.01770
7326.10 9.09719
7349.72 9.17737
7373.41 9.25825
7397.19 9.33986
7421.03 9.42219
7444.96 9.50534
7468.96 9.58928
7493.04 9.67395
7517.20 9.75939
7541.44 9.84557
7565.75 9.93250
7590.14 10.0202
7614.62 10.1087
7639.17 10.1979
7663.79 10.2880
7688.50 10.3788
7713.29 10.4705
7738.16 10.5630
7763.11 10.6563
7788.14 10.7505
7813.25 10.8454
7838.44 10.9412
7863.71 11.0378
7889.06 11.1352
7914.50 11.2336
7940.01 11.3329
7965.61 11.4331
7991.29 11.5342
8017.06 11.6362
8042.91 11.7391
8068.84 11.8430
8094.85 11.9478
8120.95 12.0536
8147.13 12.1603
8173.40 12.2679
8199.75 12.3765
8226.19 12.4860
8252.71 12.5964
8279.32 12.7079
8306.01 12.8204
8332.79 12.9337
8359.65 13.0481
8386.60 13.1635
8413.64 13.2799
8440.77 13.3973
8467.98 13.5160
8495.29 13.6358
8522.67 13.7566
8550.15 13.8785
8577.72 14.0015
8605.37 14.1256
8633.12 14.2508
8660.95 14.3771
8688.87 14.5045
8716.89 14.6331
8744.99 14.7628
8773.19 14.8936
8801.47 15.0255
8829.85 15.1586
8858.32 15.2930
8886.88 15.4285
8915.53 15.5652
8944.27 15.7032
8973.11 15.8424
9002.04 15.9828
9031.06 16.1245
9060.18 16.2675
9089.39 16.4118
9118.69 16.5574
9148.09 16.7043
9177.59 16.8524
9207.18 17.0019
9236.86 17.1527
9266.64 17.3049
9296.52 17.4583
9326.49 17.6133
9356.56 17.7696
9386.72 17.9273
9416.99 18.0864
9447.35 18.2469
9477.81 18.4089
9508.37 18.5722
9539.02 18.7371
9569.78 18.9033
9600.63 19.0711
9631.58 19.2404
9662.63 19.4113
9693.79 19.5836
9725.04 19.7575
9756.39 19.9329
9787.85 20.1100
9819.41 20.2886
9851.07 20.4689
9882.83 20.6507
9914.69 20.8342
9946.65 21.0193
9978.72 21.2060
10010.9 21.3945
10043.2 21.5845
10075.5 21.7763
10108.0 21.9698
10140.6 22.1651
10173.3 22.3620
10206.1 22.5607
10239.0 22.7612
10272.0 22.9635
10305.2 23.1677
10338.4 23.3736
10371.7 23.5814
10405.1 23.7910
10438.7 24.0026
10472.3 24.2160
10506.1 24.4312
10540.0 24.6484
10574.0 24.8676
10608.1 25.0887
10642.3 25.3119
10676.6 25.5370
10711.0 25.7642
10745.5 25.9933
10780.2 26.2246
10814.9 26.4579
10849.8 26.6933
10884.8 26.9308
10919.9 27.1703
10955.1 27.4121
10990.4 27.6560
11025.8 27.9021
11061.4 28.1503
11097.0 28.4008
11132.8 28.6537
11168.7 28.9088
11204.7 29.1662
11240.8 29.4259
11277.1 29.6879
11313.4 29.9523
11349.9 30.2190
11386.5 30.4880
11423.2 30.7595
11460.0 31.0334
11497.0 31.3098
11534.1 31.5887
11571.2 31.8700
11608.6 32.1539
11646.0 32.4403
11683.5 32.7293
11721.2 33.0208
11759.0 33.3149
11796.9 33.6117
11834.9 33.9111
11873.1 34.2133
11911.4 34.5182
11949.8 34.8259
11988.3 35.1362
12026.9 35.4493
12065.7 35.7652
12104.6 36.0840
12143.7 36.4055
12182.8 36.7300
12222.1 37.0573
12261.5 37.3877
12301.0 37.7211
12340.7 38.0574
12380.5 38.3967
12420.4 38.7391
12460.4 39.0846
12500.6 39.4331
12540.9 39.7847
12581.3 40.1394
12621.9 40.4974
12662.6 40.8587
12703.4 41.2232
12744.4 41.5910
12785.5 41.9621
12826.7 42.3364
12868.0 42.7141
12909.5 43.0951
12951.1 43.4795
12992.9 43.8674
13034.8 44.2587
13076.8 44.6538
13119.0 45.0524
13161.3 45.4547
13203.7 45.8604
13246.3 46.2698
13289.0 46.6829
13331.8 47.0997
13374.8 47.5201
13417.9 47.9443
13461.2 48.3724
13504.6 48.8045
13548.1 49.2404
13591.8 49.6802
13635.6 50.1239
13679.6 50.5715
13723.7 51.0233
13767.9 51.4790
13812.3 51.9388
13856.9 52.4027
13901.5 52.8708
13946.4 53.3433
13991.3 53.8201
14036.4 54.3010
14081.7 54.7863
14127.1 55.2759
14172.6 55.7700
14218.3 56.2685
14264.2 56.7715
14310.2 57.2788
14356.3 57.7909
14402.6 58.3077
14449.0 58.8292
14495.6 59.3554
14542.3 59.8861
14589.2 60.4216
14636.2 60.9620
14683.4 61.5072
14730.8 62.0572
14778.3 62.6121
14825.9 63.1721
14873.7 63.7375
14921.7 64.3078
14969.8 64.8832
15018.0 65.4638
15066.5 66.0497
15115.0 66.6410
15163.8 67.2375
15212.7 67.8394
15261.7 68.4467
15310.9 69.0595
15360.3 69.6780
15409.8 70.3020
15459.5 70.9316
15509.3 71.5669
15559.3 72.2079
15609.5 72.8549
15659.8 73.5076
15710.3 74.1663
15761.0 74.8307
15811.8 75.5011
15862.7 76.1778
15913.9 76.8606
15965.2 77.5494
16016.7 78.2443
16068.3 78.9456
16120.1 79.6534
16172.1 80.3674
16224.2 81.0878
16276.5 81.8145
16329.0 82.5481
16381.7 83.2888
16434.5 84.0359
16487.5 84.7897
16540.6 85.5502
16593.9 86.3179
16647.4 87.0925
16701.1 87.8740
16755.0 88.6625
16809.0 89.4582
16863.2 90.2611
16917.5 91.0715
16972.1 91.8892
17026.8 92.7141
17081.7 93.5466
17136.8 94.3866
17192.0 95.2345
17247.4 96.0899
17303.1 96.9532
17358.8 97.8239
17414.8 98.7027
17470.9 99.5896
17527.3 100.485
17583.8 101.387
17640.5 102.298
17697.4 103.218
17754.4 104.146
17811.6 105.082
17869.1 106.027
17926.7 106.980
17984.0 107.934
17984.1 107.936
17984.2 107.938
17984.3 107.939
17984.4 107.941
17984.5 107.943
17984.6 107.944
17984.7 107.946
17984.8 107.948
17984.9 107.949
17985.0 107.951
17985.1 107.953
17985.2 107.954
17985.3 107.956
17985.4 107.958
17985.5 107.959
17985.6 107.961
17985.7 107.963
17985.8 107.964
17985.9 107.966
17986.0 107.968
17986.1 107.969
17986.2 107.971
17986.3 107.973
17986.4 107.974
17986.5 107.976
17986.6 107.978
17986.7 107.979
17986.8 107.981
17986.9 107.983
17987.0 107.984
17987.1 107.986
17987.2 107.988
17987.3 107.989
17987.4 107.991
17987.5 107.993
17987.6 107.994
17987.7 107.996
17987.8 107.998
17987.9 107.999
17988.0 108.001
17988.1 108.003
17988.2 108.004
17988.3 108.006
17988.4 108.008
17988.5 108.009
17988.6 108.011
17988.7 108.013
17988.8 108.014
17988.9 108.016
17989.0 108.018
17989.1 108.019
17989.2 108.021
17989.3 108.023
17989.4 108.024
17989.5 108.026
17989.6 108.028
17989.7 108.029
17989.8 108.031
17989.9 108.033
17990.0 108.034
17990.1 108.036
17990.2 108.038
17990.3 108.039
17990.4 108.041
17990.5 108.043
17990.6 108.044
17990.7 108.046
17990.8 108.048
17990.9 108.049
17991.0 108.051
17991.1 108.053
17991.2 108.054
17991.3 108.056
17991.4 108.058
17991.5 108.059
17991.6 108.061
17991.7 108.063
17991.8 108.064
17991.9 108.066
17992.0 108.068
17992.1 108.069
17992.2 108.071
17992.3 108.073
17992.4 108.074
17992.5 108.076
17992.6 108.078
17992.7 108.079
17992.8 108.081
17992.9 108.083
17993.0 108.084
17993.1 108.086
17993.2 108.088
17993.3 108.089
17993.4 108.091
17993.5 108.093
17993.6 108.094
17993.7 108.096
17993.8 108.098
17993.9 108.099
17994.0 108.101
17994.1 108.103
17994.2 108.105
17994.3 108.106
17994.4 108.108
17994.5 108.109
17994.6 108.111
17994.7 108.113
17994.8 108.115
17994.9 108.116
17995.0 108.118
17995.1 108.119
17995.2 108.121
17995.3 108.123
17995.4 108.125
17995.5 108.126
17995.6 108.128
17995.7 108.130
17995.8 108.131
17995.9 108.133
17996.0 108.134
17996.1 108.136
17996.2 108.138
17996.3 108.140
17996.4 108.141
17996.5 108.143
17996.6 108.144
17996.7 108.146
17996.8 108.148
17996.9 108.150
17997.0 108.151
17997.1 108.153
17997.2 108.155
17997.3 108.156
17997.4 108.158
17997.5 108.160
17997.6 41.5713
17997.7 15.9385
17997.8 15.9387
17997.9 15.9389
17998.0 15.9391
17998.1 15.9394
17998.2 15.9396
17998.3 15.9398
17998.4 15.9400
17998.5 15.9403
17998.6 15.9405
17998.7 15.9407
17998.8 15.9410
17998.9 15.9412
17999.0 15.9414
17999.1 15.9416
17999.2 15.9419
17999.3 15.9421
17999.4 15.9423
17999.5 15.9425
17999.6 15.9427
17999.7 15.9430
17999.8 15.9432
17999.9 15.9434
18000.0 15.9437
18000.1 15.9439
18000.2 15.9441
18000.3 15.9443
18000.4 15.9446
18000.5 15.9448
18000.6 15.9450
18000.7 15.9452
18000.8 15.9455
18000.9 15.9457
18001.0 15.9459
18001.1 15.9461
18001.2 15.9463
18001.3 15.9466
18001.4 15.9468
18001.5 15.9470
18001.6 15.9473
18001.7 15.9475
18001.8 15.9477
18001.9 15.9479
18002.0 15.9482
18002.1 15.9484
18002.2 15.9486
18002.3 15.9489
18002.4 15.9491
18002.5 15.9493
18002.6 15.9495
18002.7 15.9497
18002.8 15.9500
18002.9 15.9502
18003.0 15.9504
18003.1 15.9506
18003.2 15.9509
18003.3 15.9511
18003.4 15.9513
18003.5 15.9515
18003.6 15.9518
18003.7 15.9520
18003.8 15.9522
18003.9 15.9525
18004.0 15.9527
18004.1 15.9529
18004.2 15.9531
18004.3 15.9534
18004.4 15.9536
18004.5 15.9538
18004.6 15.9540
18004.7 15.9542
18004.8 15.9545
18004.9 15.9547
18005.0 15.9549
18005.1 15.9552
18005.2 15.9554
18005.3 15.9556
18005.4 15.9558
18005.5 15.9561
18005.6 15.9563
18005.7 15.9565
18005.8 15.9567
18005.9 15.9570
18006.0 15.9572
18006.1 15.9574
18006.2 15.9576
18006.3 15.9579
18006.4 15.9581
18006.5 15.9583
18006.6 15.9585
18006.7 15.9588
18006.8 15.9590
18006.9 15.9592
18007.0 15.9594
18007.1 15.9597
18007.2 15.9599
18007.3 15.9601
18007.4 15.9604
18007.5 15.9606
18007.6 15.9608
18007.7 15.9610
18007.8 15.9613
18007.9 15.9615
18008.0 15.9617
18008.1 15.9619
18008.2 15.9621
18008.3 15.9624
18008.4 15.9626
18008.5 15.9628
18008.6 15.9631
18008.7 15.9633
18008.8 15.9635
18008.9 15.9637
18009.0 15.9640
18009.1 15.9642
18009.2 15.9644
18009.3 15.9646
18009.4 15.9649
18009.5 15.9651
18009.6 15.9653
18009.7 15.9655
18009.8 15.9658
18009.9 15.9660
18010.0 15.9662
18010.1 15.9664
18010.2 15.9667
18010.3 15.9669
18010.4 15.9671
18010.5 15.9673
18010.6 15.9676
18010.7 15.9678
18010.8 15.9680
18010.9 15.9683
18011.0 15.9685
18011.1 15.9687
18011.2 15.9689
18011.3 15.9692
18011.4 15.9694
18011.5 15.9696
18011.6 15.9698
18011.7 15.9700
18011.8 15.9703
18011.9 15.9705
18012.0 15.9707
18012.1 15.9709
18012.2 15.9712
18012.3 15.9714
18012.4 15.9716
18012.5 15.9719
18012.6 15.9721
18012.7 15.9723
18012.8 15.9725
18012.9 15.9728
18013.0 15.9730
18013.1 15.9732
18013.2 15.9735
18013.3 15.9737
18013.4 15.9739
18013.5 15.9741
18013.6 15.9743
18013.7 15.9746
18013.8 15.9748
18013.9 15.9750
18014.0 15.9752
18042.5 16.0396
18100.6 16.1715
18159.0 16.3045
18217.5 16.4386
18276.3 16.5738
18335.2 16.7101
18394.3 16.8475
18453.6 16.9861
18513.1 17.1258
18572.8 17.2670
18632.7 17.4095
18692.8 17.5533
18753.0 17.6983
18813.5 17.8444
18874.1 17.9921
18935.0 18.1412
18996.0 18.2916
19057.3 18.4432
19118.7 18.5961
19180.4 18.7506
19242.2 18.9066
19304.2 19.0639
19366.5 19.2225
19428.9 19.3824
19491.6 19.5440
19554.4 19.7072
19617.4 19.8717
19680.7 20.0376
19744.1 20.2048
19807.8 20.3739
19871.7 20.5446
19935.7 20.7167
20000.0 20.8903
20081.3 21.1110
20162.8 21.3347
20244.8 21.5609
20327.0 21.7894
20409.6 22.0204
20492.5 22.2546
20575.8 22.4913
20659.4 22.7305
20743.3 22.9722
20827.6 23.2173
20912.2 23.4650
20997.2 23.7153
21082.5 23.9683
21168.1 24.2248
21254.1 24.4840
21340.5 24.7460
21427.2 25.0109
21514.3 25.2792
21601.7 25.5505
21689.4 25.8246
21777.6 26.1018
21866.0 26.3827
21954.9 26.6666
22044.1 26.9536
22133.6 27.2438
22223.6 27.5378
22313.9 27.8350
22404.5 28.1354
22495.5 28.4392
22586.9 28.7469
22678.7 29.0579
22770.8 29.3723
22863.4 29.6904
22956.3 30.0124
23049.5 30.3380
23143.2 30.6670
23237.2 30.9999
23331.6 31.3370
23426.4 31.6777
23521.6 32.0221
23617.1 32.3705
23713.1 32.7234
23809.4 33.0801
23906.2 33.4407
24003.3 33.8055
24100.8 34.1749
24198.7 34.5483
24297.1 34.9258
24395.8 35.3077
24494.9 35.6943
24594.4 36.0851
24694.3 36.4802
24794.7 36.8800
24895.4 37.2846
24996.6 37.6937
25098.1 38.1073
25200.1 38.5259
25302.5 38.9496
25405.3 39.3779
25508.5 39.8109
25612.1 40.2493
25716.2 40.6927
25820.7 41.1411
25925.6 41.5944
26030.9 42.0533
26136.7 42.5177
26242.9 42.9871
26349.5 43.4618
26456.5 43.9422
26564.0 44.4283
26672.0 44.9197
26780.3 45.4165
26889.1 45.9195
26998.4 46.4284
27108.1 46.9428
27218.2 47.4630
27328.8 47.9896
27439.8 48.5223
27551.3 49.0609
27663.2 49.6054
27775.6 50.1569
27888.5 50.7146
28001.8 51.2787
28115.6 51.8489
28229.8 52.4262
28344.5 53.0102
28459.6 53.6005
28575.3 54.1975
28691.4 54.8021
28807.9 55.4134
28925.0 56.0316
29042.5 56.6567
29160.5 57.2896
29279.0 57.9297
29397.9 58.5769
29517.4 59.2313
29637.3 59.8941
29757.7 60.5643
29878.6 61.2421
30000.0 61.9272

View File

@@ -0,0 +1,672 @@
"""
cSAXS exposure-box filter transmission utilities.
This method has been created based on previous spec implementations
The translation was mainly done by copilot AI.
Implements fil_trans physics:
- Per-material attenuation-length tables loaded from package 'filter_data/'.
- Linear interpolation of attenuation length vs. energy (eV).
- Transmission T = exp(-t / lambda), t in micrometers.
- Enumeration of all enabled combinations across 4 units x 6 positions.
- Selection of the combination with transmission closest to a target.
Motion is executed using provided position tables for each filter_array_*_x stage.
"""
#todo
#check dmm is off
#X12SA-OP-DMM-EMLS-3010:THRU translation THROUGH
#X12SA-OP-DMM-EMLS-3030:THRU bragg through
#X12SA-OP-CCM1:ENERGY-GET > 1
from __future__ import annotations
import math
from typing import Dict, List, Optional, Tuple
from importlib import resources
# Resolve the filter_data/ folder via importlib.resources
import csaxs_bec.bec_ipython_client.plugins.cSAXS.filter_transmission as ft_pkg
from csaxs_bec.bec_ipython_client.plugins.cSAXS import epics_get
from bec_lib import bec_logger
import builtins
if builtins.__dict__.get("bec") is not None:
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
umv = builtins.__dict__.get("umv")
umvr = builtins.__dict__.get("umvr")
class cSAXSFilterTransmission:
"""
Mixin providing the fil_trans command.
Assumes self.client and self.OMNYTools exist.
Example:
csaxs.fil_trans(0.10, energy_kev=6.2) # dry-run first, then asks to execute
"""
# -----------------------------
# Material naming & file mapping
# -----------------------------
_MATERIAL_FILES: Dict[str, Optional[str]] = {
"none": None,
"diam": "filter_attenuation-length_diamond.txt",
"al": "filter_attenuation-length_al.txt",
"si": "filter_attenuation-length_si.txt",
"ti": "filter_attenuation-length_ti.txt",
"cu": "filter_attenuation-length_cu.txt",
"ge": "filter_attenuation-length_ge.txt",
"zr": "filter_attenuation-length_zr.txt",
# optional; provide file if you enable Fe5
"fe": "filter_attenuation-length_fe.txt",
}
# -----------------------------------------
# Exposure-box filter configuration (4 x 6)
#
# Current hardware (per your message):
# Unit 1 (filter_array_1_x): out, Si400, Ge300, Ti800, Zr20
# Unit 2 (filter_array_2_x): out, Si200, Si3200, Ti400, Cu20
# Unit 3 (filter_array_3_x): out, Si100, Si1600, Ti200, Ti3200, Fe5
# Unit 4 (filter_array_4_x): out, Si50, Si800, Ti100, Ti1600, Ti20
#
# Positions 1..6 = [out, m1, m2, m3, m4, m5]
# Each entry: ((mat1, th1_um), (mat2, th2_um), enabled_bool)
# -----------------------------------------
_FILTERS: List[Tuple[Tuple[str, float], Tuple[str, float], bool]] = [
# Unit 1
(("none", 0.0), ("none", 0.0), True), # out
(("si", 400.0), ("none", 0.0), True), # Si400
(("ge", 300.0), ("none", 0.0), True), # Ge300
(("ti", 800.0), ("none", 0.0), True), # Ti800
(("zr", 20.0), ("none", 0.0), True), # Zr20
(("none", 0.0), ("none", 0.0), False), # unused
# Unit 2
(("none", 0.0), ("none", 0.0), True), # out
(("si", 200.0), ("none", 0.0), True), # Si200
(("si", 3200.0), ("none", 0.0), True), # Si3200
(("ti", 400.0), ("none", 0.0), True), # Ti400
(("cu", 20.0), ("none", 0.0), True), # Cu20
(("none", 0.0), ("none", 0.0), False), # unused
# Unit 3
(("none", 0.0), ("none", 0.0), True), # out
(("si", 100.0), ("none", 0.0), True), # Si100
(("si", 1600.0), ("none", 0.0), True), # Si1600
(("ti", 200.0), ("none", 0.0), True), # Ti200
(("ti", 3200.0), ("none", 0.0), True), # Ti3200
(("fe", 5.0), ("none", 0.0), False), # Fe5 (disabled unless data file provided)
# Unit 4
(("none", 0.0), ("none", 0.0), True), # out
(("si", 50.0), ("none", 0.0), True), # Si50
(("si", 800.0), ("none", 0.0), True), # Si800
(("ti", 100.0), ("none", 0.0), True), # Ti100
(("ti", 1600.0), ("none", 0.0), True), # Ti1600
(("ti", 20.0), ("none", 0.0), True), # Ti20
]
_UNITS = 4
_PER_UNIT = 6
# -----------------------------------------
# Motion mapping: user-scale coordinates
# [out, m1, m2, m3, m4, m5]
# -----------------------------------------
_POSITIONS_USER: List[List[Optional[float]]] = [
# Unit 1 (filter_array_1_x)
[25.0, 17.9, 7.9, -2.3, -12.1, None],
# Unit 2 (filter_array_2_x)
[25.5, 17.6, 7.8, -2.3, -12.3, None],
# Unit 3 (filter_array_3_x)
[25.8, 17.6, 7.8, -2.2, -12.3, -22.3], # Fe5 at -22.3
# Unit 4 (filter_array_4_x)
[25.0, 17.5, 7.5, -2.2, -12.4, -22.2],
]
# Device axis names (adjust if different)
_AXES: List[str] = [
"filter_array_1_x",
"filter_array_2_x",
"filter_array_3_x",
"filter_array_4_x",
]
# -----------------------------
# Construction / Internals
# -----------------------------
def __init__(self, **kwargs):
super().__init__(**kwargs)
# In multiple-inheritance setups our __init__ might be skipped; guard lazily too.
self._attlen_cache: Dict[str, Tuple[List[float], List[float]]] = {}
def _ensure_internal_state(self):
"""Lazy guard for robustness if __init__ wasnt called."""
if not hasattr(self, "_attlen_cache"):
self._attlen_cache = {}
# -----------------------------
# Public API
# -----------------------------
def fil_trans(
self,
transmission: Optional[float] = None,
energy_kev: Optional[float] = None,
print_only: bool = True,
) -> Optional[None]:
"""
Set exposure-box filters to achieve a target transmission.
If called without 'transmission', prints usage and current status.
Safety:
- fil_trans(1) is always allowed.
- fil_trans(<1) is only allowed if:
- epics_get("X12SA-OP-DMM-EMLS-3010:THRU") == 1 (DMM translation THROUGH)
- epics_get("X12SA-OP-DMM-EMLS-3030:THRU") == 1 (DMM rotation THROUGH)
- epics_get("X12SA-OP-CCM1:ENERGY-GET") > 1 (CCM active, energy in keV)
Otherwise, prompt with default NO.
"""
# --- No-arg usage helper ---
if transmission is None:
print("\nUsage example:")
print(" csaxs.fil_trans(0.10, energy_kev=6.2)")
print(" First parameter is the transmission factor requested.")
print(" If energy is not specified it will be read from the CCM energy PV.")
print("\nCurrent filter transmission:")
self._fil_trans_report(energy_kev=energy_kev)
return None
# --- Validation of transmission ---
try:
transmission = float(transmission)
except Exception:
raise ValueError("Transmission must be numeric.")
if not (0.0 < transmission <= 1.0):
raise ValueError("Transmission must be between 0 and 1.")
# -------------------------------------------------------
# SAFETY CHECK (before any calculation/motion):
# Only allow fil_trans < 1 when DMM is in THROUGH (both)
# and CCM energy > 1 keV. fil_trans(1) is always allowed.
# -------------------------------------------------------
if transmission < 1.0:
try:
dmm_trans = float(epics_get("X12SA-OP-DMM-EMLS-3010:THRU"))
except Exception:
dmm_trans = -1
try:
dmm_rot = float(epics_get("X12SA-OP-DMM-EMLS-3030:THRU"))
except Exception:
dmm_rot = -1
try:
ccm_energy = float(epics_get("X12SA-OP-CCM1:ENERGY-GET"))
except Exception:
ccm_energy = -1
allowed = (dmm_trans == 1) and (dmm_rot == 1) and (ccm_energy > 1)
if not allowed:
print("\n⚠️ SAFETY WARNING: Reducing transmission (< 1) typically requires:")
print(" - DMM translation in THROUGH (THRU == 1)")
print(" - DMM rotation in THROUGH (THRU == 1)")
print(" - CCM energy > 1 keV")
print("\nCurrent state:")
print(f" DMM translation THRU : {dmm_trans}")
print(f" DMM rotation THRU : {dmm_rot}")
print(f" CCM energy (keV) : {ccm_energy}")
# Ask user (default = NO)
if hasattr(self, "OMNYTools") and hasattr(self.OMNYTools, "yesno"):
proceed = self.OMNYTools.yesno(
"Conditions not satisfied. Proceed anyway?",
default="n",
)
else:
# Safe fallback
proceed = False
if not proceed:
print("Aborted. Transmission unchanged.")
return None
# --- Energy handling (EPICS only) ---
if energy_kev is None:
try:
energy_kev = float(epics_get("X12SA-OP-CCM1:ENERGY-GET"))
except Exception as exc:
raise RuntimeError(
"Energy not specified and could not read EPICS PV "
"'X12SA-OP-CCM1:ENERGY-GET'."
) from exc
else:
energy_kev = float(energy_kev)
# --- Summary header ---
print("\nExposure-box filter transmission request")
print("-" * 60)
print(f"Target transmission : {transmission:.6e}")
print(f"Photon energy : {energy_kev:.3f} keV")
print(f"Mode : {'PRINT ONLY' if print_only else 'EXECUTE'}")
print("-" * 60)
# --- Compute best combination ---
best = self._find_best_combination(transmission, energy_kev)
# --- Report selected combination ---
self._print_combination(best, energy_kev, header="Selected combination")
# Nearby: print only code + transmission
neighbors = self._neighbors_around_best(transmission, energy_kev, span=5)
if neighbors:
print("\nNearby combinations (by transmission proximity):")
for row in neighbors:
print(f"{row['transmission']:9.3e}")
# --- Dry run prompt ---
if print_only:
print("\n[DRY RUN] No motion executed yet.")
if hasattr(self, "OMNYTools") and hasattr(self.OMNYTools, "yesno"):
if self.OMNYTools.yesno(
"Execute motion to the selected filter combination now?",
"y", # default YES
):
self._execute_combination(best, energy_kev)
else:
print("Execution skipped.")
else:
# If yesno not available, default to 'skip' on print_only
print("No interactive prompt available. Execution skipped (print_only=True).")
return None
# --- Execute motion directly ---
self._execute_combination(best, energy_kev)
return None
# -----------------------------
# Physics helpers
# -----------------------------
def _load_attlen(self, material: str) -> Tuple[List[float], List[float]]:
"""
Load and cache attenuation-length table for a material from package resources.
Returns:
energies_eV (list), attlen_um (list)
"""
self._ensure_internal_state()
material = material.lower()
if material in self._attlen_cache:
return self._attlen_cache[material]
fname = self._MATERIAL_FILES.get(material)
if not fname:
# 'none' or unsupported mapped to empty
self._attlen_cache[material] = ([], [])
return ([], [])
energies_eV: List[float] = []
attlen_um: List[float] = []
# Load from installed package: <this_module>/filter_data/<fname>
res = resources.files(ft_pkg) / "filter_data" / fname
try:
# as_file yields a concrete filesystem path even if package is zipped
with resources.as_file(res) as p:
with p.open("r") as f:
for line in f:
line = line.strip()
if not line:
continue
parts = line.split()
if len(parts) < 2:
continue
energies_eV.append(float(parts[0]))
attlen_um.append(float(parts[1]))
except FileNotFoundError as e:
raise FileNotFoundError(
f"Attenuation data file not found for '{material}': {fname} "
f"(looked in package filter_data)"
) from e
if not energies_eV:
raise ValueError(
f"Attenuation data for material '{material}' is empty or invalid: {fname}"
)
self._attlen_cache[material] = (energies_eV, attlen_um)
return energies_eV, attlen_um
def _attenuation_length_um(self, energy_kev: float, material: str) -> float:
"""
Linear interpolation of attenuation length λ(energy) in µm.
Input energy in keV; tables are in eV.
"""
material = material.lower()
if material == "none":
return float("inf") # No attenuation
energies_eV, attlen_um = self._load_attlen(material)
if not energies_eV:
# unsupported mapped above
raise ValueError(f"Unsupported material or missing data: '{material}'")
e_ev = energy_kev * 1000.0
# Clip to the nearest edge for practicality
if e_ev <= energies_eV[0]:
bec_logger.logger.warning(
f"[cSAXS] energy {energy_kev:.3f} keV below table range for {material}; "
f"clipping to {energies_eV[0]/1000.0:.3f} keV."
)
return attlen_um[0]
if e_ev >= energies_eV[-1]:
bec_logger.logger.warning(
f"[cSAXS] energy {energy_kev:.3f} keV above table range for {material}; "
f"clipping to {energies_eV[-1]/1000.0:.3f} keV."
)
return attlen_um[-1]
# Binary search for interval
lo, hi = 0, len(energies_eV) - 1
while hi - lo > 1:
mid = (lo + hi) // 2
if energies_eV[mid] >= e_ev:
hi = mid
else:
lo = mid
e_lo, e_hi = energies_eV[lo], energies_eV[hi]
lam_lo, lam_hi = attlen_um[lo], attlen_um[hi]
lam = (e_ev - e_lo) / (e_hi - e_lo) * (lam_hi - lam_lo) + lam_lo
return lam
def _position_transmission(
self,
pos_entry: Tuple[Tuple[str, float], Tuple[str, float], bool],
energy_kev: float,
) -> Optional[float]:
"""Transmission for a single position (possibly two layers)."""
(mat1, th1), (mat2, th2), enabled = pos_entry
if not enabled:
return None
T1 = 1.0
T2 = 1.0
if mat1 != "none" and th1 > 0.0:
lam1 = self._attenuation_length_um(energy_kev, mat1)
T1 = math.exp(-th1 / lam1)
if mat2 != "none" and th2 > 0.0:
lam2 = self._attenuation_length_um(energy_kev, mat2)
T2 = math.exp(-th2 / lam2)
return T1 * T2
def _all_combinations(self, energy_kev: float) -> List[dict]:
"""
Enumerate all enabled combinations across 4 units.
Returns a list of dicts sorted by transmission ascending:
{
'code': 'abcd' # positions 1..6 per unit
'indices': [i0, i1, i2, i3], # 0..5
'materials': [ ((m1,t1), (m2,t2)|None ), ... for 4 units ]
'transmission': float
}
"""
# Slice filters per unit
units = [
self._FILTERS[u * self._PER_UNIT : (u + 1) * self._PER_UNIT]
for u in range(self._UNITS)
]
# Precompute per-position transmissions
per_pos_T: List[List[Optional[float]]] = [
[self._position_transmission(pos_entry, energy_kev) for pos_entry in unit]
for unit in units
]
combos: List[dict] = []
for i0 in range(self._PER_UNIT):
T0 = per_pos_T[0][i0]
if T0 is None:
continue
for i1 in range(self._PER_UNIT):
T1 = per_pos_T[1][i1]
if T1 is None:
continue
for i2 in range(self._PER_UNIT):
T2 = per_pos_T[2][i2]
if T2 is None:
continue
for i3 in range(self._PER_UNIT):
T3 = per_pos_T[3][i3]
if T3 is None:
continue
T = T0 * T1 * T2 * T3
indices = [i0, i1, i2, i3]
code = "".join(str(i + 1) for i in indices)
# Collect materials/thickness for reporting
mats = []
for u, idx in enumerate(indices):
(m1, t1), (m2, t2), _ = units[u][idx]
mats.append(((m1, t1), (m2, t2) if (m2 != "none" and t2 > 0.0) else None))
combos.append(
{
"code": code,
"indices": indices,
"materials": mats,
"transmission": T,
}
)
combos.sort(key=lambda c: c["transmission"]) # ascending
return combos
def _find_best_combination(self, target_T: float, energy_kev: float) -> dict:
"""Pick combination with transmission closest to target."""
combos = self._all_combinations(energy_kev)
best = min(combos, key=lambda c: abs(c["transmission"] - target_T))
return best
def _neighbors_around_best(
self, target_T: float, energy_kev: float, span: int = 5
) -> List[dict]:
"""Return a few combinations around the best, by proximity in transmission."""
combos = self._all_combinations(energy_kev)
ranked = sorted(combos, key=lambda c: abs(c["transmission"] - target_T))
return ranked[1 : 1 + span] # exclude best itself
# -----------------------------
# Formatting & motion
# -----------------------------
def _print_combination(self, comb: dict, energy_kev: float, header: Optional[str] = None):
if header:
print(f"\n{header}:")
code = comb["code"]
T = comb["transmission"]
print(f" Filters {code} transmission at {energy_kev:.3f} keV = {T:9.3e}")
# Per-unit detail for the selected combination only
for u, matinfo in enumerate(comb["materials"], start=1):
first, second = matinfo
(m1, t1) = first
pos = int(code[u - 1])
if second is not None:
(m2, t2) = second
print(
f" unit {u}: #{pos} "
f"{t1:4.0f} mu {m1:<4} + {t2:4.0f} mu {m2:<4}"
)
else:
if m1 != "none" and t1 > 0.0:
print(f" unit {u}: #{pos} {t1:4.0f} mu {m1:<4}")
else:
print(f" unit {u}: #{pos} ----- out")
def _execute_combination(self, comb: dict, energy_kev: float):
"""
Execute motion to the indices encoded in 'comb["code"]'.
Mapping:
- code 'abcd' → per-unit index (a,b,c,d) ∈ {1..6}
- positions are looked up from _POSITIONS_USER
- axes are defined in _AXES
Motion uses: umv(dev.axis, position, dev.axis2, position2, ...)
"""
indices = comb["indices"] # 0-based per unit
move_args = [] # Collect (device, position) pairs
print("\nExecuting combined motion:")
for unit_idx, pos_idx in enumerate(indices):
pos_list = self._POSITIONS_USER[unit_idx]
target_pos = pos_list[pos_idx]
if target_pos is None:
raise RuntimeError(
f"Unit {unit_idx+1} position {pos_idx+1} has no defined coordinate."
)
axis_name = self._AXES[unit_idx]
axis_obj = getattr(dev, axis_name)
print(f" {axis_name}{target_pos:.3f}")
move_args.extend([axis_obj, target_pos])
umv(*move_args)
print("\nVerifying final positions:")
for unit_idx in range(self._UNITS):
axis_name = self._AXES[unit_idx]
axis_obj = getattr(dev, axis_name)
try:
actual = float(axis_obj.readback.get())
print(f" {axis_name} = {actual:.3f}")
except Exception:
print(f" {axis_name}: readback unavailable")
achieved_T = comb["transmission"]
print(f"\nAchieved transmission (approx.): {achieved_T:9.3e} at {energy_kev:.3f} keV")
def _fil_trans_report(self, tol: float = 0.1, energy_kev: Optional[float] = None) -> None:
"""
Report the currently active exposurebox filter combination.
Determines stage positions via dev.<axis>.readback.get()
with a tolerance window of ±tol relative to nominal positions.
Parameters
----------
tol : float
Tolerance for matching positions.
energy_kev : float, optional
Photon energy in keV. If None, tries to read from dev.mokev or defaults to 6.2 keV.
"""
# Ensure global dev is available
if dev is None:
print("ERROR: Global 'dev' object not found.")
return
# --- Energy handling (EPICS only) ---
if energy_kev is None:
try:
energy_kev = float(epics_get("X12SA-OP-CCM1:ENERGY-GET"))
except Exception as exc:
raise RuntimeError(
"Energy not specified and could not read EPICS PV "
"'X12SA-OP-CCM1:ENERGY-GET'."
) from exc
else:
energy_kev = float(energy_kev)
print("\nCurrent filter transmission report")
print("-" * 60)
print(f"Photon energy : {energy_kev:.3f} keV")
indices = [] # 05 per unit
for unit_idx, axis_name in enumerate(self._AXES):
axis_obj = getattr(dev, axis_name, None)
if axis_obj is None:
print(f"ERROR: Device axis '{axis_name}' not found.")
return
try:
rb = float(axis_obj.readback.get())
except Exception:
print(f"ERROR: readback unavailable for axis {axis_name}")
return
# Match readback to nearest nominal
pos_list = self._POSITIONS_USER[unit_idx]
best_idx = None
for i, nominal in enumerate(pos_list):
if nominal is None:
continue
if abs(rb - nominal) <= tol:
best_idx = i
break
if best_idx is None:
print(f"Unit {unit_idx+1}: readback {rb:.3f} does not match any known position.")
return
indices.append(best_idx)
# Build combination code
code = "".join(str(i + 1) for i in indices)
print(f"Matched filter code: {code}")
# Compute transmission and materials
units = [
self._FILTERS[u * self._PER_UNIT : (u + 1) * self._PER_UNIT]
for u in range(self._UNITS)
]
materials = []
total_T = 1.0
for u, pos_idx in enumerate(indices):
(m1, t1), (m2, t2), enabled = units[u][pos_idx]
T = self._position_transmission(units[u][pos_idx], energy_kev)
if T is None:
print(f"Unit {u+1}: position disabled")
T = 1.0
total_T *= T
if m2 != "none" and t2 > 0:
materials.append(((m1, t1), (m2, t2)))
else:
materials.append(((m1, t1), None))
# Print detailed report
self.OMNYTools.printgreenbold(f"Total transmission: {total_T:.6e}")
print("-" * 60)
for u, matinfo in enumerate(materials, start=1):
(m1, t1), second = matinfo
pos = indices[u - 1] + 1
if second:
m2, t2 = second
print(f" unit {u}: #{pos} {t1:4.0f} µm {m1:<4} + {t2:4.0f} µm {m2:<4}")
else:
if m1 != "none" and t1 > 0:
print(f" unit {u}: #{pos} {t1:4.0f} µm {m1:<4}")
else:
print(f" unit {u}: #{pos} ----- out")

View File

@@ -0,0 +1,447 @@
import builtins
import time
from bec_lib import bec_logger
# Logger initialization
logger = bec_logger.logger
# Pull BEC globals if present
bec = builtins.__dict__.get("bec")
dev = builtins.__dict__.get("dev")
umv = builtins.__dict__.get("umv")
umvr = builtins.__dict__.get("umvr")
class cSAXSInitSmaractStagesError(Exception):
pass
class cSAXSInitSmaractStages:
"""
Runtime SmarAct utilities for referencing and moving to initial positions.
This class no longer relies on static mappings. Instead, it:
- discovers available devices from `list(dev.keys())`
- reads the numeric channel/axis from each device's `user_parameter['bl_smar_stage']`
- reads `init_position` from `user_parameter['init_position']`
"""
def __init__(self, client) -> None:
self.client = client
# ------------------------------
# Internal helpers (runtime-based)
# ------------------------------
def _yesno(self, question: str, default: str = "y") -> bool:
"""
Use OMNYTools.yesno if available; otherwise default to 'yes' (or fallback to input()).
"""
try:
if hasattr(self, "OMNYTools") and hasattr(self.OMNYTools, "yesno"):
return self.OMNYTools.yesno(question, default)
except Exception:
pass
# Fallback: default answer without interaction
# (Safe default: 'y' proceeds; adjust if you want interactive input)
logger.info(f"[cSAXS] (yesno fallback) {question} -> default '{default}'")
return (default or "y").lower().startswith("y")
def _get_user_param_safe(self, device_name: str, key: str):
"""
Safe access to device user parameters from current BEC session.
"""
try:
return dev[device_name].user_parameter.get(key)
except Exception:
return None
def _iter_session_devices(self):
"""
Yield device names available in current BEC session.
"""
if dev is None:
return
for name in list(dev.keys()):
yield name
def _build_session_axis_map(self, selection: set | None = None) -> dict:
"""
Build runtime axis map {device_name: channel} for devices that define 'bl_smar_stage'.
If 'selection' is provided, restrict to names in selection.
"""
axis_map = {}
missing = []
for name in self._iter_session_devices() or []:
if selection is not None and name not in selection:
continue
ch = self._get_user_param_safe(name, "bl_smar_stage")
if ch is None:
missing.append(name)
continue
try:
axis_map[name] = int(ch)
except Exception:
missing.append(name)
if missing and selection is None:
logger.info(
"[cSAXS] Devices without 'bl_smar_stage' (ignored): " + ", ".join(sorted(missing))
)
return axis_map
def _get_device_object(self, device_name: str):
"""
Return the live device object from BEC 'dev'.
"""
try:
return getattr(dev, device_name)
except Exception:
return None
# ------------------------------
# Public API
# ------------------------------
def smaract_reference_stages(self, force: bool = False, devices_to_reference=None):
"""
Reference SmarAct stages using runtime discovery.
Parameters
----------
force : bool, optional
If True, re-reference ALL selected stages.
If False (default), only reference stages that are currently NOT referenced.
devices_to_reference : iterable of str or str, optional
If provided, only these devices will be considered for referencing.
If None, all devices in the current session that define 'bl_smar_stage' are considered.
Behavior
--------
- Runtime-based: reads axis channel from user_parameter['bl_smar_stage'].
- If devices_to_reference is given → restrict referencing to those.
- If force=False → skip devices already referenced.
- If force=True → re-reference selected devices always.
- Only newly referenced devices are passed to
smaract_components_to_initial_position(devices_to_move=[...]) afterwards.
Examples
--------
Reference only stages that are NOT referenced yet (default)
csaxs.smaract_reference_stages()
Force re-reference of all stages
csaxs.smaract_reference_stages(force=True)
Reference only specific stages
csaxs.smaract_reference_stages(
devices_to_reference=["sl3trxi", "sl3trxo", "xbpm3x"]
)
Reference selected stages and force re-referencing
csaxs.smaract_reference_stages(
devices_to_reference=["sl4trxi", "sl4trxo"],
force=True
)
Reference a single device
csaxs.smaract_reference_stages(
devices_to_reference="xbimtrx"
)
Reference only the selected devices (skip already-referenced ones)
csaxs.smaract_reference_stages(
devices_to_reference=["sl3trxi", "sl4trxo", "sl5trxt"]
)
Check referencing status of all stages
csaxs.smaract_check_all_referenced()
"""
# Normalize selection
if isinstance(devices_to_reference, str):
devices_to_reference = [devices_to_reference]
selection = set(devices_to_reference) if devices_to_reference else None
# Build axis map for selected devices (or all devices present)
axis_map = self._build_session_axis_map(selection=selection)
if selection:
unknown = sorted(list(selection - set(axis_map.keys())))
if unknown:
print(f"Unknown devices requested or missing 'bl_smar_stage' (ignored): {unknown}")
newly_referenced = []
already_referenced = []
failed = []
to_verify = [] # devices that need a final verification
print("\nStarting SmarAct referencing...\n")
for dev_name in sorted(axis_map.keys()):
ch = axis_map[dev_name]
d = self._get_device_object(dev_name)
if d is None:
print(f"{dev_name}: device not accessible, skipping.")
failed.append(dev_name)
continue
try:
is_ref = d.controller.axis_is_referenced(ch)
# Skip if already referenced and not forcing
if is_ref and not force:
print(f"{dev_name}: already referenced, skipping.")
already_referenced.append(dev_name)
continue
# Start referencing
print(f"{dev_name}: referencing axis...")
d.controller.set_closed_loop_move_speed(ch, 1)
d.controller.find_reference_mark(ch, 0, 1000, 1)
time.sleep(0.1)
# Add to list for final verification
to_verify.append((dev_name, ch, d))
except Exception as e:
print(f"Error referencing {dev_name} (axis {ch}): {e}")
failed.append(dev_name)
time.sleep(1.0)
print("\nVerifying referencing state...\n")
for dev_name, ch, d in to_verify:
try:
if d.controller.axis_is_referenced(ch):
print(f"{dev_name}: successfully referenced.")
newly_referenced.append(dev_name)
else:
print(f"{dev_name}: referencing FAILED.")
failed.append(dev_name)
except Exception as e:
print(f"{dev_name}: verification error: {e}")
failed.append(dev_name)
# --- Summary ---
print("\n--- Referencing summary ---")
print(f"Newly referenced: {newly_referenced}")
print(f"Already referenced (kept): {already_referenced}")
print(f"Failed: {failed}")
print("-----------------------------------------\n")
# --- Move newly referenced only ---
if newly_referenced:
print("Moving newly referenced stages to initial positions...")
self.smaract_components_to_initial_position(devices_to_move=newly_referenced)
else:
print("No newly referenced stages.")
def smaract_check_all_referenced(self):
"""
Check reference state for all SmarAct devices that define 'bl_smar_stage'.
"""
axis_map = self._build_session_axis_map(selection=None)
for dev_name in sorted(axis_map.keys()):
ch = axis_map[dev_name]
d = self._get_device_object(dev_name)
if d is None:
print(f"{dev_name}: device not accessible or unsupported.")
continue
try:
if d.controller.axis_is_referenced(ch):
print(f"{dev_name} (axis {ch}) is referenced.")
else:
print(f"{dev_name} (axis {ch}) is NOT referenced.")
except Exception as e:
print(f"Error checking {dev_name} (axis {ch}): {e}")
def smaract_components_to_initial_position(self, devices_to_move=None):
"""
Move selected (or all) SmarAct-based components to their configured init_position.
Parameters
----------
devices_to_move : iterable of str or str, optional
Specific device names to move (e.g. ["xbpm3x", "sl3trxi"]).
If None, all devices in the current session that define 'bl_smar_stage' are considered.
Behavior
--------
- Runtime-based: uses user_parameter['bl_smar_stage'] (numeric channel) and 'init_position'.
- Only axes that are referenced will be moved.
- Unreferenced axes are skipped with a WARNING; the operation continues.
- Devices missing `init_position` are skipped and listed.
- At the end, a summary warns if some stages could not be moved because they were not referenced.
"""
# Normalize selection
if isinstance(devices_to_move, str):
devices_to_move = [devices_to_move]
selection = set(devices_to_move) if devices_to_move else None
# Resolve axis map based on selection
axis_map = self._build_session_axis_map(selection=selection)
unknown_requested = []
if selection:
unknown_requested = sorted(list(selection - set(axis_map.keys())))
if unknown_requested:
logger.warning(
"[cSAXS] Requested devices unknown or missing 'bl_smar_stage': "
+ ", ".join(unknown_requested)
)
# First confirmation: intent
scope_desc = "all SmarAct-based components" if selection is None else "the selected SmarAct-based components"
if not self._yesno(
f"Do you want to move {scope_desc} to the init position as defined in the config file?",
"y",
):
return
planned_moves = []
not_referenced = []
missing_params = []
inaccessible_devices = []
# --- Pre-check phase ---
for dev_name in sorted(axis_map.keys()):
d = self._get_device_object(dev_name)
if d is None:
logger.warning(f"[cSAXS] Device {dev_name} not accessible, skipping.")
inaccessible_devices.append(dev_name)
continue
ch = axis_map[dev_name]
try:
# Reference check
if not d.controller.axis_is_referenced(ch):
not_referenced.append(dev_name)
continue
# Fetch init_position (from user parameters)
init_pos = self._get_user_param_safe(dev_name, "init_position")
if init_pos is None:
missing_params.append(dev_name)
continue
planned_moves.append((dev_name, float(init_pos)))
except Exception as exc:
logger.error(f"[cSAXS] Error during pre-check for {dev_name}: {exc}")
if not planned_moves:
# Nothing to move—still summarize why.
header = "\nNo motions planned. Summary of issues:"
lines = []
if not_referenced:
lines.append(" - Not referenced: " + ", ".join(sorted(not_referenced)))
if missing_params:
lines.append(" - Missing init_position: " + ", ".join(sorted(missing_params)))
if inaccessible_devices:
lines.append(" - Not accessible: " + ", ".join(sorted(inaccessible_devices)))
if unknown_requested:
lines.append(" - Unknown requested: " + ", ".join(sorted(unknown_requested)))
if not lines:
lines.append(" - (No eligible devices or nothing to do.)")
print(header)
for line in lines:
print(line)
logger.warning("[cSAXS] Nothing to do.")
return
# --- Summary table ---
print("\nPlanned SmarAct motions to initial position:")
print("-" * 60)
print(f"{'Device':<35} {'Init position':>20}")
print("-" * 60)
for dev_name, init_pos in planned_moves:
print(f"{dev_name:<35} {init_pos:>20.6g}")
print("-" * 60)
# Notes / diagnostics
if selection is not None:
print("\nNote: Only the following devices were requested to move:")
print(", ".join(sorted(selection)))
if unknown_requested:
print("\nNote: The following requested devices are unknown and were ignored:")
print(", ".join(unknown_requested))
if not_referenced:
print("\nNote: The following devices are NOT referenced and will be skipped:")
print(", ".join(sorted(not_referenced)))
if missing_params:
print("\nNote: The following devices have no init_position defined and will be skipped:")
print(", ".join(sorted(missing_params)))
if inaccessible_devices:
print("\nNote: The following devices were not accessible and will be skipped:")
print(", ".join(sorted(inaccessible_devices)))
# Second confirmation: execution
if not self._yesno("Proceed with the motions listed above?", "y"):
logger.info("[cSAXS] Motion to initial position aborted by user.")
return
# --- Execution phase (SIMULTANEOUS MOTION) ---
if umv is None:
logger.error("[cSAXS] 'umv' is not available in this session.")
return
# Build a flat argument list: [dev1, pos1, dev2, pos2, ...]
move_args = []
for dev_name, init_pos in planned_moves:
d = self._get_device_object(dev_name)
if d is None:
logger.error(f"[cSAXS] Could not access {dev_name}, skipping.")
continue
move_args.append(d)
move_args.append(init_pos)
logger.info(f"[cSAXS] Preparing move: {dev_name} -> {init_pos}")
if not move_args:
logger.warning("[cSAXS] No valid devices left for simultaneous motion.")
return
# Trigger simultaneous move
try:
logger.info(f"[cSAXS] Starting simultaneous motion of {len(planned_moves)} devices.")
umv(*move_args) # simultaneous move
except Exception as exc:
logger.error(f"[cSAXS] Simultaneous motion failed: {exc}")
return
logger.info("[cSAXS] Simultaneous SmarAct motion to initial positions completed.")
# Final warning summary about unreferenced devices
if not_referenced:
logger.warning(
"[cSAXS] Some stages were NOT moved because they are not referenced:\n"
+ ", ".join(sorted(not_referenced))
+ "\nPlease reference these axes and re-run if needed."
)
class cSAXSSmaract:
def __init__(self, client) -> None:
self.client = client
def _get_user_param_safe(self, device_name: str, key: str):
try:
return dev[device_name].user_parameter.get(key)
except Exception:
return None
def fshn1in(self):
"""
Move fast shutter n1 to its 'in' position defined in user parameters.
"""
in_pos = self._get_user_param_safe("fast_shutter_n1_x", "in")
if in_pos is None:
logger.error("[cSAXS] No 'in' position defined for fast_shutter_n1_x.")
return
if umv is None:
logger.error("[cSAXS] 'umv' is not available in this session.")
return
umv(dev.fast_shutter_n1_x, in_pos)

View File

@@ -91,9 +91,9 @@ class flomniGuiTools:
print("Cannot open camera_overview. Device does not exist.")
def flomnigui_remove_all_docks(self):
dev.cam_flomni_overview.stop_live_mode()
dev.cam_flomni_gripper.stop_live_mode()
dev.cam_xeye.live_mode = False
#dev.cam_flomni_overview.stop_live_mode()
#dev.cam_flomni_gripper.stop_live_mode()
#dev.cam_xeye.live_mode = False
self.gui.flomni.delete_all()
self.progressbar = None
self.text_box = None
@@ -114,6 +114,58 @@ class flomniGuiTools:
)
idle_text_box.set_html_text(text)
def flomnigui_docs(self, filename: str | None = None):
import csaxs_bec
from pathlib import Path
print("The general flOMNI documentation is at \nhttps://sls-csaxs.readthedocs.io/en/latest/user/ptychography/flomni.html#user-ptychography-flomni")
csaxs_bec_basepath = Path(csaxs_bec.__file__).parent
docs_folder = (
csaxs_bec_basepath /
"bec_ipython_client" / "plugins" / "flomni" / "docs"
)
if not docs_folder.is_dir():
raise NotADirectoryError(f"Docs folder not found: {docs_folder}")
pdfs = sorted(docs_folder.glob("*.pdf"))
if not pdfs:
raise FileNotFoundError(f"No PDF files found in {docs_folder}")
# --- Resolve PDF ------------------------------------------------------
if filename is not None:
pdf_file = docs_folder / filename
if not pdf_file.exists():
raise FileNotFoundError(f"Requested file not found: {filename}")
else:
print("\nAvailable flOMNI documentation PDFs:\n")
for i, pdf in enumerate(pdfs, start=1):
print(f" {i:2d}) {pdf.name}")
print()
while True:
try:
choice = int(input(f"Select a file (1{len(pdfs)}): "))
if 1 <= choice <= len(pdfs):
pdf_file = pdfs[choice - 1]
break
print(f"Enter a number between 1 and {len(pdfs)}.")
except ValueError:
print("Invalid input. Please enter a number.")
# --- GUI handling (active existence check) ----------------------------
self.flomnigui_show_gui()
if self._flomnigui_check_attribute_not_exists("PdfViewerWidget"):
self.flomnigui_remove_all_docks()
self.pdf_viewer = self.gui.flomni.new(widget="PdfViewerWidget")
# --- Load PDF ---------------------------------------------------------
self.pdf_viewer.PdfViewerWidget.load_pdf(str(pdf_file.resolve()))
print(f"\nLoaded: {pdf_file.name}\n")
def _flomnicam_check_device_exists(self, device):
try:
device
@@ -156,8 +208,8 @@ class flomniGuiTools:
)
self.progressbar.set_value([progress, subtomo_progress, 0])
if self.text_box is not None:
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
self.text_box.set_plain_text(text)
text = f"Progress report:\n Tomo type: ....................... {self.progress['tomo_type']}\n Projection: ...................... {self.progress['projection']:.0f}\n Total projections expected ....... {self.progress['total_projections']}\n Angle: ........................... {self.progress['angle']}\n Current subtomo: ................. {self.progress['subtomo']}\n Current projection within subtomo: {self.progress['subtomo_projection']}\n Total projections per subtomo: ... {self.progress['subtomo_total_projections']}"
self.text_box.set_plain_text(text)
if __name__ == "__main__":

View File

@@ -0,0 +1,250 @@
"""Module providing debugging tools for the BEC IPython client at cSAXS."""
from __future__ import annotations
import inspect
import json
import os
import re
import socket
from concurrent.futures import ThreadPoolExecutor
from functools import partial
from typing import TYPE_CHECKING, Literal
import numpy as np
from pydantic import BaseModel
from rich.console import Console
from rich.panel import Panel
from rich.table import Table
from rich.text import Text
from slugify import slugify
if TYPE_CHECKING:
from bec_ipython_client.main import BECIPythonClient
from bec_lib.devicemanager import DeviceManagerBase
from bec_lib.scans import Scans
from bec_widgets.cli.client_utils import BECGuiClient
scans: Scans # type: ignore[no-redef]
bec: BECIPythonClient # type: ignore[no-redef]
dev: DeviceManagerBase # type: ignore[no-redef]
class Detector(BaseModel):
"""Model representing a detector configuration."""
name: str
hostnames: list[str]
cfg: dict
def to_identifier(text: str) -> str:
"""
Convert an unsafe string into a valid Python identifier.
"""
name = slugify(text.strip(), separator="_")
name = re.sub(r"[^a-zA-Z0-9_]", "", name)
if not name:
raise ValueError(f"Cannot convert '{text}' to a valid identifier.")
if name[0].isdigit():
name = f"_{name}"
return name
class DebugTools:
"""A collection of debugging tools for the BEC IPython client at cSAXS."""
_PURPOSE = (
"Debugging helpers for the cSAXS BEC IPython client. These tools are intended for advanced users "
"and developers to diagnose and troubleshoot issues within the BEC environment. "
"Below are the available methods together with a brief description of their functionality."
)
######################
## Internal Methods ##
######################
def _describe(self) -> None:
"""Pretty-print a description of this debugging tool."""
console = Console()
# Offset for IPython prompt misplacement
console.print("\n\n", end="")
header = Text("DebugTools", style="bold cyan")
purpose = Text(self._PURPOSE, style="dim")
console.print(Panel(purpose, title=header, expand=False))
table = Table(show_header=True, header_style="bold magenta")
table.add_column("Method", style="bold", no_wrap=True)
table.add_column("Description")
for name, member in inspect.getmembers(self, predicate=inspect.ismethod):
if name.startswith("_"):
continue
doc = inspect.getdoc(member)
short_doc = doc.splitlines()[0] if doc else ""
table.add_row(name, short_doc)
console.print(table)
def _repr_pretty_(self, p, cycle: bool) -> None:
if cycle:
p.text("DebugTools(...)")
else:
self._describe()
#####################
### MCS Card Check ###
#####################
def _check_if_device_is_loaded(self, device_name: str):
"""Check if a device is loaded in the current BEC session."""
if device_name not in dev:
raise RuntimeError(
f"Device {device_name} was not loaded in the current active BEC session."
)
def mcs_test_acquire(
self, mode: Literal["high_frame", "medium_frame", "low_frame"] = "high_frame"
):
"""
Method to perform a test acquisition with randomized exposure time, burst frames, and cycles
on the MCS card using the DDG trigger setup.
Args:
mode (Literal["high_frame", "medium_frame", "low_frame"]): The mode of the test.
- 'high_frame': Tests high frame rates with short exposure times.
- 'medium_frame': Tests medium frame rates with moderate exposure times.
- 'low_frame': Tests low frame rates with longer exposure times.
"""
self._check_if_device_is_loaded("mcs")
self._check_if_device_is_loaded("ddg1")
self._check_if_device_is_loaded("ddg2")
if mode == "high_frame":
burst_frames = np.random.randint(10_000, 100_000) # between 10000 and 100000
cycles = np.random.randint(5, 20) # between 5 and 20
exp_time = (
np.random.rand() * (0.001 - 0.201e-3) + 0.201e-3
) # between 0.000201 ms and 0.001 s
elif mode == "medium_frame":
burst_frames = np.random.randint(50, 500) # between 50 and 500
cycles = np.random.randint(1, 10) # between 1 and 10
exp_time = np.random.rand() * (0.01 - 0.001) + 0.001 # between 0.001 ms and 0.01 s
elif mode == "low_frame":
burst_frames = np.random.randint(5, 20) # between 5 and 20
cycles = np.random.randint(1, 5) # between 1 and 5
exp_time = np.random.rand() * (2 - 0.1) + 0.1 # between 0.1 ms and 2 s
else:
raise ValueError(f"Invalid mode '{mode}' specified for acquire scan test.")
print(
f"Starting acquire measurement with exp_time={exp_time:.6f}, burst_frames={burst_frames}, cycles={cycles}"
)
s = scans.acquire(
exp_time=exp_time, frames_per_trigger=burst_frames, burst_at_each_point=cycles
)
s.wait(file_written=True)
print("Acquire measurement finished.")
print("Checking MCS data...")
scan_data = bec.history.get_by_scan_id(s.scan.scan_id)
mcs_data = scan_data.devices.mcs
print(mcs_data)
shape = mcs_data._info["mcs_mca_mca1"]["value"]["shape"]
expected_shape = (cycles * burst_frames,)
# Assert will raise an error if the shapes do not match
assert (
shape == expected_shape
), f"MCS data shape {shape} does not match expected shape {expected_shape}."
########################
### JFJ/Eiger Checks ###
########################
def _get_jfj_eiger_config(self) -> dict[str, Detector]:
"""Retrieve the current JFJ/Eiger detector configuration from the BEC client."""
# FIXME: Implement REST API call once ready for use from Leo Sala's team.
ret = {}
base_path = os.path.dirname(__file__)
config_path = os.path.join(base_path, "jfj_config.json")
with open(config_path, "r", encoding="utf-8") as fh:
cfg = json.load(fh)
for entry in cfg["detector"]:
det = Detector(
name=to_identifier(entry["description"]), hostnames=entry["hostname"], cfg=cfg
)
ret[det.name] = det
return ret
def list_detectors(self) -> list[str]:
"""
List the names of all JFJ/Eiger detectors configured in the BEC client.
Returns:
list[str]: A list of detector names.
"""
detectors = self._get_jfj_eiger_config()
return list(detectors.keys())
def ping_detector(self, detector_name: str) -> bool:
"""
Ping a JFJ/Eiger detector to check if it is reachable.
Args:
detector_name (str): The name of the detector to ping.
Returns:
bool: True if the detector is reachable, False otherwise.
"""
detectors = self._get_jfj_eiger_config()
if detector_name not in detectors:
raise ValueError(f"Detector '{detector_name}' not found in configuration.")
det = detectors[detector_name]
results = self._ping_many(det.hostnames)
table = Table(title=f"Ping results for detector '{detector_name}'")
table.add_column("Hostname", style="cyan", no_wrap=True)
table.add_column("Status", style="magenta")
for host, alive in results.items():
status = "[green]OK[/green]" if alive else "[red]DOWN[/red]"
table.add_row(host, status)
console = Console()
console.print(table)
def _ping_many(self, hosts: list[str], port=22, timeout=2, max_workers=None):
max_workers = max_workers or len(hosts)
with ThreadPoolExecutor(max_workers=max_workers) as executor:
primed_ping = partial(self._ping, port=port, timeout=timeout)
pings = executor.map(primed_ping, hosts)
return dict(zip(hosts, pings))
def _ping(self, host: str, port=23, timeout=2): # telnet is port 23
address = (host, port)
try:
with socket.create_connection(address, timeout):
return True
except OSError:
return False
def open_it_service_page(self):
"""Open the overview of IT services hosted by Science IT Infrastructure and Services for cSAXS."""
gui: BECGuiClient = bec.gui
dock_area = gui.new()
print("Opening IT service page in new dock...")
url = "https://metrics.psi.ch/d/saf8mxv/x12sa?orgId=1&from=now-24h&to=now&timezone=browser&var-receiver_hosts=sls-jfjoch-001.psi.ch&var-writer_hosts=xbl-daq-34.psi.ch&var-beamline=X12SA&var-slurm_partitions=csaxs&var-receiver_services=broker&var-writer_services=writer&refresh=15m"
# FIXME BEC WIDGETS v3
dock = dock_area.new()
wb = dock.new(widget=gui.available_widgets.WebsiteWidget)
wb.set_url(url)

View File

@@ -0,0 +1,162 @@
{
"zeromq" : {
"image_socket": ["tcp://0.0.0.0:5500"]
},
"zeromq_preview": {
"socket_address": "tcp://0.0.0.0:5400",
"enabled": true,
"period_ms": 1000
},
"zeromq_metadata" : {
"socket_address": "tcp://0.0.0.0:5600",
"enabled": true,
"period_ms": 100
},
"instrument" : {
"source_name": "Swiss Light Source",
"instrument_name": "cSAXS",
"source_type": "Synchrotron X-ray Source"
},
"detector": [
{
"description": "EIGER 9M",
"serial_number": "E1",
"type": "EIGER",
"mirror_y": true,
"base_data_ipv4_address": "10.10.10.10",
"calibration_file":["/opt/jfjoch/calibration/"],
"standard_geometry" : {
"nmodules": 18,
"modules_in_row": 3,
"gap_x": 8,
"gap_y": 36
},
"hostname": [
"beb101",
"beb103",
"beb014",
"beb078",
"beb060",
"beb030",
"beb092",
"beb178",
"beb009",
"beb038",
"beb056",
"beb058",
"beb033",
"beb113",
"beb005",
"beb017",
"beb119",
"beb095",
"beb186",
"beb042",
"beb106",
"beb059",
"beb111",
"beb203",
"beb100",
"beb093",
"beb123",
"beb061",
"beb121",
"beb055",
"beb004",
"beb190",
"beb054",
"beb189",
"beb107",
"beb115"
]
},
{
"description": "EIGER 8.5M (tmp)",
"serial_number": "E1-tmp",
"type": "EIGER",
"mirror_y": true,
"base_data_ipv4_address": "10.10.10.10",
"calibration_file":["/opt/jfjoch/calibration/"],
"standard_geometry" : {
"nmodules": 17,
"modules_in_row": 3,
"gap_x": 8,
"gap_y": 36
},
"hostname": [
"beb101",
"beb103",
"beb014",
"beb078",
"beb060",
"beb030",
"beb092",
"beb178",
"beb009",
"beb038",
"beb056",
"beb058",
"beb033",
"beb113",
"beb005",
"beb017",
"beb119",
"beb095",
"beb186",
"beb042",
"beb106",
"beb059",
"beb100",
"beb093",
"beb123",
"beb061",
"beb121",
"beb055",
"beb004",
"beb190",
"beb054",
"beb189",
"beb107",
"beb115"
]
},
{
"description": "EIGER 1.5M",
"serial_number": "E2",
"type": "EIGER",
"mirror_y": true,
"base_data_ipv4_address": "10.10.11.10",
"calibration_file":["/opt/jfjoch/calibration_e1p5m/"],
"standard_geometry" : {
"nmodules": 3,
"modules_in_row": 1,
"gap_x": 8,
"gap_y": 36
},
"hostname": ["beb062", "beb026", "beb099", "beb084", "beb120", "beb108"]
}
],
"frontend_directory": "/usr/share/jfjoch/frontend/",
"image_pusher": "ZeroMQ",
"numa_policy": "n2g2",
"receiver_threads": 64,
"image_buffer_MiB": 96000,
"pcie": [
{
"blk": "/dev/jfjoch0",
"ipv4": "10.10.10.1"
},
{
"blk": "/dev/jfjoch1",
"ipv4": "10.10.10.2"
},
{
"blk": "/dev/jfjoch2",
"ipv4": "10.10.10.3"
},
{
"blk": "/dev/jfjoch3",
"ipv4": "10.10.10.4"
}
]
}

View File

@@ -48,6 +48,11 @@ elif _args.session.lower() == "csaxs":
logger.success("cSAXS session loaded.")
from csaxs_bec.bec_ipython_client.plugins.tool_box.debug_tools import DebugTools
debug = DebugTools()
logger.success("Debug tools loaded. Use 'debug' to access them.")
# SETUP BEAMLINE INFO
from bec_ipython_client.plugins.SLS.sls_info import OperatorInfo, SLSInfo
@@ -61,25 +66,3 @@ bec._beamline_mixin._bl_info_register(OperatorInfo)
# SETUP PROMPTS
bec._ip.prompts.session_name = _session_name
bec._ip.prompts.status = 1
# REGISTER BEAMLINE CHECKS
from bec_lib.bl_conditions import (
FastOrbitFeedbackCondition,
LightAvailableCondition,
ShutterCondition,
)
if "sls_machine_status" in dev:
print("Registering light available condition for SLS machine status")
_light_available_condition = LightAvailableCondition(dev.sls_machine_status)
bec.bl_checks.register(_light_available_condition)
if "x12sa_es1_shutter_status" in dev:
print("Registering shutter condition for X12SA ES1 shutter status")
_shutter_condition = ShutterCondition(dev.x12sa_es1_shutter_status)
bec.bl_checks.register(_shutter_condition)
# if hasattr(dev, "sls_fast_orbit_feedback"):
# print("Registering fast orbit feedback condition for SLS fast orbit feedback")
# _fast_orbit_feedback_condition = FastOrbitFeedbackCondition(dev.sls_fast_orbit_feedback)
# bec.bl_checks.register(_fast_orbit_feedback_condition)

View File

@@ -0,0 +1,24 @@
eiger_1_5:
description: Eiger 1.5M in-vacuum detector
deviceClass: csaxs_bec.devices.jungfraujoch.eiger_1_5m.Eiger1_5M
deviceConfig:
detector_distance: 100
beam_center: [0, 0]
onFailure: raise
enabled: true
readoutPriority: async
softwareTrigger: False
ids_cam:
description: IDS camera for live image acquisition
deviceClass: csaxs_bec.devices.ids_cameras.IDSCamera
deviceConfig:
camera_id: 201
bits_per_pixel: 24
m_n_colormode: 1
live_mode: True
onFailure: raise
enabled: true
readoutPriority: async
softwareTrigger: True

View File

@@ -0,0 +1,553 @@
##########################################################################
###################### Delay generators ##################################
##########################################################################
ddg1:
description: Main delay Generator for triggering
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DDG1
enabled: true
deviceConfig:
prefix: 'X12SA-CPCL-DDG1:'
onFailure: raise
readOnly: false
readoutPriority: baseline
softwareTrigger: true
ddg2:
description: Detector delay Generator for trigger burst
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DDG2
enabled: true
deviceConfig:
prefix: 'X12SA-CPCL-DDG2:'
onFailure: raise
readOnly: false
readoutPriority: baseline
softwareTrigger: false
##########################################################################
###################### Multichannel Scaler################################
##########################################################################
mcs:
description: Mcs scalar card for transmission readout
deviceClass: csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs.MCSCardCSAXS
deviceConfig:
prefix: 'X12SA-MCS:'
onFailure: raise
enabled: true
readoutPriority: monitored
softwareTrigger: false
##########################################################################
######################## SMARACT STAGES ##################################
##########################################################################
################## XBOX 1 ES #####################
xbpm3x:
description: X-ray beam position x monitor 1 in ESbox1
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -22.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 0
xbpm3y:
description: X-ray beam position y monitor 1 in ESbox1
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -2
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 1
sl3trxi:
description: ESbox1 slit 3 inner blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 2
sl3trxo:
description: ESbox1 slit 3 outer blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: D
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 3
sl3trxb:
description: ESbox1 slit 3 bottom blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: E
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -5.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 4
sl3trxt:
description: ESbox1 slit 3 top blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: F
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 5
fast_shutter_n1_x:
description: ESbox1 New fast shutter 1 x movment
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: H
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -7
in: 0
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 7
fast_shutter_o1_x:
description: ESbox1 Old fast shutter 1 x movment
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: G
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -15.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 6
fast_shutter_o2_x:
description: ESbox1 Old fast shutter 2 x movment
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: F
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -15.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 5
filter_array_1_x:
description: ESbox1 Filter Array 1 x movment
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 25
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 1
filter_array_2_x:
description: ESbox1 Filter Array 2 x movment
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 25.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 2
filter_array_3_x:
description: ESbox1 Filter Array 3 x movment
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: D
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 25.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 3
filter_array_4_x:
description: ESbox1 Filter Array 4 x movment
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: E
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 25
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 4
sl4trxi:
description: ESbox1 slit 4 inner blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: G
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 6
sl4trxo:
description: ESbox1 slit 4 outer blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: H
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 7
sl4trxb:
description: ESbox1 slit 4 bottom blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: I
host: x12sa-eb-smaract-mcs-04.psi.ch
limits:
- -200
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -5.8
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 8
sl4trxt:
description: ESbox1 slit 4 top blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: x12sa-eb-smaract-mcs-01.psi.ch
limits:
- -200
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 0
################## XBOX 2 ES #####################
sl5trxi:
description: ESbox2 slit 5 inner blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: C
host: x12sa-eb-smaract-mcs-02.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 2
sl5trxo:
description: ESbox2 slit 5 outer blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: D
host: x12sa-eb-smaract-mcs-02.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 3
sl5trxb:
description: ESbox2 slit 5 bottom blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: E
host: x12sa-eb-smaract-mcs-02.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -5.5
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 4
sl5trxt:
description: ESbox1 slit 5 top blade movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: F
host: x12sa-eb-smaract-mcs-02.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 6
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 5
xbimtrx:
description: ESbox2 beam intensity monitor x movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: A
host: x12sa-eb-smaract-mcs-02.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: -14.7
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 0
xbimtry:
description: ESbox2 beam intensity monitor y movement
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
deviceConfig:
axis_Id: B
host: x12sa-eb-smaract-mcs-02.psi.ch
limits:
- -200
- 200
port: 5000
sign: -1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
init_position: 0
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 1

View File

@@ -68,6 +68,12 @@ ccmx:
- cSAXS
- optics
##########################################################################
######################## SMARACT STAGES ##################################
##########################################################################
xbpm2x:
description: X-ray beam position monitor 1 in OPbox
deviceClass: csaxs_bec.devices.smaract.smaract_ophyd.SmaractMotor
@@ -79,12 +85,13 @@ xbpm2x:
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 0
xbpm2y:
description: X-ray beam position monitor 1 in OPbox
@@ -97,12 +104,13 @@ xbpm2y:
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 1
cu_foilx:
description: Cu foil in OPbox
@@ -115,12 +123,13 @@ cu_foilx:
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 2
scinx:
description: scintillator in OPbox
@@ -133,12 +142,14 @@ scinx:
- 200
port: 5000
sign: 1
# precision: 3
# tolerance: 0.005
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
userParameter:
# bl_smar_stage to use csaxs reference method. assign number according to axis channel
bl_smar_stage: 3
# dmm1_trx_readback_example: # This is the same template as for i.e. bpm4i
# description: 'This is an example of a read-only Epics signal'

View File

@@ -1,55 +0,0 @@
ddg1:
description: Main delay Generator for triggering
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DDG1
enabled: true
deviceConfig:
prefix: 'X12SA-CPCL-DDG1:'
onFailure: raise
readOnly: false
readoutPriority: baseline
softwareTrigger: true
ddg2:
description: Detector delay Generator for trigger burst
deviceClass: csaxs_bec.devices.epics.delay_generator_csaxs.DDG2
enabled: true
deviceConfig:
prefix: 'X12SA-CPCL-DDG2:'
onFailure: raise
readOnly: false
readoutPriority: baseline
softwareTrigger: false
mcs:
description: Mcs scalar card for transmission readout
deviceClass: csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs.MCSCardCSAXS
deviceConfig:
prefix: 'X12SA-MCS:'
onFailure: raise
enabled: true
readoutPriority: monitored
softwareTrigger: false
ids_cam:
description: IDS camera for live image acquisition
deviceClass: csaxs_bec.devices.ids_cameras.IDSCamera
deviceConfig:
camera_id: 201
bits_per_pixel: 24
m_n_colormode: 1
live_mode: True
onFailure: raise
enabled: true
readoutPriority: async
softwareTrigger: True
eiger_1_5:
description: Eiger 1.5M in-vacuum detector
deviceClass: csaxs_bec.devices.jungfraujoch.eiger_1_5m.Eiger1_5M
deviceConfig:
detector_distance: 100
beam_center: [0, 0]
onFailure: raise
enabled: true
readoutPriority: async
softwareTrigger: False

View File

@@ -1,8 +0,0 @@
optics:
- !include ./optics_hutch.yaml
frontend:
- !include ./frontend.yaml
endstation:
- !include ./endstation.yaml

View File

@@ -115,7 +115,7 @@ samy:
softwareTrigger: false
micfoc:
description: Focusing motor of Microscope stage
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES06
motor_resolution: 0.00125
@@ -133,7 +133,7 @@ micfoc:
softwareTrigger: false
owis_samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES01
motor_resolution: 0.00125
@@ -151,7 +151,7 @@ owis_samx:
softwareTrigger: false
owis_samy:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES02
motor_resolution: 0.00125
@@ -169,7 +169,7 @@ owis_samy:
softwareTrigger: false
rotx:
description: Rotation stage rotx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES05
motor_resolution: 0.0025
@@ -190,7 +190,7 @@ rotx:
softwareTrigger: false
roty:
description: Rotation stage rotx
deviceClass: ophyd_devices.devices.EpicsMotorEx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES04
motor_resolution: 0.0025

View File

@@ -0,0 +1,29 @@
# This is the main configuration file that is
# commented or uncommented according to the type of experiment
optics:
- !include ./bl_optics_hutch.yaml
frontend:
- !include ./bl_frontend.yaml
endstation:
- !include ./bl_endstation.yaml
detectors:
- !include ./bl_detectors.yaml
#sastt:
# - !include ./sastt.yaml
#flomni:
# - !include ./ptycho_flomni.yaml
#omny:
# - !include ./ptycho_omny.yaml
#lamni:
# - !include ./ptycho_lamni.yaml
#user setup:
# - !include ./user_setup.yaml

View File

@@ -1,38 +0,0 @@
############################################################
#################### npoint motors #########################
############################################################
npx:
description: nPoint x axis on the big npoint controller
deviceClass: csaxs_bec.devices.npoint.npoint.NPointAxis
deviceConfig:
axis_Id: A
host: "nPoint000003.psi.ch"
limits:
- -50
- 50
port: 23
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
deviceTags:
- npoint
npy:
description: nPoint y axis on the big npoint controller
deviceClass: csaxs_bec.devices.npoint.npoint.NPointAxis
deviceConfig:
axis_Id: B
host: "nPoint000003.psi.ch"
limits:
- -50
- 50
port: 23
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
deviceTags:
- npoint

View File

@@ -0,0 +1,97 @@
samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES01
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 1
deviceTags:
- cSAXS
- owis_samx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
samy:
description: Owis motor stage samy
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES02
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 0
deviceTags:
- cSAXS
- owis_samx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
rotx:
description: Rotation stage rotx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES03
motor_resolution: 0.0025
base_velocity: 0.5
velocity: 7.5
backlash_distance: 0.25
acceleration: 0.2
user_offset_dir: 1
limits:
- -0.1
- 0.1
deviceTags:
- cSAXS
- rotx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
roty:
description: Rotation stage roty
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES04
motor_resolution: 0.0025
base_velocity: 0.5
velocity: 7.5
backlash_distance: 0.25
acceleration: 0.2
user_offset_dir: 0
limits:
- -0.1
- 0.1
deviceTags:
- cSAXS
- roty
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
micfoc:
description: Focusing motor of Microscope stage
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES05
motor_resolution: 0.00125
base_velocity: 0.25
velocity: 2.5
backlash_distance: 0.125
acceleration: 0.4
user_offset_dir: 0
deviceTags:
- cSAXS
- micfoc
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false

View File

@@ -0,0 +1 @@
############################################################

View File

@@ -0,0 +1,85 @@
############################################################
#################### OWIS LTM80 ############################
############################################################
samx:
description: Owis motor stage samx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES02
motor_resolution: 0.00125
base_velocity: 0.0625
velocity: 10
backlash_distance: 0.125
acceleration: 0.2
user_offset_dir: 0
deviceTags:
- cSAXS
- owis_samx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
############################################################
#################### OWIS Rotation DMT65 ###################
############################################################
rotx:
description: Rotation stage rotx
deviceClass: ophyd_devices.devices.psi_motor.EpicsUserMotorVME
deviceConfig:
prefix: X12SA-ES2-ES03
motor_resolution: 0.0025
base_velocity: 0.5
velocity: 7.5
backlash_distance: 0.25
acceleration: 0.2
user_offset_dir: 1
limits:
- -0.1
- 0.1
deviceTags:
- cSAXS
- rotx
onFailure: buffer
enabled: true
readoutPriority: baseline
softwareTrigger: false
############################################################
#################### npoint motors #########################
############################################################
npx:
description: nPoint x axis on the big npoint controller
deviceClass: csaxs_bec.devices.npoint.npoint.NPointAxis
deviceConfig:
axis_Id: A
host: "nPoint000003.psi.ch"
limits:
- -50
- 50
port: 23
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
deviceTags:
- npoint
npy:
description: nPoint y axis on the big npoint controller
deviceClass: csaxs_bec.devices.npoint.npoint.NPointAxis
deviceConfig:
axis_Id: B
host: "nPoint000003.psi.ch"
limits:
- -50
- 50
port: 23
sign: 1
enabled: true
onFailure: buffer
readOnly: false
readoutPriority: baseline
deviceTags:
- npoint

View File

@@ -0,0 +1,58 @@
# Delay Generator implementation at the CSAXS beamline
This module provides an ophyd device implementation for the Stanford Research Systems Delay Generator DDG645, used at the cSAXS beamline as a master timing source for detector triggering and other beamline devices. Detailed information about the DDG manual can be found here:
https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf.
The implementation is based on a community EPICS driver (https://github.com/epics-modules/delaygen?tab=readme-ov-file).
**EPICS Interface**
At the cSAXS beamline, the DDG panel is avaiable via caqtdm on the beamline consoles.
``` bash
caqtdm -noMsg -attach -macro P=X12SA-CPCL-DDG,R=1: srsDG645.ui
```
with R=1,2,3,4,5 for 5 different DDG units installed at CSAXS.
# Ophyd Device integration at cSAXS
For cSAXS, a custom ophyd device class implementation of the DDG is provided [here](./delay_generator_csaxs.py). This class provides a basic interface to the DDG PVs. The interface provides channels 'A', B', 'C', ... with setpoint, readback and references, as well as high level parameters such as *width* and *delay*. Please check the source code of the class for more details of the implementation.
In addition, the class provides a set of utility methods to configure sets of channel pairs 'AB', 'CD', ... as commonly needed in operation at the beamline. At the cSAXS beamline, a single DDG device is used as a master timing source for other devices. The general scheme is described in a [PDF document here](./trigger_scheme_ddg1_ddg2.pdf). Below is a description of the configuration of the two DDG units used at cSAXS for detector triggering and beamline shutter control.
## Master card: DDG1 (X12SA-CPCL-DDG1)
The master [delay generator DDG1](./ddg_1.py) is configured to provide the following signals:
**Connection Scheme**:
- EXT/EN: May be connected to external devices, e.g. SGalil motion controller for fly scans.
- Operation Mode: Burst mode, but with single burst (burst count = 1). This is for practical reasons as it allows
to interrupt and ongoing sequence if needed.
- Software Trigger: Controlled through BEC.
- State Control: BEC checks the *state* of this DDG to wait for the completion of a timing sequence.
**Delay Pairs**:
- DelayPair 'AB': Provides the external enable (EXT/EN) signal to the second DDG (R=2).
- DelayPair 'CD': Controls the beamline shutter.
- DelayPair 'EF': Generates pulses for the MCS card, combined with the detector pulse train via an OR gate. This ensures the MCS card receives an additional pulse required for proper operation.
**Delay Channels**:
- a = t0 + 2ms (2ms delay to allow the shutter to open)
- b = a + 1us (short pulse)
- c = t0
- d = a + exp_time * burst_count + 1ms (to allow the shutter to close)
- e = d
- f = e + 1us (short pulse to OR gate for MCS triggering)
## Detector card: DDG2 (X12SA-CPCL-DDG2)
The second [delay generator DDG2](./ddg_2.py) is configured to provide the following signals:
**Connection Scheme**:
- EXT/EN: Connected to the DelayPair AB of the master DDG (R=1).
- Operation Mode: Burst mode: The *burst count* is set to the number of frames per trigger. The *burst delay* is set to 0, and the *burst period* is set to the exposure time.
- Software Trigger: Irrelevant, as the device is externally triggered by DDG1.
**Delay Pairs**:
- DelayPair 'AB': Provides the trigger signal to the detector.
**Delay Channels**:
- a = t0
- b = a + (exp_time - READOUT_TIMES)

View File

@@ -52,7 +52,7 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
LiteralChannels,
StatusBitsCompareStatus,
)
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING, READYTOREAD
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import ACQUIRING
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import DeviceManagerBase, ScanInfo
@@ -61,6 +61,13 @@ if TYPE_CHECKING: # pragma: no cover
logger = bec_logger.logger
########################
## DEFAULT SETTINGS ####
########################
# NOTE Default channel configuration for all channels of the DDG1 delay generator
# This can be adapted as needed, or fine-tuned per channel. On every reload of the
# device configuration in BEC, these values will be set into the DDG1 device.
_DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
"amplitude": 5.0,
"offset": 0.0,
@@ -68,6 +75,8 @@ _DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
"mode": "ttl",
}
# NOTE Here you can adapt the default IO configuration for all channels of the DDG1
# Currently, all channels are set to the same default configuration `_DEFAULT_CHANNEL_CONFIG`.
DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
"t0": _DEFAULT_CHANNEL_CONFIG,
"ab": _DEFAULT_CHANNEL_CONFIG,
@@ -75,9 +84,19 @@ DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
"ef": _DEFAULT_CHANNEL_CONFIG,
"gh": _DEFAULT_CHANNEL_CONFIG,
}
DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.SINGLE_SHOT
# NOTE Default readout times for each channel, can be adapted as needed.
# These values are relevant to calculate proper widths of the timing signals.
# They also define a minimum exposure time that can be used as they are subtracted
# as dead times from the exposure time.
DEFAULT_READOUT_TIMES = {"ab": 2e-4, "cd": 2e-4, "ef": 2e-4, "gh": 2e-4} # 0.2 ms 5kHz
# NOTE Default channel references for each channel of the DDG1 delay generator.
# This needs to be carefully adjusted to match the envisioned trigger scheme.
# If the trigger scheme changes, adapt the values here together with the README and
# PDF `trigger_scheme_ddg1_ddg2.pdf`.
DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
("A", CHANNELREFERENCE.T0), # T0 + 2ms delay
("B", CHANNELREFERENCE.A),
@@ -89,14 +108,27 @@ DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
("H", CHANNELREFERENCE.G),
]
###############################
## DDG1 IMPLEMENTATION ########
###############################
class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
"""
Implementation of DelayGeneratorCSAXS for master trigger delay generator at X12SA-CPCL-DDG1.
It will be triggered by a soft trigger from BEC or a hardware trigger from a beamline device
(e.g. the Galil stages). It is operated in standard mode, not burst mode and will trigger the
EXT/EN of DDG2 (channel ab). It is responsible for opening the shutter (channel cd) and sending
an extra trigger to an or gate for the MCS card (channel ef).
Implementation of the DelayGenerator DDG1 for the cSAXS beamline. It is the main trigger
source for the cSAXS beamline, and will be triggered by BEC through a software trigger or
by a hardware trigger from a beamline device (e.g. Galil stages). Specific implementation
of the cabling logic expected for this device are described in the module README, the attached
PDF 'trigger_scheme_ddg1_ddg2.pdf' and the module docstring.
The IOC prefix is 'X12SA-CPCL-DDG1:'.
Args:
name (str): Name of the device.
prefix (str, optional): EPICS prefix for the device. Defaults to ''.
scan_info (ScanInfo | None, optional): Scan info object. Defaults to None.
device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None.
"""
def __init__(
@@ -107,9 +139,6 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
device_manager: DeviceManagerBase | None = None,
**kwargs,
):
"""
Initialize the MCSCardCSAXS with the given arguments and keyword arguments.
"""
super().__init__(
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
)
@@ -123,70 +152,172 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# pylint: disable=attribute-defined-outside-init
def on_connected(self) -> None:
"""
Set the default values on the device - intended to overwrite everything to a usable default state.
Sets DEFAULT_IO_CONFIG into each channel, sets the trigger source to DEFAULT_TRIGGER_SOURCE,
and turns off burst mode.
This method is called after the device is initialized and all signals are connected. This happens
when a device configuration is loaded in BEC.
It sets the default values for this device - intended to overwrite everything to a usable default state.
For this purpose, we use the DEFAULT SETTINGS defined at the top of this module.
To ensure that this process is robust, we follow these steps:
- First, we stop any ongoing burst mode operation.
- Then, we set the DEFAULT_IO_CONFIG for each channel, the trigger source to DEFAULT_TRIGGER_SOURCE,
and the channel references to DEFAULT_REFERENCES.
- We set the state proc_status to be event based. This triggers readouts of the EventStatusLI bit
based on events. This was empirically found to be a stable solution in combination with the poll
loop of the state.
- Finally, we set the burst delay to 0, to set it to be of no delay.
"""
self.burst_disable() # it is possible to miss setting settings if burst is enabled
# NOTE First we make sure that there is nothing running on the DDG. This seems to
# help to tackle that the DDG occasionally freezes during the first scan
# after reconnecting to it. Do not remove.
self.stop_ddg()
# NOTE Setting DEFAULT configurations for IO config, trigger config and references.
# The three dictionaries above 'DEFAULT_IO_CONFIG', 'DEFAULT_TRIGGER_SOURCE' and
# 'DEFAULT_REFERNCES' should be used to adapt configurations if needed.
for channel, config in DEFAULT_IO_CONFIG.items():
self.set_io_values(channel, **config)
self.set_trigger(DEFAULT_TRIGGER_SOURCE)
self.set_references_for_channels(DEFAULT_REFERENCES)
# Set proc status to passively update with 5Hz (0.2s)
# NOTE Set state proc_status to be event based. This triggers readouts of the EventStatusLI bit
# based on events. This was empirically found to be a stable solution in combination with the poll
# loop of the state.
self.state.proc_status_mode.put(PROC_EVENT_MODE.EVENT)
# NOTE Burst delay should be set to 0, don't remove as this will not be checked
# Also set the burst count to 1 to only have a single pulse for DDG1.
self.burst_delay.put(0)
self.burst_count.put(1)
def on_stage(self) -> None:
"""
Stage logic for the DDG1 device, being th main trigger delay generator for CSAXS.
For standard scans, it will be triggered by a soft trigger from BEC.
It also has a hardware trigger feeded into the EXT/EN for fly-scanning, i.e. Galil stages.
This DDG is always not in burst mode.
This method is called in preparation for a scan. All information about the upcoming
scan is available in self.scan_info.msg at this point. We use this information to
configure the DDG1 for the upcoming scan.
The DDG is operated in burst mode for the scan, but with only a single burst pulse.
THe length of the pulse is set to the expected exposure time for a single trigger,
which includes any burst acquisitions if frames_per_trigger > 1.
The logic is as follows:
- We check if any default burst parameters need to be set, and set them if needed.
- We calculate the burst pulse width based on the exposure time and frames_per_trigger.
- We set the burst_period and the shutter signal (delay pairs cd) to be
exposure_time * frames_per_trigger + 3ms (2ms for shutter to open, 1ms to close).
- We set the delay pairs ab to be 2ms delayed (to allow the shutter to open) with a width of 1us to trigger DDG2.
- We set the delay pairs ef to be triggered after the shutter closes with a width of 1us to trigger the MCS card.
- Finally, we add a short sleep to ensure that the IOC and DDG HW process the values properly.
"""
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
self.burst_enable(1, 0, exp_time)
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
start_time = time.time()
########################################
### Burst mode settings ################
########################################
# NOTE We check here if the delay generator is not in burst mode. We check these values
# and set them to the requried values if they differ from the expected ones.
# This has been found empirically to improve stability and avoid HW getting stuck in triggering cycles.
if self.burst_mode.get() == 0:
self.burst_mode.put(1)
if self.burst_delay.get() != 0:
self.burst_delay.put(0)
if self.burst_count.get() != 1:
self.burst_count.put(1)
#########################################
### Setup timing for burst and delays ###
#########################################
frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"]
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
# Burst Period DDG1
# Set burst_period to shutter width
# c/t0 + 2ms + exp_time * burst_count + 1ms
shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3
if self.burst_period.get() != shutter_width:
self.burst_period.put(shutter_width)
# Trigger DDG2
# a = t0 + 2ms, b = a + 1us
# a has reference to t0, b has reference to a
# Add delay of 2ms to allow shutter to open
self.set_delay_pairs(channel="ab", delay=2e-3, width=1e-6)
# Trigger shutter
shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3
# d = c/t0 + 2ms + exp_time * burst_count + 1ms
# c has reference to t0, d has reference to c
# Shutter opens without delay at t0, closes after exp_time * burst_count + 3ms (2ms open, 1ms close)
self.set_delay_pairs(channel="cd", delay=0, width=shutter_width)
# Trigger extra pulse for MCS OR gate
# f = e + 1us
# e has refernce to d, f has reference to e
self.set_delay_pairs(channel="ef", delay=0, width=1e-6)
time.sleep(
0.2
) # After staging, make sure that the DDG HW has some time to process changes properly.
# NOTE Add additional sleep to make sure that the IOC and DDG HW process the values properly
# This value has been choosen empirically after testing with the HW. It's
# also just called once per scan and has been found to improve stability of the HW.
time.sleep(0.2)
logger.info(f"DDG {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
def _prepare_mcs_on_trigger(self, mcs: MCSCardCSAXS) -> None:
"""Prepare the MCS card for the next trigger.
This method holds the logic to ensure that the MCS card is ready to read.
It's logic is coupled to the MCS card implementation and the DDG1 trigger logic.
"""
status_ready_read = CompareStatus(mcs.ready_to_read, READYTOREAD.DONE)
mcs.stop_all.put(1)
status_acquiring = TransitionStatus(mcs.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING])
self.cancel_on_stop(status_ready_read)
self.cancel_on_stop(status_acquiring)
status_ready_read.wait(10)
mcs.ready_to_read.put(READYTOREAD.PROCESSING)
This method is used by the DDG1 on_trigger method to prepare the MCS card for the next trigger.
It checks that the MCS card is properly prepared before BEC sends a software trigger to the DDG1,
which is needed for step scans.
It relies on the MCS card implementation and needs to be adapted if the MCS card logic changes.
"""
# NOTE First we wait that the MCS card is not acquiring. We add here a timeout of 5s to avoid
# a deadlock in case the MCS card is stuck for some reason. This should not happen normally.
status = CompareStatus(mcs.acquiring, ACQUIRING.DONE)
self.cancel_on_stop(status)
status.wait(timeout=5)
# NOTE Clear the '_omit_mca_callbacks' flag. This makes sure that data received from the mca1...mca3
# counters are forwarded to BEC. Once the flag is set, we create a TransitionStatus DONE->ACQUIRING
# and start the acquisition through erase_start.put(1). Finally, we wait for the card to go to ACQUIRING state.
mcs._omit_mca_callbacks.clear() # pylint: disable=protected-access
status_acquiring = TransitionStatus(mcs.acquiring, [ACQUIRING.DONE, ACQUIRING.ACQUIRING])
self.cancel_on_stop(status_acquiring)
mcs.erase_start.put(1)
status_acquiring.wait(timeout=10) # Allow 10 seconds in case communication is slow
return status_acquiring
def _poll_event_status(self) -> None:
"""
Poll the event status register in a background thread. Control
the polling with the _poll_thread_run_event and _poll_thread_kill_event.
Polling loop to retrieve the event status register of the delay generator DDG1.
This method runs in a background thread and the polling is controlled through the
'_poll_thread_run_event' and '_poll_thread_kill_event'. Polling should only become
active when a software trigger was sent in BEC and we are waiting for the burst to complete.
"""
# Main loop of the polling thread. As long as the kill event is not set, the loop continues.
while not self._poll_thread_kill_event.is_set():
# NOTE Main wait event for the polling thread. If the _poll_thread_run_event is not set,
# The thread will wait here. This event is used to start/stop polling from outside the thread,
# as used in on_trigger and on_stop. Please make sure to set this event also when the thread
# should be killed as its otherwise stuck inside the wait.
self._poll_thread_run_event.wait()
# NOTE Set the event to indicate that we are currently still in the poll_loop. This is needed
# as we have to use sleeps of 20ms within the poll loop. These sleeps were empirically detetermined
# to ensure that no state changes are missed. However, these sleeps have the side effect that
# setting the '_poll_thread_run_event' may not immediately stop the polling. Therefore, we need the
# '_poll_thread_poll_loop_done' event to indicate that polling has finished. If this logic is changed,
# it requires careful testing as failure rates can be in the 1 out of 500 events rate, which are still
# not acceptable for operation. The current implementation has been tested with failure rates smaller then
# ~ 1:100000 if failures happened at all.
self._poll_thread_poll_loop_done.clear()
while (
self._poll_thread_run_event.is_set() and not self._poll_thread_kill_event.is_set()
@@ -198,29 +329,49 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
logger.error(
f"Exception in polling loop thread, polling continues...\n Error content:\n{content}"
)
# NOTE Set the _poll_thread_poll_loop_done event to indicate that we are done polling. Do not remove!
self._poll_thread_poll_loop_done.set()
def _poll_loop(self) -> None:
"""
Poll loop to update event status.
The checks ensure that the loop exist after each operation and be stuck in sleep.
The 20ms sleep was added to ensure that the event status is not polled too frequently,
and to give the device time to process the previous command. This was found empirically
to be necessary to avoid missing events.
IMPORTANT: Do not remove sleeps or try to optimize this logic. This seems to be a
fragile balance between polling frequency and device processing time. Also in between
start/stop of polling. Please also consider that there is a sleep in on_trigger and
that this might also be necessary to avoid that HW becomes unavailable/unstable.
This method is the actual poll loop to update the event status from the satus register
of the delay generator DDG1.
It follows a procedure that was established empirically after extended testing with the HW.
Any adaptations to this logic need to be carefully tested to avoid that the HW becomes unstable.
NOTE: Sleeps are important in this logic, and should not be removed or optimized without extensive testing.
20ms has been found to be the minimum sleep time that proofed to be stable in operation.
The logic is as follows:
- Set the 'proc_status' to 1 with use_complete=True to trigger an event based readout of the EventStatusLI.
- Sleep 20ms to give the device time to process the command.
- Check if the kill event or run event are cleared, and exit the loop if so.
- Read the EventStatusLI channel to update the event status.
- Check again if the kill event or run event are cleared, and exit the loop if so.
Please note that any important changes of the status register reading will trigger callbacks
if attached to the event status signal. These callbacks hold the logic to resolve status objects
when waiting for specific events (e.g. end of burst).
"""
self.state.proc_status.put(1, use_complete=True)
time.sleep(0.02) # 20ms delay for processing, important for not missing events
# NOTE: Important sleep that has been empirically determined after testing for a long time
# Only remove if absolutely certain that the DDG logic of polling the EventStatusLI works without it.
time.sleep(0.02)
if self._poll_thread_kill_event.is_set() or not self._poll_thread_run_event.is_set():
return
self.state.event_status.get(use_monitor=False)
if self._poll_thread_kill_event.is_set() or not self._poll_thread_run_event.is_set():
return
time.sleep(0.02) # 20ms delay for processing, important for not missing events
# NOTE: Again important sleep that has been empirically determined after testing for a long time
# Only remove if certain that logic can be replaced to not risk HW failures.
time.sleep(0.02)
def _start_polling(self) -> None:
"""Start the polling loop in the background thread."""
@@ -240,8 +391,23 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
else:
logger.info("Polling thread stopped.")
def _prepare_trigger_status_event(self, timeout: float | None = None) -> DeviceStatus:
"""Prepare the trigger status event for the DDG1, and trigger the de"""
def _prepare_trigger_status_event(
self, timeout: float | None = None
) -> StatusBitsCompareStatus:
"""
Method to prepare a status object that indicates the end of a burst cycle.
It also sets up a callback to cancel the polling of the event status register
if the status is cancelled externally (e.g. by stopping the device). In addition,
a timeout can either be specified, or is automatically calculated based on the
exposure time, frames_per_trigger and a default extra time of 5 seconds.
Args:
timeout (float | None, optional): Timeout for the status object. If None, a
default timeout based on exposure time and frames_per_trigger is used.
Returns:
StatusBitsCompareStatus:
"""
if timeout is None:
# Default timeout of 5 seconds + exposure time * frames_per_trigger
timeout = 5 + self.scan_info.msg.scan_parameters.get(
@@ -251,7 +417,9 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
# Callback to cancel the status if the device is stopped
def cancel_cb(status: CompareStatus) -> None:
"""Callback to cancel the status if the device is stopped."""
self._stop_polling()
logger.debug("DDG1 end of burst detected, stopping polling loop.")
if status.done:
self._stop_polling()
# Run false is important to ensure that the status is only checked on the next event status update
status = StatusBitsCompareStatus(
@@ -262,38 +430,63 @@ class DDG1(PSIDeviceBase, DelayGeneratorCSAXS):
return status
def on_trigger(self) -> DeviceStatus:
"""Note, we need to add a delay to the StatusBits callback on the event_status.
If we don't then subsequent triggers may reach the DDG too early, and will be ignored. To
avoid this, we've added the option to specify a delay via add_delay, default here is 50ms.
"""
# Stop polling, poll once manually to ensure that the register is clean
This method is called from BEC as a software trigger.
It follows a specific procedure to ensure that the DDG1 and MCS card are properly handled
on a trigger event. The established logic is as follows:
- Stop polling the event status register to avoid that the polling loop is still active
before sending the software trigger. This needs to be done to avoid conflicts
in reading the event status register.
- Wait for the _poll_thread_poll_loop_done event to ensure that the polling loop is no
longer active. A timeout of 1s is plenty as sleeps of 20ms are used in the poll loop.
- Add an extra sleep of 20ms to make sure that the HW is again ready to process new commands.
This has been found empirically after long testing to improve stability.
- If the MCS card is present in the current session of BEC, prepare the card for the next trigger.
- Prepare a status StatusBitsCompareStatus that will be resolved once the burst is done.
- Start the polling loop again to monitor the event status register.
- Send the software trigger to the DDG1
- Return the status object to BEC which will automatically resolve once the status register has
the END_OF_BURST bit set. The callback of the status object will also stop the polling loop.
"""
self._stop_polling()
self._poll_thread_poll_loop_done.wait(timeout=1)
# IMPORTANT: Keep this sleep setting, as it is necessary to avoid that the HW
# becomes unresponsive. This was found empirically and seems to be necessary
# NOTE: This sleep is important to ensure that the HW is ready to process new commands.
# It has been empirically determined after long testing that this improves stability.
time.sleep(0.02)
# NOTE If the MCS card is present in the current session of BEC,
# we prepare the card for the next trigger. The procedure is implemented
# in the '_prepare_mcs_on_trigger' method.
# Prepare the MCS card for the next software trigger
mcs = self.device_manager.devices.get("mcs", None)
if mcs is None:
if mcs is None or mcs.enabled is False:
logger.info("Did not find mcs card with name 'mcs' in current session")
else:
self._prepare_mcs_on_trigger(mcs)
# Prepare status with callback to cancel the polling once finished
status_mcs = self._prepare_mcs_on_trigger(mcs)
# NOTE Timeout of 3s should be plenty, any longer wait should checked. If this happens to crash
# an acquisition regularly with a WaitTimeoutError, the timeout can be increased but it should
# be investigated why the EPICS interface is slow to respond.
status_mcs.wait(timeout=3)
# Prepare StatusBitsCompareStatus to resolve once the END_OF_BURST bit was set.
status = self._prepare_trigger_status_event()
# Start polling
# Start polling thread again to monitor event status
self._start_polling()
# Trigger the DDG1
self.trigger_shot.put(1, use_complete=True)
return status
def on_stop(self) -> None:
"""Stop the delay generator by setting the burst mode to 0"""
"""Stop the delay generator HW and polling thread when the device is stopped."""
self.stop_ddg()
self._stop_polling()
def on_destroy(self) -> None:
"""Clean up resources when the device is destroyed."""
self.stop_ddg()
self._kill_poll_thread()

View File

@@ -25,7 +25,7 @@ Burst mode is enabled:
import time
from bec_lib.logger import bec_logger
from ophyd import DeviceStatus, StatusBase
from ophyd_devices import DeviceStatus, StatusBase
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
@@ -41,6 +41,11 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
logger = bec_logger.logger
########################
## DEFAULT SETTINGS ####
########################
# NOTE Default channel configuration for the DDG2 delay generator channels
_DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
"amplitude": 5.0,
"offset": 0.0,
@@ -48,6 +53,9 @@ _DEFAULT_CHANNEL_CONFIG: ChannelConfig = {
"mode": "ttl",
}
# NOTE Default IO configuration for all channels in DDG2
# Each channel uses the same default configuration as defined above
# If needed, individual channel configurations should be modified here.
DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
"t0": _DEFAULT_CHANNEL_CONFIG,
"ab": _DEFAULT_CHANNEL_CONFIG,
@@ -55,9 +63,16 @@ DEFAULT_IO_CONFIG: dict[AllChannelNames, ChannelConfig] = {
"ef": _DEFAULT_CHANNEL_CONFIG,
"gh": _DEFAULT_CHANNEL_CONFIG,
}
DEFAULT_TRIGGER_SOURCE: TRIGGERSOURCE = TRIGGERSOURCE.EXT_RISING_EDGE
# NOTE Default readout times for the detectors connected to DDG2
# These values are used to calculate the difference between the burst_period and the pulse width of
# individual channel pairs. They also mark a lower limit for the exposure time. Needs to be
# adjusted if the exposure time should possibly go below 0.2 ms.
DEFAULT_READOUT_TIMES = {"ab": 2e-4, "cd": 2e-4, "ef": 2e-4, "gh": 2e-4} # 0.2 ms 5kHz
# NOTE Default refernce settings for each channel in DDG2
DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
("A", CHANNELREFERENCE.T0),
("B", CHANNELREFERENCE.A),
@@ -69,9 +84,27 @@ DEFAULT_REFERENCES: list[tuple[LiteralChannels, CHANNELREFERENCE]] = [
("H", CHANNELREFERENCE.G),
]
###############################
## DDG2 IMPLEMENTATION ########
###############################
class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
"""
Implementation of the DelayGenerator DDG2 for the cSAXS beamline. This delay generator is
reponsible to create triggers for the detectors. It is configured in burst mode. Please
check the module docstring, the module README and the attached PDF 'trigger_scheme_ddg1_ddg2.pdf'
for more information about the expected cabling and trigger logic.
The IOC prefix is 'X12SA-CPCL-DDG2:'.
Args:
name (str): Name of the device.
prefix (str, optional): EPICS prefix for the device. Defaults to ''.
scan_info (ScanInfo | None, optional): Scan info object. Defaults to None.
device_manager (DeviceManagerBase | None, optional): Device manager. Defaults to None.
Implementation of DelayGeneratorCSAXS for the CSAXS master trigger delay generator at X12SA-CPCL-DDG2.
This device is responsible for creating triggers in burst mode and is connected to a multiplexer that
distributes the trigger to the detectors. The DDG2 is triggered by the DDG1 through the EXT/EN channel.
@@ -80,10 +113,22 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
# pylint: disable=attribute-defined-outside-init
def on_connected(self) -> None:
"""
Set the default values on the device - intended to overwrite everything to a usable default state.
Sets DEFAULT_IO_CONFIG into each channel, sets the trigger source to DEFAULT_TRIGGER_SOURCE.
This method is called after the device is initialized and all signals are connected. This happens
when a device configuration is loaded in BEC.
It sets the default values for this device - intended to overwrite everything to a usable default state.
For this purpose, we use the DEFAULT SETTINGS defined at the top of this module.
The following procedure is followed:
- Stop the DDG to ensure it is not running.
- Then, we set the DEFAULT_IO_CONFIG for each channel, the trigger source to DEFAULT_TRIGGER_SOURCE,
and the channel references to DEFAULT_REFERENCES.
"""
self.burst_disable() # it is possible to miss setting settings if burst is enabled
self.stop_ddg()
# NOTE Please adjust the default settings under 'DEFAULT SETTINGS' at the top of this module if needed.
# This makes sure that we have a well defined default state for the DDG2 device.
for channel, config in DEFAULT_IO_CONFIG.items():
self.set_io_values(channel, **config)
self.set_trigger(DEFAULT_TRIGGER_SOURCE)
@@ -91,66 +136,76 @@ class DDG2(PSIDeviceBase, DelayGeneratorCSAXS):
def on_stage(self) -> DeviceStatus | StatusBase | None:
"""
Stage logic for the DDG1 device, being th main trigger delay generator for CSAXS.
For standard scans, it will be triggered by a soft trigger from BEC.
It also has a hardware trigger feeded into the EXT/EN for fly-scanning, i.e. Galil stages.
This DDG is always not in burst mode.
This method is called when the device is staged before a scan. All information about the scan
is available through self.scan_info.msg at this point. The DDG2 needs to be configured to
create a sequence of TTL pulses in burst mode that are sent to the detectors. It therefore needs
to know the exposure time and frames per trigger from the self.scan_info.msg.scan_parameters.
This logic is robust for step scans as well as fly scans, as the DDG2 is triggered by the DDG1
through the EXT/EN channel.
"""
start_time = time.time()
########################################
### Burst mode settings ################
########################################
# NOTE Only adjust settings if needed. DDG2 should always be in burst mode when used at CSAXS.
if self.burst_mode.get() == 0:
self.burst_mode.put(1)
# Ensure that there is no delay for the burst
if self.burst_delay.get() != 0:
self.burst_delay.put(0)
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
frames_per_trigger = self.scan_info.msg.scan_parameters["frames_per_trigger"]
# a = t0
# a has reference to t0, b has reference to a
# NOTE Check if the exposure time is longer than all readout times.
# Raise a ValueError if requested exposure time is too short.
if any(exp_time <= rt for rt in DEFAULT_READOUT_TIMES.values()):
raise ValueError(
f"Exposure time {exp_time} is too short for the readout times {DEFAULT_READOUT_TIMES}"
)
#########################################
### Setup timing for burst and delays ###
#########################################
# Burst Period DDG2 settings. Only adjust them if needed.
if self.burst_count.get() != frames_per_trigger:
self.burst_count.put(frames_per_trigger)
if self.burst_period.get() != exp_time:
self.burst_period.put(exp_time)
# Calculate the pulse width for the channel pair 'ab'
burst_pulse_width = exp_time - DEFAULT_READOUT_TIMES["ab"]
# Trigger detectors with delay 0, and pulse width = exp_time - readout_time
self.set_delay_pairs(channel="ab", delay=0, width=burst_pulse_width)
self.burst_enable(count=frames_per_trigger, delay=0, period=exp_time)
logger.info(f"DDG {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
def on_pre_scan(self):
"""
The delay generator occasionally needs a bit extra time to process all
commands from stage. Therefore, we introduce here a short sleep
Method that is called just before a scan starts. It was observed that a short delay of 50ms
improves the overall stability in operation. This may be removed as other parts were adjusted,
but for now we will keep it as the delay is short.
"""
# Delay Generator occasionaly needs a bit extra time to process all commands, sleep 50ms
# NOTE Short delay to allow for the HW to process the commands before the scan starts.
# This may no longer be needed after other adjustments, and may be removed in the future.
time.sleep(0.05)
def on_trigger(self) -> DeviceStatus | StatusBase | None:
"""
DDG2 will not receive a trigger from BEC, but will be triggered by the DDG1 through the EXT/EN channel.
"""
def wait_for_status(
self, status: DeviceStatus, bit_event: STATUSBITS, timeout: float = 5
) -> None:
"""Wait for a event status bit to be set.
Args:
status (StatusBase): The status object to update.
bit_event (STATUSBITS): The event status bit to wait for.
timeout (float): Maximum time to wait for the event status bit to be set.
DDG2 does not implement any trigger specific logic as it is triggered by DDG1 through the EXT/EN channel.
"""
current_time = time.time()
while not status.done:
self.state.proc_status.put(1, use_complete=True)
event_status = self.state.event_status.get()
if (STATUSBITS(event_status) & bit_event) == bit_event:
status.set_finished()
if time.time() - current_time > timeout:
status.set_exception(
TimeoutError(
f"Timeout waiting for status of device {self.name} for event_status {bit_event}"
)
)
break
time.sleep(0.1)
time.sleep(0.05) # Give time for the IOC to be ready again
return status
pass
def on_stop(self) -> None:
"""Stop the delay generator by setting the burst mode to 0"""
"""Stop the delay generator"""
self.stop_ddg()

View File

@@ -3,6 +3,11 @@ Delay generator implementation for CSAXS.
Detailed information can be found in the manual:
https://www.thinksrs.com/downloads/pdfs/manuals/DG645m.pdf
On the beamline consoles, the caqtdm panel can be started via:
caqtdm -noMsg -attach -macro P=X12SA-CPCL-DDG,R=1: srsDG645.ui
R=1,2,3 for 3 different DDG units installed at CSAXS.
"""
import enum
@@ -151,8 +156,9 @@ class StatusBitsCompareStatus(SubscriptionStatus):
run=run,
)
def _compare_callback(self, value, **kwargs) -> bool:
def _compare_callback(self, *args, value, **kwargs) -> bool:
"""Callback for subscription status"""
logger.debug(f"StatusBitsCompareStatus: Received value {value}")
obj = kwargs.get("obj", None)
if obj is None:
name = "no object received"
@@ -167,7 +173,9 @@ class StatusBitsCompareStatus(SubscriptionStatus):
return False
if self._add_delay != 0:
time.sleep(self._add_delay)
logger.debug(
f"Returning comparison for {name}: {(STATUSBITS(value) & self._value) == self._value}"
)
return (STATUSBITS(value) & self._value) == self._value
@@ -533,6 +541,7 @@ class DelayGeneratorCSAXS(Device):
write_pv="BurstDelayAO",
name="burst_delay",
kind=Kind.omitted,
auto_monitor=True,
doc="Delay before bursts start in seconds. Must be >=0.",
)
burst_period = Cpt(

View File

@@ -1,15 +1,17 @@
"""Falcon Sitoro detector class for cSAXS beamline."""
import enum
import os
import threading
from typing import Literal
from bec_lib.file_utils import get_full_path
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV
from ophyd.mca import EpicsMCARecord
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
from ophyd_devices import CompareStatus, FileEventSignal
from ophyd_devices.devices.areadetector.plugins import HDF5Plugin_V35 as HDF5Plugin
from ophyd_devices.devices.dxp import EpicsDXPFalcon, EpicsMCARecord, Falcon
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
logger = bec_logger.logger
@@ -18,15 +20,11 @@ class FalconError(Exception):
"""Base class for exceptions in this module."""
class FalconTimeoutError(FalconError):
"""Raised when the Falcon does not respond in time."""
class DetectorState(enum.IntEnum):
class ACQUIRESTATUS(enum.IntEnum):
"""Detector states for Falcon detector"""
DONE = 0
ACQUIRING = 1
ACQUIRING = 1 # or Capturing
class TriggerSource(enum.IntEnum):
@@ -44,238 +42,56 @@ class MappingSource(enum.IntEnum):
MAPPING = 1
class EpicsDXPFalcon(Device):
"""
DXP parameters for Falcon detector
class FalconControl(Falcon):
"""Falcon Control class at cSAXS. prefix: 'X12SA-SITORO:'"""
Base class to map EPICS PVs from DXP parameters to ophyd signals.
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
mca = Cpt(EpicsMCARecord, "mca1")
hdf5 = Cpt(HDF5Plugin, "HDF1:")
class FalconcSAXS(PSIDeviceBase, FalconControl):
"""
Falcon Sitoro detector for CSAXS
class attributes:
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
mca (EpicsMCARecord) : MCA parameters for Falcon detector
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
MIN_READOUT (float) : Minimum readout time for the detector
"""
elapsed_live_time = Cpt(EpicsSignal, "ElapsedLiveTime")
elapsed_real_time = Cpt(EpicsSignal, "ElapsedRealTime")
elapsed_trigger_live_time = Cpt(EpicsSignal, "ElapsedTriggerLiveTime")
# specify minimum readout time for detector
MIN_READOUT = 3e-3
_pv_timeout = 3 # Timeout for PV operations in seconds
# Energy Filter PVs
energy_threshold = Cpt(EpicsSignalWithRBV, "DetectionThreshold")
min_pulse_separation = Cpt(EpicsSignalWithRBV, "MinPulsePairSeparation")
detection_filter = Cpt(EpicsSignalWithRBV, "DetectionFilter", string=True)
scale_factor = Cpt(EpicsSignalWithRBV, "ScaleFactor")
risetime_optimisation = Cpt(EpicsSignalWithRBV, "RisetimeOptimization")
# Misc PVs
detector_polarity = Cpt(EpicsSignalWithRBV, "DetectorPolarity")
decay_time = Cpt(EpicsSignalWithRBV, "DecayTime")
current_pixel = Cpt(EpicsSignalRO, "CurrentPixel")
class FalconHDF5Plugins(Device):
"""
HDF5 parameters for Falcon detector
Base class to map EPICS PVs from HDF5 Plugin to ophyd signals.
"""
capture = Cpt(EpicsSignalWithRBV, "Capture")
enable = Cpt(EpicsSignalWithRBV, "EnableCallbacks", string=True, kind="config")
xml_file_name = Cpt(EpicsSignalWithRBV, "XMLFileName", string=True, kind="config")
lazy_open = Cpt(EpicsSignalWithRBV, "LazyOpen", string=True, doc="0='No' 1='Yes'")
temp_suffix = Cpt(EpicsSignalWithRBV, "TempSuffix", string=True)
file_path = Cpt(EpicsSignalWithRBV, "FilePath", string=True, kind="config")
file_name = Cpt(EpicsSignalWithRBV, "FileName", string=True, kind="config")
file_template = Cpt(EpicsSignalWithRBV, "FileTemplate", string=True, kind="config")
num_capture = Cpt(EpicsSignalWithRBV, "NumCapture", kind="config")
file_write_mode = Cpt(EpicsSignalWithRBV, "FileWriteMode", kind="config")
queue_size = Cpt(EpicsSignalWithRBV, "QueueSize", kind="config")
array_counter = Cpt(EpicsSignalWithRBV, "ArrayCounter", kind="config")
class FalconSetup(CustomDetectorMixin):
"""
Falcon setup class for cSAXS
Parent class: CustomDetectorMixin
"""
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
super().__init__(*args, parent=parent, **kwargs)
self._lock = threading.RLock()
file_event = Cpt(FileEventSignal, name="file_event")
def on_init(self) -> None:
"""Initialize Falcon detector"""
self.initialize_default_parameter()
self.initialize_detector()
self.initialize_detector_backend()
"""Initialize Falcon Sitoro detector"""
self._lock = threading.RLock()
self._readout_time = self.MIN_READOUT
self._value_pixel_per_buffer = 20
self._queue_size = 2000
self._full_path = ""
def initialize_default_parameter(self) -> None:
def on_connected(self):
"""
Set default parameters for Falcon
This will set:
- readout (float): readout time in seconds
- value_pixel_per_buffer (int): number of spectra in buffer of Falcon Sitoro
Setup Falcon Sitoro detector default parameters once signals are connected
"""
self.parent.value_pixel_per_buffer = 20
self.update_readout_time()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector(self) -> None:
"""Initialize Falcon detector"""
self.stop_detector()
self.stop_detector_backend()
self.on_stop()
self._initialize_detector()
self._initialize_detector_backend()
self.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
# 1 Realtime
self.parent.preset_mode.put(1)
# 0 Normal, 1 Inverted
self.parent.input_logic_polarity.put(0)
# 0 Manual 1 Auto
self.parent.auto_pixels_per_buffer.put(0)
# Sets the number of pixels/spectra in the buffer
self.parent.pixels_per_buffer.put(self.parent.value_pixel_per_buffer)
def initialize_detector_backend(self) -> None:
"""Initialize the detector backend for Falcon."""
self.parent.hdf5.enable.put(1)
# file location of h5 layout for cSAXS
self.parent.hdf5.xml_file_name.put("layout.xml")
# TODO Check if lazy open is needed and wanted!
self.parent.hdf5.lazy_open.put(1)
self.parent.hdf5.temp_suffix.put("")
# size of queue for number of spectra allowed in the buffer, if too small at high throughput, data is lost
self.parent.hdf5.queue_size.put(2000)
# Segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
self.parent.nd_array_mode.put(1)
def on_stage(self) -> None:
"""Prepare detector and backend for acquisition"""
self.prepare_detector()
self.prepare_data_backend()
self.publish_file_location(done=False, successful=False)
self.arm_acquisition()
def prepare_detector(self) -> None:
"""Prepare detector for acquisition"""
self.set_trigger(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
self.parent.preset_real.put(self.parent.scaninfo.exp_time)
self.parent.pixels_per_run.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
def prepare_data_backend(self) -> None:
"""Prepare data backend for acquisition"""
self.parent.filepath.set(
self.parent.filewriter.compile_full_filename(f"{self.parent.name}.h5")
).wait()
file_path, file_name = os.path.split(self.parent.filepath.get())
self.parent.hdf5.file_path.put(file_path)
self.parent.hdf5.file_name.put(file_name)
self.parent.hdf5.file_template.put("%s%s")
self.parent.hdf5.num_capture.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
self.parent.hdf5.file_write_mode.put(2)
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
self.parent.hdf5.array_counter.put(0)
# Start file writing
self.parent.hdf5.capture.put(1)
def arm_acquisition(self) -> None:
"""Arm detector for acquisition"""
self.parent.start_all.put(1)
signal_conditions = [
(
lambda: self.parent.state.read()[self.parent.state.name]["value"],
DetectorState.ACQUIRING,
)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.TIMEOUT_FOR_SIGNALS,
check_stopped=True,
all_signals=False,
):
raise FalconTimeoutError(
f"Failed to arm the acquisition. Detector state {signal_conditions[0][0]}"
)
def on_unstage(self) -> None:
"""Unstage detector and backend"""
pass
def on_complete(self) -> None:
"""Complete detector and backend"""
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
self.publish_file_location(done=True, successful=True)
def on_stop(self) -> None:
"""Stop detector and backend"""
self.stop_detector()
self.stop_detector_backend()
def stop_detector(self) -> None:
"""Stops detector"""
self.parent.stop_all.put(1)
self.parent.erase_all.put(1)
signal_conditions = [
(lambda: self.parent.state.read()[self.parent.state.name]["value"], DetectorState.DONE)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=self.parent.TIMEOUT_FOR_SIGNALS - self.parent.TIMEOUT_FOR_SIGNALS // 2,
all_signals=False,
):
# Retry stop detector and wait for remaining time
raise FalconTimeoutError(
f"Failed to stop detector, timeout with state {signal_conditions[0][0]}"
)
def stop_detector_backend(self) -> None:
"""Stop the detector backend"""
self.parent.hdf5.capture.put(0)
def finished(self, timeout: int = 5) -> None:
"""Check if scan finished succesfully"""
with self._lock:
total_frames = int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
)
signal_conditions = [
(self.parent.dxp.current_pixel.get, total_frames),
(self.parent.hdf5.array_counter.get, total_frames),
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=timeout,
check_stopped=True,
all_signals=True,
):
logger.debug(
f"Falcon missed a trigger: received trigger {self.parent.dxp.current_pixel.get()},"
f" send data {self.parent.hdf5.array_counter.get()} from total_frames"
f" {total_frames}"
)
self.stop_detector()
self.stop_detector_backend()
def set_trigger(
self, mapping_mode: MappingSource, trigger_source: TriggerSource, ignore_gate: int = 0
self,
mapping_mode: MappingSource,
trigger_source: TriggerSource,
ignore_gate: Literal[0, 1] = 0,
) -> None:
"""
Set triggering mode for detector
@@ -287,63 +103,140 @@ class FalconSetup(CustomDetectorMixin):
"""
mapping = int(mapping_mode)
trigger = trigger_source
self.parent.collect_mode.put(mapping)
self.parent.pixel_advance_mode.put(trigger)
self.parent.ignore_gate.put(ignore_gate)
trigger = int(trigger_source)
self.collect_mode.put(mapping)
self.pixel_advance_mode.put(trigger)
self.ignore_gate.put(ignore_gate)
def _initialize_detector(self) -> None:
"""Initialize Falcon detector"""
class FalconcSAXS(PSIDetectorBase):
"""
Falcon Sitoro detector for CSAXS
# 1 Realtime
self.preset_mode.put(1)
Parent class: PSIDetectorBase
# 0 Normal, 1 Inverted
self.input_logic_polarity.put(0)
class attributes:
custom_prepare_cls (FalconSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
PSIDetectorBase.set_min_readout (float) : Minimum readout time for the detector
dxp (EpicsDXPFalcon) : DXP parameters for Falcon detector
mca (EpicsMCARecord) : MCA parameters for Falcon detector
hdf5 (FalconHDF5Plugins) : HDF5 parameters for Falcon detector
MIN_READOUT (float) : Minimum readout time for the detector
"""
# 0 Manual 1 Auto
self.auto_pixels_per_buffer.put(0)
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = ["describe"]
# Sets the number of pixels/spectra in the buffer
self.pixels_per_buffer.put(self._value_pixel_per_buffer)
# specify Setup class
custom_prepare_cls = FalconSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
TIMEOUT_FOR_SIGNALS = 5
def _initialize_detector_backend(self) -> None:
"""Initialize the detector backend for Falcon."""
# Enable HDF5 plugin
self.hdf5.enable.put(1)
# specify class attributes
dxp = Cpt(EpicsDXPFalcon, "dxp1:")
mca = Cpt(EpicsMCARecord, "mca1")
hdf5 = Cpt(FalconHDF5Plugins, "HDF1:")
# Use layout.xml file for cSAXS Falcon. FIXME:Should be checked if IOC runs on different host.
self.hdf5.xml_file_name.put("layout.xml")
stop_all = Cpt(EpicsSignal, "StopAll")
erase_all = Cpt(EpicsSignal, "EraseAll")
start_all = Cpt(EpicsSignal, "StartAll")
state = Cpt(EpicsSignal, "Acquiring")
preset_mode = Cpt(EpicsSignal, "PresetMode") # 0 No preset 1 Real time 2 Events 3 Triggers
preset_real = Cpt(EpicsSignal, "PresetReal")
preset_events = Cpt(EpicsSignal, "PresetEvents")
preset_triggers = Cpt(EpicsSignal, "PresetTriggers")
triggers = Cpt(EpicsSignalRO, "MaxTriggers", lazy=True)
events = Cpt(EpicsSignalRO, "MaxEvents", lazy=True)
input_count_rate = Cpt(EpicsSignalRO, "MaxInputCountRate", lazy=True)
output_count_rate = Cpt(EpicsSignalRO, "MaxOutputCountRate", lazy=True)
collect_mode = Cpt(EpicsSignal, "CollectMode") # 0 MCA spectra, 1 MCA mapping
pixel_advance_mode = Cpt(EpicsSignal, "PixelAdvanceMode")
ignore_gate = Cpt(EpicsSignal, "IgnoreGate")
input_logic_polarity = Cpt(EpicsSignal, "InputLogicPolarity")
auto_pixels_per_buffer = Cpt(EpicsSignal, "AutoPixelsPerBuffer")
pixels_per_buffer = Cpt(EpicsSignal, "PixelsPerBuffer")
pixels_per_run = Cpt(EpicsSignal, "PixelsPerRun")
nd_array_mode = Cpt(EpicsSignal, "NDArrayMode")
# TODO Check if lazy open is needed and wanted!
self.hdf5.lazy_open.put(1)
self.hdf5.temp_suffix.put("")
# Size of the queue for the number of spectra allowed in the buffer. If too small, data is lost at high throughput
self.hdf5.queue_size.put(self._queue_size)
self.hdf5.file_template.put("%s%s")
self.hdf5.file_write_mode.put(2)
# Set nd_array mode to 1: This means segmentation into Spectra within EPICS, 1 is activate, 0 is deactivate
self.nd_array_mode.put(1)
def on_stage(self):
"""
This method is called when the detector is staged for acquisition.
We use the information in scan_info.msg about the upcoming scan to set all relevant parameters on the detector.
"""
# Calculate relevant parameters
num_points = self.scan_info.msg.num_points
frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
overall_frames = int(num_points * frames_per_trigger)
exp_time = self.scan_info.msg.scan_parameters["exp_time"]
self._full_path = get_full_path(self.scan_info.msg, self.name)
# Check that exposure time is larger than readout time
readout_time = max(
self.scan_info.msg.scan_parameters.get("readout_time", self.MIN_READOUT),
self.MIN_READOUT,
)
if exp_time < readout_time:
raise ValueError(
f"Exposure time {exp_time} is less than minimum readout time {readout_time}"
)
# TODO: Add h5_entries for linking the Falcon NEXUS entries with the master file
self.file_event.put(file_path=self._full_path, done=False, successful=False)
self.preset_real_time.put(exp_time)
self.pixels_per_run.put(overall_frames)
# Prepare detector backend PVs
file_path, file_name = os.path.split(self._full_path)
self.hdf5.file_path.put(file_path)
self.hdf5.file_name.put(file_name)
self.hdf5.num_capture.put(overall_frames)
# Reset spectrum counter in filewriter, used for indexing & identifying missing triggers
self.hdf5.array_counter.put(0)
# Start file writing
self.hdf5.capture.put(1)
# Start the acquisition
self.start_all.put(1)
def on_pre_scan(self):
"""
Method for actions just before the scan starts.
"""
status_camera = CompareStatus(
self.acquire_busy, ACQUIRESTATUS.ACQUIRING, timeout=self._pv_timeout
)
status_writer = CompareStatus(
self.hdf5.capture, ACQUIRESTATUS.ACQUIRING, timeout=self._pv_timeout
)
# Logical combine of statuses
status = status_camera & status_writer
self.cancel_on_stop(status)
return status
def _complete_callback(self, status: CompareStatus) -> None:
"""Callback for when the device completes a scan."""
# FIXME Add proper h5 entries once checked
if status.success:
self.file_event.put(
file_path=self._full_path, # pylint: disable:protected-access
done=True,
successful=True,
)
else:
self.file_event.put(
file_path=self._full_path, # pylint: disable:protected-access
done=True,
successful=False,
)
def on_complete(self) -> None:
"""Complete detector and backend"""
# Calculate relevant parameters
num_points = self.scan_info.msg.num_points
frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
overall_frames = int(num_points * frames_per_trigger)
status_detector = CompareStatus(self.dxp.current_pixel, overall_frames, run=True)
status_backend = CompareStatus(self.hdf5.array_counter, overall_frames, run=True)
status = status_detector & status_backend
self.cancel_on_stop(status)
status.add_callback(self._complete_callback)
return status
def on_stop(self) -> None:
"""Stop detector and backend"""
self.stop_all.put(1)
self.hdf5.capture.put(0)
self.erase_all.put(1)
if __name__ == "__main__":
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:", sim_mode=True)
falcon = FalconcSAXS(name="falcon", prefix="X12SA-SITORO:")

View File

@@ -0,0 +1,13 @@
# MCS Card implementation at the CSAXS beamline
This module provides an ophyd device implementation for the SIS3820 Multi-Channel Scaler (MCS) card, used at the cSAXS beamline for time-resolved data acquisition. It interfaces with the EPICS IOC for the SIS3820 MCS card.
Information about the EPICS driver can be found here (https://millenia.cars.aps.anl.gov/software/epics/mcaStruck.html).
# Important Notes
Operation of the MCS card requires proper configuration as some of the parameters are interdependent. In addition, empirical adjustments have been found to be necessary for optimal performance at the beamline. In its current implementation, comments about these dependencies are highlighted in the source code of the ophyd device classes [MCSCard](./mcs_card.py) and [MCSCardCSAXS](./mcs_card_csaxs.py). It is highly recommended to review these comments before refactoring, modifying, or extending the code.
## Ophyd Device Implementation
The ophyd device implementation is provided [MCSCard](./mcs_card.py). This class provides a basic interface to the MCS PVs, including configuration of parameters such as number of channels, dwell time, and control of acquisition start/stop. Please check the source code of the class for more details of the implementation.
The [MCSCardCSAXS](./mcs_card_csaxs.py) class extends the basic MCSCard implementation with cSAXS-specific logic and configurations. Please be aware that this is also linked to the implementation of other devices, most notably the [delay generator integration](../delay_generator_csaxs/README.md), which is used as the trigger source for the MCS card during operation.

View File

@@ -170,11 +170,12 @@ class MCSCard(Device):
kind=Kind.omitted,
doc="Indicates whether the SNL program has connected to all PVs.",
)
# NOTE: Please note that the erase_all command sends the mca or waveform records to process after erasing, potentially also values of 0. This logic needs to be considered when running callbacks on the mca channels.
erase_all = Cpt(
EpicsSignal,
"EraseAll",
kind=Kind.omitted,
doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0.",
doc="Erases all mca or waveform records, setting elapsed times and counts in all channels to 0. Please note that this operation sends the mca or waveform records to process after erasing, potentially also 0s.",
)
erase_start = Cpt(
EpicsSignal,
@@ -192,6 +193,7 @@ class MCSCard(Device):
EpicsSignalRO,
"Acquiring",
kind=Kind.omitted,
auto_monitor=True,
doc="Acquiring (=1) when acquisition is in progress and Done (=0) when acquisition is complete.",
)
stop_all = Cpt(EpicsSignal, "StopAll", kind=Kind.omitted, doc="Stops acquisition.")
@@ -279,11 +281,12 @@ class MCSCard(Device):
kind=Kind.omitted,
doc="The current acquisition mode (MCS=0 or Scaler=1). This record is used to turn off the scaler record Autocount in MCS mode.",
)
# NOTE: Setting mux_output programmatically results in occasional errors on the IOC; it is recommended to avoid using it.
mux_output = Cpt(
EpicsSignal,
"MUXOutput",
kind=Kind.omitted,
doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3.",
doc="Value of 0-32 used to select which input signal is routed to output signal 7 on the SIS3820 in output mode 3. NOTE: This settings seems to occasionally result in errors on the IOC; it is recommended to avoid using it.",
)
user_led = Cpt(
EpicsSignal,

View File

@@ -1,16 +1,28 @@
"""Module for the MCSCard CSAXS implementation."""
"""
Module for the MCSCard CSAXS implementation at cSAXS.
Please respect the comments regarding timing and procedures of the MCS card. These
are highlighted with NOTE comments directly in the code, indicating requirements
for stable device operation. Most of these constraints were identified
empirically through extensive testing with the SIS3820 MCS card IOC and are intended
to prevent unexpected hardware or IOC behavior.
"""
from __future__ import annotations
import enum
import threading
import time
import traceback
from contextlib import contextmanager
from functools import partial
from threading import RLock
from typing import TYPE_CHECKING
from typing import TYPE_CHECKING, Callable, Literal
import numpy as np
from bec_lib.logger import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, EpicsSignalRO, Kind, Signal
from ophyd_devices import CompareStatus, ProgressSignal, TransitionStatus
from ophyd import EpicsSignalRO, Kind
from ophyd_devices import AsyncMultiSignal, CompareStatus, ProgressSignal, StatusBase
from ophyd_devices.interfaces.base_classes.psi_device_base import PSIDeviceBase
from csaxs_bec.devices.epics.mcs_card.mcs_card import (
@@ -24,7 +36,37 @@ from csaxs_bec.devices.epics.mcs_card.mcs_card import (
READMODE,
MCSCard,
)
from csaxs_bec.devices.epics.xbpms import DiffXYSignal, SumSignal
@contextmanager
def suppress_mca_callbacks(mcs_card: MCSCard, restore_after_timeout: None | float = None):
"""
Utility context manager to suppress MCA channel callbacks temporarily.
It is required because erasing all channels via 'erase_all' PV triggers
callbacks for each channel. Depending on timing, this can interfere with
ongoing data acquisition so this context manager can be used to suppress
those callbacks temporarily. If used with restore_after_timeout, the suppression
will be automatically cleared after the specified timeout in seconds.
NOTE: Please be aware that it does not restore previous state, which means
that _omit_mca_callbacks will remain set after exiting the context. It has
to be cleared manually if needed. This can be improved in the future, but
should be carefully coordinated with the logic implemented within '_on_counter_update'.
Args:
mcs_card (MCSCard): The MCSCard instance to suppress callbacks for.
restore_after_timeout (float | None): Optional timeout in seconds to automatically
clear the suppression after the specified time. If None, the original state
is not restored.
"""
mcs_card._omit_mca_callbacks.set() # pylint: disable=protected-access
try:
yield
finally:
if restore_after_timeout is not None:
time.sleep(restore_after_timeout)
mcs_card._omit_mca_callbacks.clear() # pylint: disable=protected-access
if TYPE_CHECKING: # pragma: no cover
from bec_lib.devicemanager import DeviceManagerBase, ScanInfo
@@ -32,76 +74,50 @@ if TYPE_CHECKING: # pragma: no cover
logger = bec_logger.logger
class READYTOREAD(int, enum.Enum):
PROCESSING = 0
DONE = 1
class BPMDevice(Device):
"""Class for BPM device of the MCSCard."""
current1 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 1")
current2 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 2")
current3 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 3")
current4 = Cpt(Signal, kind=Kind.normal, doc="Normalized current 4")
count_time = Cpt(Signal, kind=Kind.normal, doc="Count time for bpm signal counts")
sum = Cpt(SumSignal, kind="hinted", doc="Sum of all currents")
x = Cpt(
DiffXYSignal,
sum1=["current1", "current2"],
sum2=["current3", "current4"],
doc="X difference signal",
)
y = Cpt(
DiffXYSignal,
sum1=["current1", "current3"],
sum2=["current2", "current4"],
doc="Y difference signal",
)
diag = Cpt(
DiffXYSignal,
sum1=["current1", "current4"],
sum2=["current2", "current3"],
doc="Diagonal difference signal",
)
class MCSRaw(Device):
"""Class for BPM device of the MCSCard with normalized currents."""
mca1 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca1 channel")
mca2 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca2 channel")
mca3 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca3 channel")
mca4 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca4 channel")
mca5 = Cpt(Signal, kind=Kind.normal, doc="Raw counts on mca5 channel")
class MCSCardCSAXS(PSIDeviceBase, MCSCard):
"""
Implementation of the MCSCard SIS3820 for CSAXS, prefix 'X12SA-MCS:'.
The basic functionality is inherited from the MCSCard class.
Please note that the number of channels is fixed to 32, so there will be data for all
32 channels. In addition, the logic of the card is linked to the timing system (DDG)
and therefore changes have to be coordinated with the logic on the DDG side.
Args:
name (str): Name of the device.
prefix (str, optional): Prefix for the EPICS PVs. Defaults to "".
"""
ready_to_read = Cpt(
Signal,
kind=Kind.omitted,
doc="Signal that indicates if mcs card is ready to be read from after triggers. 0 not ready, 1 ready",
)
progress: ProgressSignal = Cpt(ProgressSignal, name="progress")
# Make this an async signal..
mcs = Cpt(
MCSRaw,
name="mcs",
USER_ACCESS = ["mcs_recovery"]
# NOTE The number of MCA channels is fixed to 32 for the CSAXS MCS card.
# On the IOC, we receive a 'warning' or 'error' once we set this channel for the
# envisioned input/output mode settings of the card. However, we need to know the
# channels set as callback timing relies on the channels to be set.
# For the future, we may consider adding an initialization parameter to set
# the number of channels, which in return limits the number of subscriptions
# on the channels. However, mux_output should still be set to 32 on the IOC side.
# If this limits performance, this should be investigated with Controls engineers and
# the IOC.
NUM_MCA_CHANNELS: int = 32
# MCA counters for the card. Channels 1-32 will be sent to BEC.
mca = Cpt(
AsyncMultiSignal,
name="counters",
signals=[
f"mca{i}" for i in range(1, 33)
], # NOTE Channels 1-32, they need to be in sync with the 'counters' component (DynamicDeviceComponent) of the MCSCard
ndim=1,
async_update={"type": "add", "max_shape": [None]},
max_size=1000,
kind=Kind.normal,
doc="MCS device with raw current and count time readings",
)
bpm = Cpt(
BPMDevice,
name="bpm",
kind=Kind.normal,
doc="BPM device for MCSCard with count times and normalized currents",
doc=(
"AsyncMultiSignal for MCA card channels 1-32."
"Cabling of the MCS card determines which channel corresponds to which input."
),
)
progress = Cpt(ProgressSignal, doc="ProgressSignal indicating the progress of the device")
def __init__(
self,
@@ -111,39 +127,77 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
device_manager: DeviceManagerBase | None = None,
**kwargs,
):
"""
Initialize the MCSCardCSAXS with the given arguments and keyword arguments.
"""
super().__init__(
name=name, prefix=prefix, scan_info=scan_info, device_manager=device_manager, **kwargs
)
# NOTE MCS Clock frequency. This is linked to the settings of the SIS3820 IOC and
# cabeling of the card. Currently, the 'output_mode' is set to MODE_2 and one of the outputs
# 6 or 7 (both 10MHz clocks) is used on channel 5 input for the timing signal of the IOC.
# Please adjust this comment if the cabling or IOC settings change.
self._mcs_clock = 1e7 # 10MHz clock -> 1e7 Hz
self._pv_timeout = 3 # TODO remove timeout once #129 in ophyd_devices is solved
self._rlock = RLock() # Needed to ensure thread safety for counter updates
self.counter_mapping = { # Any mca counter that should be updated has to be added here
f"{self.counters.name}_mca1": "current1",
f"{self.counters.name}_mca2": "current2",
f"{self.counters.name}_mca3": "current3",
f"{self.counters.name}_mca4": "current4",
f"{self.counters.name}_mca5": "count_time",
}
self.counter_updated = []
self._pv_timeout = 2.0 # seconds
self._rlock = RLock()
# NOTE This parameter will be sent with async data of the mcs counters.
# Based on scan-paramters, e.g. frames_per_trigger, this will be either
# 'monitored' or 'burst_group'. This means whether data from this channel
# is in sync with monitored devices or another group. In this scenario,
# the other group is called burst_group. Other detectors connected and
# triggered through the same timing system should implement the same logic
# to allow data to be properly grouped afterwards.
self._acquisition_group: str = "monitored" # default value, will be updated in on_stage
self._num_total_triggers: int = 0
# Thread and event logic for monitoring async data emission after scan is done
# These are mostly internal variables for which values should not be changed externally.
# Adjusting the logic of them should also be handled with care and proper testing.
self._scan_done_thread_kill_event: threading.Event = threading.Event()
self._start_monitor_async_data_emission: threading.Event = threading.Event()
self._scan_done_callbacks: list[Callable[[], None]] = []
self._scan_done_thread: threading.Thread = threading.Thread(
target=self._monitor_async_data_emission, daemon=True
)
self._current_data_index: int = 0
self._mca_counter_index: int = 0
self._current_data: dict[str, dict[Literal["value", "timestamp"], list[int] | float]] = {}
self._omit_mca_callbacks: threading.Event = threading.Event()
def on_connected(self):
"""
Called when the device is connected.
This method is called once the device and all its PVs are connected. Any initial
setup of PVs should be managed here. Please be aware that settings of the MCS card
correlate with its operation mode, input/output modes, and timing. Changing single
parameters without understanding the overall logic may lead to unexpected behavior
of the device.Therefore, any modification of these parameters should be handled
with care and tested.
A brief summary of the procesdure that is implemented here:
- Stop any ongoing acquisiton.
- Setup the Initial initial settings of the MCS card with respective operation modes
- Run 'mcs_recovery' procedure to ensure that no pending acquisition data is scheduled
to be pushed through mcs channels
- Subscribe a callback '_on_counter_update' to mcs counter PVs to forward
data through AsyncMultiSignal to BEC
- Start the monitoring thread for async data emission after scan is done
"""
# Make sure card is not running
# NOTE Stop any ongoing acquisition first. This shut be done before setting any PVs.
self.stop_all.put(1)
# TODO Check channel1_source !!
#########################
### Setup MCS Card ###
#########################
# Setup the MCS card settings. Please note that any runtime modification
# these parameter may lead to unexpected behavior of the device.
# Therefore this has to be set up correctly.
self.channel_advance.set(CHANNELADVANCE.EXTERNAL).wait(timeout=self._pv_timeout)
self.channel1_source.set(CHANNEL1SOURCE.EXTERNAL).wait(timeout=self._pv_timeout)
self.prescale.set(1).wait(timeout=self._pv_timeout)
# Set the user LED to off
self.user_led.set(0).wait(timeout=self._pv_timeout)
# Only channel 1-5 are connected so far, adjust if more are needed
self.mux_output.set(5).wait(timeout=self._pv_timeout)
# NOTE The number of output channels has to be set to NUM_MCA_CHANNELS.
# The logic to send data to BEC relies on knowing how many channels are active.
self.mux_output.put(self.NUM_MCA_CHANNELS)
# Set the input and output modes & polarities
self.input_mode.set(INPUTMODE.MODE_3).wait(timeout=self._pv_timeout)
self.input_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout)
@@ -151,134 +205,334 @@ class MCSCardCSAXS(PSIDeviceBase, MCSCard):
self.output_polarity.set(POLARITY.NORMAL).wait(timeout=self._pv_timeout)
self.count_on_start.set(0).wait(timeout=self._pv_timeout)
# Set appropriate read mode
# NOTE Data is read out when the MCS card finishes an acquisition. The logic for this
# is also linked to triggering on the DDG.
# Set ReadMode to PASSIVE, the card will wait either wait for readout command or
# automatically readout once acquisition is done.
self.read_mode.set(READMODE.PASSIVE).wait(timeout=self._pv_timeout)
# Set the acquire mode
self.acquire_mode.set(ACQUIREMODE.MCS).wait(timeout=self._pv_timeout)
# Subscribe the progress signal
# self.current_channel.subscribe(self._progress_update, run=False)
self.current_channel.subscribe(self._progress_update, run=False)
# Subscribe to the mca updates
for name in self.counter_mapping.keys():
sig: EpicsSignalRO = getattr(self.counters, name.split("_")[-1])
sig.subscribe(self._on_counter_update, run=False)
# NOTE: Run a recovery procedure to ensure that the card has no pending data
# that needs to be pushed through the mca channels. The procedure involves
# stopping any ongoing acquisition and erasing all data on the card. Including
# a short sleep to allow the IOC to process the commands.
self.mcs_recovery(timeout=1)
def _on_counter_update(self, value, **kwargs) -> None:
####################################
### Setup MCS Subscriptions ###
####################################
for sig in self.counters.component_names:
sig_obj: EpicsSignalRO = getattr(self.counters, sig)
sig_obj.subscribe(self._on_counter_update, run=False)
# Start monitoring thread
self._scan_done_thread.start()
def _on_counter_update(self, value: float | np.ndarray, **kwargs) -> None:
"""
Callback for counter updates of the mca channels (1-32).
Callback for counter updates of the mca channels (1-32). This callback is attached
to each mca channel PV on the MCS card. It collects data from all channels
and once all channels have been updated for a given acquisition, it pushes
the data to BEC through the AsyncMultiSignal 'mca'.
The raw data is pushed to the mcs sub-device (MCSRaw). We need to ensure that
the MCSRaw device has all signals defined for which we want to push the values.
It is important that mux_output is set to the correct number of channels in on_connected,
because the callback here waits for updates on all channels before pushing data to BEC.
As we may receive multiple readings per point, e.g. if frames_per_trigger > 1,
we also create a mean value for the counter signals. These are then pushed to the bpm device
for plotting and further processing. The signal names are defined and mapped in the
self.counter_mapping dictionary & the bpm sub-device.
The _rlock is used to ensure thread safety as multiple callbacks may be executed
simultaneously from different threads.
There are multiple mca channels, each giving individual updates. We want to ensure that
each is updated before we signal that we are ready to read. In future, these signals may
become asynchronous, but we first need to ensure that we can properly combine monitored
signals with async signals for plotting. Until then, we will keep this logic.
If _omit_mca_callbacks is set, the callback will return immediately without processing the
data. This is used when erasing all channels to avoid interference with ongoing acquisition.
It has to manually cleared after the context manager 'suppress_mca_callbacks' is used.
Args:
value: The new value from the counter PV.
**kwargs: Additional keyword arguments from the subscription, including 'obj' (the EpicsSignalRO instance).
"""
with self._rlock:
# Retrieve the signal object which executes this callback
signal = kwargs.get("obj", None)
if signal is None: # This should never happen, but just in case
logger.info(f"Called without 'obj' in kwargs: {kwargs}")
if self._omit_mca_callbacks.is_set():
return # Suppress callbacks when erasing all channels
self._mca_counter_index += 1
signal: EpicsSignalRO | None = kwargs.get("obj", None)
if signal is None:
logger.error(f"Called without 'obj' in kwargs: {kwargs}")
return
# Get the maped signal name from the mapping dictionary
mapped_signal_name = self.counter_mapping.get(signal.name, None)
# If we did not map the signal name in counter_mapping, but receive an update
# we will skip it.
if mapped_signal_name is None:
# NOTE: This relies on the naming convention of the mca channels being 'mca1', 'mca2', ..., 'mca32'.
# for the MCSCard class with the 'counters' DynamicDeviceComponent.
# Ignore any updates from channels beyond NUM_MCA_CHANNELS
attr_name = signal.attr_name
index = int(attr_name[3:]) # Extract index from 'mcaX'
if index > self.NUM_MCA_CHANNELS:
return
# Push the raw values of the mca channels. The signal name has to be defined
# in the self.mcs sub-device (MCSRaw) to be able to push the values. Otherwise
# we will skip the update.
mca_raw = getattr(self.mcs, signal.name.split("_")[-1], None)
if mca_raw is None:
return
# In case there was more than one value received, i.e. frames_per_trigger > 1,
# we will receive a np.array of values.
# NOTE Depending on the scan parameters, we may either receive single values or numpy arrays.
# Therefore, we need to handle both cases here to ensure that data is always stored. We do
# this by converting single values to a list with one element, and numpy arrays to lists.
if isinstance(value, np.ndarray):
# We push the raw values as a list to the mca_raw signal
# And otherwise compute the mean value for plotting of counter signals
mca_raw.put(value.tolist())
# compute the count_time in seconds
if mapped_signal_name == "count_time":
value = value / self._mcs_clock
value = float(value.mean())
value = value.tolist() # Convert numpy array to list
else:
# We received a single value, so we can directly push it
mca_raw.put(value)
# compute the count_time in seconds
if mapped_signal_name == "count_time":
value = value / self._mcs_clock
value = [value] # Received single value, convert to list
# Get the mapped signal from the bpm device and update it
sig = getattr(self.bpm, mapped_signal_name)
sig.put(value)
self.counter_updated.append(signal.name)
# Once all mca channels have been updated, we can signal that we are ready to read
received_all_updates = set(self.counter_updated) == set(self.counter_mapping.keys())
if received_all_updates:
self.ready_to_read.put(READYTOREAD.DONE)
# The reset of the signal is done in the on_trigger method of ddg1 for the next trigger
self.counter_updated.clear() # Clear the list for the next update cycle
# Store the value with timestamp. If available in kwargs, use provided timestamp from CA,
# otherwise use current time when received.
self._current_data.update(
{attr_name: {"value": value, "timestamp": kwargs.get("timestamp") or time.time()}}
)
def _progress_update(self, value, **kwargs) -> None:
"""Callback for progress updates from ophyd subscription on current_channel."""
# This logic needs to be further refined as this is currently reporting the progress
# of a single trigger from BEC within a burst scan.
frames_per_trigger = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
self.progress.put(
value=value, max_value=frames_per_trigger, done=bool(value == frames_per_trigger)
)
# Once we have received all channels, push data to BEC and reset for next accumulation
logger.debug(
f"Received update for {attr_name}, index {self._mca_counter_index}/{self.NUM_MCA_CHANNELS}"
)
if len(self._current_data) == self.NUM_MCA_CHANNELS:
logger.debug(
f"Current data index {self._current_data_index} complete, pushing to BEC."
)
self.mca.put(self._current_data, acquisition_group=self._acquisition_group)
self._current_data.clear()
self._mca_counter_index = 0
self._current_data_index += 1
# NOTE The logic for the device progress is not yet fully refined for all scan types.
# This has to be adjusted once fly scan and step scan logic is fully implemented.
# pylint: disable=unused-argument
def _progress_update(self, *args, old_value: any, value: any, **kwargs) -> None:
"""
Callback to update the progress signals base on values of current_channel in respect to expected total triggers.
Logic for these updates need to be extended once fly and step scan logic is fully implemented.
Args:
old_value: Previous value of the signal.
value: New value of the signal.
"""
scan_done = bool(value == self._num_total_triggers)
self.progress.put(value=value, max_value=self._num_total_triggers, done=scan_done)
if scan_done:
self._scan_done_event.set()
def on_stage(self) -> None:
"""
Called when the device is staged.
This method is called when the device is staged before a scan. Any bootstrapping required
for the scan should be handled here. We also need to handle MCS card specific logic to ensure
that the card is properly prepared for the scan.
The following procedure is implemented here:
- Ensure that any ongoing acquisition is stopped (should never happen if not interfered with manually)
- Erase all data on the MCS card to ensure a clean start (should never
- Set acquisition parameters based on scan parameters (frames_per_trigger, num_points, acquisition_group)
- Clear any events and buffers related to async data emission. This includes '_omit_mca_callbacks',
'_start_monitor_async_data_emission', '_scan_done_callbacks', and '_current_data'.
"""
self.erase_all.set(1).wait(timeout=self._pv_timeout)
start_time = time.time()
# NOTE: If for some reason, the card is still acquiring, we need to stop it first
# This should never happen as the card is properly stopped during unstage
# Can only happen if user manually interferes with the IOC through other means
if self.acquiring.get() == ACQUIRING.ACQUIRING:
logger.warning(
f"MCS Card {self.name} was still acquiring on staging. Stopping acquisition."
)
self.stop_all.put(1)
status = CompareStatus(self.acquiring, ACQUIRING.DONE)
status.wait(timeout=10)
# NOTE: If current_channel != 0, erase all data on the card. This
# needs to be done with the 'suppress_mca_callbacks' context manager as erase_all will result
# in data emission through mca callback subscriptions.
# The buffer needs to be cleared as this will otherwise lead to missing
# triggers during the scan. Again, this should not happen if unstage is properly called.
# But user interference or a restart of the device_server may lead to this situation.
if self.current_channel.get() != 0:
with suppress_mca_callbacks(self, restore_after_timeout=1.0):
logger.warning(
f"MCS Card {self.name} had still data in buffer Erased all data on staging and sleeping for 1 second."
)
# Erase all data on the MCS card
self.erase_all.put(1)
#####################################
### Setup Acquisition Parameters ###
#####################################
triggers = self.scan_info.msg.scan_parameters.get("frames_per_trigger", 1)
num_points = self.scan_info.msg.num_points
self._num_total_triggers = triggers * num_points
self._acquisition_group = "monitored" if triggers == 1 else "burst_group"
self.preset_real.set(0).wait(timeout=self._pv_timeout)
self.num_use_all.set(triggers).wait(timeout=self._pv_timeout)
# Clear any previous data, just to be sure
with self._rlock:
self._current_data.clear()
self._mca_counter_index = 0
# NOTE Reset events for monitoring async_data_emission thread which is
# running during complete to wait for all data from the card
# to be emitted to BEC.
self._start_monitor_async_data_emission.clear()
# Clear any previous scan done callbacks
self._scan_done_callbacks.clear()
# Reset counter for data index of emitted data, NOTE for fly scans, this logic may have to be adjusted.
self._current_data_index = 0
# NOTE Make sure that the signal that omits mca callbacks is cleared
self._omit_mca_callbacks.clear()
logger.info(f"MCS Card {self.name} on_stage completed in {time.time() - start_time:.3f}s.")
def on_unstage(self) -> None:
"""
Called when the device is unstaged.
Called when the device is unstaged. This method should be omnipotent and resolve fast.
It stops any ongoing acquisition, erases all data on the MCS and clears the local buffer '_current_data'.
NOTE: It is important that the logic for on_complete is solid and properly waiting for mca data to be emitted
to BEC. Otherwise, unstage may interfere with ongoing data emission. Unstage is called after complete during scans.
It is crucial that the device itself calls '_omit_mca_callbacks' in its on_stage method to make sure
that data is emitted once the card is properly staged.
"""
self.stop_all.put(1)
self.ready_to_read.put(READYTOREAD.DONE)
# TODO why 0?
self.erase_all.set(0).wait(timeout=self._pv_timeout)
with suppress_mca_callbacks(self):
with self._rlock:
self._current_data.clear()
self._current_data_index = 0
self.erase_all.put(1)
def on_trigger(self) -> None:
status = TransitionStatus(
self.ready_to_read, strict=True, transitions=[READYTOREAD.PROCESSING, READYTOREAD.DONE]
)
self.cancel_on_stop(status)
return status
def _monitor_async_data_emission(self) -> None:
"""
Monitoring loop that runs in a separate thread to check if all async data has been emitted to BEC.
It is IDLE most of the time, but activate in the 'on_complete' method called by 'complete'.
def on_pre_scan(self) -> None:
"""
Called before the scan starts.
The check is done by comparing the number of data updates '_current_data_index' received through
mca channel callbacks with the expected number of points in the scan. Once they match, all
callbacks in _scan_done_callbacks are called to indicate that data emission is done.
Callbacks need to also accept and handle exceptions to properly report failure.
NOTE! This logic currently works for any step scan, but has to be extended for fly scans.
"""
while not self._scan_done_thread_kill_event.is_set():
while self._start_monitor_async_data_emission.wait():
try:
logger.debug(f"Monitoring async data emission for {self.name}...")
if (
hasattr(self.scan_info.msg, "num_points")
and self.scan_info.msg.num_points is not None
):
if self._current_data_index == self.scan_info.msg.num_points:
for callback in self._scan_done_callbacks:
callback(exception=None)
time.sleep(0.02) # 20ms delay to avoid busy loop
except Exception as exc: # pylint: disable=broad-except
content = traceback.format_exc()
logger.error(
f"Exception in monitoring thread of complete for {self.name}:\n{content}"
"Running callbacks to avoid deadlock."
)
for callback in self._scan_done_callbacks:
callback(exception=exc)
def _status_callback(self, status: StatusBase, exception=None) -> None:
"""Callback for status completion."""
self._start_monitor_async_data_emission.clear() # Stop monitoring
# NOTE Important check as set_finished or set_exception should not be called
# if the status is already done (e.g. cancelled externally)
with self._rlock:
if status.done:
return # Already done and cancelled externally.
if exception is not None:
status.set_exception(exception)
else:
status.set_finished()
def _status_failed_callback(self, status: StatusBase) -> None:
"""Callback for status failure, the monitoring thread should be stopped."""
# NOTE Check for status.done and status.success is important to avoid
if status.done:
self._start_monitor_async_data_emission.clear() # Stop monitoring
def on_complete(self) -> CompareStatus:
"""On scan completion."""
# Check if we should get a signal based on updates from the MCA channels
"""
Method that is called at the end of scan core, but before unstage. This method is
used to report whether the device successfully completed its data acquisition for the scan.
The check has to be implemented asynchronously and resolve through a status (future) object
returned by this method.
NOTE: For the MCS card, we need to ensure that all data has been acquired
and emitted to BEC as updates after 'on_complete' resolved will be rejected by BEC.
Therefore, we need to ensure that all data has been emitted to BEC before
reporting completion of the device.
This method implements the following procedure:
- Starts the IDLE async data monitoring thread that checks if all expected data
has been emitted to BEC through the mca channel callbacks.
- Use a CompareStatus to monitor when the MCS card becomes DONE. Please note that this
only indicates that the card has finished acquisition, but not that all data has been
emitted to BEC.
- Return combined status object. A callback is registered to handle failure of the status
if it is stopped externally, e.g. through scan abort. This should ensure that the
monitoring thread is stopped properly.
"""
# Prepare and register status callback for the async monitoring loop
status_async_data = StatusBase(obj=self)
self._scan_done_callbacks.append(partial(self._status_callback, status_async_data))
# Set the event to start monitoring async data emission
logger.debug(f"Starting to monitor async data emission for {self.name}...")
self._start_monitor_async_data_emission.set()
# Add CompareStatus for Acquiring DONE
status = CompareStatus(self.acquiring, ACQUIRING.DONE)
self.cancel_on_stop(status)
return status
# Combine both statuses
ret_status = status & status_async_data
# Handle external stop/cancel, and stop monitoring
ret_status.add_callback(self._status_failed_callback)
self.cancel_on_stop(ret_status)
return ret_status
def on_destroy(self):
"""
The on destroy hook is called when the device is destroyed, but also reloaded.
Here, we need to clean up all resources used up by the device, including running threads.
"""
self._scan_done_thread_kill_event.set()
self._start_monitor_async_data_emission.set()
if self._scan_done_thread.is_alive():
self._scan_done_thread.join(timeout=2.0)
if self._scan_done_thread.is_alive():
logger.warning(f"Thread for device {self.name} did not terminate properly.")
def on_stop(self) -> None:
"""
Called when the scan is stopped.
"""
"""Hook called when the device is stopped. In addition, any status that is registered through cancel_on_stop will be cancelled here."""
self.stop_all.put(1)
self.ready_to_read.put(READYTOREAD.DONE)
# Reset the progress signal
# self.progress.put(0, done=True)
self.erase_all.put(1)
def mcs_recovery(self, timeout: int = 1) -> None:
"""
Recovery procedure for the mcs card. This procedure has been empirically found and can
be used to ensure that the MCS card is stopped and has no pending data to be emitted.
It involves stopping any ongoing acquisition and erasing all data on the card, with
a sleep in between to allow the IOC to process the commands.
Args:
timeout (int): Total timeout for the recovery procedure. Defaults to 1 second.
"""
sleep_time = timeout / 2 # 2 sleeps
logger.debug(
f"Running recovery procedure for MCS card {self.name} with {sleep_time}s sleep, calling stop_all and erase_all, and another {sleep_time}s sleep"
)
# First erase and start ongoing acquisition.
self.erase_start.put(1)
time.sleep(sleep_time)
# After a brief processing time, we stop any ongoing acquisition.
self.stop_all.put(1)
# Finally, we erase all data while suppressing mca callbacks to avoid interference.
# We restore the callback suppression after timeout to ensure proper operation afterwards.
with suppress_mca_callbacks(self, restore_after_timeout=sleep_time):
self.erase_all.put(1)

View File

@@ -1,400 +0,0 @@
import enum
import json
import os
import threading
import time
import numpy as np
import requests
from bec_lib import bec_logger
from ophyd import ADComponent as ADCpt
from ophyd import Device, EpicsSignal, EpicsSignalRO, EpicsSignalWithRBV, Staged
from ophyd_devices.interfaces.base_classes.psi_detector_base import (
CustomDetectorMixin,
PSIDetectorBase,
)
logger = bec_logger.logger
class PilatusError(Exception):
"""Base class for exceptions in this module."""
class PilatusTimeoutError(PilatusError):
"""Raised when the Pilatus does not respond in time during unstage."""
class TriggerSource(enum.IntEnum):
"""Trigger source options for the detector"""
INTERNAL = 0
EXT_ENABLE = 1
EXT_TRIGGER = 2
MULTI_TRIGGER = 3
ALGINMENT = 4
class SLSDetectorCam(Device):
"""SLS Detector Camera - Pilatus
Base class to map EPICS PVs to ophyd signals.
"""
num_images = ADCpt(EpicsSignalWithRBV, "NumImages")
num_frames = ADCpt(EpicsSignalWithRBV, "NumExposures")
delay_time = ADCpt(EpicsSignalWithRBV, "NumExposures")
trigger_mode = ADCpt(EpicsSignalWithRBV, "TriggerMode")
acquire = ADCpt(EpicsSignal, "Acquire")
armed = ADCpt(EpicsSignalRO, "Armed")
read_file_timeout = ADCpt(EpicsSignal, "ImageFileTmot")
detector_state = ADCpt(EpicsSignalRO, "StatusMessage_RBV")
status_message_camserver = ADCpt(EpicsSignalRO, "StringFromServer_RBV", string=True)
acquire_time = ADCpt(EpicsSignal, "AcquireTime")
acquire_period = ADCpt(EpicsSignal, "AcquirePeriod")
threshold_energy = ADCpt(EpicsSignalWithRBV, "ThresholdEnergy")
file_path = ADCpt(EpicsSignalWithRBV, "FilePath")
file_name = ADCpt(EpicsSignalWithRBV, "FileName")
file_number = ADCpt(EpicsSignalWithRBV, "FileNumber")
auto_increment = ADCpt(EpicsSignalWithRBV, "AutoIncrement")
file_template = ADCpt(EpicsSignalWithRBV, "FileTemplate")
file_format = ADCpt(EpicsSignalWithRBV, "FileNumber")
gap_fill = ADCpt(EpicsSignalWithRBV, "GapFill")
class PilatusSetup(CustomDetectorMixin):
"""Pilatus setup class for cSAXS
Parent class: CustomDetectorMixin
"""
def __init__(self, *args, parent: Device = None, **kwargs) -> None:
super().__init__(*args, parent=parent, **kwargs)
self._lock = threading.RLock()
def on_init(self) -> None:
"""Initialize the detector"""
self.initialize_default_parameter()
self.initialize_detector()
def initialize_default_parameter(self) -> None:
"""Set default parameters for Eiger9M detector"""
self.update_readout_time()
def update_readout_time(self) -> None:
"""Set readout time for Eiger9M detector"""
readout_time = (
self.parent.scaninfo.readout_time
if hasattr(self.parent.scaninfo, "readout_time")
else self.parent.MIN_READOUT
)
self.parent.readout_time = max(readout_time, self.parent.MIN_READOUT)
def initialize_detector(self) -> None:
"""Initialize detector"""
# Stops the detector
self.stop_detector()
# Sets the trigger source to GATING
self.parent.cam.trigger_mode.put(TriggerSource.EXT_ENABLE)
def on_stage(self) -> None:
"""Stage the detector for scan"""
self.prepare_detector()
self.prepare_data_backend()
self.publish_file_location(
done=False, successful=False, metadata={"input_path": self.parent.filepath_raw}
)
def prepare_detector(self) -> None:
"""
Prepare detector for scan.
Includes checking the detector threshold,
setting the acquisition parameters and setting the trigger source
"""
self.set_detector_threshold()
self.set_acquisition_params()
self.parent.cam.trigger_mode.put(TriggerSource.EXT_ENABLE)
def prepare_data_backend(self) -> None:
"""
Prepare the detector backend of pilatus for a scan
A zmq service is running on xbl-daq-34 that is waiting
for a zmq message to start the writer for the pilatus_2 x12sa-pd-2
"""
self.stop_detector_backend()
self.parent.filepath.set(
self.parent.filewriter.compile_full_filename("pilatus_2.h5")
).wait()
self.parent.cam.file_path.put("/dev/shm/zmq/")
self.parent.cam.file_name.put(
f"{self.parent.scaninfo.username}_2_{self.parent.scaninfo.scan_number:05d}"
)
self.parent.cam.auto_increment.put(1) # auto increment
self.parent.cam.file_number.put(0) # first iter
self.parent.cam.file_format.put(0) # 0: TIFF
self.parent.cam.file_template.put("%s%s_%5.5d.cbf")
# TODO better to remove hard coded path with link to home directory/pilatus_2
basepath = f"/sls/X12SA/data/{self.parent.scaninfo.username}/Data10/pilatus_2/"
self.parent.filepath_raw = os.path.join(
basepath,
self.parent.filewriter.get_scan_directory(self.parent.scaninfo.scan_number, 1000, 5),
)
# Make directory if needed
self.create_directory(self.parent.filepath_raw)
headers = {"Content-Type": "application/json", "Accept": "application/json"}
# start the stream on x12sa-pd-2
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
data_msg = {
"source": [
{
"searchPath": "/",
"searchPattern": "glob:*.cbf",
"destinationPath": self.parent.filepath_raw,
}
]
}
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok:
res.raise_for_status()
# start the data receiver on xbl-daq-34
url = "http://xbl-daq-34:8091/pilatus_2/run"
data_msg = [
"zmqWriter",
self.parent.scaninfo.username,
{
"addr": "tcp://x12sa-pd-2:8888",
"dst": ["file"],
"numFrm": int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
),
"timeout": 2000,
"ifType": "PULL",
"user": self.parent.scaninfo.username,
},
]
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res.status_code} - {res.text} - {res.content}")
if not res.ok:
res.raise_for_status()
# Wait for server to become available again
time.sleep(0.1)
logger.info(f"{res.status_code} -{res.text} - {res.content}")
# Send requests.put to xbl-daq-34 to wait for data
url = "http://xbl-daq-34:8091/pilatus_2/wait"
data_msg = [
"zmqWriter",
self.parent.scaninfo.username,
{
"frmCnt": int(
self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger
),
"timeout": 2000,
},
]
try:
res = self.send_requests_put(url=url, data=data_msg, headers=headers)
logger.info(f"{res}")
if not res.ok:
res.raise_for_status()
except Exception as exc:
logger.info(f"Pilatus2 wait threw Exception: {exc}")
def set_detector_threshold(self) -> None:
"""
Set correct detector threshold to 1/2 of current X-ray energy, allow 5% tolerance
Threshold might be in ev or keV
"""
# get current beam energy from device manageer
mokev = self.parent.device_manager.devices.mokev.obj.read()[
self.parent.device_manager.devices.mokev.name
]["value"]
factor = 1
# Check if energies are eV or keV, assume keV as the default
unit = getattr(self.parent.cam.threshold_energy, "units", None)
if unit is not None and unit == "eV":
factor = 1000
# set energy on detector
setpoint = int(mokev * factor)
# set threshold on detector
threshold = self.parent.cam.threshold_energy.read()[self.parent.cam.threshold_energy.name][
"value"
]
if not np.isclose(setpoint / 2, threshold, rtol=0.05):
self.parent.cam.threshold_energy.set(setpoint / 2)
def set_acquisition_params(self) -> None:
"""Set acquisition parameters for the detector"""
# Set number of images and frames (frames is for internal burst of detector)
self.parent.cam.num_images.put(
int(self.parent.scaninfo.num_points * self.parent.scaninfo.frames_per_trigger)
)
self.parent.cam.num_frames.put(1)
# Update the readout time of the detector
self.update_readout_time()
def create_directory(self, filepath: str) -> None:
"""Create directory if it does not exist"""
os.makedirs(filepath, exist_ok=True)
def close_file_writer(self) -> None:
"""
Close the file writer for pilatus_2
Delete the data from x12sa-pd-2
"""
url = "http://x12sa-pd-2:8080/stream/pilatus_2"
try:
res = self.send_requests_delete(url=url)
if not res.ok:
res.raise_for_status()
except Exception as exc:
logger.info(f"Pilatus2 close threw Exception: {exc}")
def stop_file_writer(self) -> None:
"""
Stop the file writer for pilatus_2
Runs on xbl-daq-34
"""
url = "http://xbl-daq-34:8091/pilatus_2/stop"
res = self.send_requests_put(url=url)
if not res.ok:
res.raise_for_status()
def send_requests_put(self, url: str, data: list = None, headers: dict = None) -> object:
"""
Send a put request to the given url
Args:
url (str): url to send the request to
data (dict): data to be sent with the request (optional)
headers (dict): headers to be sent with the request (optional)
Returns:
status code of the request
"""
return requests.put(url=url, data=json.dumps(data), headers=headers, timeout=5)
def send_requests_delete(self, url: str, headers: dict = None) -> object:
"""
Send a delete request to the given url
Args:
url (str): url to send the request to
headers (dict): headers to be sent with the request (optional)
Returns:
status code of the request
"""
return requests.delete(url=url, headers=headers, timeout=5)
def on_pre_scan(self) -> None:
"""Prepare detector for scan"""
self.arm_acquisition()
def arm_acquisition(self) -> None:
"""Arms the detector for the acquisition"""
self.parent.cam.acquire.put(1)
# TODO is this sleep needed? to be tested with detector and for how long
time.sleep(0.5)
def on_unstage(self) -> None:
"""Unstage the detector"""
pass
def on_complete(self) -> None:
"""Complete the scan"""
self.finished(timeout=self.parent.TIMEOUT_FOR_SIGNALS)
self.publish_file_location(
done=True, successful=True, metadata={"input_path": self.parent.filepath_raw}
)
def finished(self, timeout: int = 5) -> None:
"""Check if acquisition is finished."""
# pylint: disable=protected-access
# TODO: at the moment this relies on device.mcs.obj._staged attribute
signal_conditions = [
(lambda: self.parent.device_manager.devices.mcs.obj._staged, Staged.no)
]
if not self.wait_for_signals(
signal_conditions=signal_conditions,
timeout=timeout,
check_stopped=True,
all_signals=True,
):
raise PilatusTimeoutError(
f"Reached timeout with detector state {signal_conditions[0][0]}, std_daq state"
f" {signal_conditions[1][0]} and received frames of {signal_conditions[2][0]} for"
" the file writer"
)
self.stop_detector()
self.stop_detector_backend()
def on_stop(self) -> None:
"""Stop detector"""
self.stop_detector()
self.stop_detector_backend()
def stop_detector(self) -> None:
"""Stop detector"""
self.parent.cam.acquire.put(0)
def stop_detector_backend(self) -> None:
"""Stop the file writer zmq service for pilatus_2"""
self.close_file_writer()
time.sleep(0.1)
self.stop_file_writer()
time.sleep(0.1)
class PilatuscSAXS(PSIDetectorBase):
"""Pilatus_2 300k detector for CSAXS
Parent class: PSIDetectorBase
class attributes:
custom_prepare_cls (Eiger9MSetup) : Custom detector setup class for cSAXS,
inherits from CustomDetectorMixin
cam (SLSDetectorCam) : Detector camera
MIN_READOUT (float) : Minimum readout time for the detector
"""
# Specify which functions are revealed to the user in BEC client
USER_ACCESS = []
# specify Setup class
custom_prepare_cls = PilatusSetup
# specify minimum readout time for detector
MIN_READOUT = 3e-3
TIMEOUT_FOR_SIGNALS = 5
# specify class attributes
cam = ADCpt(SLSDetectorCam, "cam1:")
if __name__ == "__main__":
pilatus_2 = PilatuscSAXS(name="pilatus_2", prefix="X12SA-ES-PILATUS300K:", sim_mode=True)

View File

@@ -15,10 +15,10 @@ CI/CD pipelines can run without the pyueye library or the related DLLs installed
from __future__ import annotations
import atexit
import time
from typing import Literal
import numpy as np
import time
from bec_lib.logger import bec_logger
from csaxs_bec.devices.ids_cameras.base_integration.utils import check_error
@@ -67,8 +67,8 @@ class IDSCameraObject:
check_error(ueye.is_SetDisplayMode(self.h_cam, ueye.IS_SET_DM_DIB), "IDSCameraObject")
if (
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_BAYER
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_BAYER
):
logger.info("Bayer color mode detected.")
# setup the color depth to the current windows setting
@@ -77,16 +77,16 @@ class IDSCameraObject:
) # TODO This raises an error - maybe check the m_n_colormode value
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
elif (
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_CBYCRY
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_CBYCRY
):
# for color camera models use RGB32 mode
self.m_n_colormode = self.ueye.IS_CM_BGRA8_PACKED
self.n_bits_per_pixel = self.ueye.INT(32)
self.bytes_per_pixel = int(self.n_bits_per_pixel / 8)
elif (
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_MONOCHROME
int.from_bytes(self.s_info.nColorMode.value, byteorder="big")
== self.ueye.IS_COLORMODE_MONOCHROME
):
# for color camera models use RGB32 mode
self.m_n_colormode = self.ueye.IS_CM_MONO8
@@ -160,12 +160,12 @@ class Camera:
"""
def __init__(
self,
camera_id: int,
m_n_colormode: Literal[0, 1, 2, 3] = 1,
bits_per_pixel: int = 24,
connect: bool = True,
force_monochrome: bool = False,
self,
camera_id: int,
m_n_colormode: Literal[0, 1, 2, 3] = 1,
bits_per_pixel: int = 24,
connect: bool = True,
force_monochrome: bool = False,
):
self.ueye = ueye
self.camera_id = camera_id
@@ -173,8 +173,13 @@ class Camera:
self.force_monochrome = force_monochrome
self._connected = False
self.cam = None
atexit.register(self.on_disconnect)
self._enable_warning_rate_limit: bool = False
self._last_rate_limited_log: float = 0
self._warning_log_rate_limit_s: float = 10
if connect:
self.on_connect()
@@ -255,7 +260,7 @@ class Camera:
def get_image_data(self) -> np.ndarray | None:
"""Get the image data from the camera."""
if not self._connected:
logger.warning("Camera is not connected.")
self._rate_limited_warning_log("Camera is not connected.")
return None
array = self.ueye.get_data(
self.cam.pc_image_mem,
@@ -282,6 +287,22 @@ class Camera:
return img
def set_camera_rate_limiting(self, enabled: bool, rate_limit_s: float | None = None):
if rate_limit_s is not None:
if rate_limit_s <= 0:
raise ValueError(f"Invalid rate limit: {rate_limit_s}, must be positive nonzero.")
self._warning_log_rate_limit_s = rate_limit_s
self._enable_warning_rate_limit = enabled
def _rate_limited_warning_log(self, msg: "str"):
if (
self._enable_warning_rate_limit
and time.monotonic() < self._last_rate_limited_log + self._warning_log_rate_limit_s
):
return
self._last_rate_limited_log = time.monotonic()
logger.warning(msg)
if __name__ == "__main__":
# Example usage

View File

@@ -29,8 +29,14 @@ class IDSCamera(PSIDeviceBase):
to interact with the IDS camera using the pyueye library.
"""
image = Cpt(PreviewSignal, name="image", ndim=2, doc="Preview signal for the camera.", num_rotation_90=0,
transpose=False)
image = Cpt(
PreviewSignal,
name="image",
ndim=2,
doc="Preview signal for the camera.",
num_rotation_90=0,
transpose=False,
)
roi_signal = Cpt(
AsyncSignal,
name="roi_signal",
@@ -43,19 +49,19 @@ class IDSCamera(PSIDeviceBase):
USER_ACCESS = ["live_mode", "mask", "set_rect_roi", "get_last_image"]
def __init__(
self,
*,
name: str,
camera_id: int,
prefix: str = "",
scan_info: ScanInfo | None = None,
m_n_colormode: Literal[0, 1, 2, 3] = 1,
bits_per_pixel: Literal[8, 24] = 24,
live_mode: bool = False,
num_rotation_90: int = 0,
transpose: bool = False,
force_monochrome: bool = False,
**kwargs,
self,
*,
name: str,
camera_id: int,
prefix: str = "",
scan_info: ScanInfo | None = None,
m_n_colormode: Literal[0, 1, 2, 3] = 1,
bits_per_pixel: Literal[8, 24] = 24,
live_mode: bool = False,
num_rotation_90: int = 0,
transpose: bool = False,
force_monochrome: bool = False,
**kwargs,
):
"""Initialize the IDS Camera.
@@ -133,7 +139,7 @@ class IDSCamera(PSIDeviceBase):
if x + width > img_shape[1] or y + height > img_shape[0]:
raise ValueError("ROI exceeds camera dimensions.")
mask = np.zeros(img_shape, dtype=np.uint8)
mask[y: y + height, x: x + width] = 1
mask[y : y + height, x : x + width] = 1
self.mask = mask
def _start_live(self):
@@ -162,6 +168,7 @@ class IDSCamera(PSIDeviceBase):
def _live_mode_loop(self, stop_event: threading.Event):
"""Loop to capture images in live mode."""
self.cam.set_camera_rate_limiting(True)
while not stop_event.is_set():
try:
self.process_data(self.cam.get_image_data())
@@ -169,6 +176,7 @@ class IDSCamera(PSIDeviceBase):
logger.error(f"Error in live mode loop: {e}")
break
stop_event.wait(0.2) # 5 Hz
self.cam.set_camera_rate_limiting(False)
def process_data(self, image: np.ndarray | None):
"""Process the image data before sending it to the preview signal."""

View File

@@ -412,10 +412,11 @@ class NPointAxis(Device, PositionerBase):
sign=1,
socket_cls=SocketIO,
tolerance: float = 0.05,
device_manager=None,
**kwargs,
):
self.controller = NPointController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.axis_Id = axis_Id
self.sign = sign
@@ -441,6 +442,14 @@ class NPointAxis(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())

View File

@@ -175,7 +175,7 @@ class FlomniGalilMotor(Device, PositionerBase):
**kwargs,
):
self.controller = FlomniGalilController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.axis_Id = axis_Id
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
@@ -212,6 +212,14 @@ class FlomniGalilMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@@ -342,10 +350,10 @@ class FlomniGalilMotor(Device, PositionerBase):
Drive an axis to the limit in a specified direction.
Args:
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
"""
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction)
#now force position read to cache
# now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())

View File

@@ -149,7 +149,7 @@ class FuprGalilMotor(Device, PositionerBase):
**kwargs,
):
self.controller = FuprGalilController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.axis_Id = axis_Id
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
@@ -185,6 +185,14 @@ class FuprGalilMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())

View File

@@ -59,12 +59,12 @@ class GalilController(Controller):
"all_axes_referenced",
]
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
OKBLUE = "\033[94m"
OKCYAN = "\033[96m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
@threadlocked
def socket_put(self, val: str) -> None:
@@ -115,29 +115,29 @@ class GalilController(Controller):
def axis_is_referenced(self, axis_Id_numeric) -> bool:
return bool(float(self.socket_put_and_receive(f"MG axisref[{axis_Id_numeric}]").strip()))
def folerr_status(self, axis_Id_numeric) -> bool:
return bool(float(self.socket_put_and_receive(f"MG folaxerr[{axis_Id_numeric}]").strip()))
def motor_temperature(self, axis_Id_numeric) -> float:
#this is only valid for omny. consider moving to ogalil
# this is only valid for omny. consider moving to ogalil
voltage = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
voltage2 = float(self.socket_put_and_receive(f"MG @AN[{axis_Id_numeric+1}]").strip())
if voltage2 < voltage:
voltage = voltage2
# convert from [-10,10]V to [0,300]degC
temperature_degC = round((voltage+10.0) / 20.0 * 300.0, 1)
temperature_degC = round((voltage + 10.0) / 20.0 * 300.0, 1)
#the motors of the parking station have a different offset
#the range is reduced, so if at the limit, we show an extreme value
# the motors of the parking station have a different offset
# the range is reduced, so if at the limit, we show an extreme value
if self.sock.port == 8082:
#controller 2
# controller 2
if axis_Id_numeric == 6:
temperature_degC = round((voltage+10.0-11.4) / 20.0 * 300.0, 1)
temperature_degC = round((voltage + 10.0 - 11.4) / 20.0 * 300.0, 1)
if voltage > 9.9:
temperature_degC = 300
if axis_Id_numeric == 7:
temperature_degC = round((voltage+.0-12) / 20.0 * 300.0, 1)
temperature_degC = round((voltage + 0.0 - 12) / 20.0 * 300.0, 1)
if voltage > 9.9:
temperature_degC = 300
return temperature_degC
@@ -147,16 +147,15 @@ class GalilController(Controller):
Check if all axes are referenced.
"""
return bool(float(self.socket_put_and_receive("MG allaxref").strip()))
def _omny_get_microstep_position(self,axis_Id):
def _omny_get_microstep_position(self, axis_Id):
return float(self.socket_put_and_receive(f"MG _TD{axis_Id}").strip())
def _omny_get_reference_limit(self,axis_Id):
def _omny_get_reference_limit(self, axis_Id):
get_axis_no = float(self.socket_put_and_receive(f"MG frmmv").strip())
if(get_axis_no>0):
if get_axis_no > 0:
reference_is_before = float(self.socket_put_and_receive(f"MG _FL{axis_Id}").strip())
elif(get_axis_no<0):
elif get_axis_no < 0:
reference_is_before = float(self.socket_put_and_receive(f"MG _BL{axis_Id}").strip())
else:
reference_is_before = 0
@@ -187,7 +186,11 @@ class GalilController(Controller):
while self.is_axis_moving(None, axis_Id_numeric):
time.sleep(0.01)
if verbose:
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f}", scope="drive axis to limit", show_asap=True)
self.device_manager.connector.send_client_info(
f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f}",
scope="drive axis to limit",
show_asap=True,
)
time.sleep(0.5)
# check if we actually hit the limit
@@ -201,13 +204,7 @@ class GalilController(Controller):
else:
print("Limit reached.")
def get_device_manager(self):
for axis in self._axis:
if hasattr(axis, "device_manager") and axis.device_manager:
return axis.device_manager
raise BECConfigError("Could not access the device_manager")
def find_reference(self, axis_Id_numeric: int, verbose=0, raise_error = 1) -> None:
def find_reference(self, axis_Id_numeric: int, verbose=0, raise_error=1) -> None:
"""
Find the reference of an axis.
@@ -224,7 +221,11 @@ class GalilController(Controller):
while self.is_axis_moving(None, axis_Id_numeric):
time.sleep(0.1)
if verbose:
self.get_device_manager().connector.send_client_info(f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f} reference is before {self._omny_get_reference_limit(axis_Id)}", scope="find axis reference", show_asap=True)
self.device_manager.connector.send_client_info(
f"Current microstep position {self._omny_get_microstep_position(axis_Id):.0f} reference is before {self._omny_get_reference_limit(axis_Id)}",
scope="find axis reference",
show_asap=True,
)
time.sleep(0.5)
if not self.axis_is_referenced(axis_Id_numeric):
@@ -236,7 +237,6 @@ class GalilController(Controller):
logger.info(f"Successfully found reference of axis {axis_Id_numeric}.")
print(f"Successfully found reference of axis {axis_Id_numeric}.")
def show_running_threads(self) -> None:
t = PrettyTable()
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
@@ -251,7 +251,7 @@ class GalilController(Controller):
def is_motor_on(self, axis_Id) -> bool:
return not bool(float(self.socket_put_and_receive(f"MG _MO{axis_Id}").strip()))
def get_motor_limit_switch(self, axis_Id) -> list:
"""
Get the status of the motor limit switches.
@@ -269,14 +269,7 @@ class GalilController(Controller):
def describe(self) -> None:
t = PrettyTable()
t.title = f"{self.__class__.__name__} on {self.sock.host}:{self.sock.port}"
field_names = [
"Axis",
"Name",
"Referenced",
"Motor On",
"Limits",
"Position",
]
field_names = ["Axis", "Name", "Referenced", "Motor On", "Limits", "Position"]
# in case of OMNY
if self.sock.host == "mpc3217.psi.ch":
field_names.append("Temperature")
@@ -286,7 +279,7 @@ class GalilController(Controller):
axis = self._axis[ax]
if axis is not None:
if self.sock.host == "mpc3217.psi.ch":
#case of omny. possibly consider moving to ogalil
# case of omny. possibly consider moving to ogalil
motor_on = self.is_motor_on(axis.axis_Id)
if motor_on == True:
motor_on = self.WARNING + "ON" + self.ENDC
@@ -299,7 +292,7 @@ class GalilController(Controller):
else:
folerr_status = "False"
position = axis.readback.read().get(axis.name).get("value")
position = f'{position:.3f}'
position = f"{position:.3f}"
t.add_row(
[
f"{axis.axis_Id_numeric}/{axis.axis_Id}",
@@ -330,8 +323,6 @@ class GalilController(Controller):
self.show_running_threads()
self.show_status_other()
def show_status_other(self) -> None:
"""
Show additional device-specific status information.
@@ -420,7 +411,7 @@ class GalilSetpointSignal(GalilSignalBase):
while self.controller.is_thread_active(0):
time.sleep(0.1)
#in the case of lamni, consider moving to lgalil
# in the case of lamni, consider moving to lgalil
if self.parent.axis_Id_numeric == 2 and self.controller.sock.host == "mpc2680.psi.ch":
try:
rt = self.parent.device_manager.devices[self.parent.rt]

View File

@@ -0,0 +1,35 @@
from ophyd_devices.utils.controller import Controller, threadlocked
from ophyd_devices.utils.socket import SocketSignal
from csaxs_bec.devices.omny.galil.galil_ophyd import GalilCommunicationError, retry_once
class GalilRIO(Controller):
@threadlocked
def socket_put(self, val: str) -> None:
self.sock.put(f"{val}\r".encode())
@retry_once
def socket_put_confirmed(self, val: str) -> None:
"""Send message to controller and ensure that it is received by checking that the socket receives a colon.
Args:
val (str): Message that should be sent to the socket
Raises:
GalilCommunicationError: Raised if the return value is not a colon.
"""
return_val = self.socket_put_and_receive(val)
if return_val != ":":
raise GalilCommunicationError(
f"Expected return value of ':' but instead received {return_val}"
)
class GalilRIOSignalBase(SocketSignal):
def __init__(self, signal_name, **kwargs):
self.signal_name = signal_name
super().__init__(**kwargs)
self.rio_controller = self.parent.rio_controller

View File

@@ -73,6 +73,7 @@ class LamniGalilController(GalilController):
air_off = bool(self.socket_put_and_receive("MG@OUT[13]"))
return rt_not_blocked_by_galil and air_off
class LamniGalilReadbackSignal(GalilSignalRO):
@retry_once
@threadlocked
@@ -99,6 +100,7 @@ class LamniGalilReadbackSignal(GalilSignalRO):
logger.warning("Failed to set RT value during readback.")
return val
class LamniGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller", "drive_axis_to_limit", "find_reference"]
readback = Cpt(LamniGalilReadbackSignal, signal_name="readback", kind="hinted")
@@ -132,7 +134,7 @@ class LamniGalilMotor(Device, PositionerBase):
**kwargs,
):
self.controller = LamniGalilController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.axis_Id = axis_Id
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
@@ -168,6 +170,14 @@ class LamniGalilMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@@ -292,7 +302,7 @@ class LamniGalilMotor(Device, PositionerBase):
Find the reference of the axis.
"""
self.controller.find_reference(self.axis_Id_numeric)
#now force position read to cache
# now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
@@ -301,10 +311,10 @@ class LamniGalilMotor(Device, PositionerBase):
Drive an axis to the limit in a specified direction.
Args:
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
"""
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction)
#now force position read to cache
# now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())

View File

@@ -46,7 +46,7 @@ class GalilMotorResolution(GalilSignalRO):
@threadlocked
def _socket_get(self):
if self.controller.sock.port == 8083 and self.parent.axis_Id_numeric == 2:
# rotation stage
# rotation stage
return 89565.8666667
else:
return 51200
@@ -69,37 +69,43 @@ class OMNYGalilReadbackSignal(GalilSignalRO):
current_pos = float(self.controller.socket_put_and_receive(f"TP{self.parent.axis_Id}"))
current_pos *= self.parent.sign
step_mm = self.parent.motor_resolution.get()
#here we introduce an offset of 25 to the rotation axis
#when setting a position this is taken into account in the controller
#that way we just do tomography from 0 to 180 degrees
# here we introduce an offset of 25 to the rotation axis
# when setting a position this is taken into account in the controller
# that way we just do tomography from 0 to 180 degrees
if self.parent.axis_Id_numeric == 2 and self.controller.sock.port == 8083:
return (current_pos / step_mm)+25
return (current_pos / step_mm) + 25
else:
return current_pos / step_mm
def read(self):
self._metadata["timestamp"] = time.time()
val = super().read()
#if reading rotation stage angle
# if reading rotation stage angle
if self.parent.axis_Id_numeric == 2 and self.controller.sock.port == 8083:
current_readback_value = val[self.parent.name]["value"]
#print (f"previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}.")
# print (f"previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}.")
if np.fabs((self.previous_rotation_angle-current_readback_value)>10):
if np.fabs((self.previous_rotation_angle - current_readback_value) > 10):
message = f"Glitch detected in rotation stage. Previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}."
print(message)
self.parent.device_manager.connector.send_client_info(message, scope="glitch detector", show_asap=True)
self.parent.device_manager.connector.send_client_info(
message, scope="glitch detector", show_asap=True
)
val = super().read()
current_readback_value = val[self.parent.name]["value"]
if np.fabs((self.previous_rotation_angle-current_readback_value)>10):
if np.fabs((self.previous_rotation_angle - current_readback_value) > 10):
message = f"Glitch detected in rotation stage second read. Previous rotation angle {self.previous_rotation_angle}, current readback {current_readback_value}. Disabling the controller."
print(message)
self.parent.device_manager.connector.send_client_info(message, scope="glitch detector", show_asap=True)
self.parent.device_manager.devices["osamroy"].obj.controller.socket_put_confirmed("allaxref=0")
self.parent.device_manager.connector.send_client_info(
message, scope="glitch detector", show_asap=True
)
self.parent.device_manager.devices["osamroy"].obj.controller.socket_put_confirmed(
"allaxref=0"
)
self.parent.device_manager.devices["osamroy"].obj.enabled = False
return val
@@ -108,13 +114,12 @@ class OMNYGalilReadbackSignal(GalilSignalRO):
try:
rt = self.parent.device_manager.devices["rtx"]
if rt.enabled:
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"]-25+54)
rt.obj.controller.set_rotation_angle(val[self.parent.name]["value"] - 25 + 54)
except KeyError:
logger.warning("Failed to set RT value during ogalil readback.")
logger.warning("Failed to set RT value during ogalil readback.")
return val
class OMNYGalilController(GalilController):
USER_ACCESS = [
"describe",
@@ -132,18 +137,18 @@ class OMNYGalilController(GalilController):
"_ogalil_folerr_not_ignore",
]
OKBLUE = '\033[94m'
OKCYAN = '\033[96m'
OKGREEN = '\033[92m'
WARNING = '\033[93m'
FAIL = '\033[91m'
ENDC = '\033[0m'
def on(self) -> None:
OKBLUE = "\033[94m"
OKCYAN = "\033[96m"
OKGREEN = "\033[92m"
WARNING = "\033[93m"
FAIL = "\033[91m"
ENDC = "\033[0m"
def on(self, timeout: int = 10) -> None:
"""Open a new socket connection to the controller"""
self._ogalil_switchsocket_switch_all_on()
time.sleep(0.3)
super().on()
super().on(timeout=timeout)
def _ogalil_switchsocket(self, number: int, switch: bool):
# number is socket number ranging from 1 to 4
@@ -185,15 +190,16 @@ class OMNYGalilController(GalilController):
self.socket_put_confirmed("IgNoFol=1")
self.socket_put_confirmed("XQ#STOP,1")
def _ogalil_set_axis_to_pos_wo_reference_search(self, axis_id_numeric, axis_id, pos_mm, motor_resolution, motor_sign):
def _ogalil_set_axis_to_pos_wo_reference_search(
self, axis_id_numeric, axis_id, pos_mm, motor_resolution, motor_sign
):
self.socket_put_confirmed("IgNoFol=1")
# pos_mm = pos_encoder / motor_resolution
pos_encoder = pos_mm * motor_resolution * motor_sign
#print(motor_resolution)
# print(motor_resolution)
self.socket_put_confirmed(f"DE{axis_id}={pos_encoder:.0f}")
self.socket_put_confirmed(f"DP{axis_id}=_TP{axis_id}*ratio[{axis_id_numeric:.0f}]")
@@ -203,7 +209,6 @@ class OMNYGalilController(GalilController):
self._ogalil_folerr_not_ignore()
def _ogalil_folerr_not_ignore(self):
self.socket_put_confirmed("IgNoFol=0")
@@ -240,7 +245,18 @@ class OMNYGalilController(GalilController):
class OMNYGalilMotor(Device, PositionerBase):
USER_ACCESS = ["controller", "find_reference", "omny_osamx_to_scan_center", "drive_axis_to_limit", "_ogalil_folerr_reset_and_ignore", "_ogalil_set_axis_to_pos_wo_reference_search", "get_motor_limit_switch", "axis_is_referenced", "get_motor_temperature", "folerr_status"]
USER_ACCESS = [
"controller",
"find_reference",
"omny_osamx_to_scan_center",
"drive_axis_to_limit",
"_ogalil_folerr_reset_and_ignore",
"_ogalil_set_axis_to_pos_wo_reference_search",
"get_motor_limit_switch",
"axis_is_referenced",
"get_motor_temperature",
"folerr_status",
]
readback = Cpt(OMNYGalilReadbackSignal, signal_name="readback", kind="hinted")
user_setpoint = Cpt(GalilSetpointSignal, signal_name="setpoint")
motor_resolution = Cpt(GalilMotorResolution, signal_name="resolution", kind="config")
@@ -272,7 +288,7 @@ class OMNYGalilMotor(Device, PositionerBase):
**kwargs,
):
self.controller = OMNYGalilController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.axis_Id = axis_Id
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
@@ -308,6 +324,14 @@ class OMNYGalilMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@@ -433,8 +457,10 @@ class OMNYGalilMotor(Device, PositionerBase):
def _ogalil_set_axis_to_pos_wo_reference_search(self, pos_mm):
motor_resolution = self.motor_resolution.get()
self.controller._ogalil_set_axis_to_pos_wo_reference_search(self.axis_Id_numeric, self.axis_Id, pos_mm, motor_resolution, self.sign)
#now force position read to cache
self.controller._ogalil_set_axis_to_pos_wo_reference_search(
self.axis_Id_numeric, self.axis_Id, pos_mm, motor_resolution, self.sign
)
# now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
@@ -442,9 +468,9 @@ class OMNYGalilMotor(Device, PositionerBase):
"""
Find the reference of the axis.
"""
verbose=1
verbose = 1
self.controller.find_reference(self.axis_Id_numeric, verbose, raise_error)
#now force position read to cache
# now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
@@ -453,10 +479,10 @@ class OMNYGalilMotor(Device, PositionerBase):
Drive an axis to the limit in a specified direction.
Args:
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
direction (str): Direction in which the axis should be driven to the limit. Either 'forward' or 'reverse'.
"""
self.controller.drive_axis_to_limit(self.axis_Id_numeric, direction, verbose=1)
#now force position read to cache
# now force position read to cache
val = self.readback.read()
self._run_subs(sub_type=self.SUB_READBACK, value=val, timestamp=time.time())
@@ -487,29 +513,31 @@ class OMNYGalilMotor(Device, PositionerBase):
def omny_osamx_to_scan_center(self, cenx):
if self.controller.sock.port == 8082 and self.axis_Id_numeric == 0:
# get last setpoint
osamx = self.device_manager.devices["osamx"]
osamx_current_setpoint = osamx.obj.readback.get()
omny_samx_in = self._get_user_param_safe("osamx","in")
if np.fabs(osamx_current_setpoint-(omny_samx_in+cenx/1000)) > 0.025:
message=f"Moving osamx to scan center. new osamx target {omny_samx_in+cenx/1000:.3f}."
logger.info(message)
osamx = self.device_manager.devices["osamx"]
osamx_current_setpoint = osamx.obj.readback.get()
omny_samx_in = self._get_user_param_safe("osamx", "in")
if np.fabs(osamx_current_setpoint - (omny_samx_in + cenx / 1000)) > 0.025:
message = (
f"Moving osamx to scan center. new osamx target {omny_samx_in+cenx/1000:.3f}."
)
logger.info(message)
osamx.read_only = False
#osamx.controller.("osamx", "controller.socket_put_confirmed('axspeed[0]=1000')")
osamx.set(omny_samx_in+cenx/1000)
time.sleep(0.1)
while(osamx.motor_is_moving.get()):
time.sleep(0.05)
osamx.read_only = True
time.sleep(2)
rt = self.device_manager.devices["rtx"]
if rt.enabled:
rt.obj.controller.laser_tracker_on()
rt.obj.controller.laser_tracker_check_and_wait_for_signalstrength()
osamx.read_only = False
# osamx.controller.("osamx", "controller.socket_put_confirmed('axspeed[0]=1000')")
osamx.set(omny_samx_in + cenx / 1000)
time.sleep(0.1)
while osamx.motor_is_moving.get():
time.sleep(0.05)
osamx.read_only = True
time.sleep(2)
rt = self.device_manager.devices["rtx"]
if rt.enabled:
rt.obj.controller.laser_tracker_on()
rt.obj.controller.laser_tracker_check_and_wait_for_signalstrength()
def folerr_status(self) -> bool:
return self.controller.folerr_status(self.axis_Id_numeric)
def stop(self, *, success=False):
self.controller.stop_all_axes()
return super().stop(success=success)

View File

@@ -52,33 +52,12 @@ class GalilController(Controller):
"fly_grid_scan",
"read_encoder_position",
]
_axes_per_controller = 8
def __init__(
self,
*,
name="GalilController",
kind=None,
parent=None,
socket=None,
attr_name="",
labels=None,
):
if not hasattr(self, "_initialized") or not self._initialized:
self._galil_axis_per_controller = 8
self._axis = [None for axis_num in range(self._galil_axis_per_controller)]
super().__init__(
name=name,
socket=socket,
attr_name=attr_name,
parent=parent,
labels=labels,
kind=kind,
)
def on(self, controller_num=0) -> None:
def on(self, timeout: int = 10) -> None:
"""Open a new socket connection to the controller"""
if not self.connected:
self.sock.open()
self.sock.open(timeout=timeout)
self.connected = True
else:
logger.info("The connection has already been established.")
@@ -165,11 +144,11 @@ class GalilController(Controller):
def show_running_threads(self) -> None:
t = PrettyTable()
t.title = f"Threads on {self.sock.host}:{self.sock.port}"
t.field_names = [str(ax) for ax in range(self._galil_axis_per_controller)]
t.field_names = [str(ax) for ax in range(self._axes_per_controller)]
t.add_row(
[
"active" if self.is_thread_active(t) else "inactive"
for t in range(self._galil_axis_per_controller)
for t in range(self._axes_per_controller)
]
)
print(t)
@@ -199,7 +178,7 @@ class GalilController(Controller):
"Limits",
"Position",
]
for ax in range(self._galil_axis_per_controller):
for ax in range(self._axes_per_controller):
axis = self._axis[ax]
if axis is not None:
t.add_row(
@@ -516,7 +495,9 @@ class SGalilMotor(Device, PositionerBase):
):
self.axis_Id = axis_Id
self.sign = sign
self.controller = GalilController(socket=socket_cls(host=host, port=port))
self.controller = GalilController(
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.tolerance = kwargs.pop("tolerance", 0.5)
self.device_mapping = kwargs.pop("device_mapping", {})
@@ -549,6 +530,14 @@ class SGalilMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())

View File

@@ -57,6 +57,7 @@ class RtFlomniController(Controller):
socket_cls=None,
socket_host=None,
socket_port=None,
device_manager=None,
attr_name="",
parent=None,
labels=None,
@@ -67,6 +68,7 @@ class RtFlomniController(Controller):
socket_cls=socket_cls,
socket_host=socket_host,
socket_port=socket_port,
device_manager=device_manager,
attr_name=attr_name,
parent=parent,
labels=labels,
@@ -126,15 +128,15 @@ class RtFlomniController(Controller):
while not self.slew_rate_limiters_on_target() or np.abs(self.pid_y()) > 0.1:
time.sleep(0.05)
self.get_device_manager().devices.rty.update_user_parameter({"tomo_additional_offsety": 0})
self.device_manager.devices.rty.update_user_parameter({"tomo_additional_offsety": 0})
self.clear_trajectory_generator()
self.laser_tracker_on()
# move to 0. FUPR will set the rotation angle during readout
self.get_device_manager().devices.fsamroy.obj.move(0, wait=True)
self.device_manager.devices.fsamroy.obj.move(0, wait=True)
fsamx = self.get_device_manager().devices.fsamx
fsamx = self.device_manager.devices.fsamx
fsamx.obj.pid_x_correction = 0
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
@@ -164,18 +166,18 @@ class RtFlomniController(Controller):
self.show_cyclic_error_compensation()
self.rt_pid_voltage = self.get_pid_x()
rtx = self.get_device_manager().devices.rtx
rtx = self.device_manager.devices.rtx
rtx.update_user_parameter({"rt_pid_voltage": self.rt_pid_voltage})
self.set_device_enabled("fsamx", False)
self.set_device_enabled("fsamy", False)
self.set_device_enabled("foptx", False)
self.set_device_enabled("fopty", False)
self.set_device_read_write("fsamx", False)
self.set_device_read_write("fsamy", False)
self.set_device_read_write("foptx", False)
self.set_device_read_write("fopty", False)
def move_samx_to_scan_region(self, fovx: float, cenx: float):
time.sleep(0.05)
if self.rt_pid_voltage is None:
rtx = self.get_device_manager().devices.rtx
rtx = self.device_manager.devices.rtx
self.rt_pid_voltage = rtx.user_parameter.get("rt_pid_voltage")
if self.rt_pid_voltage is None:
raise RtError(
@@ -192,7 +194,7 @@ class RtFlomniController(Controller):
break
wait_on_exit = True
self.socket_put("v0")
fsamx = self.get_device_manager().devices.fsamx
fsamx = self.device_manager.devices.fsamx
fsamx.read_only = False
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=0.1*stppermm[4]")
fsamx.obj.pid_x_correction -= (self.get_pid_x() - expected_voltage) * 0.007
@@ -223,22 +225,22 @@ class RtFlomniController(Controller):
print("Feedback is not running; likely an error in the interferometer.")
raise RtError("Feedback is not running; likely an error in the interferometer.")
self.set_device_enabled("fsamx", False)
self.set_device_enabled("fsamy", False)
self.set_device_enabled("foptx", False)
self.set_device_enabled("fopty", False)
self.set_device_read_write("fsamx", False)
self.set_device_read_write("fsamy", False)
self.set_device_read_write("foptx", False)
self.set_device_read_write("fopty", False)
def feedback_disable(self):
self.clear_trajectory_generator()
self.move_to_zero()
self.socket_put("l0")
self.set_device_enabled("fsamx", True)
self.set_device_enabled("fsamy", True)
self.set_device_enabled("foptx", True)
self.set_device_enabled("fopty", True)
self.set_device_read_write("fsamx", True)
self.set_device_read_write("fsamy", True)
self.set_device_read_write("foptx", True)
self.set_device_read_write("fopty", True)
fsamx = self.get_device_manager().devices.fsamx
fsamx = self.device_manager.devices.fsamx
fsamx.obj.controller.socket_put_confirmed("axspeed[4]=025*stppermm[4]")
print("rt feedback is now disalbed.")
@@ -289,12 +291,8 @@ class RtFlomniController(Controller):
self.socket_put("T1")
time.sleep(0.5)
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
"trackyct=0"
)
self.get_device_manager().devices.ftrackz.obj.controller.socket_put_confirmed(
"trackzct=0"
)
self.device_manager.devices.ftrackz.obj.controller.socket_put_confirmed("trackyct=0")
self.device_manager.devices.ftrackz.obj.controller.socket_put_confirmed("trackzct=0")
self.laser_tracker_wait_on_target()
logger.info("Laser tracker running!")
@@ -341,7 +339,7 @@ class RtFlomniController(Controller):
}
def laser_tracker_galil_enable(self):
ftrackz_con = self.get_device_manager().devices.ftrackz.obj.controller
ftrackz_con = self.device_manager.devices.ftrackz.obj.controller
ftrackz_con.socket_put_confirmed("tracken=1")
ftrackz_con.socket_put_confirmed("trackyct=0")
ftrackz_con.socket_put_confirmed("trackzct=0")
@@ -389,7 +387,7 @@ class RtFlomniController(Controller):
self.laser_tracker_wait_on_target()
signal = self.read_ssi_interferometer(1)
rtx = self.get_device_manager().devices.rtx
rtx = self.device_manager.devices.rtx
min_signal = rtx.user_parameter.get("min_signal")
low_signal = rtx.user_parameter.get("low_signal")
print(f"low signal: {low_signal}")
@@ -478,12 +476,6 @@ class RtFlomniController(Controller):
current_position_in_scan = int(float(return_table[2]))
return (mode, number_of_positions_planned, current_position_in_scan)
def get_device_manager(self):
for axis in self._axis:
if hasattr(axis, "device_manager") and axis.device_manager:
return axis.device_manager
raise BECConfigError("Could not access the device_manager")
def read_positions_from_sampler(self):
# this was for reading after the scan completed
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
@@ -498,7 +490,7 @@ class RtFlomniController(Controller):
# if not (mode==2 or mode==3):
# error
self.get_device_manager().connector.set(
self.device_manager.connector.set(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=1, metadata=self.readout_metadata
@@ -533,7 +525,7 @@ class RtFlomniController(Controller):
signals = self._get_signals_from_table(return_table)
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
self.get_device_manager().connector.set(
self.device_manager.connector.set(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=0, metadata=self.readout_metadata
@@ -547,7 +539,7 @@ class RtFlomniController(Controller):
)
def publish_device_data(self, signals, point_id):
self.get_device_manager().connector.set_and_publish(
self.device_manager.connector.set_and_publish(
MessageEndpoints.device_read("rt_flomni"),
messages.DeviceMessage(
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
@@ -658,7 +650,7 @@ class RtFlomniMotor(Device, PositionerBase):
self.axis_Id = axis_Id
self.sign = sign
self.controller = RtFlomniController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.device_manager = device_manager
@@ -686,6 +678,14 @@ class RtFlomniMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@@ -813,7 +813,7 @@ class RtFlomniMotor(Device, PositionerBase):
if __name__ == "__main__":
rtcontroller = RtFlomniController(
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222, device_manager=None
)
rtcontroller.on()
rtcontroller.laser_tracker_on()

View File

@@ -71,6 +71,7 @@ class RtLamniController(Controller):
socket_cls=None,
socket_host=None,
socket_port=None,
device_manager=None,
attr_name="",
parent=None,
labels=None,
@@ -81,6 +82,7 @@ class RtLamniController(Controller):
socket_cls=socket_cls,
socket_host=socket_host,
socket_port=socket_port,
device_manager=device_manager,
attr_name=attr_name,
parent=parent,
labels=labels,
@@ -92,11 +94,11 @@ class RtLamniController(Controller):
def feedback_disable(self):
self.socket_put("J0")
logger.info("LamNI Feedback disabled.")
self.set_device_enabled("lsamx", True)
self.set_device_enabled("lsamy", True)
self.set_device_enabled("loptx", True)
self.set_device_enabled("lopty", True)
self.set_device_enabled("loptz", True)
self.set_device_read_write("lsamx", True)
self.set_device_read_write("lsamy", True)
self.set_device_read_write("loptx", True)
self.set_device_read_write("lopty", True)
self.set_device_read_write("loptz", True)
def is_axis_moving(self, axis_Id) -> bool:
# this checks that axis is on target
@@ -150,25 +152,25 @@ class RtLamniController(Controller):
# set these as closed loop target position
self.socket_put(f"pa0,{x_curr:.4f}")
self.socket_put(f"pa1,{y_curr:.4f}")
self.get_device_manager().devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
self.get_device_manager().devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
self.device_manager.devices.rtx.obj.user_setpoint.set_with_feedback_disabled(x_curr)
self.device_manager.devices.rty.obj.user_setpoint.set_with_feedback_disabled(y_curr)
self.socket_put("J5")
logger.info("LamNI Feedback enabled (without reset).")
self.set_device_enabled("lsamx", False)
self.set_device_enabled("lsamy", False)
self.set_device_enabled("loptx", False)
self.set_device_enabled("lopty", False)
self.set_device_enabled("loptz", False)
self.set_device_read_write("lsamx", False)
self.set_device_read_write("lsamy", False)
self.set_device_read_write("loptx", False)
self.set_device_read_write("lopty", False)
self.set_device_read_write("loptz", False)
@threadlocked
def feedback_disable_and_even_reset_lamni_angle_interferometer(self):
self.socket_put("J6")
logger.info("LamNI Feedback disabled including the angular interferometer.")
self.set_device_enabled("lsamx", True)
self.set_device_enabled("lsamy", True)
self.set_device_enabled("loptx", True)
self.set_device_enabled("lopty", True)
self.set_device_enabled("loptz", True)
self.set_device_read_write("lsamx", True)
self.set_device_read_write("lsamy", True)
self.set_device_read_write("loptx", True)
self.set_device_read_write("lopty", True)
self.set_device_read_write("loptz", True)
@threadlocked
def clear_trajectory_generator(self):
@@ -284,7 +286,7 @@ class RtLamniController(Controller):
# if not (mode==2 or mode==3):
# error
self.get_device_manager().connector.set(
self.device_manager.connector.set(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=1, metadata=self.readout_metadata
@@ -319,7 +321,7 @@ class RtLamniController(Controller):
signals = self._get_signals_from_table(return_table)
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
self.get_device_manager().connector.set(
self.device_manager.connector.set(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=0, metadata=self.readout_metadata
@@ -331,7 +333,7 @@ class RtLamniController(Controller):
)
def publish_device_data(self, signals, point_id):
self.get_device_manager().connector.set_and_publish(
self.device_manager.connector.set_and_publish(
MessageEndpoints.device_read("rt_lamni"),
messages.DeviceMessage(
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
@@ -366,10 +368,10 @@ class RtLamniController(Controller):
) # we set all three outputs of the traj. gen. although in LamNI case only 0,1 are used
self.clear_trajectory_generator()
self.get_device_manager().devices.lsamrot.obj.move(0, wait=True)
self.device_manager.devices.lsamrot.obj.move(0, wait=True)
galil_controller_rt_status = (
self.get_device_manager().devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
self.device_manager.devices.lsamx.obj.controller.lgalil_is_air_off_and_orchestra_enabled()
)
if galil_controller_rt_status == 0:
@@ -382,16 +384,16 @@ class RtLamniController(Controller):
time.sleep(0.03)
lsamx_user_params = self.get_device_manager().devices.lsamx.user_parameter
lsamx_user_params = self.device_manager.devices.lsamx.user_parameter
if lsamx_user_params is None or lsamx_user_params.get("center") is None:
raise RuntimeError("lsamx center is not defined")
lsamy_user_params = self.get_device_manager().devices.lsamy.user_parameter
lsamy_user_params = self.device_manager.devices.lsamy.user_parameter
if lsamy_user_params is None or lsamy_user_params.get("center") is None:
raise RuntimeError("lsamy center is not defined")
lsamx_center = lsamx_user_params.get("center")
lsamy_center = lsamy_user_params.get("center")
self.get_device_manager().devices.lsamx.obj.move(lsamx_center, wait=True)
self.get_device_manager().devices.lsamy.obj.move(lsamy_center, wait=True)
self.device_manager.devices.lsamx.obj.move(lsamx_center, wait=True)
self.device_manager.devices.lsamy.obj.move(lsamy_center, wait=True)
self.socket_put("J1")
_waitforfeedbackctr = 0
@@ -405,11 +407,11 @@ class RtLamniController(Controller):
(self.socket_put_and_receive("J2")).split(",")[0]
)
self.set_device_enabled("lsamx", False)
self.set_device_enabled("lsamy", False)
self.set_device_enabled("loptx", False)
self.set_device_enabled("lopty", False)
self.set_device_enabled("loptz", False)
self.set_device_read_write("lsamx", False)
self.set_device_read_write("lsamy", False)
self.set_device_read_write("loptx", False)
self.set_device_read_write("lopty", False)
self.set_device_read_write("loptz", False)
if interferometer_feedback_not_running == 1:
logger.error(
@@ -559,7 +561,7 @@ class RtLamniMotor(Device, PositionerBase):
self.axis_Id = axis_Id
self.sign = sign
self.controller = RtLamniController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.device_manager = device_manager
@@ -586,6 +588,14 @@ class RtLamniMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())

View File

@@ -1,8 +1,8 @@
import builtins
import socket
import threading
import time
from typing import List
import builtins
import socket
import numpy as np
from bec_lib import bec_logger, messages
@@ -34,12 +34,15 @@ from csaxs_bec.devices.omny.rt.rt_ophyd import (
logger = bec_logger.logger
class RtOMNY_mirror_switchbox_Error(Exception):
pass
class RtOMNY_Error(Exception):
pass
class RtOMNYController(Controller):
_axes_per_controller = 3
red = "\x1b[91m"
@@ -87,6 +90,7 @@ class RtOMNYController(Controller):
socket_cls=None,
socket_host=None,
socket_port=None,
device_manager=None,
attr_name="",
parent=None,
labels=None,
@@ -97,6 +101,7 @@ class RtOMNYController(Controller):
socket_cls=socket_cls,
socket_host=socket_host,
socket_port=socket_port,
device_manager=device_manager,
attr_name=attr_name,
parent=parent,
labels=labels,
@@ -234,7 +239,7 @@ class RtOMNYController(Controller):
"opt_amplitude1_neg": 3000,
"opt_amplitude2_pos": 3000,
"opt_amplitude2_neg": 3000,
}
},
}
# def is_axis_moving(self, axis_Id) -> bool:
@@ -261,42 +266,60 @@ class RtOMNYController(Controller):
threading.Thread(target=send_positions, args=(self, positions), daemon=True).start()
def get_mirror_parameters(self,channel):
def get_mirror_parameters(self, channel):
return self.mirror_parameters[channel]
def laser_tracker_check_and_wait_for_signalstrength(self):
self.get_device_manager().connector.send_client_info("Checking laser tracker...", scope="", show_asap=True)
self.device_manager.connector.send_client_info(
"Checking laser tracker...", scope="", show_asap=True
)
if not self.laser_tracker_check_enabled():
print("laser_tracker_check_and_wait_for_signalstrength: The laser tracker is not even enabled.")
print(
"laser_tracker_check_and_wait_for_signalstrength: The laser tracker is not even enabled."
)
return
#first check on target
# first check on target
self.laser_tracker_wait_on_target()
#when on target, check interferometer signal
signal = self._omny_interferometer_get_signalsample("ssi_4",0.1)
rtx = self.get_device_manager().devices.rtx
# when on target, check interferometer signal
signal = self._omny_interferometer_get_signalsample("ssi_4", 0.1)
rtx = self.device_manager.devices.rtx
min_signal = rtx.user_parameter.get("min_signal")
low_signal = rtx.user_parameter.get("low_signal")
wait_counter = 0
while signal < min_signal and wait_counter<10:
self.get_device_manager().connector.send_client_info(f"The signal of the tracker {signal} is below the minimum required signal of {min_signal}. Waiting...", scope="laser_tracker_check_and_wait_for_signalstrength", show_asap=True)
while signal < min_signal and wait_counter < 10:
self.device_manager.connector.send_client_info(
f"The signal of the tracker {signal} is below the minimum required signal of {min_signal}. Waiting...",
scope="laser_tracker_check_and_wait_for_signalstrength",
show_asap=True,
)
wait_counter+=1
wait_counter += 1
time.sleep(0.2)
signal = self._omny_interferometer_get_signalsample("ssi_4",0.1)
signal = self._omny_interferometer_get_signalsample("ssi_4", 0.1)
if signal < low_signal:
self.get_device_manager().connector.send_client_info(f"\x1b[91mThe signal of the tracker {signal} is below the low limit of {low_signal}. Auto readjustment...\x1b[0m", scope="laser_tracker_check_and_wait_for_signalstrength", show_asap=True)
self.device_manager.connector.send_client_info(
f"\x1b[91mThe signal of the tracker {signal} is below the low limit of {low_signal}. Auto readjustment...\x1b[0m",
scope="laser_tracker_check_and_wait_for_signalstrength",
show_asap=True,
)
self.omny_interferometer_align_tracking()
self.get_device_manager().connector.send_client_info("Checking laser tracker completed.", scope="", show_asap=True)
self.omny_interferometer_align_tracking()
self.device_manager.connector.send_client_info(
"Checking laser tracker completed.", scope="", show_asap=True
)
def omny_interferometer_align_tracking(self):
mirror_channel=6
signal = self._omny_interferometer_get_signalsample(self.mirror_parameters[mirror_channel]["opt_signalchannel"], self.mirror_parameters[mirror_channel]["opt_averaging_time"])
mirror_channel = 6
signal = self._omny_interferometer_get_signalsample(
self.mirror_parameters[mirror_channel]["opt_signalchannel"],
self.mirror_parameters[mirror_channel]["opt_averaging_time"],
)
if signal > self.mirror_parameters[mirror_channel]["opt_signal_stop"]:
print(f"Interferometer signal of axis {self.mirror_parameters[mirror_channel]['opt_mirrorname']} is good, no alignment needed.")
print(
f"Interferometer signal of axis {self.mirror_parameters[mirror_channel]['opt_mirrorname']} is good, no alignment needed."
)
else:
self._omny_interferometer_switch_channel(mirror_channel)
time.sleep(0.1)
@@ -307,16 +330,19 @@ class RtOMNYController(Controller):
self._omny_interferometer_switch_alloff()
self.show_signal_strength_interferometer()
mirror_channel=-1
mirror_channel = -1
def omny_interferometer_align_incoupling_angle(self):
mirror_channel=1
signal = self._omny_interferometer_get_signalsample(self.mirror_parameters[mirror_channel]["opt_signalchannel"], self.mirror_parameters[mirror_channel]["opt_averaging_time"])
mirror_channel = 1
signal = self._omny_interferometer_get_signalsample(
self.mirror_parameters[mirror_channel]["opt_signalchannel"],
self.mirror_parameters[mirror_channel]["opt_averaging_time"],
)
if signal > self.mirror_parameters[mirror_channel]["opt_signal_stop"]:
print(f"Interferometer signal of axis {self.mirror_parameters[mirror_channel]['opt_mirrorname']} is good, no alignment needed.")
print(
f"Interferometer signal of axis {self.mirror_parameters[mirror_channel]['opt_mirrorname']} is good, no alignment needed."
)
else:
self._omny_interferometer_switch_channel(mirror_channel)
time.sleep(0.1)
@@ -327,19 +353,18 @@ class RtOMNYController(Controller):
self._omny_interferometer_switch_alloff()
self.show_signal_strength_interferometer()
mirror_channel=-1
mirror_channel = -1
def _omny_interferometer_openloop_steps(self, channel, steps, amplitude):
if channel not in range(3,5):
if channel not in range(3, 5):
raise RtOMNY_Error(f"invalid channel number {channel}.")
if amplitude > 4090:
amplitude = 4090
elif amplitude < 10:
amplitude = 10
oshield = self.get_device_manager().devices.oshield
oshield = self.device_manager.devices.oshield
oshield.obj.controller.move_open_loop_steps(
channel, steps, amplitude=amplitude, frequency=500
@@ -351,7 +376,7 @@ class RtOMNYController(Controller):
def _omny_interferometer_optimize(self, mirror_channel, channel):
if mirror_channel == -1:
raise RtOMNY_Error("no mirror channel selected")
#mirror channel is mirror number and channel is smaract channel, i.e. axis of the mirror
# mirror channel is mirror number and channel is smaract channel, i.e. axis of the mirror
if channel == 3:
steps_pos = self.mirror_parameters[mirror_channel]["opt_steps1_pos"]
steps_neg = self.mirror_parameters[mirror_channel]["opt_steps1_neg"]
@@ -365,67 +390,80 @@ class RtOMNYController(Controller):
else:
raise RtOMNY_Error(f"invalid channel number {channel}.")
previous_signal = self._omny_interferometer_get_signalsample(self.mirror_parameters[mirror_channel]["opt_signalchannel"], self.mirror_parameters[mirror_channel]["opt_averaging_time"])
previous_signal = self._omny_interferometer_get_signalsample(
self.mirror_parameters[mirror_channel]["opt_signalchannel"],
self.mirror_parameters[mirror_channel]["opt_averaging_time"],
)
min_begin = self.mirror_parameters[mirror_channel]["opt_signal_min_begin"]
min_begin = self.mirror_parameters[mirror_channel]["opt_signal_min_begin"]
if previous_signal < min_begin:
#raise OMNY_rt_clientError("error1") #(f"Minimum signal of axis {self.mirror_parameters[mirror_channel]["opt_mirrorname"]} to start alignment not present.")
# raise OMNY_rt_clientError("error1") #(f"Minimum signal of axis {self.mirror_parameters[mirror_channel]["opt_mirrorname"]} to start alignment not present.")
print(f"\rMinimum signal for auto alignment {min_begin} not reached.")
return
elif previous_signal > self.mirror_parameters[mirror_channel]["opt_signal_stop"]:
print(f"\rInterferometer signal of axis is good") # {self.mirror_parameters[mirror_channel]["opt_mirrorname"]} is good.")
return
print(
f"\rInterferometer signal of axis is good"
) # {self.mirror_parameters[mirror_channel]["opt_mirrorname"]} is good.")
return
else:
direction = 1
cycle_counter=0
cycle_max=20
reversal_counter=0
reversal_max=4
self.mirror_amplitutde_increase=0
cycle_counter = 0
cycle_max = 20
reversal_counter = 0
reversal_max = 4
self.mirror_amplitutde_increase = 0
current_sample = self._omny_interferometer_get_signalsample(self.mirror_parameters[mirror_channel]["opt_signalchannel"], self.mirror_parameters[mirror_channel]["opt_averaging_time"])
max=current_sample
current_sample = self._omny_interferometer_get_signalsample(
self.mirror_parameters[mirror_channel]["opt_signalchannel"],
self.mirror_parameters[mirror_channel]["opt_averaging_time"],
)
max = current_sample
while current_sample < self.mirror_parameters[mirror_channel]["opt_signal_stop"] and cycle_counter<cycle_max and reversal_counter < reversal_max:
while (
current_sample < self.mirror_parameters[mirror_channel]["opt_signal_stop"]
and cycle_counter < cycle_max
and reversal_counter < reversal_max
):
# if current_sample < self.mirror_parameters[mirror_channel]["opt_signal_min_begin"]:
# raise OMNY_rt_clientError("error2") #(f"Minimum signal of axis {self.mirror_parameters[mirror_channel]["opt_mirrorname"]} to start alignment not present.")
if direction>0:
if direction > 0:
self._omny_interferometer_openloop_steps(channel, steps_pos, opt_amplitude_pos)
verbose_str = f"channel {channel}, steps {steps_pos}"
else:
self._omny_interferometer_openloop_steps(channel, -steps_neg, opt_amplitude_neg)
verbose_str = f"auto action {channel}, steps {-steps_pos}"
#print(f"Aligning axis ") #{self.mirror_parameters[mirror_channel]["opt_mirrorname"]}. Target: {self.mirror_parameters[mirror_channel]["opt_signal_stop"]}, current {current_sample}")
# print(f"Aligning axis ") #{self.mirror_parameters[mirror_channel]["opt_mirrorname"]}. Target: {self.mirror_parameters[mirror_channel]["opt_signal_stop"]}, current {current_sample}")
current_sample = self._omny_interferometer_get_signalsample(self.mirror_parameters[mirror_channel]["opt_signalchannel"], self.mirror_parameters[mirror_channel]["opt_averaging_time"])
current_sample = self._omny_interferometer_get_signalsample(
self.mirror_parameters[mirror_channel]["opt_signalchannel"],
self.mirror_parameters[mirror_channel]["opt_averaging_time"],
)
opt_mirrorname = self.mirror_parameters[mirror_channel]["opt_mirrorname"]
info_str = f"\rAuto aligning Channel {mirror_channel}, {opt_mirrorname}, Current signal: {current_sample:.0f}"
message=info_str +" (q)uit \r"
self.get_device_manager().connector.send_client_info(message, scope="_omny_interferometer_optimize", show_asap=True)
if previous_signal>current_sample:
if direction<0:
steps_pos=int(steps_pos/2)
steps_neg=int(steps_neg/2)
if steps_pos<1:
steps_pos=1
if steps_neg<1:
steps_neg=1
direction=direction*(-1)
reversal_counter+=1
previous_signal=current_sample
cycle_counter+=1
print(f"\r\nFinished aligning channel {channel} of mirror {mirror_channel}\n\r") # {self.mirror_parameters[mirror_channel]["opt_mirrorname"]}. Target: {self.mirror_parameters[mirror_channel]["opt_signal_stop"]}, current {current_sample}")
message = info_str + " (q)uit \r"
self.device_manager.connector.send_client_info(
message, scope="_omny_interferometer_optimize", show_asap=True
)
if previous_signal > current_sample:
if direction < 0:
steps_pos = int(steps_pos / 2)
steps_neg = int(steps_neg / 2)
if steps_pos < 1:
steps_pos = 1
if steps_neg < 1:
steps_neg = 1
direction = direction * (-1)
reversal_counter += 1
previous_signal = current_sample
cycle_counter += 1
print(
f"\r\nFinished aligning channel {channel} of mirror {mirror_channel}\n\r"
) # {self.mirror_parameters[mirror_channel]["opt_mirrorname"]}. Target: {self.mirror_parameters[mirror_channel]["opt_signal_stop"]}, current {current_sample}")
def move_to_zero(self):
self.socket_put("pa0,0")
@@ -457,7 +495,7 @@ class RtOMNYController(Controller):
if ret == 1:
return True
return False
def feedback_is_running(self) -> bool:
self.feedback_get_status_and_ssi()
interferometer_feedback_not_running = int(self.ssi["feedback_error"])
@@ -466,7 +504,9 @@ class RtOMNYController(Controller):
return True
def feedback_enable_with_reset(self):
self.get_device_manager().connector.send_client_info(f"Enabling the feedback...", scope="", show_asap=True)
self.device_manager.connector.send_client_info(
f"Enabling the feedback...", scope="", show_asap=True
)
self.socket_put("J0") # disable feedback
time.sleep(0.01)
@@ -485,14 +525,16 @@ class RtOMNYController(Controller):
self.laser_tracker_on()
time.sleep(0.01)
osamroy = self.get_device_manager().devices.osamroy
osamroy = self.device_manager.devices.osamroy
# the following read will also upate the angle in rt during readout
readback = osamroy.obj.readback.get()
if (np.fabs(readback) > 0.1):
self.get_device_manager().connector.send_client_info(f"Rotating to zero", scope="feedback enable", show_asap=True)
if np.fabs(readback) > 0.1:
self.device_manager.connector.send_client_info(
f"Rotating to zero", scope="feedback enable", show_asap=True
)
osamroy.obj.move(0, wait=True)
osamx = self.get_device_manager().devices.osamx
osamx = self.device_manager.devices.osamx
osamx_in = osamx.user_parameter.get("in")
if not np.isclose(osamx.obj.readback.get(), osamx_in, atol=0.01):
@@ -514,16 +556,15 @@ class RtOMNYController(Controller):
time.sleep(1.5)
self.set_device_enabled("osamx", False)
self.set_device_enabled("osamy", False)
self.set_device_enabled("ofzpx", False)
self.set_device_enabled("ofzpy", False)
self.set_device_enabled("oosax", False)
self.set_device_enabled("oosax", False)
self.set_device_read_write("osamx", False)
self.set_device_read_write("osamy", False)
self.set_device_read_write("ofzpx", False)
self.set_device_read_write("ofzpy", False)
self.set_device_read_write("oosax", False)
self.set_device_read_write("oosax", False)
print("Feedback is running.")
@threadlocked
def clear_trajectory_generator(self):
self.socket_put("sc")
@@ -534,16 +575,15 @@ class RtOMNYController(Controller):
self.move_to_zero()
self.socket_put("J0")
self.set_device_enabled("osamx", True)
self.set_device_enabled("osamy", True)
self.set_device_enabled("ofzpx", True)
self.set_device_enabled("ofzpy", True)
self.set_device_enabled("oosax", True)
self.set_device_enabled("oosax", True)
self.set_device_read_write("osamx", True)
self.set_device_read_write("osamy", True)
self.set_device_read_write("ofzpx", True)
self.set_device_read_write("ofzpy", True)
self.set_device_read_write("oosax", True)
self.set_device_read_write("oosax", True)
print("rt feedback is now disabled.")
def set_rotation_angle(self, val: float) -> None:
self.socket_put(f"a{val/180*np.pi}")
@@ -578,12 +618,13 @@ class RtOMNYController(Controller):
"enabled_z": bool(tracker_values[10]),
}
def laser_tracker_on(self):
#update variables and enable if not yet active
# update variables and enable if not yet active
if not self.laser_tracker_check_enabled():
logger.info("Enabling the laser tracker. Please wait...")
self.get_device_manager().connector.send_client_info(f"Enabling the laser tracker. Please wait...", scope="", show_asap=True)
self.device_manager.connector.send_client_info(
f"Enabling the laser tracker. Please wait...", scope="", show_asap=True
)
tracker_intensity = self.tracker_info["tracker_intensity"]
if (
@@ -598,18 +639,13 @@ class RtOMNYController(Controller):
self.socket_put("T1")
time.sleep(0.5)
self.get_device_manager().devices.otracky.obj.controller.socket_put_confirmed(
"trackyct=0"
)
self.get_device_manager().devices.otracky.obj.controller.socket_put_confirmed(
"trackzct=0"
)
self.device_manager.devices.otracky.obj.controller.socket_put_confirmed("trackyct=0")
self.device_manager.devices.otracky.obj.controller.socket_put_confirmed("trackzct=0")
self.laser_tracker_wait_on_target()
logger.info("Laser tracker running!")
print("Laser tracker running!")
def laser_tracker_check_enabled(self) -> bool:
self.laser_update_tracker_info()
if self.tracker_info["enabled_z"] and self.tracker_info["enabled_y"]:
@@ -628,11 +664,10 @@ class RtOMNYController(Controller):
return True
return False
def laser_tracker_wait_on_target(self):
max_repeat = 15
count = 0
while not self.laser_tracker_check_on_target() and count<max_repeat:
while not self.laser_tracker_check_on_target() and count < max_repeat:
logger.info("Waiting for laser tracker to reach target position.")
time.sleep(0.5)
count += 1
@@ -641,75 +676,74 @@ class RtOMNYController(Controller):
raise RtError("Failed to reach laser target position.")
def laser_tracker_print_intensity_for_otrack_tweaking(self):
#self.laser_update_tracker_info()
#_laser_tracker_intensity = self.tracker_info["tracker_intensity"]
#print(f"\r PSD beam intensity: {_laser_tracker_intensity:.2f}\r")
# self.laser_update_tracker_info()
# _laser_tracker_intensity = self.tracker_info["tracker_intensity"]
# print(f"\r PSD beam intensity: {_laser_tracker_intensity:.2f}\r")
self.laser_tracker_show_all(extra_endline="\r")
def laser_tracker_show_all(self,extra_endline=""):
def laser_tracker_show_all(self, extra_endline=""):
self.laser_update_tracker_info()
enabled_y = self.tracker_info["enabled_y"]
print(extra_endline+f"Tracker enabled: {bool(enabled_y)}"+extra_endline)
print(extra_endline + f"Tracker enabled: {bool(enabled_y)}" + extra_endline)
if self.tracker_info["tracker_intensity"] < self.tracker_info["threshold_intensity_y"]:
print(self.red+" LOW INTENSITY"+self.white+extra_endline)
print(self.red + " LOW INTENSITY" + self.white + extra_endline)
_laser_tracker_intensity = self.tracker_info["tracker_intensity"]
print(f" PSD beam intensity: {_laser_tracker_intensity:.2f}"+extra_endline)
print(f" PSD beam intensity: {_laser_tracker_intensity:.2f}" + extra_endline)
_laser_tracker_y_beampos = self.tracker_info["beampos_y"]
print(f" Y beam position: {_laser_tracker_y_beampos:.2f}"+extra_endline)
print(f" Y beam position: {_laser_tracker_y_beampos:.2f}" + extra_endline)
_laser_tracker_y_target = self.tracker_info["target_y"]
print(f" target position: {_laser_tracker_y_target:.2f}"+extra_endline)
print(f" target position: {_laser_tracker_y_target:.2f}" + extra_endline)
_laser_tracker_y_threshold_intensity = self.tracker_info["threshold_intensity_y"]
print(f" threshold intensity: {_laser_tracker_y_threshold_intensity:.2f}"+extra_endline)
print(
f" threshold intensity: {_laser_tracker_y_threshold_intensity:.2f}" + extra_endline
)
_laser_tracker_y_piezo_voltage = self.tracker_info["piezo_voltage_y"]
print(f" Piezo voltage: {_laser_tracker_y_piezo_voltage:.2f}"+extra_endline)
print(f" Piezo voltage: {_laser_tracker_y_piezo_voltage:.2f}" + extra_endline)
_laser_tracker_z_beampos = self.tracker_info["beampos_z"]
print(f" Z beam position: {_laser_tracker_z_beampos:.2f}"+extra_endline)
print(f" Z beam position: {_laser_tracker_z_beampos:.2f}" + extra_endline)
_laser_tracker_z_target = self.tracker_info["target_z"]
print(f" target position: {_laser_tracker_z_target:.2f}"+extra_endline)
print(f" target position: {_laser_tracker_z_target:.2f}" + extra_endline)
_laser_tracker_z_threshold_intensity = self.tracker_info["threshold_intensity_z"]
print(f" threshold intensity: {_laser_tracker_z_threshold_intensity:.2f}"+extra_endline)
print(
f" threshold intensity: {_laser_tracker_z_threshold_intensity:.2f}" + extra_endline
)
_laser_tracker_z_piezo_voltage = self.tracker_info["piezo_voltage_z"]
print(f" Piezo voltage: {_laser_tracker_z_piezo_voltage:.2f}"+extra_endline)
print(" Reminder - there is also an upper threshold active in rt\n"+extra_endline)
print(f" Piezo voltage: {_laser_tracker_z_piezo_voltage:.2f}" + extra_endline)
print(" Reminder - there is also an upper threshold active in rt\n" + extra_endline)
if extra_endline == "":
self.laser_tracker_galil_status()
def laser_tracker_galil_enable(self):
otracky_con = self.get_device_manager().devices.otracky.obj.controller
otracky_con = self.device_manager.devices.otracky.obj.controller
otracky_con.socket_put_confirmed("tracken=1")
otracky_con.socket_put_confirmed("trackyct=0")
otracky_con.socket_put_confirmed("trackzct=0")
def laser_tracker_galil_disable(self):
otracky_con = self.get_device_manager().devices.otracky.obj.controller
otracky_con = self.device_manager.devices.otracky.obj.controller
otracky_con.socket_put_confirmed("tracken=0")
def laser_tracker_galil_status(self):
otracky_con = self.get_device_manager().devices.otracky.obj.controller
otracky_con = self.device_manager.devices.otracky.obj.controller
if bool(float(otracky_con.socket_put_and_receive("MGtracken").strip())) == 0:
print(self.red+"Tracking in the Galil Controller is disabled."+self.white)
print(self.red + "Tracking in the Galil Controller is disabled." + self.white)
print("Use dev.rtx.controller.laser_tracker_galil_enable to enable.\n")
return(0)
return 0
else:
print("Tracking in the Galil Controller is enabled.")
trackyct=int(float(otracky_con.socket_put_and_receive("MGtrackyct").strip()))
trackzct=int(float(otracky_con.socket_put_and_receive("MGtrackzct").strip()))
trackyct = int(float(otracky_con.socket_put_and_receive("MGtrackyct").strip()))
trackzct = int(float(otracky_con.socket_put_and_receive("MGtrackzct").strip()))
print(f"Galil Trackcounters y={trackyct}, z={trackzct}")
def show_signal_strength_interferometer(self):
channelnames={1:"OSA FZP Y",2:"ST OSA Y",3:"OSA FZP X",4:"ST OSA X",5:"Angle"}
channelnames = {1: "OSA FZP Y", 2: "ST OSA Y", 3: "OSA FZP X", 4: "ST OSA X", 5: "Angle"}
self.feedback_get_status_and_ssi()
t = PrettyTable()
t.title = f"Interferometer signal strength"
t.field_names = ["Channel", "Name", "Value"]
for i in range(1,6):
for i in range(1, 6):
ssi = self.ssi[f"ssi_{i}"]
t.add_row([i,channelnames[i], ssi])
t.add_row([i, channelnames[i], ssi])
print(t)
def _omny_interferometer_switch_open_socket(self):
@@ -722,44 +756,42 @@ class RtOMNYController(Controller):
self._omny_interferometer_switch_put_and_receive("?000\r")
time.sleep(1)
def _omny_interferometer_switch_channel(self, channel):
self._omny_interferometer_switch_alloff()
time.sleep(0.1)
if channel == 1: #Relais 1 and 2
if channel == 1: # Relais 1 and 2
rback = self._omny_interferometer_switch_put_and_receive("!0020003\r")
#if "|0003\r" != self._omny_interferometer_switch_put_and_receive("!0020003\r"):
# if "|0003\r" != self._omny_interferometer_switch_put_and_receive("!0020003\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 2: #Relais 3 and 4
elif channel == 2: # Relais 3 and 4
rback = self._omny_interferometer_switch_put_and_receive("!002000C\r")
# if "|000C\r" != self._omny_interferometer_switch_put_and_receive("!002000C\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 3: #Relais 5 and 6
elif channel == 3: # Relais 5 and 6
rback = self._omny_interferometer_switch_put_and_receive("!0020030\r")
# if "|0030\r" != self._omny_interferometer_switch_put_and_receive("!0020030\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 4: #Relais 7 and 8
elif channel == 4: # Relais 7 and 8
rback = self._omny_interferometer_switch_put_and_receive("!00200C0\r")
# if "|00C0\r" != self._omny_interferometer_switch_put_and_receive("!00200C0\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 5: #Relais 9 and 10
elif channel == 5: # Relais 9 and 10
rback = self._omny_interferometer_switch_put_and_receive("!0020300\r")
# if "|0300\r" != self._omny_interferometer_switch_put_and_receive("!0020300\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 6: #Relais 11 and 12
elif channel == 6: # Relais 11 and 12
rback = self._omny_interferometer_switch_put_and_receive("!0020C00\r")
# if "|0C00\r" != self._omny_interferometer_switch_put_and_receive("!0020C00\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 7: #Relais 13 and 14
elif channel == 7: # Relais 13 and 14
rback = self._omny_interferometer_switch_put_and_receive("!0023000\r")
# if "|3000\r" != self._omny_interferometer_switch_put_and_receive("!0023000\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 8: #Relais 7 and 8 SPECIAL CASE use osafzp y signal to align osa y
elif channel == 8: # Relais 7 and 8 SPECIAL CASE use osafzp y signal to align osa y
rback = self._omny_interferometer_switch_put_and_receive("!00200C0\r")
# if "|00C0\r" != self._omny_interferometer_switch_put_and_receive("!00200C0\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
elif channel == 9: #Relais 15 and 16
elif channel == 9: # Relais 15 and 16
rback = self._omny_interferometer_switch_put_and_receive("!002C000\r")
# if "|C000\r" != self._omny_interferometer_switch_put_and_receive("!002C000\r"):
# raise RtOMNY_mirror_switchbox_Error("Channel switching failed.")
@@ -785,14 +817,14 @@ class RtOMNYController(Controller):
self._omny_interferometer_switch_put_and_receive("!00S01\r")
def _omny_interferometer_switch_alloff(self):
#switch all off
# switch all off
self._omny_interferometer_switch_put_and_receive("!0020000\r")
#LED OFF
# LED OFF
time.sleep(0.1)
self._omny_interferometer_switch_put_and_receive("!00S00\r")
self._omny_interferometer_switch_put_and_receive("!00S00\r")
time.sleep(0.1)
alloff = self._omny_interferometer_switch_put_and_receive("?002\r")
#check all off
alloff = self._omny_interferometer_switch_put_and_receive("?002\r")
# check all off
if "00" not in alloff:
raise RtOMNY_mirror_switchbox_Error("Not all channels switched off.")
@@ -800,17 +832,16 @@ class RtOMNYController(Controller):
self.socket_put("J3")
def _omny_interferometer_get_signalsample(self, channel, averaging_duration):
#channel is string, eg ssi_1
#ensure no averaging running currently
# channel is string, eg ssi_1
# ensure no averaging running currently
self.feedback_is_running()
#measure first sample
# measure first sample
self._rt_start_averaging_SSI()
time.sleep(averaging_duration)
self.feedback_is_running()
return self.ssi[channel]
def _get_signals_from_table(self, return_table) -> dict:
self.average_stdeviations_x_st_fzp += float(return_table[16])
self.average_stdeviations_y_st_fzp += float(return_table[18])
@@ -831,7 +862,6 @@ class RtOMNYController(Controller):
"stdev_x_st_fzp": {"value": float(return_table[16])},
"average_y_st_fzp": {"value": float(return_table[17])},
"stdev_y_st_fzp": {"value": float(return_table[18])},
"average_stdeviations_x_st_fzp": {
"value": self.average_stdeviations_x_st_fzp / (int(return_table[0]) + 1)
},
@@ -840,7 +870,7 @@ class RtOMNYController(Controller):
},
}
return signals
@threadlocked
def start_scan(self):
if not self.feedback_is_running():
@@ -862,7 +892,6 @@ class RtOMNYController(Controller):
# start a point-by-point scan (for cont scan in flomni it would be "sa")
self.socket_put_and_receive("sd")
@retry_once
@threadlocked
def get_scan_status(self):
@@ -881,13 +910,6 @@ class RtOMNYController(Controller):
current_position_in_scan = int(float(return_table[2]))
return (mode, number_of_positions_planned, current_position_in_scan)
def get_device_manager(self):
for axis in self._axis:
if hasattr(axis, "device_manager") and axis.device_manager:
return axis.device_manager
raise BECConfigError("Could not access the device_manager")
def read_positions_from_sampler(self):
# this was for reading after the scan completed
number_of_samples_to_read = 1 # self.get_scan_status()[1] #number of valid samples, will be updated upon first data read
@@ -901,7 +923,7 @@ class RtOMNYController(Controller):
# if not (mode==2 or mode==3):
# error
self.get_device_manager().connector.set(
self.device_manager.connector.set(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=1, metadata=self.readout_metadata
@@ -936,7 +958,7 @@ class RtOMNYController(Controller):
signals = self._get_signals_from_table(return_table)
self.publish_device_data(signals=signals, point_id=int(return_table[0]))
self.get_device_manager().connector.set(
self.device_manager.connector.set(
MessageEndpoints.device_status("rt_scan"),
messages.DeviceStatusMessage(
device="rt_scan", status=0, metadata=self.readout_metadata
@@ -949,15 +971,16 @@ class RtOMNYController(Controller):
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}."
)
self.get_device_manager().connector.send_client_info(
self.device_manager.connector.send_client_info(
"OMNY statistics: Average of all standard deviations [nm]: x"
f" {self.average_stdeviations_x_st_fzp/read_counter*1000:.1f}, y"
f" {self.average_stdeviations_y_st_fzp/read_counter*1000:.1f}.",
scope="", show_asap=True)
scope="",
show_asap=True,
)
def publish_device_data(self, signals, point_id):
self.get_device_manager().connector.set_and_publish(
self.device_manager.connector.set_and_publish(
MessageEndpoints.device_read("rt_omny"),
messages.DeviceMessage(
signals=signals, metadata={"point_id": point_id, **self.readout_metadata}
@@ -1068,7 +1091,7 @@ class RtOMNYMotor(Device, PositionerBase):
self.axis_Id = axis_Id
self.sign = sign
self.controller = RtOMNYController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.controller.set_axis(axis=self, axis_nr=self.axis_Id_numeric)
self.device_manager = device_manager
@@ -1096,6 +1119,14 @@ class RtOMNYMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())
@@ -1182,7 +1213,6 @@ class RtOMNYMotor(Device, PositionerBase):
return status
@property
def axis_Id(self):
return self._axis_Id_alpha
@@ -1227,7 +1257,7 @@ class RtOMNYMotor(Device, PositionerBase):
if __name__ == "__main__":
rtcontroller = RtOMNYController(
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222
socket_cls=SocketIO, socket_host="mpc2844.psi.ch", socket_port=2222, device_manager=None
)
rtcontroller.on()
rtcontroller.laser_tracker_on()

View File

@@ -70,7 +70,7 @@ class SmaractSensors:
class SmaractController(Controller):
_axes_per_controller = 6
_axes_per_controller = 9
_initialized = False
USER_ACCESS = [
"socket_put_and_receive",
@@ -93,6 +93,7 @@ class SmaractController(Controller):
socket_cls=None,
socket_host=None,
socket_port=None,
device_manager=None,
attr_name="",
labels=None,
):
@@ -102,6 +103,7 @@ class SmaractController(Controller):
socket_cls=socket_cls,
socket_host=socket_host,
socket_port=socket_port,
device_manager=device_manager,
attr_name=attr_name,
parent=parent,
labels=labels,

View File

@@ -6,6 +6,7 @@ import numpy as np
from bec_lib import bec_logger
from ophyd import Component as Cpt
from ophyd import Device, PositionerBase, Signal
from ophyd.device import DEFAULT_CONNECTION_TIMEOUT
from ophyd.status import wait as status_wait
from ophyd.utils import LimitError, ReadOnlyError
from ophyd_devices.utils.controller import threadlocked
@@ -123,10 +124,11 @@ class SmaractMotor(Device, PositionerBase):
limits=None,
sign=1,
socket_cls=SocketIO,
device_manager=None,
**kwargs,
):
self.controller = SmaractController(
socket_cls=socket_cls, socket_host=host, socket_port=port
socket_cls=socket_cls, socket_host=host, socket_port=port, device_manager=device_manager
)
self.axis_Id = axis_Id
self.sign = sign
@@ -152,6 +154,14 @@ class SmaractMotor(Device, PositionerBase):
self.low_limit_travel.put(limits[0])
self.high_limit_travel.put(limits[1])
def wait_for_connection(self, timeout: float = 30.0) -> bool:
self.controller.on(timeout=timeout)
def destroy(self):
"""Make sure to turn off the controller socket on destroy."""
self.controller.off(update_config=False)
return super().destroy()
@property
def limits(self):
return (self.low_limit_travel.get(), self.high_limit_travel.get())

View File

@@ -6,9 +6,35 @@ from unittest import mock
import numpy as np
import ophyd
import pytest
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
from bec_server.device_server.tests.utils import DMMock
from ophyd_devices.tests.utils import patched_device
from csaxs_bec.devices.epics.delay_generator_csaxs import DDG1, DDG2
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_1 import (
DEFAULT_IO_CONFIG as DDG1_DEFAULT_IO_CONFIG,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_1 import (
DEFAULT_READOUT_TIMES as DDG1_DEFAULT_READOUT_TIMES,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_1 import (
DEFAULT_REFERENCES as DDG1_DEFAULT_REFERENCES,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_1 import (
DEFAULT_TRIGGER_SOURCE as DDG1_DEFAULT_TRIGGER_SOURCE,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_1 import PROC_EVENT_MODE
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_2 import (
DEFAULT_IO_CONFIG as DDG2_DEFAULT_IO_CONFIG,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_2 import (
DEFAULT_READOUT_TIMES as DDG2_DEFAULT_READOUT_TIMES,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_2 import (
DEFAULT_REFERENCES as DDG2_DEFAULT_REFERENCES,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.ddg_2 import (
DEFAULT_TRIGGER_SOURCE as DDG2_DEFAULT_TRIGGER_SOURCE,
)
from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import (
BURSTCONFIG,
CHANNELREFERENCE,
@@ -16,68 +42,46 @@ from csaxs_bec.devices.epics.delay_generator_csaxs.delay_generator_csaxs import
TRIGGERSOURCE,
DelayGeneratorCSAXS,
)
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import MCSCardCSAXS
@pytest.fixture(scope="function")
def mock_ddg1() -> Generator[DDG1, DDG1, DDG1]:
"""Fixture to mock the DDG1 device."""
name = "ddg1"
prefix = "test_ddg1:"
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = DDG1(name=name, prefix=prefix)
patch_dual_pvs(dev)
yield dev
@pytest.fixture(scope="function")
def mock_ddg2() -> Generator[DDG2, DDG2, DDG2]:
"""Fixture to mock the DDG1 device."""
name = "ddg2"
prefix = "test_ddg2:"
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = DDG2(name=name, prefix=prefix)
patch_dual_pvs(dev)
yield dev
############################
### Test Delay Generator ###
############################
@pytest.fixture(scope="function")
def mock_ddg() -> Generator[DelayGeneratorCSAXS, DelayGeneratorCSAXS, DelayGeneratorCSAXS]:
"""Fixture to mock the camera device."""
name = "ddg"
prefix = "test:"
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
dev = DelayGeneratorCSAXS(name=name, prefix=prefix)
patch_dual_pvs(dev)
yield dev
with patched_device(
DelayGeneratorCSAXS, name="ddg", prefix="test:", _mock_pv_initial_value=0
) as dev:
try:
yield dev
finally:
dev.destroy()
def test_ddg_init(mock_ddg):
def test_ddg_init(mock_ddg: DelayGeneratorCSAXS):
"""Test the proc event status method."""
assert mock_ddg.name == "ddg"
assert mock_ddg.prefix == "test:"
def test_ddg_proc_event_status(mock_ddg):
def test_ddg_proc_event_status(mock_ddg: DelayGeneratorCSAXS):
"""Test the proc event status method."""
mock_ddg.state.proc_status.put(0)
mock_ddg.proc_event_status()
assert mock_ddg.state.proc_status.get() == 1
def test_ddg_set_trigger(mock_ddg):
def test_ddg_set_trigger(mock_ddg: DelayGeneratorCSAXS):
"""Test setting the trigger."""
for trigger in TRIGGERSOURCE:
mock_ddg.set_trigger(trigger)
assert mock_ddg.trigger_source.get() == trigger.value
def test_ddg_burst_enable(mock_ddg):
def test_ddg_burst_enable(mock_ddg: DelayGeneratorCSAXS):
"""Test enabling burst mode."""
mock_ddg.burst_enable(count=100, delay=0.1, period=0.02, config=BURSTCONFIG.ALL_CYCLES)
mock_ddg.burst_mode.get() == 1
@@ -101,7 +105,7 @@ def test_ddg_burst_enable(mock_ddg):
mock_ddg.burst_mode.get() == BURSTCONFIG.FIRST_CYCLE.value
def test_ddg_wait_for_event_status(mock_ddg):
def test_ddg_wait_for_event_status(mock_ddg: DelayGeneratorCSAXS):
"""Test setting wait for event status."""
mock_ddg: DelayGeneratorCSAXS
mock_ddg.state.event_status._read_pv.mock_data = 0
@@ -117,7 +121,7 @@ def test_ddg_wait_for_event_status(mock_ddg):
# assert status.done is True
def test_ddg_set_io_values(mock_ddg):
def test_ddg_set_io_values(mock_ddg: DelayGeneratorCSAXS):
"""Test setting IO values."""
mock_ddg.set_io_values(channel="ab", amplitude=3, offset=2, polarity=1, mode="ttl")
assert mock_ddg.ab.io.amplitude.get() == 3
@@ -138,7 +142,7 @@ def test_ddg_set_io_values(mock_ddg):
assert attr.nim_mode.get() == 1
def test_ddg_set_delay_pairs(mock_ddg):
def test_ddg_set_delay_pairs(mock_ddg: DelayGeneratorCSAXS):
"""Test setting delay pairs."""
mock_ddg.set_delay_pairs(channel="ab", delay=0.1, width=0.2)
assert np.isclose(mock_ddg.ab.delay.get(), 0.1)
@@ -156,52 +160,143 @@ def test_ddg_set_delay_pairs(mock_ddg):
assert np.isclose(getattr(mock_ddg, channel).ch2.setpoint.get(), delay + 0.2)
def test_ddg1_on_connected(mock_ddg1):
#########################
### Test DDG1 Device ####
#########################
@pytest.fixture(scope="function")
def mock_mcs_csaxs() -> Generator[MCSCardCSAXS, None, None]:
"""Fixture to mock the MCSCardCSAXS device."""
dm = DMMock()
with patched_device(
MCSCardCSAXS,
name="mcs",
prefix="X12SA-MCS-CSAXS:",
device_manager=dm,
_mock_pv_initial_value=0,
) as dev:
dev.enabled = True
dev.device_manager.devices["mcs"] = dev
try:
yield dev
finally:
dev.destroy()
@pytest.fixture(scope="function")
def mock_ddg1(mock_mcs_csaxs: MCSCardCSAXS) -> Generator[DDG1, None, None]:
"""Fixture to mock the DDG1 device."""
# Add enabled to mock_mcs_csaxs
dm_mock = mock_mcs_csaxs.device_manager
with patched_device(
DDG1, name="ddg1", prefix="test_ddg1:", device_manager=dm_mock, _mock_pv_initial_value=0
) as dev:
dev.enabled = True
dev.device_manager.devices["ddg1"] = dev
try:
yield dev
finally:
dev.destroy()
def test_ddg1_on_connected(mock_ddg1: DDG1):
"""Test the on_connected method of DDG1."""
mock_ddg1.on_connected()
# IO defaults
assert mock_ddg1.burst_mode.get() == 0
assert mock_ddg1.ab.io.amplitude.get() == 5.0
assert mock_ddg1.cd.io.offset.get() == 0.0
assert mock_ddg1.ef.io.polarity.get() == 1
assert mock_ddg1.gh.io.ttl_mode.get() == 1
mock_ddg1.burst_mode.put(1) # Set burst mode to 1, if connected should reset it to 0
mock_ddg1.burst_delay.put(5) # Set to non-zero, should reset to 0 on connected
mock_ddg1.burst_count.put(10) # Set to non-default, should reset to 1 on connected
with mock.patch.object(mock_ddg1, "set_io_values") as mock_set_io_values:
mock_ddg1.on_connected()
# reference defaults
assert mock_ddg1.ab.ch1.reference.get() == 0 # CHANNELREFERENCE.T0.value
assert mock_ddg1.ab.ch2.reference.get() == 1 # CHANNELREFERENCE.A.value
assert mock_ddg1.cd.ch1.reference.get() == 0 # CHANNELREFERENCE.T0.value
assert mock_ddg1.cd.ch2.reference.get() == 3 # CHANNELREFERENCE.C.value
assert mock_ddg1.ef.ch1.reference.get() == 4 # CHANNELREFERENCE.D.value
assert mock_ddg1.ef.ch2.reference.get() == 5 # CHANNELREFERENCE.E.value
assert mock_ddg1.gh.ch1.reference.get() == 0 # CHANNELREFERENCE.T0.value
assert mock_ddg1.gh.ch2.reference.get() == 7 # CHANNELREFERENCE.G.value
# Burst mode Defaults
assert mock_ddg1.burst_mode.get() == 0
assert mock_ddg1.burst_delay.get() == 0
assert mock_ddg1.burst_count.get() == 1
# Default trigger source
assert mock_ddg1.trigger_source.get() == 5 # TRIGGERSOURCE.SINGLE_SHOT.value
assert mock_set_io_values.call_count == len(DDG1_DEFAULT_IO_CONFIG)
for ch, config in DDG1_DEFAULT_IO_CONFIG.items():
assert mock.call(ch, **config) in mock_set_io_values.call_args_list
# Check reference values from DEFAULT_REFERENCES
for ch, refs in DDG1_DEFAULT_REFERENCES:
if ch == "A":
sub_ch = mock_ddg1.ab.ch1
elif ch == "B":
sub_ch = mock_ddg1.ab.ch2
elif ch == "C":
sub_ch = mock_ddg1.cd.ch1
elif ch == "D":
sub_ch = mock_ddg1.cd.ch2
elif ch == "E":
sub_ch = mock_ddg1.ef.ch1
elif ch == "F":
sub_ch = mock_ddg1.ef.ch2
elif ch == "G":
sub_ch = mock_ddg1.gh.ch1
elif ch == "H":
sub_ch = mock_ddg1.gh.ch2
assert sub_ch.reference.get() == refs.value
# Check Default trigger source
assert mock_ddg1.trigger_source.get() == DDG1_DEFAULT_TRIGGER_SOURCE.value
# Check proc state mode
assert mock_ddg1.state.proc_status_mode.get() == PROC_EVENT_MODE.EVENT.value
# Check the poll thread is started
assert mock_ddg1._poll_thread.is_alive()
assert not mock_ddg1._poll_thread_kill_event.is_set()
assert not mock_ddg1._poll_thread_poll_loop_done.is_set()
assert not mock_ddg1._poll_thread_run_event.is_set()
def test_ddg1_stage(mock_ddg1):
def test_ddg1_prepare_mcs(mock_ddg1: DDG1, mock_mcs_csaxs: MCSCardCSAXS):
"""Test the prepare_mcs method of DDG1."""
mcs = mock_mcs_csaxs
ddg = mock_ddg1
# Simulate default state
mcs.acquiring._read_pv.mock_data = 0 # not acquiring
mcs.erase_start.put(0) # reset erase start
# Prepare MCS on trigger
st = ddg._prepare_mcs_on_trigger(mcs)
assert st.done is False
assert st.success is False
assert mcs.erase_start.get() == 1 # erase started
# Simulate acquiring started
mcs.acquiring._read_pv.mock_data = 1 # acquiring
st.wait(2)
assert st.done is True
assert st.success is True
def test_ddg1_stage(mock_ddg1: DDG1):
"""Test the on_stage method of DDG1."""
exp_time = 0.1
frames_per_trigger = 10
mock_ddg1.burst_mode.put(1)
mock_ddg1.burst_mode.put(0) # Non-default, should be reset on stage
mock_ddg1.burst_delay.put(5) # Non-default, should be reset on stage
mock_ddg1.burst_count.put(10) # Non-default, should be reset on stage
mock_ddg1.scan_info.msg.scan_parameters["exp_time"] = exp_time
mock_ddg1.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
mock_ddg1.stage()
shutter_width = 2e-3 + exp_time * frames_per_trigger + 1e-3
assert np.isclose(mock_ddg1.burst_mode.get(), 1) # burst mode is enabled
assert np.isclose(mock_ddg1.burst_delay.get(), 0)
assert np.isclose(mock_ddg1.burst_period.get(), exp_time)
assert np.isclose(mock_ddg1.burst_period.get(), shutter_width)
# Trigger DDG2 through EXT/EN
assert np.isclose(mock_ddg1.ab.delay.get(), 2e-3)
assert np.isclose(mock_ddg1.ab.width.get(), 1e-6)
# Shutter channel cd
assert np.isclose(mock_ddg1.cd.delay.get(), 0)
assert np.isclose(mock_ddg1.cd.width.get(), 2e-3 + exp_time * frames_per_trigger + 1e-3)
assert np.isclose(mock_ddg1.cd.width.get(), shutter_width)
# MCS channel ef or gate
assert np.isclose(mock_ddg1.ef.delay.get(), 0)
assert np.isclose(mock_ddg1.ef.width.get(), 1e-6)
@@ -209,96 +304,266 @@ def test_ddg1_stage(mock_ddg1):
assert mock_ddg1.staged == ophyd.Staged.yes
def test_ddg1_trigger(mock_ddg1):
"""Test the on_trigger method of DDG1."""
mock_ddg1.state.event_status._read_pv.mock_data = STATUSBITS.NONE.value
def test_ddg1_on_trigger(mock_ddg1: DDG1):
"""
Test the on_trigger method of the DDG1.
We will test two scenarios:
I. Trigger is prepared, and resolves successfully after END_OF_BURST is reached in event status register.
II. Trigger is called while _poll_thread_loop_done is not yet finished from a previous trigger.
This may be the case if polling is yet to finsish. The next on_trigger should terminate the previous
polling, and work as expected. In addition, we will simulate that the mcs card is disabled, thus not prepared.
"""
ddg = mock_ddg1
# Make sure DDG is setup in default state through on_connected
ddg.on_connected()
# Check that poll thread is running and run event is not set
assert ddg._poll_thread.is_alive()
assert not ddg._poll_thread_run_event.is_set()
assert not ddg._poll_thread_poll_loop_done.is_set()
# Set the status register bit
ddg.state.event_status._read_pv.mock_data = STATUSBITS.ABORT_DELAY.value
#################################
# Scenario I - normal operation #
#################################
with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs:
mock_prepare_mcs.return_value = ophyd.StatusBase(done=True, success=True)
status = ddg.trigger()
# Check that the poll thread run event is set
assert ddg._poll_thread_run_event.is_set()
assert not ddg._poll_thread_poll_loop_done.is_set()
with mock.patch.object(mock_ddg1, "device_manager") as mock_device_manager:
# TODO add device manager DMMock, and properly test logic for mcs triggering.
mock_get = mock_device_manager.devices.get = mock.Mock(return_value=None)
status = mock_ddg1.trigger()
assert mock_get.call_args == mock.call("mcs", None)
assert status.done is False
assert status.success is False
assert mock_ddg1.trigger_shot.get() == 1
mock_ddg1.state.event_status._read_pv.mock_data = STATUSBITS.END_OF_BURST.value
assert ddg.trigger_shot.get() == 1
# Simulate that the event status bit reaches END_OF_BURST
ddg.state.event_status._read_pv.mock_data = STATUSBITS.END_OF_BURST.value
status.wait(timeout=1) # Wait for the status to be done
assert status.done is True
assert status.success is True
# Should finish the poll loop
ddg._poll_thread_poll_loop_done.wait(timeout=1)
assert not ddg._poll_thread_run_event.is_set()
def test_ddg1_stop(mock_ddg1):
"""Test the on_stop method of DDG1."""
mock_ddg1.burst_mode.put(1) # Enable burst mode
mock_ddg1.stop()
assert mock_ddg1.burst_mode.get() == 0 # Burst mode is disabled
############################################
# Scenario II - previous poll not finished #
# MCS card disabled #
############################################
# Set mcs card to enabled = False
ddg.device_manager.devices["mcs"].enabled = False
ddg.state.event_status._read_pv.mock_data = STATUSBITS.ABORT_DELAY.value
ddg._start_polling()
assert ddg._poll_thread_run_event.is_set()
with mock.patch.object(ddg, "_prepare_mcs_on_trigger") as mock_prepare_mcs:
status = ddg.trigger()
mock_prepare_mcs.assert_not_called() # MCS is disabled, should not be called
assert status.done is False
assert status.success is False
# Resolve the status by simulating END_OF_BURST
ddg.state.event_status._read_pv.mock_data = STATUSBITS.END_OF_BURST.value
status.wait(timeout=1) # Wait for the status to be done
assert status.done is True
assert status.success is True
# Wait for poll loop to finish
ddg._poll_thread_poll_loop_done.wait(timeout=1)
assert not ddg._poll_thread_run_event.is_set()
def test_ddg2_on_connected(mock_ddg2):
"""Test on connected method of DDG2."""
mock_ddg2.on_connected()
# IO defaults
assert mock_ddg2.burst_mode.get() == 0
assert mock_ddg2.ab.io.amplitude.get() == 5.0
assert mock_ddg2.cd.io.offset.get() == 0.0
assert mock_ddg2.ef.io.polarity.get() == 1
assert mock_ddg2.gh.io.ttl_mode.get() == 1
# def test_ddg1_trigger(mock_ddg1):
# """Test the on_trigger method of DDG1."""
# mock_ddg1.state.event_status._read_pv.mock_data = STATUSBITS.NONE.value
# reference defaults
assert mock_ddg2.ab.ch1.reference.get() == 0 # CHANNELREFERENCE.T0.value
assert mock_ddg2.ab.ch2.reference.get() == 1 # CHANNELREFERENCE.A.value
assert mock_ddg2.cd.ch1.reference.get() == 0 # CHANNELREFERENCE.T0.value
assert mock_ddg2.cd.ch2.reference.get() == 3 # CHANNELREFERENCE.C.value
assert mock_ddg2.ef.ch1.reference.get() == 0 # CHANNELREFERENCE.T0.value
assert mock_ddg2.ef.ch2.reference.get() == 5 # CHANNELREFERENCE.E.value
assert mock_ddg2.gh.ch1.reference.get() == 0 # CHANNELREFERENCE.T0.value
assert mock_ddg2.gh.ch2.reference.get() == 7 # CHANNELREFERENCE.G.value
# Default trigger source
assert mock_ddg2.trigger_source.get() == 1 # TRIGGERSOURCE.EXT_RISING_EDGE.value
# with mock.patch.object(mock_ddg1, "device_manager") as mock_device_manager:
# # TODO add device manager DMMock, and properly test logic for mcs triggering.
# mock_get = mock_device_manager.devices.get = mock.Mock(return_value=None)
# status = mock_ddg1.trigger()
# assert mock_get.call_args == mock.call("mcs", None)
# assert status.done is False
# assert status.success is False
# assert mock_ddg1.trigger_shot.get() == 1
# mock_ddg1.state.event_status._read_pv.mock_data = STATUSBITS.END_OF_BURST.value
# status.wait(timeout=1) # Wait for the status to be done
# assert status.done is True
# assert status.success is True
def test_ddg2_stage(mock_ddg2):
"""Test the on_stage method of DDG2."""
# def test_ddg1_stop(mock_ddg1):
# """Test the on_stop method of DDG1."""
# mock_ddg1.burst_mode.put(1) # Enable burst mode
# mock_ddg1.stop()
# assert mock_ddg1.burst_mode.get() == 0 # Burst mode is disabled
#########################
### Test DDG2 Device ####
#########################
@pytest.fixture(scope="function")
def mock_ddg2(mock_mcs_csaxs: MCSCardCSAXS) -> Generator[DDG2, None, None]:
"""Fixture to mock the DDG1 device."""
# Add enabled to mock_mcs_csaxs
dm_mock = mock_mcs_csaxs.device_manager
with patched_device(
DDG2, name="ddg2", prefix="test_ddg2:", device_manager=dm_mock, _mock_pv_initial_value=0
) as dev:
dev.enabled = True
dev.device_manager.devices["ddg2"] = dev
try:
yield dev
finally:
dev.destroy()
def test_ddg2_on_connected(mock_ddg2: DDG2):
"""Test the on_connected method of DDG1."""
mock_ddg2.burst_mode.put(1) # Set burst mode to 1, if connected should reset it to 0
mock_ddg2.burst_delay.put(5) # Set to non-zero, should reset to 0 on connected
mock_ddg2.burst_count.put(10) # Set to non-default, should reset to 1 on connected
with mock.patch.object(mock_ddg2, "set_io_values") as mock_set_io_values:
mock_ddg2.on_connected()
# Burst mode Defaults
assert mock_ddg2.burst_mode.get() == 0
assert mock_set_io_values.call_count == len(DDG2_DEFAULT_IO_CONFIG)
for ch, config in DDG2_DEFAULT_IO_CONFIG.items():
assert mock.call(ch, **config) in mock_set_io_values.call_args_list
# Check reference values from DEFAULT_REFERENCES
for ch, refs in DDG2_DEFAULT_REFERENCES:
if ch == "A":
sub_ch = mock_ddg2.ab.ch1
elif ch == "B":
sub_ch = mock_ddg2.ab.ch2
elif ch == "C":
sub_ch = mock_ddg2.cd.ch1
elif ch == "D":
sub_ch = mock_ddg2.cd.ch2
elif ch == "E":
sub_ch = mock_ddg2.ef.ch1
elif ch == "F":
sub_ch = mock_ddg2.ef.ch2
elif ch == "G":
sub_ch = mock_ddg2.gh.ch1
elif ch == "H":
sub_ch = mock_ddg2.gh.ch2
assert sub_ch.reference.get() == refs.value
# Check Default trigger source
assert mock_ddg2.trigger_source.get() == DDG2_DEFAULT_TRIGGER_SOURCE.value
def test_ddg2_on_stage(mock_ddg2: DDG2):
"""
Test the on_stage method of DDG2.
We will test two scenarios:
I. Stage device with valid parameters.
II. Stage device with invalid parameters (too short exp_time). Should raise ValueError.
"""
ddg = mock_ddg2
exp_time = 0.1
frames_per_trigger = 10
mock_ddg2.on_connected()
ddg.on_connected()
ddg.scan_info.msg.scan_parameters["exp_time"] = exp_time
ddg.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
mock_ddg2.burst_mode.put(0)
mock_ddg2.scan_info.msg.scan_parameters["exp_time"] = exp_time
mock_ddg2.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
# Set non-default burst mode settings
ddg.burst_mode.put(0)
ddg.burst_delay.put(5)
mock_ddg2.stage()
# Stage device with valid parameters
ddg.stage()
assert ddg.staged == ophyd.Staged.yes
assert ddg.burst_mode.get() == 1 # Burst mode is enabled
assert ddg.burst_delay.get() == 0 # Burst delay is set to 0
assert ddg.burst_count.get() == frames_per_trigger
assert ddg.burst_period.get() == exp_time
assert np.isclose(mock_ddg2.burst_mode.get(), 1) # Burst mode is enabled
assert np.isclose(mock_ddg2.ab.delay.get(), 0)
assert np.isclose(mock_ddg2.ab.width.get(), exp_time - 2e-4) # DEFAULT_READOUT_TIMES["ab"])
assert mock_ddg2.burst_count.get() == frames_per_trigger
assert np.isclose(mock_ddg2.burst_delay.get(), 0)
assert np.isclose(mock_ddg2.burst_period.get(), exp_time)
assert mock_ddg2.trigger_source.get() == TRIGGERSOURCE.EXT_RISING_EDGE.value
assert mock_ddg2.staged == ophyd.Staged.yes
mock_ddg2.unstage() # Reset staged state for next test
# Pulse width is exp_time - readout_time
burst_pulse_width = exp_time - DDG2_DEFAULT_READOUT_TIMES["ab"]
assert np.isclose(ddg.ab.delay.get(), 0)
assert np.isclose(ddg.ab.width.get(), burst_pulse_width)
# Unstage to reset
ddg.unstage() # Reset staged state for next test
exp_time_short = 2e-4 # too short exposure time
with pytest.raises(ValueError):
mock_ddg2.scan_info.msg.scan_parameters["exp_time"] = 2e-4 # too short exposure time
mock_ddg2.stage()
ddg.scan_info.msg.scan_parameters["exp_time"] = exp_time_short
ddg.stage()
def test_ddg2_trigger(mock_ddg2):
def test_ddg2_on_trigger(mock_ddg2: DDG2):
"""Test the on_trigger method of DDG2."""
mock_ddg2.trigger_shot.put(0)
status = mock_ddg2.trigger()
assert mock_ddg2.trigger_shot.get() == 0 # Should not trigger DDG2 via soft trigger
ddg = mock_ddg2
ddg.on_connected()
ddg.trigger_shot.put(0)
status = ddg.trigger()
assert ddg.trigger_shot.get() == 0 # Should not trigger DDG2 via soft trigger
status.wait()
assert status.done is True
assert status.success is True
def test_ddg2_stop(mock_ddg2):
def test_ddg2_on_stop(mock_ddg2: DDG2):
"""Test the on_stop method of DDG2."""
mock_ddg2.burst_mode.put(1) # Enable burst mode
mock_ddg2.stop()
assert mock_ddg2.burst_mode.get() == 0 # Burst mode is disabled
ddg = mock_ddg2
ddg.on_connected()
ddg.burst_mode.put(1) # Enable burst mode
ddg.stop()
assert ddg.burst_mode.get() == 0 # Burst mode is disabled
# def test_ddg2_stage(mock_ddg2):
# """Test the on_stage method of DDG2."""
# exp_time = 0.1
# frames_per_trigger = 10
# mock_ddg2.on_connected()
# mock_ddg2.burst_mode.put(0)
# mock_ddg2.scan_info.msg.scan_parameters["exp_time"] = exp_time
# mock_ddg2.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
# mock_ddg2.stage()
# assert np.isclose(mock_ddg2.burst_mode.get(), 1) # Burst mode is enabled
# assert np.isclose(mock_ddg2.ab.delay.get(), 0)
# assert np.isclose(mock_ddg2.ab.width.get(), exp_time - 2e-4) # DEFAULT_READOUT_TIMES["ab"])
# assert mock_ddg2.burst_count.get() == frames_per_trigger
# assert np.isclose(mock_ddg2.burst_delay.get(), 0)
# assert np.isclose(mock_ddg2.burst_period.get(), exp_time)
# assert mock_ddg2.trigger_source.get() == TRIGGERSOURCE.EXT_RISING_EDGE.value
# assert mock_ddg2.staged == ophyd.Staged.yes
# mock_ddg2.unstage() # Reset staged state for next test
# with pytest.raises(ValueError):
# mock_ddg2.scan_info.msg.scan_parameters["exp_time"] = 2e-4 # too short exposure time
# mock_ddg2.stage()
# def test_ddg2_trigger(mock_ddg2):
# """Test the on_trigger method of DDG2."""
# mock_ddg2.trigger_shot.put(0)
# status = mock_ddg2.trigger()
# assert mock_ddg2.trigger_shot.get() == 0 # Should not trigger DDG2 via soft trigger
# status.wait()
# assert status.done is True
# assert status.success is True
# def test_ddg2_stop(mock_ddg2):
# """Test the on_stop method of DDG2."""
# mock_ddg2.burst_mode.put(1) # Enable burst mode
# mock_ddg2.stop()
# assert mock_ddg2.burst_mode.get() == 0 # Burst mode is disabled

View File

@@ -1,298 +1,230 @@
# pylint: skip-file
import os
import threading
from typing import Generator
from unittest import mock
import ophyd
import pytest
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_lib.file_utils import get_full_path
from bec_server.device_server.tests.utils import DMMock
from ophyd_devices.tests.utils import MockPV
from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError
from ophyd_devices.tests.utils import patched_device
from csaxs_bec.devices.epics.falcon_csaxs import FalconcSAXS, FalconTimeoutError
from csaxs_bec.devices.tests_utils.utils import patch_dual_pvs
from csaxs_bec.devices.epics.falcon_csaxs import (
ACQUIRESTATUS,
FalconcSAXS,
MappingSource,
TriggerSource,
)
@pytest.fixture(scope="function")
def mock_det():
name = "falcon"
prefix = "X12SA-SITORO:"
def mock_det() -> Generator[FalconcSAXS, None, None]:
"""Fixture to mock the FalconcSAXS device."""
name = "mcs_csaxs"
prefix = "X12SA-MCS-CSAXS:"
dm = DMMock()
with mock.patch.object(dm, "connector"):
with (
mock.patch(
"ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"
) as filemixin,
mock.patch(
"ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
) as mock_service_config,
):
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
with mock.patch.object(FalconcSAXS, "_init"):
det = FalconcSAXS(name=name, prefix=prefix, device_manager=dm)
patch_dual_pvs(det)
det.TIMEOUT_FOR_SIGNALS = 0.1
yield det
with patched_device(
FalconcSAXS,
name="falcon",
prefix="X12SA-SITORO:",
device_manager=dm,
_mock_pv_initial_value=1,
) as dev:
try:
for dotted_name, device in dev.walk_subdevices(include_lazy=True):
device.stage_sigs = {} # Remove stage signals
device.trigger_sigs = {} # Remove trigger signals
if hasattr(device, "plugin_type"):
device.plugin_type._read_pv.mock_data = device._plugin_type
yield dev
finally:
dev.destroy()
@pytest.mark.parametrize(
"trigger_source, mapping_source, ignore_gate, pixels_per_buffer, detector_state,"
" expected_exception",
[(1, 1, 0, 20, 0, False), (1, 1, 0, 20, 1, True)],
)
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
def test_init_detector(
mock_det,
trigger_source,
mapping_source,
ignore_gate,
pixels_per_buffer,
detector_state,
expected_exception,
):
"""Test the _init function:
def test_falcon_init(mock_det: FalconcSAXS):
"""Test the initialization of the FalconcSAXS device."""
assert mock_det._readout_time == mock_det.MIN_READOUT
assert mock_det._value_pixel_per_buffer == 20
assert mock_det._queue_size == 2000
assert mock_det._full_path == ""
This includes testing the functions:
- _init_detector
- _stop_det
- _set_trigger
--> Testing the filewriter is done in test_init_filewriter
Validation upon setting the correct PVs
def test_falcon_on_connected(mock_det: FalconcSAXS):
"""Test the on_connected method of the FalconcSAXS device."""
falcon = mock_det
# Set known default values
falcon.preset_mode.put(-1)
falcon.input_logic_polarity.put(-1)
falcon.auto_pixels_per_buffer.put(-1)
falcon.hdf5.enable.put(-1)
with (
mock.patch.object(falcon, "on_stop") as mock_on_stop,
mock.patch.object(falcon, "set_trigger") as mock_set_trigger,
):
falcon.on_connected()
mock_on_stop.assert_called_once()
mock_set_trigger.assert_called_once_with(
mapping_mode=MappingSource.MAPPING, trigger_source=TriggerSource.GATE, ignore_gate=0
)
# Detector default PV values
assert falcon.preset_mode.get() == "1" # Real Time
assert falcon.input_logic_polarity.get() == 0
assert falcon.auto_pixels_per_buffer.get() == 0
assert falcon.pixels_per_buffer.get() == falcon._value_pixel_per_buffer
# Backend default PV values
assert falcon.hdf5.enable.get() == "1" # Enabled
assert falcon.hdf5.xml_file_name.get() == "layout.xml"
assert falcon.hdf5.lazy_open.get() == "1" # Enabled
assert falcon.hdf5.temp_suffix.get() == ""
assert falcon.hdf5.queue_size.get() == falcon._queue_size
assert falcon.nd_array_mode.get() == 1
assert falcon.hdf5.file_template.get() == "%s%s"
assert falcon.hdf5.file_write_mode.get() == 2
def test_falcon_on_stage(mock_det: FalconcSAXS):
"""
mock_det.value_pixel_per_buffer = pixels_per_buffer
mock_det.state._read_pv.mock_data = detector_state
if expected_exception:
with pytest.raises(FalconTimeoutError):
mock_det.timeout = 0.1
mock_det.custom_prepare.initialize_detector()
else:
mock_det.custom_prepare.initialize_detector()
assert mock_det.state.get() == detector_state
assert mock_det.collect_mode.get() == mapping_source
assert mock_det.pixel_advance_mode.get() == trigger_source
assert mock_det.ignore_gate.get() == ignore_gate
assert mock_det.preset_mode.get() == 1
assert mock_det.erase_all.get() == 1
assert mock_det.input_logic_polarity.get() == 0
assert mock_det.auto_pixels_per_buffer.get() == 0
assert mock_det.pixels_per_buffer.get() == pixels_per_buffer
@pytest.mark.parametrize(
"readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)]
)
def test_update_readout_time(mock_det, readout_time, expected_value):
if readout_time is None:
mock_det.custom_prepare.update_readout_time()
assert mock_det.readout_time == expected_value
else:
mock_det.scaninfo.readout_time = readout_time
mock_det.custom_prepare.update_readout_time()
assert mock_det.readout_time == expected_value
def test_initialize_default_parameter(mock_det):
with mock.patch.object(
mock_det.custom_prepare, "update_readout_time"
) as mock_update_readout_time:
mock_det.custom_prepare.initialize_default_parameter()
assert mock_det.value_pixel_per_buffer == 20
mock_update_readout_time.assert_called_once()
@pytest.mark.parametrize(
"scaninfo",
[
(
{
"eacc": "e12345",
"num_points": 500,
"frames_per_trigger": 1,
"exp_time": 0.1,
"filepath": "test.h5",
"scan_id": "123",
"mokev": 12.4,
}
)
],
)
def test_stage(mock_det, scaninfo):
"""Test the stage function:
This includes testing _prep_det
Test the on_stage method of the FalconcSAXS device.
All relevant information is available in the scan_info attribute and used
to bootstrap the detector for the upcoming acquisition. Two scenarios are tested:
I. Normal case with exposure time larger than readout time
II. Case where exposure time is smaller than readout time, which should raise an exception.
"""
with (
mock.patch.object(mock_det.custom_prepare, "set_trigger") as mock_set_trigger,
mock.patch.object(
mock_det.custom_prepare, "prepare_data_backend"
) as mock_prep_data_backend,
mock.patch.object(
mock_det.custom_prepare, "publish_file_location"
) as mock_publish_file_location,
mock.patch.object(mock_det.custom_prepare, "arm_acquisition") as mock_arm_acquisition,
):
mock_det.scaninfo.exp_time = scaninfo["exp_time"]
mock_det.scaninfo.num_points = scaninfo["num_points"]
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
mock_det.stage()
mock_set_trigger.assert_called_once()
assert mock_det.preset_real.get() == scaninfo["exp_time"]
assert mock_det.pixels_per_run.get() == int(
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
)
mock_prep_data_backend.assert_called_once()
mock_publish_file_location.assert_called_once_with(done=False, successful=False)
mock_arm_acquisition.assert_called_once()
falcon = mock_det
num_points = 10
exp_time = 0.2
frames_per_trigger = 5
falcon.scan_info.msg.num_points = num_points
falcon.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
falcon.scan_info.msg.scan_parameters["exp_time"] = exp_time
falcon.hdf5.array_counter.put(5) # Set to non-zero to check reset
# I. Normal case
falcon.stage()
assert falcon.staged is ophyd.Staged.yes
assert falcon._full_path == get_full_path(falcon.scan_info.msg, falcon.name)
file_path = falcon.hdf5.file_path.get()
file_name = falcon.hdf5.file_name.get()
assert os.path.join(file_path, file_name) == falcon._full_path
assert falcon.preset_real_time.get() == exp_time
assert falcon.pixels_per_run.get() == num_points * frames_per_trigger
assert falcon.hdf5.num_capture.get() == num_points * frames_per_trigger
assert falcon.hdf5.array_counter.get() == 0
assert falcon.hdf5.capture.get() == 1
assert falcon.start_all.get() == 1
# II. Unstage device first
falcon.unstage()
exp_time = 1e-3 # Smaller than readout time
falcon.scan_info.msg.scan_parameters["exp_time"] = exp_time
with pytest.raises(ValueError):
falcon.stage()
assert falcon.staged is not ophyd.Staged.no
@pytest.mark.parametrize(
"scaninfo",
[
(
{
"filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data.h5",
"num_points": 500,
"frames_per_trigger": 1,
}
),
(
{
"filepath": "/das/work/p18/p18533/data/S00000-S00999/S00001/data1234.h5",
"num_points": 500,
"frames_per_trigger": 1,
}
),
],
)
def test_prepare_data_backend(mock_det, scaninfo):
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
mock_det.scaninfo.num_points = scaninfo["num_points"]
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
mock_det.scaninfo.scan_number = 1
mock_det.custom_prepare.prepare_data_backend()
file_path, file_name = os.path.split(scaninfo["filepath"])
assert mock_det.hdf5.file_path.get() == file_path
assert mock_det.hdf5.file_name.get() == file_name
assert mock_det.hdf5.file_template.get() == "%s%s"
assert mock_det.hdf5.num_capture.get() == int(
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
def test_falcon_on_pre_scan(mock_det: FalconcSAXS):
"""Test the on_pre_scan method of the FalconcSAXS device."""
falcon = mock_det
# I. Test normal case with success
falcon.acquire_busy._read_pv.mock_data = ACQUIRESTATUS.DONE
falcon.hdf5.capture._read_pv.mock_data = ACQUIRESTATUS.DONE
falcon = mock_det
st = falcon.on_pre_scan()
assert st.done is False
assert st.success is False
falcon.acquire_busy._read_pv.mock_data = ACQUIRESTATUS.ACQUIRING
assert st.done is False
assert st.success is False
falcon.hdf5.capture._read_pv.mock_data = ACQUIRESTATUS.ACQUIRING
st.wait(3)
assert st.done is True
assert st.success is True
# II. Test abort case with stop called
falcon.acquire_busy._read_pv.mock_data = ACQUIRESTATUS.DONE
falcon.hdf5.capture._read_pv.mock_data = ACQUIRESTATUS.DONE
st = falcon.on_pre_scan()
assert st.done is False
assert st.success is False
falcon.stop()
with pytest.raises(DeviceStoppedError):
st.wait(3)
assert st.done is True
assert st.success is False
def test_falcon_stop(mock_det: FalconcSAXS):
"""Test the stop method of the FalconcSAXS device."""
falcon = mock_det
falcon.stop_all.put(0)
falcon.hdf5.capture.put(1)
falcon.erase_all.put(0)
falcon.stop()
assert falcon.stop_all.get() == 1
assert falcon.hdf5.capture.get() == 0
assert falcon.erase_all.get() == 1
def test_falcon_complete(mock_det: FalconcSAXS):
"""Test the complete method of the FalconcSAXS device."""
falcon = mock_det
num_points = 10
frames_per_trigger = 5
falcon.scan_info.msg.num_points = num_points
falcon.scan_info.msg.scan_parameters["frames_per_trigger"] = frames_per_trigger
# I. Test normal case with success
falcon.dxp.current_pixel._read_pv.mock_data = num_points * frames_per_trigger - 1
falcon.hdf5.array_counter._read_pv.mock_data = num_points * frames_per_trigger - 1
falcon._full_path = "/tmp/fake_path/test.h5"
st = falcon.on_complete()
assert st.done is False
assert st.success is False
falcon.dxp.current_pixel._read_pv.mock_data = num_points * frames_per_trigger
assert st.done is False
assert st.success is False
falcon.hdf5.array_counter._read_pv.mock_data = num_points * frames_per_trigger
st.wait(3)
assert st.done is True
assert st.success is True
assert falcon.file_event.get() == messages.FileMessage(
file_path="/tmp/fake_path/test.h5",
done=True,
successful=True,
device_name=falcon.name,
file_type="h5",
hinted_h5_entries=None,
metadata={},
)
assert mock_det.hdf5.file_write_mode.get() == 2
assert mock_det.hdf5.array_counter.get() == 0
assert mock_det.hdf5.capture.get() == 1
@pytest.mark.parametrize(
"scaninfo",
[
({"filepath": "test.h5", "successful": True, "done": False, "scan_id": "123"}),
({"filepath": "test.h5", "successful": False, "done": True, "scan_id": "123"}),
],
)
def test_publish_file_location(mock_det, scaninfo):
mock_det.scaninfo.scan_id = scaninfo["scan_id"]
mock_det.filepath.set(scaninfo["filepath"]).wait()
mock_det.custom_prepare.publish_file_location(
done=scaninfo["done"], successful=scaninfo["successful"]
# II. Test case where acquisition fails due to interruption
falcon.dxp.current_pixel._read_pv.mock_data = num_points * frames_per_trigger - 1
st = falcon.on_complete()
assert st.done is False
assert st.success is False
falcon.stop()
with pytest.raises(DeviceStoppedError):
st.wait(3)
assert falcon.file_event.get() == messages.FileMessage(
file_path="/tmp/fake_path/test.h5",
done=True,
successful=False,
device_name=falcon.name,
file_type="h5",
hinted_h5_entries=None,
metadata={},
)
if scaninfo["successful"] is None:
msg = messages.FileMessage(file_path=scaninfo["filepath"], done=scaninfo["done"])
else:
msg = messages.FileMessage(
file_path=scaninfo["filepath"], done=scaninfo["done"], successful=scaninfo["successful"]
)
expected_calls = [
mock.call(
MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name),
msg,
pipe=mock_det.connector.pipeline.return_value,
),
mock.call(
MessageEndpoints.file_event(mock_det.name),
msg,
pipe=mock_det.connector.pipeline.return_value,
),
]
assert mock_det.connector.set_and_publish.call_args_list == expected_calls
@pytest.mark.parametrize("detector_state, expected_exception", [(1, False), (0, True)])
def test_arm_acquisition(mock_det, detector_state, expected_exception):
with mock.patch.object(mock_det, "stop") as mock_stop:
mock_det.state._read_pv.mock_data = detector_state
if expected_exception:
with pytest.raises(FalconTimeoutError):
mock_det.timeout = 0.1
mock_det.custom_prepare.arm_acquisition()
mock_stop.assert_called_once()
else:
mock_det.custom_prepare.arm_acquisition()
assert mock_det.start_all.get() == 1
def test_trigger(mock_det):
with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
mock_det.trigger()
mock_on_trigger.assert_called_once()
def test_complete(mock_det):
with (
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
mock.patch.object(
mock_det.custom_prepare, "publish_file_location"
) as mock_publish_file_location,
):
mock_det.stopped = False
mock_det.complete()
assert mock_finished.call_count == 1
call = mock.call(done=True, successful=True)
assert mock_publish_file_location.call_args == call
def test_stop(mock_det):
with (
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
mock.patch.object(
mock_det.custom_prepare, "stop_detector_backend"
) as mock_stop_detector_backend,
):
mock_det.stop()
mock_stop_det.assert_called_once()
mock_stop_detector_backend.assert_called_once()
assert mock_det.stopped is True
@pytest.mark.parametrize(
"stopped, scaninfo",
[
(False, {"num_points": 500, "frames_per_trigger": 1}),
(True, {"num_points": 500, "frames_per_trigger": 1}),
],
)
def test_finished(mock_det, stopped, scaninfo):
with (
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
mock.patch.object(
mock_det.custom_prepare, "stop_detector_backend"
) as mock_stop_file_writer,
):
mock_det.stopped = stopped
mock_det.dxp.current_pixel._read_pv.mock_data = int(
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
)
mock_det.hdf5.array_counter._read_pv.mock_data = int(
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
)
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
mock_det.scaninfo.num_points = scaninfo["num_points"]
mock_det.custom_prepare.finished()
assert mock_det.stopped is stopped
mock_stop_det.assert_called_once()
mock_stop_file_writer.assert_called_once()

View File

@@ -7,10 +7,15 @@ from csaxs_bec.devices.omny.galil.fupr_ophyd import FuprGalilController, FuprGal
@pytest.fixture
def fsamroy():
def fsamroy(dm_with_devices):
FuprGalilController._reset_controller()
fsamroy_motor = FuprGalilMotor(
"A", name="fsamroy", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
"A",
name="fsamroy",
host="mpc2680.psi.ch",
port=8081,
socket_cls=SocketMock,
device_manager=dm_with_devices,
)
fsamroy_motor.controller.on()
assert isinstance(fsamroy_motor.controller, FuprGalilController)

View File

@@ -2,16 +2,31 @@ import copy
from unittest import mock
import pytest
from bec_server.device_server.tests.utils import DMMock
from ophyd_devices.tests.utils import SocketMock
from csaxs_bec.devices.npoint.npoint import NPointAxis, NPointController
from csaxs_bec.devices.omny.galil.fgalil_ophyd import FlomniGalilController, FlomniGalilMotor
from csaxs_bec.devices.omny.galil.fupr_ophyd import FuprGalilController, FuprGalilMotor
from csaxs_bec.devices.omny.galil.lgalil_ophyd import LamniGalilController, LamniGalilMotor
from csaxs_bec.devices.omny.galil.ogalil_ophyd import OMNYGalilController, OMNYGalilMotor
from csaxs_bec.devices.omny.galil.sgalil_ophyd import GalilController, SGalilMotor
from csaxs_bec.devices.omny.rt.rt_flomni_ophyd import RtFlomniController, RtFlomniMotor
from csaxs_bec.devices.omny.rt.rt_lamni_ophyd import RtLamniController, RtLamniMotor
from csaxs_bec.devices.omny.rt.rt_omny_ophyd import RtOMNYController, RtOMNYMotor
from csaxs_bec.devices.smaract.smaract_ophyd import SmaractController, SmaractMotor
@pytest.fixture(scope="function")
def leyey():
def leyey(dm_with_devices):
LamniGalilController._reset_controller()
leyey_motor = LamniGalilMotor(
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
"H",
name="leyey",
host="mpc2680.psi.ch",
port=8081,
socket_cls=SocketMock,
device_manager=dm_with_devices,
)
leyey_motor.controller.on()
yield leyey_motor
@@ -20,10 +35,15 @@ def leyey():
@pytest.fixture(scope="function")
def leyex():
def leyex(dm_with_devices):
LamniGalilController._reset_controller()
leyex_motor = LamniGalilMotor(
"A", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
"A",
name="leyey",
host="mpc2680.psi.ch",
port=8081,
socket_cls=SocketMock,
device_manager=dm_with_devices,
)
leyex_motor.controller.on()
yield leyex_motor
@@ -151,3 +171,51 @@ def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages
except Exception as e:
print(e)
assert leyex.controller.sock.buffer_put == socket_put_messages
def test_wait_for_connection_called():
"""Test that wait_for_connection is called on all motors that have a socket controller."""
dm = DMMock()
testable_connections = [
(NPointAxis, NPointController),
(FlomniGalilMotor, FlomniGalilController),
(FuprGalilMotor, FuprGalilController),
(LamniGalilMotor, LamniGalilController),
(OMNYGalilMotor, OMNYGalilController),
(SGalilMotor, GalilController),
(RtFlomniMotor, RtFlomniController),
(RtLamniMotor, RtLamniController),
(RtOMNYMotor, RtOMNYController),
(SmaractMotor, SmaractController),
]
for motor_cls, controller_cls in testable_connections:
# Store values to restore later
ctrl_axis_backup = controller_cls._axes_per_controller
try:
controller_cls._reset_controller()
controller_cls._axes_per_controller = 3
motor = motor_cls(
"C",
name="test_motor",
host="mpc2680.psi.ch",
port=8081,
socket_cls=SocketMock,
device_manager=dm,
)
with mock.patch.object(motor.controller, "on") as mock_on:
motor.wait_for_connection(timeout=5.0)
assert mock_on.call_args_list[-1] == mock.call(timeout=5.0)
# Make sure destroy calls controller off
with mock.patch.object(motor.controller, "off") as mock_off:
motor.destroy()
assert mock_off.call_count == 1
assert mock_off.call_args_list[0] == mock.call(update_config=False)
assert motor._destroyed is True
finally:
controller_cls._reset_controller()
controller_cls._axes_per_controller = ctrl_axis_backup

View File

@@ -7,10 +7,15 @@ from csaxs_bec.devices.omny.galil.fgalil_ophyd import FlomniGalilController, Flo
@pytest.fixture(scope="function")
def leyey():
def leyey(dm_with_devices):
FlomniGalilController._reset_controller()
leyey_motor = FlomniGalilMotor(
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
"H",
name="leyey",
host="mpc2680.psi.ch",
port=8081,
socket_cls=SocketMock,
device_manager=dm_with_devices,
)
leyey_motor.controller.on()
yield leyey_motor
@@ -19,10 +24,15 @@ def leyey():
@pytest.fixture(scope="function")
def leyex():
def leyex(dm_with_devices):
FlomniGalilController._reset_controller()
leyex_motor = FlomniGalilMotor(
"H", name="leyey", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
"H",
name="leyey",
host="mpc2680.psi.ch",
port=8081,
socket_cls=SocketMock,
device_manager=dm_with_devices,
)
leyex_motor.controller.on()
yield leyex_motor
@@ -157,11 +167,16 @@ def test_find_reference(leyex, axis_nr, socket_put_messages, socket_get_messages
],
)
def test_fosaz_light_curtain_is_triggered(
axis_Id, socket_put_messages, socket_get_messages, triggered
axis_Id, socket_put_messages, socket_get_messages, triggered, dm_with_devices
):
"""test that the light curtain is triggered"""
fosaz = FlomniGalilMotor(
axis_Id, name="fosaz", host="mpc2680.psi.ch", port=8081, socket_cls=SocketMock
axis_Id,
name="fosaz",
host="mpc2680.psi.ch",
port=8081,
socket_cls=SocketMock,
device_manager=dm_with_devices,
)
fosaz.controller.on()
fosaz.controller.sock.flush_buffer()

View File

@@ -1,5 +1,7 @@
# pylint: skip-file
import threading
from copy import deepcopy
from typing import Generator
from unittest import mock
import numpy as np
@@ -8,6 +10,7 @@ import pytest
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_server.device_server.tests.utils import DMMock
from ophyd_devices.interfaces.base_classes.psi_device_base import DeviceStoppedError
from ophyd_devices.tests.utils import MockPV, patch_dual_pvs
from csaxs_bec.devices.epics.mcs_card.mcs_card import (
@@ -21,7 +24,7 @@ from csaxs_bec.devices.epics.mcs_card.mcs_card import (
READMODE,
MCSCard,
)
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import READYTOREAD, MCSCardCSAXS
from csaxs_bec.devices.epics.mcs_card.mcs_card_csaxs import MCSCardCSAXS
@pytest.fixture(scope="function")
@@ -46,434 +49,237 @@ def test_mcs_card(mock_mcs_card):
@pytest.fixture(scope="function")
def mock_mcs_csaxs():
def mock_mcs_csaxs() -> Generator[MCSCardCSAXS, None, None]:
"""Fixture to mock the MCSCardCSAXS device."""
name = "mcs_csaxs"
prefix = "X12SA-MCS-CSAXS:"
dm = DMMock()
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
mcs_card_csaxs = MCSCardCSAXS(name=name, prefix=prefix, device_manager=dm)
patch_dual_pvs(mcs_card_csaxs)
yield mcs_card_csaxs
try:
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
mcs_card_csaxs = MCSCardCSAXS(name=name, prefix=prefix, device_manager=dm)
patch_dual_pvs(mcs_card_csaxs)
yield mcs_card_csaxs
finally:
mcs_card_csaxs.on_destroy()
def test_mcs_card_csaxs(mock_mcs_csaxs):
def test_mcs_card_csaxs(mock_mcs_csaxs: MCSCardCSAXS):
"""Test the MCSCardCSAXS initialization."""
assert mock_mcs_csaxs.name == "mcs_csaxs"
assert mock_mcs_csaxs.prefix == "X12SA-MCS-CSAXS:"
assert mock_mcs_csaxs.counter_mapping == {
"mcs_csaxs_counters_mca1": "current1",
"mcs_csaxs_counters_mca2": "current2",
"mcs_csaxs_counters_mca3": "current3",
"mcs_csaxs_counters_mca4": "current4",
"mcs_csaxs_counters_mca5": "count_time",
}
assert mock_mcs_csaxs._mcs_clock == 1e7 # 10 MHz
assert mock_mcs_csaxs._acquisition_group == "monitored"
assert mock_mcs_csaxs._num_total_triggers == 0
assert mock_mcs_csaxs._mcs_clock == 1e7
assert mock_mcs_csaxs._pv_timeout == 2.0
assert mock_mcs_csaxs._mca_counter_index == 0
assert mock_mcs_csaxs._current_data_index == 0
assert mock_mcs_csaxs._current_data == {}
assert mock_mcs_csaxs.NUM_MCA_CHANNELS == 32
def test_mcs_card_csaxs_on_connected(mock_mcs_csaxs):
def test_mcs_card_csaxs_on_connected(mock_mcs_csaxs: MCSCardCSAXS):
"""Test the on_connected method of MCSCardCSAXS."""
mcs = mock_mcs_csaxs
mcs.on_connected()
# Stop called
assert mcs.stop_all.get() == 1
# Channel advance settings
assert mcs.channel_advance.get() == CHANNELADVANCE.EXTERNAL
assert mcs.channel1_source.get() == CHANNEL1SOURCE.EXTERNAL
assert mcs.prescale.get() == 1
#
assert mcs.user_led.get() == 0
# Only 5 channels are connected
assert mcs.mux_output.get() == 5
# input output settings
assert mcs.input_mode.get() == INPUTMODE.MODE_3
assert mcs.input_polarity.get() == POLARITY.NORMAL
assert mcs.output_mode.get() == OUTPUTMODE.MODE_2
assert mcs.output_polarity.get() == POLARITY.NORMAL
assert mcs.count_on_start.get() == 0
assert mcs.read_mode.get() == READMODE.PASSIVE
assert mcs.acquire_mode.get() == ACQUIREMODE.MCS
with mock.patch.object(mcs.counters.mca1, "subscribe") as mock_mca_subscribe:
with (
mock.patch.object(mcs.counters.mca1, "subscribe") as mock_mca_subscribe,
mock.patch.object(mcs, "mcs_recovery") as mock_mcs_recovery,
mock.patch.object(mcs._scan_done_thread, "start") as mock_scan_done_thread_start,
):
mcs.on_connected()
# Stop called
assert mcs.stop_all.get() == 1
# Channel advance settings
assert mcs.channel_advance.get() == CHANNELADVANCE.EXTERNAL
assert mcs.channel1_source.get() == CHANNEL1SOURCE.EXTERNAL
assert mcs.prescale.get() == 1
assert mcs.user_led.get() == 0
# Mux output
assert mcs.mux_output.get() == mcs.NUM_MCA_CHANNELS
# input output settings
assert mcs.input_mode.get() == INPUTMODE.MODE_3
assert mcs.input_polarity.get() == POLARITY.NORMAL
assert mcs.output_mode.get() == OUTPUTMODE.MODE_2
assert mcs.output_polarity.get() == POLARITY.NORMAL
assert mcs.count_on_start.get() == 0
assert mcs.read_mode.get() == READMODE.PASSIVE
assert mcs.acquire_mode.get() == ACQUIREMODE.MCS
# Check if subscriptions are setup correctly
assert mock_mca_subscribe.call_args == mock.call(mcs._on_counter_update, run=False)
# Check if recovery is called
mock_mcs_recovery.assert_called_once_with(timeout=1)
# Check if scan done thread is started
mock_scan_done_thread_start.assert_called_once()
def test_mcs_card_csaxs_stage(mock_mcs_csaxs):
def test_mcs_card_csaxs_stage(mock_mcs_csaxs: MCSCardCSAXS):
"""Test on stage method of MCSCardCSAXS"""
mcs = mock_mcs_csaxs
triggers = 5
num_points = 10
mcs.scan_info.msg.scan_parameters["frames_per_trigger"] = triggers
mcs.erase_all.put(0)
mcs.scan_info.msg.num_points = num_points
# Simulate that the MCS card is still acquiring, and that current channel is !=0
mcs.current_channel._read_pv.mock_data = 2 # Simulate that current channel is not zero
mcs.erase_all.put(0) # Set erase_all to 0
mcs._current_data = {"mca1": [1, 2, 3]} # Simulate existing data
mcs._scan_done_callbacks = [lambda: None] # Simulate existing callbacks
mcs._start_monitor_async_data_emission.set() # Simulate that monitoring is started
mcs._omit_mca_callbacks.set() # Simulate that mca callbacks are omitted
mcs.stage()
# Check that card is staged
assert mcs._staged == ophyd.Staged.yes
assert mcs.erase_all.get() == 1
# Check that erase_all, stop_all, preset_real, num_use_all are set correctly
assert mcs.erase_all.get() == 1 # Should be set to 1 as current_channel !=0
assert mcs.preset_real.get() == 0
assert mcs.num_use_all.get() == triggers
# Check that internal variables are reset
assert mcs._num_total_triggers == triggers * num_points
assert mcs._current_data == {}
assert mcs._scan_done_callbacks == []
assert mcs._current_data_index == 0
# Check that thread events are cleared properly
assert not mcs._start_monitor_async_data_emission.is_set()
assert not mcs._omit_mca_callbacks.is_set()
def test_mcs_card_csaxs_unstage(mock_mcs_csaxs):
"""Test unstage method of MCSCardCSAXS"""
mcs = mock_mcs_csaxs
mcs.stop_all.put(0)
mcs.ready_to_read.put(0)
mcs.erase_all.put(1)
mcs.erase_all.put(0)
mcs.unstage()
assert mcs.stop_all.get() == 1
assert mcs.ready_to_read.get() == READYTOREAD.DONE
assert mcs.erase_all.get() == 0
assert mcs.erase_all.get() == 1
def test_mcs_card_csaxs_complete_and_stop(mock_mcs_csaxs):
"""Test complete method of MCSCarcCSAXS"""
def test_mcs_card_csaxs_complete_and_stop(mock_mcs_csaxs: MCSCardCSAXS):
"""
Test complete method of MCSCarcCSAXS.
Two use cases:
I. Acquisition is stopped externally
II. Acquisition completes normally
"""
mcs = mock_mcs_csaxs
mcs.acquiring._read_pv.mock_data = ACQUIRING.ACQUIRING
# Make sure that device on_connected has been called which starts the monitoring thread
mcs.on_connected()
#######################
# I. Use case where acquisition is stopped
#######################
st = mcs.complete()
assert st.done is False
mcs.stop_all.put(0)
mcs.ready_to_read.put(READYTOREAD.PROCESSING)
assert mcs._start_monitor_async_data_emission.is_set()
# Status should be cancelled by stop
mcs.stop()
with pytest.raises(Exception):
with pytest.raises(DeviceStoppedError):
st.wait(timeout=3)
# Callback on status failure should stop monitoring
mcs._start_monitor_async_data_emission.wait(2)
assert not mcs._start_monitor_async_data_emission.is_set()
#######################
# II. Use case where acquisition completes normally
#######################
mcs._current_data_index = 0
mcs.scan_info.msg.num_points = 10
mcs.acquiring._read_pv.mock_data = ACQUIRING.ACQUIRING
st = mcs.complete()
assert st.done is False
assert mcs._start_monitor_async_data_emission.is_set()
mcs.acquiring._read_pv.mock_data = ACQUIRING.DONE
# This should now automatically complete the status
mcs._current_data_index = 10
st.wait(timeout=3)
assert st.done is True
assert st.success is False
assert mcs.stop_all.get() == 1
assert mcs.ready_to_read.get() == READYTOREAD.DONE
assert st.success is True
# Clean up procedure should stop the async_data monitoring
mcs._start_monitor_async_data_emission.wait(2)
assert not mcs._start_monitor_async_data_emission.is_set()
def test_mcs_card_csaxs_on_counter_updated(mock_mcs_csaxs):
def test_mcs_recovery(mock_mcs_csaxs: MCSCardCSAXS):
mcs = mock_mcs_csaxs
# Called for mca1
# Simulate ongoing acquisition
mcs.erase_all._read_pv.mock_data = 0
mcs.stop_all._read_pv.mock_data = 0
mcs.erase_start.put(0)
mcs.mcs_recovery(timeout=0.1)
assert mcs.erase_all.get() == 1
assert mcs.stop_all.get() == 1
assert mcs.erase_start.get() == 1
assert not mcs._omit_mca_callbacks.is_set()
def test_mcs_card_csaxs_on_counter_updated(mock_mcs_csaxs: MCSCardCSAXS):
"""
Test the on_counter_update method of MCSCardCSAXS.
We will test 2 use cases:
I. Suppressed callbacks
II. Callback from 32 mca counters, should result in data being sent to BEC
"""
mcs = mock_mcs_csaxs
# I. Suppressed callbacks
mcs._omit_mca_callbacks.set()
kwargs = {"obj": mcs.counters.mca1}
mcs._on_counter_update(1, **kwargs)
assert mcs.mcs.mca1.get() == 1
assert mcs.bpm.current1.get() == 1
assert mcs.counter_updated == [mcs.counters.mca1.name]
# Called for mca2
kwargs = {"obj": mcs.counters.mca2}
mcs._on_counter_update(np.array([2, 4]), **kwargs)
assert mcs.mcs.mca2.get() == [2, 4]
assert np.isclose(mcs.bpm.current2.get(), 3)
assert mcs.counter_updated == [mcs.counters.mca1.name, mcs.counters.mca2.name]
# Called for mca3
kwargs = {"obj": mcs.counters.mca3}
mcs._on_counter_update(1000, **kwargs)
assert mcs.mcs.mca3.get() == 1000
assert mcs.bpm.current3.get() == 1000
assert mcs.counter_updated == [
mcs.counters.mca1.name,
mcs.counters.mca2.name,
mcs.counters.mca3.name,
]
# Called for mca4
kwargs = {"obj": mcs.counters.mca4}
mcs._on_counter_update(np.array([20, 40]), **kwargs)
assert mcs.mcs.mca4.get() == [20, 40]
assert np.isclose(mcs.bpm.current4.get(), 30)
assert mcs.counter_updated == [
mcs.counters.mca1.name,
mcs.counters.mca2.name,
mcs.counters.mca3.name,
mcs.counters.mca4.name,
]
# Called for mca5
assert mcs.ready_to_read.get() == 0
kwargs = {"obj": mcs.counters.mca5}
mcs._on_counter_update(np.array([10000, 10000]), **kwargs)
assert np.isclose(mcs.bpm.count_time.get(), 10000 / 1e7)
assert mcs.mcs.mca5.get() == [10000, 10000]
assert mcs._current_data == {}
# II. Callback from 32 mca counters
mcs._omit_mca_callbacks.clear()
mcs._mca_counter_index = 0
mcs._current_data_index = 0
val = mcs.mca.get()
# @pytest.fixture(scope="function")
# def mock_det():
# name = "mcs"
# prefix = "X12SA-MCS:"
# dm = DMMock()
# with mock.patch.object(dm, "connector"):
# with (
# mock.patch(
# "ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"
# ) as filemixin,
# mock.patch(
# "ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
# ) as mock_service_config,
# ):
# with mock.patch.object(ophyd, "cl") as mock_cl:
# mock_cl.get_pv = MockPV
# mock_cl.thread_class = threading.Thread
# with mock.patch.object(MCScSAXS, "_init"):
# det = MCScSAXS(name=name, prefix=prefix, device_manager=dm)
# patch_dual_pvs(det)
# det.TIMEOUT_FOR_SIGNALS = 0.1
# yield det
for ii in range(mcs.NUM_MCA_CHANNELS):
counter = getattr(mcs.counters, f"mca{ii+1}")
kwargs = {"obj": counter, "timestamp": 1.0}
if ii % 2 == 1:
value = np.array([ii, (ii + 1) * 2])
else:
value = ii
mcs._on_counter_update(value, **kwargs)
if ii < (mcs.NUM_MCA_CHANNELS - 1):
assert mcs._current_data_index == 0
assert mcs._mca_counter_index == ii + 1
assert counter.attr_name in mcs._current_data
assert (
mcs._current_data[counter.attr_name]["value"] == value.tolist()
if isinstance(value, np.ndarray)
else [value]
)
buffer = deepcopy(mcs._current_data)
assert mcs.mca.get() == val # Async mca signal should not change
else:
# On last counter, data should be sent to BEC, and internal variables reset
buffer[counter.attr_name] = {
"value": value.tolist() if isinstance(value, np.ndarray) else [value],
"timestamp": 1.0,
}
assert mcs._mca_counter_index == 0
assert mcs._current_data_index == 1
assert mcs._current_data == {}
# def test_init():
# """Test the _init function:"""
# name = "eiger"
# prefix = "X12SA-ES-EIGER9M:"
# dm = DMMock()
# with mock.patch.object(dm, "connector"):
# with (
# mock.patch("ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"),
# mock.patch(
# "ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
# ),
# ):
# with mock.patch.object(ophyd, "cl") as mock_cl:
# mock_cl.get_pv = MockPV
# with (
# mock.patch(
# "csaxs_bec.devices.epics.mcs_csaxs.MCSSetup.initialize_detector"
# ) as mock_init_det,
# mock.patch(
# "csaxs_bec.devices.epics.mcs_csaxs.MCSSetup.initialize_detector_backend"
# ) as mock_init_backend,
# ):
# MCScSAXS(name=name, prefix=prefix, device_manager=dm)
# mock_init_det.assert_called_once()
# mock_init_backend.assert_called_once()
# @pytest.mark.parametrize(
# "trigger_source, channel_advance, channel_source1, pv_channels",
# [
# (
# 3,
# 1,
# 0,
# {
# "user_led": 0,
# "mux_output": 5,
# "input_pol": 0,
# "output_pol": 1,
# "count_on_start": 0,
# "stop_all": 1,
# },
# )
# ],
# )
# def test_initialize_detector(
# mock_det, trigger_source, channel_advance, channel_source1, pv_channels
# ):
# """Test the _init function:
# This includes testing the functions:
# - initialize_detector
# - stop_det
# - parent.set_trigger
# --> Testing the filewriter is done in test_init_filewriter
# Validation upon setting the correct PVs
# """
# mock_det.custom_prepare.initialize_detector() # call the method you want to test
# assert mock_det.channel_advance.get() == channel_advance
# assert mock_det.channel1_source.get() == channel_source1
# assert mock_det.user_led.get() == pv_channels["user_led"]
# assert mock_det.mux_output.get() == pv_channels["mux_output"]
# assert mock_det.input_polarity.get() == pv_channels["input_pol"]
# assert mock_det.output_polarity.get() == pv_channels["output_pol"]
# assert mock_det.count_on_start.get() == pv_channels["count_on_start"]
# assert mock_det.input_mode.get() == trigger_source
# def test_trigger(mock_det):
# """Test the trigger function:
# Validate that trigger calls the custom_prepare.on_trigger() function
# """
# with mock.patch.object(mock_det.custom_prepare, "on_trigger") as mock_on_trigger:
# mock_det.trigger()
# mock_on_trigger.assert_called_once()
# @pytest.mark.parametrize(
# "value, num_lines, num_points, done", [(100, 5, 500, False), (500, 5, 500, True)]
# )
# def test_progress_update(mock_det, value, num_lines, num_points, done):
# mock_det.num_lines.set(num_lines)
# mock_det.scaninfo.num_points = num_points
# calls = mock.call(sub_type="progress", value=value, max_value=num_points, done=done)
# with mock.patch.object(mock_det, "_run_subs") as mock_run_subs:
# mock_det.custom_prepare._progress_update(value=value)
# mock_run_subs.assert_called_once()
# assert mock_run_subs.call_args == calls
# @pytest.mark.parametrize(
# "values, expected_nothing",
# [([[100, 120, 140], [200, 220, 240], [300, 320, 340]], False), ([100, 200, 300], True)],
# )
# def test_on_mca_data(mock_det, values, expected_nothing):
# """Test the on_mca_data function:
# Validate that on_mca_data calls the custom_prepare.on_mca_data() function
# """
# with mock.patch.object(mock_det.custom_prepare, "_send_data_to_bec") as mock_send_data:
# mock_object = mock.MagicMock()
# for ii, name in enumerate(mock_det.custom_prepare.mca_names):
# mock_object.attr_name = name
# mock_det.custom_prepare._on_mca_data(obj=mock_object, value=values[ii])
# if not expected_nothing and ii < (len(values) - 1):
# assert mock_det.custom_prepare.mca_data[name] == values[ii]
# if not expected_nothing:
# mock_send_data.assert_called_once()
# assert mock_det.custom_prepare.acquisition_done is True
# @pytest.mark.parametrize(
# "metadata, mca_data",
# [
# (
# {"scan_id": 123},
# {
# "mca1": {"value": [100, 120, 140]},
# "mca3": {"value": [200, 220, 240]},
# "mca4": {"value": [300, 320, 340]},
# },
# )
# ],
# )
# def test_send_data_to_bec(mock_det, metadata, mca_data):
# mock_det.scaninfo.scan_msg = mock.MagicMock()
# mock_det.scaninfo.scan_msg.metadata = metadata
# mock_det.scaninfo.scan_id = metadata["scan_id"]
# mock_det.custom_prepare.mca_data = mca_data
# mock_det.custom_prepare._send_data_to_bec()
# device_metadata = mock_det.scaninfo.scan_msg.metadata
# metadata.update({"async_update": "append", "num_lines": mock_det.num_lines.get()})
# data = messages.DeviceMessage(signals=dict(mca_data), metadata=device_metadata)
# calls = mock.call(
# topic=MessageEndpoints.device_async_readback(
# scan_id=metadata["scan_id"], device=mock_det.name
# ),
# msg={"data": data},
# expire=1800,
# )
# assert mock_det.connector.xadd.call_args == calls
# @pytest.mark.parametrize(
# "scaninfo, triggersource, stopped, expected_exception",
# [
# (
# {"num_points": 500, "frames_per_trigger": 1, "scan_type": "step"},
# TriggerSource.MODE3,
# False,
# False,
# ),
# (
# {"num_points": 500, "frames_per_trigger": 1, "scan_type": "fly"},
# TriggerSource.MODE3,
# False,
# False,
# ),
# (
# {"num_points": 5001, "frames_per_trigger": 2, "scan_type": "step"},
# TriggerSource.MODE3,
# False,
# True,
# ),
# (
# {"num_points": 500, "frames_per_trigger": 2, "scan_type": "random"},
# TriggerSource.MODE3,
# False,
# True,
# ),
# ],
# )
# def test_stage(mock_det, scaninfo, triggersource, stopped, expected_exception):
# mock_det.scaninfo.num_points = scaninfo["num_points"]
# mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
# mock_det.scaninfo.scan_type = scaninfo["scan_type"]
# mock_det.stopped = stopped
# with mock.patch.object(mock_det.custom_prepare, "prepare_detector_backend") as mock_prep_fw:
# if expected_exception:
# with pytest.raises(MCSError):
# mock_det.stage()
# mock_prep_fw.assert_called_once()
# else:
# mock_det.stage()
# mock_prep_fw.assert_called_once()
# # Check set_trigger
# mock_det.input_mode.get() == triggersource
# if scaninfo["scan_type"] == "step":
# assert mock_det.num_use_all.get() == int(scaninfo["frames_per_trigger"]) * int(
# scaninfo["num_points"]
# )
# elif scaninfo["scan_type"] == "fly":
# assert mock_det.num_use_all.get() == int(scaninfo["num_points"])
# mock_det.preset_real.get() == 0
# # # CHeck custom_prepare.arm_acquisition
# # assert mock_det.custom_prepare.counter == 0
# # assert mock_det.erase_start.get() == 1
# # mock_prep_fw.assert_called_once()
# # # Check _prep_det
# # assert mock_det.cam.num_images.get() == int(
# # scaninfo["num_points"] * scaninfo["frames_per_trigger"]
# # )
# # assert mock_det.cam.num_frames.get() == 1
# # mock_publish_file_location.assert_called_with(done=False)
# # assert mock_det.cam.acquire.get() == 1
# def test_prepare_detector_backend(mock_det):
# mock_det.custom_prepare.prepare_detector_backend()
# assert mock_det.erase_all.get() == 1
# assert mock_det.read_mode.get() == ReadoutMode.EVENT
# def test_complete(mock_det):
# with (mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,):
# mock_det.complete()
# assert mock_finished.call_count == 1
# def test_stop_detector_backend(mock_det):
# mock_det.custom_prepare.stop_detector_backend()
# assert mock_det.custom_prepare.acquisition_done is True
# def test_stop(mock_det):
# with (
# mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
# mock.patch.object(
# mock_det.custom_prepare, "stop_detector_backend"
# ) as mock_stop_detector_backend,
# ):
# mock_det.stop()
# mock_stop_det.assert_called_once()
# mock_stop_detector_backend.assert_called_once()
# assert mock_det.stopped is True
# @pytest.mark.parametrize(
# "stopped, acquisition_done, acquiring_state, expected_exception",
# [
# (False, True, 0, False),
# (False, False, 0, True),
# (False, True, 1, True),
# (True, True, 0, True),
# ],
# )
# def test_finished(mock_det, stopped, acquisition_done, acquiring_state, expected_exception):
# mock_det.custom_prepare.acquisition_done = acquisition_done
# mock_det.acquiring._read_pv.mock_data = acquiring_state
# mock_det.scaninfo.num_points = 500
# mock_det.num_lines.put(500)
# mock_det.current_channel._read_pv.mock_data = 1
# mock_det.stopped = stopped
# if expected_exception:
# with pytest.raises(MCSTimeoutError):
# mock_det.timeout = 0.1
# mock_det.custom_prepare.finished()
# else:
# mock_det.custom_prepare.finished()
# if stopped:
# assert mock_det.stopped is stopped
# Check that the async mca signal is properly set
assert isinstance(mcs.mca.get(), messages.DeviceMessage)
assert len(mcs.mca.get().signals) == mcs.NUM_MCA_CHANNELS

View File

@@ -16,7 +16,10 @@ def controller():
"""
with mock.patch("ophyd_devices.utils.socket.SocketIO") as socket_cls:
controller = NPointController(
socket_cls=socket_cls, socket_host="localhost", socket_port=1234
socket_cls=socket_cls,
socket_host="localhost",
socket_port=1234,
device_manager=mock.MagicMock(),
)
controller.on()
controller.sock.reset_mock()
@@ -25,13 +28,18 @@ def controller():
@pytest.fixture
def npointx():
def npointx(dm_with_devices):
"""
Fixture to create a NPointAxis object.
"""
controller = mock.MagicMock()
npointx = NPointAxis(
axis_Id="A", name="npointx", host="localhost", port=1234, socket_cls=controller
axis_Id="A",
name="npointx",
host="localhost",
port=1234,
socket_cls=controller,
device_manager=dm_with_devices,
)
npointx.controller.on()
npointx.controller.sock.reset_mock()
@@ -107,13 +115,18 @@ def test_axis_get_in(npointx, axis, msg_in, msg_out):
npointx.controller.sock.put.assert_called_once_with(msg_in)
def test_axis_out_of_range(controller):
def test_axis_out_of_range(dm_with_devices):
"""
Test that an error is raised when trying to create an NPointAxis object with an invalid axis ID.
"""
with pytest.raises(ValueError):
npointx = NPointAxis(
axis_Id="G", name="npointx", host="localhost", port=1234, socket_cls=mock.MagicMock()
axis_Id="G",
name="npointx",
host="localhost",
port=1234,
socket_cls=mock.MagicMock(),
device_manager=dm_with_devices,
)

View File

@@ -1,449 +0,0 @@
# pylint: skip-file
import os
import threading
from unittest import mock
import ophyd
import pytest
from bec_lib import messages
from bec_lib.endpoints import MessageEndpoints
from bec_server.device_server.tests.utils import DMMock
from ophyd_devices.tests.utils import MockPV
from csaxs_bec.devices.epics.pilatus_csaxs import PilatuscSAXS
from csaxs_bec.devices.tests_utils.utils import patch_dual_pvs
@pytest.fixture(scope="function")
def mock_det():
name = "pilatus"
prefix = "X12SA-ES-PILATUS300K:"
dm = DMMock()
with mock.patch.object(dm, "connector"):
with (
mock.patch("ophyd_devices.interfaces.base_classes.bec_device_base.FileWriter"),
mock.patch(
"ophyd_devices.interfaces.base_classes.psi_detector_base.PSIDetectorBase._update_service_config"
),
):
with mock.patch.object(ophyd, "cl") as mock_cl:
mock_cl.get_pv = MockPV
mock_cl.thread_class = threading.Thread
with mock.patch.object(PilatuscSAXS, "_init"):
det = PilatuscSAXS(name=name, prefix=prefix, device_manager=dm)
patch_dual_pvs(det)
yield det
@pytest.mark.parametrize("trigger_source, detector_state", [(1, 0)])
# TODO rewrite this one, write test for init_detector, init_filewriter is tested
def test_init_detector(mock_det, trigger_source, detector_state):
"""Test the _init function:
This includes testing the functions:
- _init_detector
- _stop_det
- _set_trigger
--> Testing the filewriter is done in test_init_filewriter
Validation upon setting the correct PVs
"""
mock_det.custom_prepare.on_init() # call the method you want to test
assert mock_det.cam.acquire.get() == detector_state
assert mock_det.cam.trigger_mode.get() == trigger_source
@pytest.mark.parametrize(
"scaninfo, stopped, expected_exception",
[
(
{
"eacc": "e12345",
"num_points": 500,
"frames_per_trigger": 1,
"filepath": "test.h5",
"scan_id": "123",
"mokev": 12.4,
},
False,
False,
),
(
{
"eacc": "e12345",
"num_points": 500,
"frames_per_trigger": 1,
"filepath": "test.h5",
"scan_id": "123",
"mokev": 12.4,
},
True,
False,
),
],
)
def test_stage(mock_det, scaninfo, stopped, expected_exception):
path = "tmp"
mock_det.filepath_raw = path
with mock.patch.object(
mock_det.custom_prepare, "publish_file_location"
) as mock_publish_file_location:
mock_det.scaninfo.num_points = scaninfo["num_points"]
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
mock_det.filewriter.compile_full_filename.return_value = scaninfo["filepath"]
mock_det.device_manager.add_device("mokev", value=12.4)
mock_det.stopped = stopped
with (
mock.patch.object(mock_det.custom_prepare, "prepare_data_backend") as mock_data_backend,
mock.patch.object(
mock_det.custom_prepare, "update_readout_time"
) as mock_update_readout_time,
):
mock_det.filepath.set(scaninfo["filepath"]).wait()
if expected_exception:
with pytest.raises(Exception):
mock_det.timeout = 0.1
mock_det.stage()
else:
mock_det.stage()
mock_data_backend.assert_called_once()
mock_update_readout_time.assert_called()
# Check _prep_det
assert mock_det.cam.num_images.get() == int(
scaninfo["num_points"] * scaninfo["frames_per_trigger"]
)
assert mock_det.cam.num_frames.get() == 1
mock_publish_file_location.assert_called_once_with(
done=False, successful=False, metadata={"input_path": path}
)
def test_pre_scan(mock_det):
mock_det.custom_prepare.on_pre_scan()
assert mock_det.cam.acquire.get() == 1
@pytest.mark.parametrize(
"readout_time, expected_value", [(1e-3, 3e-3), (3e-3, 3e-3), (5e-3, 5e-3), (None, 3e-3)]
)
def test_update_readout_time(mock_det, readout_time, expected_value):
if readout_time is None:
mock_det.custom_prepare.update_readout_time()
assert mock_det.readout_time == expected_value
else:
mock_det.scaninfo.readout_time = readout_time
mock_det.custom_prepare.update_readout_time()
assert mock_det.readout_time == expected_value
@pytest.mark.parametrize(
"scaninfo",
[
(
{
"filepath": "test.h5",
"filepath_raw": "test5_raw.h5",
"successful": True,
"done": False,
"scan_id": "123",
}
),
(
{
"filepath": "test.h5",
"filepath_raw": "test5_raw.h5",
"successful": False,
"done": True,
"scan_id": "123",
}
),
],
)
def test_publish_file_location(mock_det, scaninfo):
mock_det.scaninfo.scan_id = scaninfo["scan_id"]
mock_det.filepath.set(scaninfo["filepath"]).wait()
mock_det.filepath_raw = scaninfo["filepath_raw"]
mock_det.custom_prepare.publish_file_location(
done=scaninfo["done"],
successful=scaninfo["successful"],
metadata={"input_path": scaninfo["filepath_raw"]},
)
if scaninfo["successful"] is None:
msg = messages.FileMessage(
file_path=scaninfo["filepath"],
done=scaninfo["done"],
metadata={"input_path": scaninfo["filepath_raw"]},
)
else:
msg = messages.FileMessage(
file_path=scaninfo["filepath"],
done=scaninfo["done"],
metadata={"input_path": scaninfo["filepath_raw"]},
successful=scaninfo["successful"],
)
expected_calls = [
mock.call(
MessageEndpoints.public_file(scaninfo["scan_id"], mock_det.name),
msg,
pipe=mock_det.connector.pipeline.return_value,
),
mock.call(
MessageEndpoints.file_event(mock_det.name),
msg,
pipe=mock_det.connector.pipeline.return_value,
),
]
assert mock_det.connector.set_and_publish.call_args_list == expected_calls
@pytest.mark.parametrize(
"requests_state, expected_exception, url_delete, url_put",
[
(
True,
False,
"http://x12sa-pd-2:8080/stream/pilatus_2",
"http://xbl-daq-34:8091/pilatus_2/stop",
),
(
False,
False,
"http://x12sa-pd-2:8080/stream/pilatus_2",
"http://xbl-daq-34:8091/pilatus_2/stop",
),
],
)
def test_stop_detector_backend(mock_det, requests_state, expected_exception, url_delete, url_put):
with (
mock.patch.object(
mock_det.custom_prepare, "send_requests_delete"
) as mock_send_requests_delete,
mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put,
):
instance_delete = mock_send_requests_delete.return_value
instance_delete.ok = requests_state
instance_put = mock_send_requests_put.return_value
instance_put.ok = requests_state
if expected_exception:
mock_det.custom_prepare.stop_detector_backend()
mock_send_requests_delete.assert_called_once_with(url=url_delete)
mock_send_requests_put.assert_called_once_with(url=url_put)
instance_delete.raise_for_status.called_once()
instance_put.raise_for_status.called_once()
else:
mock_det.custom_prepare.stop_detector_backend()
mock_send_requests_delete.assert_called_once_with(url=url_delete)
mock_send_requests_put.assert_called_once_with(url=url_put)
@pytest.mark.parametrize(
"scaninfo, data_msgs, urls, requests_state, expected_exception",
[
(
{
"filepath_raw": "pilatus_2.h5",
"eacc": "e12345",
"scan_number": 1000,
"scan_directory": "S00000_00999",
"num_points": 500,
"frames_per_trigger": 1,
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
},
[
{
"source": [
{
"searchPath": "/",
"searchPattern": "glob:*.cbf",
"destinationPath": (
"/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999"
),
}
]
},
[
"zmqWriter",
"e12345",
{
"addr": "tcp://x12sa-pd-2:8888",
"dst": ["file"],
"numFrm": 500,
"timeout": 2000,
"ifType": "PULL",
"user": "e12345",
},
],
["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}],
],
[
"http://x12sa-pd-2:8080/stream/pilatus_2",
"http://xbl-daq-34:8091/pilatus_2/run",
"http://xbl-daq-34:8091/pilatus_2/wait",
],
True,
False,
),
(
{
"filepath_raw": "pilatus_2.h5",
"eacc": "e12345",
"scan_number": 1000,
"scan_directory": "S00000_00999",
"num_points": 500,
"frames_per_trigger": 1,
"headers": {"Content-Type": "application/json", "Accept": "application/json"},
},
[
{
"source": [
{
"searchPath": "/",
"searchPattern": "glob:*.cbf",
"destinationPath": (
"/sls/X12SA/data/e12345/Data10/pilatus_2/S00000_00999"
),
}
]
},
[
"zmqWriter",
"e12345",
{
"addr": "tcp://x12sa-pd-2:8888",
"dst": ["file"],
"numFrm": 500,
"timeout": 2000,
"ifType": "PULL",
"user": "e12345",
},
],
["zmqWriter", "e12345", {"frmCnt": 500, "timeout": 2000}],
],
[
"http://x12sa-pd-2:8080/stream/pilatus_2",
"http://xbl-daq-34:8091/pilatus_2/run",
"http://xbl-daq-34:8091/pilatus_2/wait",
],
False, # return of res.ok is False!
True,
),
],
)
def test_prep_file_writer(mock_det, scaninfo, data_msgs, urls, requests_state, expected_exception):
with (
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer,
mock.patch.object(mock_det, "filewriter") as mock_filewriter,
mock.patch.object(mock_det.custom_prepare, "create_directory") as mock_create_directory,
mock.patch.object(mock_det.custom_prepare, "send_requests_put") as mock_send_requests_put,
):
mock_det.scaninfo.scan_number = scaninfo["scan_number"]
mock_det.scaninfo.num_points = scaninfo["num_points"]
mock_det.scaninfo.frames_per_trigger = scaninfo["frames_per_trigger"]
mock_det.scaninfo.username = scaninfo["eacc"]
mock_filewriter.compile_full_filename.return_value = scaninfo["filepath_raw"]
mock_filewriter.get_scan_directory.return_value = scaninfo["scan_directory"]
instance = mock_send_requests_put.return_value
instance.ok = requests_state
instance.raise_for_status.side_effect = Exception
if expected_exception:
with pytest.raises(Exception):
mock_det.timeout = 0.1
mock_det.custom_prepare.prepare_data_backend()
mock_close_file_writer.assert_called_once()
mock_stop_file_writer.assert_called_once()
instance.raise_for_status.assert_called_once()
else:
mock_det.custom_prepare.prepare_data_backend()
mock_close_file_writer.assert_called_once()
mock_stop_file_writer.assert_called_once()
# Assert values set on detector
assert mock_det.cam.file_path.get() == "/dev/shm/zmq/"
assert (
mock_det.cam.file_name.get()
== f"{scaninfo['eacc']}_2_{scaninfo['scan_number']:05d}"
)
assert mock_det.cam.auto_increment.get() == 1
assert mock_det.cam.file_number.get() == 0
assert mock_det.cam.file_format.get() == 0
assert mock_det.cam.file_template.get() == "%s%s_%5.5d.cbf"
# Remove last / from destinationPath
mock_create_directory.assert_called_once_with(
os.path.join(data_msgs[0]["source"][0]["destinationPath"])
)
assert mock_send_requests_put.call_count == 3
calls = [
mock.call(url=url, data=data_msg, headers=scaninfo["headers"])
for url, data_msg in zip(urls, data_msgs)
]
for call, mock_call in zip(calls, mock_send_requests_put.call_args_list):
assert call == mock_call
def test_complete(mock_det):
path = "tmp"
mock_det.filepath_raw = path
with (
mock.patch.object(mock_det.custom_prepare, "finished") as mock_finished,
mock.patch.object(
mock_det.custom_prepare, "publish_file_location"
) as mock_publish_file_location,
):
mock_det.complete()
assert mock_finished.call_count == 1
call = mock.call(done=True, successful=True, metadata={"input_path": path})
assert mock_publish_file_location.call_args == call
def test_stop(mock_det):
with (
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_writer,
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
):
mock_det.stop()
mock_stop_det.assert_called_once()
mock_stop_file_writer.assert_called_once()
mock_close_file_writer.assert_called_once()
assert mock_det.stopped is True
@pytest.mark.parametrize(
"stopped, mcs_stage_state, expected_exception",
[
(False, ophyd.Staged.no, False),
(True, ophyd.Staged.no, True),
(False, ophyd.Staged.yes, True),
],
)
def test_finished(mock_det, stopped, mcs_stage_state, expected_exception):
with (
mock.patch.object(mock_det, "device_manager") as mock_dm,
mock.patch.object(mock_det.custom_prepare, "stop_file_writer") as mock_stop_file_friter,
mock.patch.object(mock_det.custom_prepare, "stop_detector") as mock_stop_det,
mock.patch.object(mock_det.custom_prepare, "close_file_writer") as mock_close_file_writer,
):
mock_dm.devices.mcs.obj._staged = mcs_stage_state
mock_det.stopped = stopped
if expected_exception:
with pytest.raises(Exception):
mock_det.timeout = 0.1
mock_det.custom_prepare.finished()
assert mock_det.stopped is stopped
mock_stop_file_friter.assert_called()
mock_stop_det.assert_called_once()
mock_close_file_writer.assert_called_once()
else:
mock_det.custom_prepare.finished()
if stopped:
assert mock_det.stopped is stopped
mock_stop_file_friter.assert_called()
mock_stop_det.assert_called_once()
mock_close_file_writer.assert_called_once()

View File

@@ -11,26 +11,29 @@ from csaxs_bec.devices.omny.rt.rt_ophyd import RtError
def rt_flomni():
RtFlomniController._reset_controller()
rt_flomni = RtFlomniController(
name="rt_flomni", socket_cls=SocketMock, socket_host="localhost", socket_port=8081
name="rt_flomni",
socket_cls=SocketMock,
socket_host="localhost",
socket_port=8081,
device_manager=mock.MagicMock(),
)
with mock.patch.object(rt_flomni, "get_device_manager"):
with mock.patch.object(rt_flomni, "sock"):
rtx = mock.MagicMock(spec=RtFlomniMotor)
rtx.name = "rtx"
rtx.axis_Id = "A"
rtx.axis_Id_numeric = 0
rty = mock.MagicMock(spec=RtFlomniMotor)
rty.name = "rty"
rty.axis_Id = "B"
rty.axis_Id_numeric = 1
rtz = mock.MagicMock(spec=RtFlomniMotor)
rtz.name = "rtz"
rtz.axis_Id = "C"
rtz.axis_Id_numeric = 2
rt_flomni.set_axis(axis=rtx, axis_nr=0)
rt_flomni.set_axis(axis=rty, axis_nr=1)
rt_flomni.set_axis(axis=rtz, axis_nr=2)
yield rt_flomni
with mock.patch.object(rt_flomni, "sock"):
rtx = mock.MagicMock(spec=RtFlomniMotor)
rtx.name = "rtx"
rtx.axis_Id = "A"
rtx.axis_Id_numeric = 0
rty = mock.MagicMock(spec=RtFlomniMotor)
rty.name = "rty"
rty.axis_Id = "B"
rty.axis_Id_numeric = 1
rtz = mock.MagicMock(spec=RtFlomniMotor)
rtz.name = "rtz"
rtz.axis_Id = "C"
rtz.axis_Id_numeric = 2
rt_flomni.set_axis(axis=rtx, axis_nr=0)
rt_flomni.set_axis(axis=rty, axis_nr=1)
rt_flomni.set_axis(axis=rtz, axis_nr=2)
yield rt_flomni
RtFlomniController._reset_controller()
@@ -52,7 +55,7 @@ def test_rt_flomni_feedback_is_running(rt_flomni, return_value, is_running):
def test_feedback_enable_with_reset(rt_flomni):
device_manager = rt_flomni.get_device_manager()
device_manager = rt_flomni.device_manager
device_manager.devices.fsamx.user_parameter.get.return_value = 0.05
device_manager.devices.fsamx.obj.readback.get.return_value = 0.05
@@ -68,7 +71,7 @@ def test_feedback_enable_with_reset(rt_flomni):
def test_move_samx_to_scan_region(rt_flomni):
device_manager = rt_flomni.get_device_manager()
device_manager = rt_flomni.device_manager
device_manager.devices.rtx.user_parameter.get.return_value = 1
rt_flomni.move_samx_to_scan_region(20, 2)
assert mock.call(b"v0\n") not in rt_flomni.sock.put.mock_calls
@@ -76,16 +79,16 @@ def test_move_samx_to_scan_region(rt_flomni):
def test_feedback_enable_without_reset(rt_flomni):
with mock.patch.object(rt_flomni, "set_device_enabled") as set_device_enabled:
with mock.patch.object(rt_flomni, "set_device_read_write") as set_device_read_write:
with mock.patch.object(rt_flomni, "feedback_is_running", return_value=True):
with mock.patch.object(rt_flomni, "laser_tracker_on") as laser_tracker_on:
rt_flomni.feedback_enable_without_reset()
laser_tracker_on.assert_called_once()
assert mock.call(b"l3\n") in rt_flomni.sock.put.mock_calls
assert mock.call("fsamx", False) in set_device_enabled.mock_calls
assert mock.call("fsamy", False) in set_device_enabled.mock_calls
assert mock.call("foptx", False) in set_device_enabled.mock_calls
assert mock.call("fopty", False) in set_device_enabled.mock_calls
assert mock.call("fsamx", False) in set_device_read_write.mock_calls
assert mock.call("fsamy", False) in set_device_read_write.mock_calls
assert mock.call("foptx", False) in set_device_read_write.mock_calls
assert mock.call("fopty", False) in set_device_read_write.mock_calls
def test_feedback_enable_without_reset_raises(rt_flomni):

View File

@@ -10,19 +10,27 @@ from csaxs_bec.devices.smaract.smaract_ophyd import SmaractMotor
@pytest.fixture
def controller():
def controller(dm_with_devices):
SmaractController._reset_controller()
controller = SmaractController(socket_cls=SocketMock, socket_host="dummy", socket_port=123)
controller = SmaractController(
socket_cls=SocketMock, socket_host="dummy", socket_port=123, device_manager=dm_with_devices
)
controller.on()
controller.sock.flush_buffer()
yield controller
@pytest.fixture
def lsmarA():
def lsmarA(dm_with_devices):
SmaractController._reset_controller()
motor_a = SmaractMotor(
"A", name="lsmarA", host="mpc2680.psi.ch", port=8085, sign=1, socket_cls=SocketMock
"A",
name="lsmarA",
host="mpc2680.psi.ch",
port=8085,
sign=1,
socket_cls=SocketMock,
device_manager=dm_with_devices,
)
motor_a.controller.on()
motor_a.controller.sock.flush_buffer()