ldcon 0.2.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (482) hide show
  1. ldcon/__init__.py +302 -0
  2. ldcon/_moved.py +49 -0
  3. ldcon/agent_tools.py +4 -0
  4. ldcon/agents/__init__.py +1 -0
  5. ldcon/agents/adjustment_guidance.py +73 -0
  6. ldcon/agents/adjustment_quality.py +399 -0
  7. ldcon/agents/agent_adapters.py +709 -0
  8. ldcon/agents/agent_background.py +307 -0
  9. ldcon/agents/agent_bundle.py +75 -0
  10. ldcon/agents/agent_cards.py +171 -0
  11. ldcon/agents/agent_config_access.py +22 -0
  12. ldcon/agents/agent_configuration.py +224 -0
  13. ldcon/agents/agent_configuration_policy.py +67 -0
  14. ldcon/agents/agent_connect.py +93 -0
  15. ldcon/agents/agent_device_parameters.py +41 -0
  16. ldcon/agents/agent_execution.py +77 -0
  17. ldcon/agents/agent_history.py +112 -0
  18. ldcon/agents/agent_inbox_watch.py +121 -0
  19. ldcon/agents/agent_interactive.py +386 -0
  20. ldcon/agents/agent_interactive_hooks.py +381 -0
  21. ldcon/agents/agent_language.py +48 -0
  22. ldcon/agents/agent_log_context.py +308 -0
  23. ldcon/agents/agent_log_read.py +250 -0
  24. ldcon/agents/agent_log_watch.py +27 -0
  25. ldcon/agents/agent_metrics.py +111 -0
  26. ldcon/agents/agent_monitor.py +272 -0
  27. ldcon/agents/agent_opencode_http.py +270 -0
  28. ldcon/agents/agent_opencode_mcp.py +64 -0
  29. ldcon/agents/agent_owner_status.py +216 -0
  30. ldcon/agents/agent_profiles.py +186 -0
  31. ldcon/agents/agent_providers.py +53 -0
  32. ldcon/agents/agent_repairs.py +105 -0
  33. ldcon/agents/agent_routing.py +90 -0
  34. ldcon/agents/agent_runtime.py +70 -0
  35. ldcon/agents/agent_sandbox.py +141 -0
  36. ldcon/agents/agent_startup_notice.py +142 -0
  37. ldcon/agents/agent_task.py +336 -0
  38. ldcon/agents/agent_tools.py +488 -0
  39. ldcon/agents/agent_window.py +70 -0
  40. ldcon/agents/agent_workspace.py +340 -0
  41. ldcon/agents/card_fallback.py +284 -0
  42. ldcon/agents/dlc_pi_adjustment.py +250 -0
  43. ldcon/agents/dlc_pi_plan.py +24 -0
  44. ldcon/agents/dlc_pi_response_plan.py +26 -0
  45. ldcon/agents/failure_policy.py +473 -0
  46. ldcon/agents/fallback_service.py +146 -0
  47. ldcon/agents/fallback_targets.py +96 -0
  48. ldcon/agents/gateway_role.py +31 -0
  49. ldcon/agents/manual_adjustment_grant.py +66 -0
  50. ldcon/agents/measurement_grant.py +95 -0
  51. ldcon/agents/monitor_quality_binding.py +33 -0
  52. ldcon/agents/notification_policy.py +346 -0
  53. ldcon/agents/operating_mode_client.py +99 -0
  54. ldcon/agents/report_pdf.py +105 -0
  55. ldcon/agents/resident_adjustment.py +572 -0
  56. ldcon/agents/resident_agent.py +438 -0
  57. ldcon/agents/resident_quality_apply.py +233 -0
  58. ldcon/agents/task_decisions.py +218 -0
  59. ldcon/agents/teams_card.py +100 -0
  60. ldcon/agents/teams_destinations.py +157 -0
  61. ldcon/cld_owner.py +4 -0
  62. ldcon/cli.py +19 -0
  63. ldcon/core/__init__.py +1 -0
  64. ldcon/core/adjustment_plan.py +47 -0
  65. ldcon/core/adjustment_registry.py +100 -0
  66. ldcon/core/agent_authority.py +903 -0
  67. ldcon/core/card_admission.py +15 -0
  68. ldcon/core/card_definition.py +14 -0
  69. ldcon/core/card_reporting.py +238 -0
  70. ldcon/core/configuration.py +357 -0
  71. ldcon/core/configuration_transaction.py +225 -0
  72. ldcon/core/contracts.py +86 -0
  73. ldcon/core/current_settings.py +86 -0
  74. ldcon/core/deck_yaml.py +45 -0
  75. ldcon/core/device_conditions.py +133 -0
  76. ldcon/core/execution_context.py +3 -0
  77. ldcon/core/instrument_client.py +190 -0
  78. ldcon/core/interface_cards.py +520 -0
  79. ldcon/core/interface_schema.py +640 -0
  80. ldcon/core/laser_assets.py +238 -0
  81. ldcon/core/metric_quantities.py +38 -0
  82. ldcon/core/modulation.py +107 -0
  83. ldcon/core/operation_registry.py +195 -0
  84. ldcon/core/paths.py +26 -0
  85. ldcon/core/period_context.py +118 -0
  86. ldcon/core/records.py +4 -0
  87. ldcon/core/result_reservations.py +469 -0
  88. ldcon/core/site_bindings.py +25 -0
  89. ldcon/core/state_layout.py +70 -0
  90. ldcon/core/target_policy.py +79 -0
  91. ldcon/core/telegraf.py +106 -0
  92. ldcon/core/telemetry.py +130 -0
  93. ldcon/core/temperature_settings.py +12 -0
  94. ldcon/dashboard_metrics.py +4 -0
  95. ldcon/device_registry.py +4 -0
  96. ldcon/drivers/__init__.py +1 -0
  97. ldcon/drivers/cld1015.py +296 -0
  98. ldcon/drivers/cld1015_config.py +31 -0
  99. ldcon/drivers/dlcpro.py +177 -0
  100. ldcon/drivers/dlcpro_analog.py +33 -0
  101. ldcon/drivers/dlcpro_console.py +133 -0
  102. ldcon/drivers/dlcpro_priority.py +46 -0
  103. ldcon/drivers/ecdl.py +334 -0
  104. ldcon/drivers/esp32_servo.py +187 -0
  105. ldcon/drivers/fizeau.py +114 -0
  106. ldcon/drivers/frequency_sweep.py +69 -0
  107. ldcon/drivers/influx_wavelength.py +70 -0
  108. ldcon/drivers/metric_sources.py +82 -0
  109. ldcon/drivers/native_dither_telemetry.py +50 -0
  110. ldcon/drivers/rigol_scope.py +126 -0
  111. ldcon/drivers/siglent.py +418 -0
  112. ldcon/drivers/staircase.py +155 -0
  113. ldcon/drivers/wsu_udp.py +164 -0
  114. ldcon/esp32_time_telemetry.py +4 -0
  115. ldcon/example_config.py +41 -0
  116. ldcon/fallback_service.py +4 -0
  117. ldcon/instrument_owner.py +4 -0
  118. ldcon/instrument_service.py +4 -0
  119. ldcon/interface_worker.py +4 -0
  120. ldcon/job_service.py +4 -0
  121. ldcon/jobs/__init__.py +1 -0
  122. ldcon/jobs/adaptive_pid.py +234 -0
  123. ldcon/jobs/adaptive_window_session.py +922 -0
  124. ldcon/jobs/adjustment_continuation.py +284 -0
  125. ldcon/jobs/axis_frequency.py +219 -0
  126. ldcon/jobs/axis_frequency_report.py +86 -0
  127. ldcon/jobs/background_parameter_session.py +142 -0
  128. ldcon/jobs/background_pid_session.py +447 -0
  129. ldcon/jobs/branch_maps.py +71 -0
  130. ldcon/jobs/branch_target_migration.py +33 -0
  131. ldcon/jobs/card_runtime.py +157 -0
  132. ldcon/jobs/change_measure.py +293 -0
  133. ldcon/jobs/change_measure_adapter.py +360 -0
  134. ldcon/jobs/change_measure_dlc.py +94 -0
  135. ldcon/jobs/change_measure_parameters.py +222 -0
  136. ldcon/jobs/change_measure_target.py +135 -0
  137. ldcon/jobs/current_cards.py +159 -0
  138. ldcon/jobs/current_devices.py +71 -0
  139. ldcon/jobs/dashboard_api.py +61 -0
  140. ldcon/jobs/device_parameters.py +426 -0
  141. ldcon/jobs/dlc_pi_measurement.py +151 -0
  142. ldcon/jobs/dlcpro_awg_analysis.py +77 -0
  143. ldcon/jobs/dlcpro_awg_card.py +116 -0
  144. ldcon/jobs/dlcpro_cards.py +240 -0
  145. ldcon/jobs/ecdl_branch_hop.py +175 -0
  146. ldcon/jobs/ecdl_branch_recovery.py +176 -0
  147. ldcon/jobs/ecdl_devices.py +306 -0
  148. ldcon/jobs/ecdl_locked_sweep_proxy.py +526 -0
  149. ldcon/jobs/ecdl_mapped_recovery.py +6 -0
  150. ldcon/jobs/ecdl_mode_map.py +469 -0
  151. ldcon/jobs/ecdl_mode_recovery.py +20 -0
  152. ldcon/jobs/ecdl_online.py +158 -0
  153. ldcon/jobs/ecdl_operating_point.py +403 -0
  154. ldcon/jobs/ecdl_pi_commission.py +161 -0
  155. ldcon/jobs/ecdl_recovery.py +25 -0
  156. ldcon/jobs/ecdl_recovery_plan.py +90 -0
  157. ldcon/jobs/ecdl_recovery_test.py +59 -0
  158. ldcon/jobs/ecdl_relock.py +7 -0
  159. ldcon/jobs/ecdl_response_study.py +369 -0
  160. ldcon/jobs/ecdl_steady.py +343 -0
  161. ldcon/jobs/ecdl_thermal_context.py +242 -0
  162. ldcon/jobs/ecdl_window_center.py +734 -0
  163. ldcon/jobs/emission_card.py +102 -0
  164. ldcon/jobs/fringe_sweep_reference.py +89 -0
  165. ldcon/jobs/interface_devices.py +122 -0
  166. ldcon/jobs/interface_worker.py +471 -0
  167. ldcon/jobs/job_service.py +125 -0
  168. ldcon/jobs/laser_live.py +270 -0
  169. ldcon/jobs/learned_settling.py +450 -0
  170. ldcon/jobs/lock_target.py +461 -0
  171. ldcon/jobs/locked_triangle.py +36 -0
  172. ldcon/jobs/measurement_acquisition.py +51 -0
  173. ldcon/jobs/measurement_analysis.py +59 -0
  174. ldcon/jobs/measurement_execution.py +40 -0
  175. ldcon/jobs/measurement_notice.py +78 -0
  176. ldcon/jobs/measurement_presets.py +52 -0
  177. ldcon/jobs/measurement_report.py +76 -0
  178. ldcon/jobs/measurement_statistics.py +22 -0
  179. ldcon/jobs/metric_cards.py +34 -0
  180. ldcon/jobs/metric_interface.py +100 -0
  181. ldcon/jobs/modulation_sweep.py +274 -0
  182. ldcon/jobs/native_background_session.py +91 -0
  183. ldcon/jobs/native_current_acquire.py +143 -0
  184. ldcon/jobs/native_current_card.py +328 -0
  185. ldcon/jobs/native_current_online.py +129 -0
  186. ldcon/jobs/native_ff_proxy.py +1361 -0
  187. ldcon/jobs/native_ff_sweep.py +802 -0
  188. ldcon/jobs/online_pid.py +178 -0
  189. ldcon/jobs/operating_point.py +1399 -0
  190. ldcon/jobs/operating_point_proxy.py +236 -0
  191. ldcon/jobs/operation_execution.py +126 -0
  192. ldcon/jobs/operation_history.py +419 -0
  193. ldcon/jobs/optical_broker.py +23 -0
  194. ldcon/jobs/pi_algorithms.py +30 -0
  195. ldcon/jobs/pi_response.py +239 -0
  196. ldcon/jobs/progress.py +63 -0
  197. ldcon/jobs/quality_baseline.py +411 -0
  198. ldcon/jobs/recovery_decisions.py +131 -0
  199. ldcon/jobs/recovery_history.py +152 -0
  200. ldcon/jobs/recovery_procedure.py +285 -0
  201. ldcon/jobs/recovery_reference.py +123 -0
  202. ldcon/jobs/recovery_stationary.py +138 -0
  203. ldcon/jobs/response_algorithms.py +47 -0
  204. ldcon/jobs/response_comparison.py +62 -0
  205. ldcon/jobs/response_record.py +90 -0
  206. ldcon/jobs/runtime_parameters.py +434 -0
  207. ldcon/jobs/settling_profile.py +140 -0
  208. ldcon/jobs/shot_analysis.py +1300 -0
  209. ldcon/jobs/simulation.py +127 -0
  210. ldcon/jobs/slow_control.py +208 -0
  211. ldcon/jobs/slow_current.py +88 -0
  212. ldcon/jobs/study_adoption.py +122 -0
  213. ldcon/jobs/study_execution.py +182 -0
  214. ldcon/jobs/sweep_ff_analysis.py +320 -0
  215. ldcon/jobs/sweep_ff_optimization.py +202 -0
  216. ldcon/jobs/sweep_ff_plan.py +166 -0
  217. ldcon/jobs/sweep_ff_report.py +254 -0
  218. ldcon/jobs/sweep_ff_transport.py +326 -0
  219. ldcon/jobs/sweep_tracking.py +424 -0
  220. ldcon/jobs/sweep_verdict.py +288 -0
  221. ldcon/jobs/target_response.py +638 -0
  222. ldcon/jobs/target_response_analysis.py +65 -0
  223. ldcon/jobs/target_response_control.py +215 -0
  224. ldcon/jobs/target_response_decisions.py +152 -0
  225. ldcon/jobs/target_response_experiment.py +110 -0
  226. ldcon/jobs/target_response_feedforward.py +130 -0
  227. ldcon/jobs/target_response_search.py +225 -0
  228. ldcon/jobs/target_response_shift.py +475 -0
  229. ldcon/jobs/target_response_thermal.py +113 -0
  230. ldcon/jobs/target_response_thermal_experiment.py +120 -0
  231. ldcon/jobs/target_sequence.py +166 -0
  232. ldcon/jobs/temperature_identification.py +86 -0
  233. ldcon/jobs/wavelength_history.py +17 -0
  234. ldcon/jobs/window_actions.py +641 -0
  235. ldcon/jobs/window_center.py +784 -0
  236. ldcon/jobs/window_control.py +130 -0
  237. ldcon/jobs/window_decisions.py +105 -0
  238. ldcon/jobs/window_sampling.py +66 -0
  239. ldcon/jobs/workflow_artifacts.py +73 -0
  240. ldcon/migration/__init__.py +810 -0
  241. ldcon/migration/migration_decks.py +239 -0
  242. ldcon/migration/migration_readiness.py +397 -0
  243. ldcon/migration/migration_relocation.py +283 -0
  244. ldcon/monitor_owner.py +4 -0
  245. ldcon/owner_telemetry.py +4 -0
  246. ldcon/owners/__init__.py +1 -0
  247. ldcon/owners/adc_log_threshold.py +209 -0
  248. ldcon/owners/background_grants.py +44 -0
  249. ldcon/owners/cld_instrument_owner.py +96 -0
  250. ldcon/owners/cld_owner.py +706 -0
  251. ldcon/owners/cld_projection.py +194 -0
  252. ldcon/owners/cld_system_owner.py +276 -0
  253. ldcon/owners/current_owner.py +68 -0
  254. ldcon/owners/device_registry.py +229 -0
  255. ldcon/owners/dlcpro_awg_plan.py +115 -0
  256. ldcon/owners/dlcpro_awg_port.py +98 -0
  257. ldcon/owners/dlcpro_awg_protocol.py +190 -0
  258. ldcon/owners/dlcpro_awg_return.py +63 -0
  259. ldcon/owners/dlcpro_awg_sweep.py +437 -0
  260. ldcon/owners/dlcpro_awg_timing.py +35 -0
  261. ldcon/owners/dlcpro_awg_tracking.py +175 -0
  262. ldcon/owners/dlcpro_awg_tracking_guard.py +87 -0
  263. ldcon/owners/dlcpro_awg_tracking_report.py +49 -0
  264. ldcon/owners/dlcpro_awg_zero.py +82 -0
  265. ldcon/owners/dlcpro_deployment.py +66 -0
  266. ldcon/owners/dlcpro_ff.py +64 -0
  267. ldcon/owners/dlcpro_ff_adjustment.py +159 -0
  268. ldcon/owners/dlcpro_ff_delta_adjustment.py +194 -0
  269. ldcon/owners/dlcpro_frequency_window.py +61 -0
  270. ldcon/owners/dlcpro_observation.py +62 -0
  271. ldcon/owners/dlcpro_owner.py +830 -0
  272. ldcon/owners/dlcpro_pi_protocol.py +44 -0
  273. ldcon/owners/dlcpro_pi_trial.py +161 -0
  274. ldcon/owners/dlcpro_readback.py +50 -0
  275. ldcon/owners/dlcpro_service.py +203 -0
  276. ldcon/owners/dlcpro_target_changes.py +330 -0
  277. ldcon/owners/dlcpro_target_step.py +44 -0
  278. ldcon/owners/dlcpro_target_store.py +75 -0
  279. ldcon/owners/dlcpro_telemetry.py +72 -0
  280. ldcon/owners/ecdl_background_parameters.py +248 -0
  281. ldcon/owners/ecdl_background_pid.py +184 -0
  282. ldcon/owners/ecdl_hot_control.py +255 -0
  283. ldcon/owners/ecdl_independent_pid.py +95 -0
  284. ldcon/owners/ecdl_locked_study.py +122 -0
  285. ldcon/owners/ecdl_locked_sweep.py +1875 -0
  286. ldcon/owners/ecdl_native_pid.py +181 -0
  287. ldcon/owners/ecdl_optical.py +37 -0
  288. ldcon/owners/ecdl_owner.py +1058 -0
  289. ldcon/owners/ecdl_policy_reload.py +494 -0
  290. ldcon/owners/ecdl_prelock.py +612 -0
  291. ldcon/owners/ecdl_projection.py +636 -0
  292. ldcon/owners/ecdl_steady_owner.py +1859 -0
  293. ldcon/owners/ecdl_steady_policy.py +212 -0
  294. ldcon/owners/ecdl_target_ff.py +214 -0
  295. ldcon/owners/ecdl_target_settling.py +163 -0
  296. ldcon/owners/ecdl_tec_pair.py +88 -0
  297. ldcon/owners/ecdl_thermal_study.py +251 -0
  298. ldcon/owners/emission.py +365 -0
  299. ldcon/owners/emission_adapters.py +268 -0
  300. ldcon/owners/esp32_native_client.py +335 -0
  301. ldcon/owners/esp32_native_owner.py +2166 -0
  302. ldcon/owners/esp32_time_telemetry.py +253 -0
  303. ldcon/owners/esp32_udp_gate.py +136 -0
  304. ldcon/owners/external_sweep_guard.py +44 -0
  305. ldcon/owners/flexible_native_trial.py +172 -0
  306. ldcon/owners/fringe_peaks.py +92 -0
  307. ldcon/owners/full_data_log.py +133 -0
  308. ldcon/owners/instrument_identity.py +125 -0
  309. ldcon/owners/instrument_owner.py +1041 -0
  310. ldcon/owners/instrument_projection.py +104 -0
  311. ldcon/owners/instrument_service.py +638 -0
  312. ldcon/owners/migration_physical_lock.py +162 -0
  313. ldcon/owners/monitor_owner.py +322 -0
  314. ldcon/owners/native_assembly.py +37 -0
  315. ldcon/owners/native_assembly_host.py +260 -0
  316. ldcon/owners/native_background.py +233 -0
  317. ldcon/owners/native_current_tuning.py +251 -0
  318. ldcon/owners/native_ff_proof.py +433 -0
  319. ldcon/owners/native_instrument_owner.py +161 -0
  320. ldcon/owners/native_modulation_limits.py +45 -0
  321. ldcon/owners/native_policy_reload.py +186 -0
  322. ldcon/owners/native_policy_safety.py +213 -0
  323. ldcon/owners/native_policy_update.py +473 -0
  324. ldcon/owners/native_system_owner.py +420 -0
  325. ldcon/owners/operating_mode_owner.py +128 -0
  326. ldcon/owners/owner_projection.py +530 -0
  327. ldcon/owners/owner_telemetry.py +166 -0
  328. ldcon/owners/pi_handover.py +154 -0
  329. ldcon/owners/projection_transport.py +143 -0
  330. ldcon/owners/remote_instruments.py +323 -0
  331. ldcon/owners/sdg_client.py +122 -0
  332. ldcon/owners/sdg_owner.py +468 -0
  333. ldcon/owners/tec_rate.py +29 -0
  334. ldcon/owners/wsu_udp_monitor.py +113 -0
  335. ldcon/report_pdf.py +4 -0
  336. ldcon/resources/agent-model-options.json +142 -0
  337. ldcon/resources/agent_contract/adjustment.txt +45 -0
  338. ldcon/resources/agent_contract/common.txt +42 -0
  339. ldcon/resources/agent_contract/diagnosis.txt +11 -0
  340. ldcon/resources/agent_contract/research.txt +20 -0
  341. ldcon/resources/agent_contract/response_optimization.txt +13 -0
  342. ldcon/resources/examples/README.html +10 -0
  343. ldcon/resources/examples/assignments/sr1092.yaml +100 -0
  344. ldcon/resources/examples/assignments/sr422.yaml +677 -0
  345. ldcon/resources/examples/assignments/thf685.yaml +7 -0
  346. ldcon/resources/examples/interface-decks/dfb-dbr-ldc-tec.yaml +430 -0
  347. ldcon/resources/examples/interface-decks/ecdl-dlcpro.yaml +288 -0
  348. ldcon/resources/examples/interface-decks/ecdl-pzt-ldc-tec.yaml +698 -0
  349. ldcon/resources/examples/lasers/sr1092.yaml +267 -0
  350. ldcon/resources/examples/lasers/sr422.yaml +403 -0
  351. ldcon/resources/examples/lasers/thf685.yaml +158 -0
  352. ldcon/resources/report_template/ldconreport.cls +42 -0
  353. ldcon/resources/report_template/report.tex +47 -0
  354. ldcon/web/__init__.py +1 -0
  355. ldcon/web/agent_approval_api.py +126 -0
  356. ldcon/web/agent_settings_api.py +54 -0
  357. ldcon/web/agent_task_api.py +211 -0
  358. ldcon/web/control_readback.py +63 -0
  359. ldcon/web/dashboard_activity.py +109 -0
  360. ldcon/web/dashboard_controls_revision.py +29 -0
  361. ldcon/web/dashboard_metrics.py +146 -0
  362. ldcon/web/dashboard_snapshot.py +163 -0
  363. ldcon/web/dashboard_state.py +104 -0
  364. ldcon/web/dashboard_wavelength.py +150 -0
  365. ldcon/web/device_parameters_api.py +52 -0
  366. ldcon/web/dither_quality.py +46 -0
  367. ldcon/web/dlcpro_target_api.py +91 -0
  368. ldcon/web/dlcpro_target_query.py +23 -0
  369. ldcon/web/failure_api.py +62 -0
  370. ldcon/web/gateway_lan.py +208 -0
  371. ldcon/web/grafana_activity.py +84 -0
  372. ldcon/web/grafana_dashboard.py +492 -0
  373. ldcon/web/grafana_dlcpro.py +158 -0
  374. ldcon/web/grafana_quality.py +167 -0
  375. ldcon/web/interface_projection.py +27 -0
  376. ldcon/web/job_registry.py +68 -0
  377. ldcon/web/job_report.py +122 -0
  378. ldcon/web/laser_operations.py +445 -0
  379. ldcon/web/lock_target_api.py +554 -0
  380. ldcon/web/lock_target_queue.py +212 -0
  381. ldcon/web/log_archive.py +191 -0
  382. ldcon/web/metric_query_profiles.py +378 -0
  383. ldcon/web/online_position.py +26 -0
  384. ldcon/web/operating_mode_api.py +72 -0
  385. ldcon/web/operation_api.py +34 -0
  386. ldcon/web/operation_observation.py +57 -0
  387. ldcon/web/operations_settings.py +92 -0
  388. ldcon/web/quality_threshold_metrics.py +64 -0
  389. ldcon/web/shot_analysis_api.py +136 -0
  390. ldcon/web/sweep_api.py +1186 -0
  391. ldcon/web/sweep_guard.py +45 -0
  392. ldcon/web/target_reference_panel.py +38 -0
  393. ldcon/web/teams_destinations_api.py +81 -0
  394. ldcon/web/web_fleet.py +482 -0
  395. ldcon/web/web_runtime.py +811 -0
  396. ldcon/web/web_server.py +402 -0
  397. ldcon/web/window_api.py +28 -0
  398. ldcon/web_assets/agent-settings.css +4 -0
  399. ldcon/web_assets/agent-settings.html +53 -0
  400. ldcon/web_assets/agent-settings.js +270 -0
  401. ldcon/web_assets/app.css +9 -0
  402. ldcon/web_assets/app.js +802 -0
  403. ldcon/web_assets/card-browser.js +160 -0
  404. ldcon/web_assets/card-guide.js +9 -0
  405. ldcon/web_assets/cards/acquire.metrics.html +82 -0
  406. ldcon/web_assets/cards/agent.history_review.html +13 -0
  407. ldcon/web_assets/cards/agent.log_watch.html +1 -0
  408. ldcon/web_assets/cards/agent.monitor.html +13 -0
  409. ldcon/web_assets/cards/agent.request.html +1 -0
  410. ldcon/web_assets/cards/agent.resident.html +1 -0
  411. ldcon/web_assets/cards/agent.task.html +17 -0
  412. ldcon/web_assets/cards/commission.ecdl_pi.html +1 -0
  413. ldcon/web_assets/cards/identify.axis_frequency.html +1 -0
  414. ldcon/web_assets/cards/identify.current_response.html +1 -0
  415. ldcon/web_assets/cards/laser.emission.html +8 -0
  416. ldcon/web_assets/cards/measure.batch.html +1 -0
  417. ldcon/web_assets/cards/measure.change.html +1 -0
  418. ldcon/web_assets/cards/measure.target_response.html +1 -0
  419. ldcon/web_assets/cards/monitor.wavelength.html +1 -0
  420. ldcon/web_assets/cards/notify.teams.html +1 -0
  421. ldcon/web_assets/cards/optimize.settling_criteria.html +1 -0
  422. ldcon/web_assets/cards/optimize.target_feedforward.html +1 -0
  423. ldcon/web_assets/cards/optimize.target_step.html +1 -0
  424. ldcon/web_assets/cards/optimize.target_thermal.html +1 -0
  425. ldcon/web_assets/cards/plan.ecdl_recovery.html +14 -0
  426. ldcon/web_assets/cards/prepare.ecdl_recovery.html +18 -0
  427. ldcon/web_assets/cards/recover.ecdl_mode_hop.html +12 -0
  428. ldcon/web_assets/cards/report.grafana_dashboard.html +22 -0
  429. ldcon/web_assets/cards/servo.current_pid.html +1 -0
  430. ldcon/web_assets/cards/servo.dlcpro_frequency.html +1 -0
  431. ldcon/web_assets/cards/servo.ecdl_steady_lock.html +11 -0
  432. ldcon/web_assets/cards/servo.native_current.html +1 -0
  433. ldcon/web_assets/cards/study.ecdl_current_response.html +1 -0
  434. ldcon/web_assets/cards/study.ecdl_locked_current_map.html +1 -0
  435. ldcon/web_assets/cards/study.ecdl_mode_map.html +6 -0
  436. ldcon/web_assets/cards/study.ecdl_thermal_context.html +5 -0
  437. ldcon/web_assets/cards/study.target_response.html +1 -0
  438. ldcon/web_assets/cards/sweep.current_modulation.html +1 -0
  439. ldcon/web_assets/cards/sweep.dlcpro_frequency.html +1 -0
  440. ldcon/web_assets/cards/sweep.servo_modulation.html +1 -0
  441. ldcon/web_assets/cards/test.ecdl_recovery.html +4 -0
  442. ldcon/web_assets/cards/tune.ecdl_initial_point.html +1 -0
  443. ldcon/web_assets/cards/tune.ecdl_operating_point.html +13 -0
  444. ldcon/web_assets/cards/tune.ecdl_window_center.html +7 -0
  445. ldcon/web_assets/cards/tune.native_current.html +1 -0
  446. ldcon/web_assets/cards/tune.pid_coordinate_search.html +1 -0
  447. ldcon/web_assets/cards/tune.pid_factor_reduction.html +1 -0
  448. ldcon/web_assets/cards/tune.pid_online.html +1 -0
  449. ldcon/web_assets/cards/tune.temperature_once.html +18 -0
  450. ldcon/web_assets/committing-input.js +35 -0
  451. ldcon/web_assets/configuration-editor.js +194 -0
  452. ldcon/web_assets/dashboard-state.js +39 -0
  453. ldcon/web_assets/failures.html +1 -0
  454. ldcon/web_assets/failures.js +19 -0
  455. ldcon/web_assets/fallback.css +3 -0
  456. ldcon/web_assets/fallback.html +1 -0
  457. ldcon/web_assets/fallback.js +21 -0
  458. ldcon/web_assets/fleet-settings.js +20 -0
  459. ldcon/web_assets/fleet.css +9 -0
  460. ldcon/web_assets/fleet.html +8 -0
  461. ldcon/web_assets/fleet.js +130 -0
  462. ldcon/web_assets/gateway-presentation.js +27 -0
  463. ldcon/web_assets/grafana-help.js +31 -0
  464. ldcon/web_assets/help.html +22 -0
  465. ldcon/web_assets/i18n.js +279 -0
  466. ldcon/web_assets/index.html +14 -0
  467. ldcon/web_assets/laser-workspace.js +279 -0
  468. ldcon/web_assets/modulation.html +1 -0
  469. ldcon/web_assets/modulation.js +14 -0
  470. ldcon/web_assets/reservations.html +16 -0
  471. ldcon/web_assets/reservations.js +77 -0
  472. ldcon/web_assets/teams-settings.js +91 -0
  473. ldcon/web_assets/window-actions.js +37 -0
  474. ldcon/web_assets/workspace.css +99 -0
  475. ldcon/web_fleet.py +4 -0
  476. ldcon/web_server.py +4 -0
  477. ldcon-0.2.0.dist-info/METADATA +85 -0
  478. ldcon-0.2.0.dist-info/RECORD +482 -0
  479. ldcon-0.2.0.dist-info/WHEEL +5 -0
  480. ldcon-0.2.0.dist-info/entry_points.txt +5 -0
  481. ldcon-0.2.0.dist-info/licenses/LICENSE +21 -0
  482. ldcon-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,330 @@
1
+ """Attached target changes, advanced only by the leased DLC owner's tick.
2
+
3
+ Admission never claims a controller lease. Every physical step uses set_target;
4
+ only verified steps are saved. A lost/unknown write or interrupted operation is
5
+ never resumed. GET and cancel never drive an output or restore an old target.
6
+ """
7
+ from copy import deepcopy
8
+ import json
9
+ import math
10
+ from pathlib import Path
11
+ import re
12
+ import time
13
+ import uuid
14
+
15
+ from ldcon.core.contracts import MeasurementUnavailable
16
+ from ldcon.core.interface_schema import digest
17
+ from ldcon.core.target_policy import laser_target, target_bounds
18
+ from ldcon.jobs.lock_target import DEFAULTS
19
+ from ldcon.owners.dlcpro_target_store import read_laser, save_target, target_file_guard, write_record
20
+
21
+ REQUEST_ID = re.compile(r'[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}')
22
+ FINAL = {'settled', 'failed', 'cancelled'}
23
+
24
+
25
+ class TargetError(ValueError):
26
+ def __init__(self, status, code, detail, **extra):
27
+ super().__init__(detail)
28
+ self.payload = dict(status=status, code=code, detail=detail, **extra)
29
+
30
+
31
+ def request_body(body):
32
+ if not isinstance(body, dict) or set(body) - {'target_frequency_hz', 'request_id', 'timeout_s'}:
33
+ raise TargetError(400, 'invalid_request', 'Expected target_frequency_hz, request_id and optional timeout_s')
34
+ target, identity = body.get('target_frequency_hz'), body.get('request_id')
35
+ timeout = body.get('timeout_s', DEFAULTS['timeout_s'])
36
+ timeout = DEFAULTS['timeout_s'] if timeout is None else timeout
37
+ if type(target) not in (int, float) or not math.isfinite(target) or target <= 0:
38
+ raise TargetError(400, 'invalid_request', 'target_frequency_hz must be positive and finite')
39
+ if not isinstance(identity, str) or not REQUEST_ID.fullmatch(identity):
40
+ raise TargetError(400, 'invalid_request', 'Invalid request_id')
41
+ if type(timeout) not in (int, float) or not math.isfinite(timeout) or not 0 < timeout <= 86400:
42
+ raise TargetError(400, 'invalid_request', 'timeout_s must be in (0, 86400]')
43
+ return dict(target_frequency_hz=float(target), request_id=identity, timeout_s=float(timeout))
44
+
45
+
46
+ class TargetChanges:
47
+ def __init__(self, protocol, laser_file, state_directory, *, wall_clock=time.time):
48
+ self.protocol, self.owner = protocol, protocol.owner
49
+ self.laser_file = Path(laser_file)
50
+ self.root = Path(state_directory)
51
+ self.root.mkdir(parents=True, exist_ok=True)
52
+ self.wall_clock = wall_clock
53
+ self.records = {}
54
+ for path in self.root.glob('*.json'):
55
+ record = json.loads(path.read_text())
56
+ if record['laser'] != self.owner.laser_id or path.stem != record['change_id']:
57
+ raise ValueError('DLC target receipt identity mismatch')
58
+ self.records[record['change_id']] = record
59
+ if record['state'] not in FINAL:
60
+ self._finish(record, 'failed', 'Owner restarted; target operation interrupted and never resumed',
61
+ review_required=record['state'] == 'applying')
62
+
63
+ def _save(self, record):
64
+ record['updated_unix_s'] = self.wall_clock()
65
+ try:
66
+ write_record(self.root / (record['change_id'] + '.json'), record)
67
+ except OSError as error:
68
+ # A durable receipt failure is not a transient optical read failure.
69
+ raise RuntimeError('Cannot persist target receipt; inspect before another step') from error
70
+
71
+ def _finish(self, record, state, message, **extra):
72
+ record.update(state=state, message=message, **extra)
73
+ self._save(record)
74
+ return self.view(record)
75
+
76
+ def active(self):
77
+ return next((r for r in self.records.values() if r['state'] not in FINAL or r.get('review_required')), None)
78
+
79
+ def _laser(self):
80
+ laser, rev = read_laser(self.laser_file)
81
+ interface = laser.get('interfaces', {}).get('dlc_lock', {})
82
+ device = laser.get('devices', {}).get(interface.get('device'), {})
83
+ if laser.get('id') != self.owner.laser_id or digest(device) != digest(self.protocol.device):
84
+ raise TargetError(409, 'configuration_changed', 'Laser definition differs from the attached owner')
85
+ if laser.get('allow_output') is not True or device.get('control_settings', {}).get('commissioned') is not True:
86
+ raise TargetError(409, 'not_configured', 'DLC output/commissioning is not enabled')
87
+ return laser, rev
88
+
89
+ def _available(self):
90
+ if self.owner.fault:
91
+ raise TargetError(409, 'owner_fault', 'Owner requires inspection')
92
+ if not self.protocol.client or self.owner.mode != 'lock':
93
+ raise TargetError(409, 'busy', 'A running DLC controller lease is required', retry_after_s=1)
94
+ sweep = getattr(self.owner, 'awg_sweep', None)
95
+ if sweep is not None and sweep.active():
96
+ raise TargetError(409, 'busy', 'An AWG sweep is armed or active', retry_after_s=1)
97
+ if self.owner.clock() - self.owner.last_client > self.owner.policy['client_timeout_s']:
98
+ raise TargetError(409, 'busy', 'Controller heartbeat is stale', retry_after_s=1)
99
+
100
+ def view(self, record):
101
+ keys = ('laser', 'request_id', 'change_id', 'target_frequency_hz', 'before_hz',
102
+ 'state', 'message', 'updated_unix_s', 'frequency_hz', 'error_hz',
103
+ 'ramped_target_hz', 'saved_target_hz', 'applied_steps', 'review_required',
104
+ 'verify', 'admitted_bounds_hz')
105
+ result = {key: deepcopy(record[key]) for key in keys if key in record}
106
+ result.update(final=record['state'] in FINAL,
107
+ applied_to_lock=record['state'] == 'settled',
108
+ ramp_active=record['state'] not in FINAL and self.owner.target != record['target_frequency_hz'])
109
+ return result
110
+
111
+ def find(self, change_id=None, request_id=None):
112
+ record = self.records.get(change_id) if change_id else next(
113
+ (r for r in self.records.values() if r['request_id'] == request_id), None)
114
+ if record is None:
115
+ raise TargetError(404, 'not_found', 'Unknown target request')
116
+ return record
117
+
118
+ def start(self, body):
119
+ body = request_body(body)
120
+ previous = next((r for r in self.records.values() if r['request_id'] == body['request_id']), None)
121
+ if previous is not None:
122
+ if previous['body'] != body:
123
+ raise TargetError(409, 'request_conflict', 'request_id identifies a different immutable request')
124
+ return dict(status=200, change=self.view(previous))
125
+ self._available()
126
+ if self.active() is not None:
127
+ raise TargetError(409, 'busy', 'Earlier target change is active or requires inspection', retry_after_s=1)
128
+ with target_file_guard(self.laser_file):
129
+ laser, rev = self._laser()
130
+ block, bounds = laser_target(laser), target_bounds(laser)
131
+ if block is None or bounds is None or block.get('max_change_hz') is None:
132
+ raise TargetError(409, 'not_configured', 'Target bounds and maximum step must be configured')
133
+ target = body['target_frequency_hz']
134
+ if not bounds[0] <= target <= bounds[1]:
135
+ raise TargetError(422, 'out_of_bounds', 'Target is outside the frozen admission range', bounds_hz=bounds)
136
+ now = self.owner.clock()
137
+ record = dict(**body, body=body, change_id=uuid.uuid4().hex, laser=self.owner.laser_id,
138
+ state='accepted', message='Accepted; waiting for fresh optical settlement',
139
+ before_hz=self.owner.target, ramped_target_hz=self.owner.target,
140
+ saved_target_hz=block['frequency_hz'], config_revision=rev,
141
+ admitted_bounds_hz=bounds, step_hz=min(block['max_change_hz'], self.owner.control['capture_hz']),
142
+ tolerance_hz=self.owner.control['tolerance_hz'],
143
+ controller_lease=self.protocol.client, target_revision=self.owner.target_revision,
144
+ created_s=now, deadline_s=now + body['timeout_s'], created_unix_s=self.wall_clock(),
145
+ acquisition_fence_s=now, samples=[], applied_steps=0)
146
+ self._save(record) # Receipt exists before any PI preemption or physical write.
147
+ self.records[record['change_id']] = record
148
+ return dict(status=202, change=self.view(record))
149
+
150
+ def cancel(self, change_id):
151
+ record = self.find(change_id=change_id)
152
+ if record['state'] not in FINAL:
153
+ self._finish(record, 'cancelled', 'Cancelled; last confirmed target retained, no rollback')
154
+ return dict(status=200, change=self.view(record))
155
+
156
+ def get_target(self):
157
+ laser, _ = self._laser()
158
+ block = laser_target(laser)
159
+ change = self.active()
160
+ sample = None
161
+ try:
162
+ sample = self.owner.read_control()
163
+ except (MeasurementUnavailable, OSError):
164
+ pass
165
+ locked = bool(sample and self.owner.mode == 'lock' and not self.owner.fault and
166
+ abs(sample.frequency_hz - self.owner.target) <= self.owner.control['tolerance_hz'])
167
+ return dict(laser=self.owner.laser_id, frequency_hz=block['frequency_hz'],
168
+ wavelength_nm=299792458e9 / block['frequency_hz'], bounds_hz=target_bounds(laser),
169
+ max_change_hz=min(block['max_change_hz'], self.owner.control['capture_hz'])
170
+ if self.owner.mode == 'lock' else block['max_change_hz'],
171
+ locked=locked, follows_laser_target=True, queue_supported=False,
172
+ lock_state=self.owner.mode, lock_job=None,
173
+ measured_frequency_hz=sample.frequency_hz if sample else None,
174
+ ramped_target_hz=self.owner.target,
175
+ change=self.view(change) if change else None)
176
+
177
+ def tick(self):
178
+ """Call under owner.mutex before its ordinary PI tick; never from GET."""
179
+ record = self.active()
180
+ if record is None or record['state'] in FINAL:
181
+ return
182
+ try:
183
+ self._available()
184
+ if (self.protocol.client != record['controller_lease'] or
185
+ self.owner.target_revision != record['target_revision']):
186
+ self._finish(record, 'failed', 'Controller or target changed; newer target retained')
187
+ return
188
+ if self.owner.clock() >= record['deadline_s']:
189
+ self._finish(record, 'failed', 'Target settlement timed out; last confirmed target retained')
190
+ return
191
+ # UDP read_control reads the validated receiver cache. Check that
192
+ # cache before any file lock, YAML parse, hash or durable receipt.
193
+ sample = self.owner.read_control()
194
+ if sample.acquired_s <= record['acquisition_fence_s']:
195
+ return
196
+ if record['state'] in ('accepted', 'ramping'):
197
+ if (sample.acquired_s <= record.get('last_step_attempt_s', float('-inf')) or
198
+ self.owner.clock() - self.owner.last_pi_write <
199
+ self.owner.policy['axes']['piezo']['update_interval_s']):
200
+ return
201
+ if abs(sample.frequency_hz - self.owner.target) > self.owner.control['capture_hz']:
202
+ raise MeasurementUnavailable('Current frequency is outside capture before target step')
203
+ # A configuration editor must not block the primary PI cycle.
204
+ with target_file_guard(self.laser_file, blocking=False):
205
+ if not self._configuration_current(record):
206
+ return
207
+ record['last_step_attempt_s'] = sample.acquired_s
208
+ self._step(record)
209
+ return
210
+ last = record.get('last_verification_acquisition_s')
211
+ if last is not None and sample.acquired_s - last < DEFAULTS['sample_interval_s']:
212
+ return
213
+ self._verify(record, sample)
214
+ except (MeasurementUnavailable, OSError) as error:
215
+ # Only fresh read failures are safe waits. Physical writes/save are
216
+ # handled by _step before reaching here.
217
+ record.update(samples=[], message='Waiting for fresh optical frequency: ' + str(error))
218
+ except TargetError as error:
219
+ self._finish(record, 'failed', str(error))
220
+ except Exception as error:
221
+ self._finish(record, 'failed', 'Target operation failed: ' + str(error), review_required=True)
222
+
223
+ def _configuration_current(self, record):
224
+ """Under the file guard, at mutation/final-confirmation boundaries only."""
225
+ _, rev = self._laser()
226
+ if rev == record['config_revision']:
227
+ return True
228
+ self._finish(record, 'failed', 'Newer laser configuration retained; sequence stopped')
229
+ return False
230
+
231
+ def _step(self, record):
232
+ o = self.owner
233
+ current, target = o.target, record['target_frequency_hz']
234
+ cap = min(record['step_hz'], o.control['capture_hz'])
235
+ step = current + math.copysign(min(abs(target - current), cap), target - current)
236
+ if abs(step - current) > cap:
237
+ step = math.nextafter(step, current)
238
+ if current != target and step == current:
239
+ raise TargetError(422, 'step_too_small', 'Configured step is below frequency representation spacing')
240
+ if not record['admitted_bounds_hz'][0] <= step <= record['admitted_bounds_hz'][1]:
241
+ raise TargetError(422, 'out_of_bounds', 'Next step left the frozen admission range')
242
+ if current != target:
243
+ record['state'] = 'applying'
244
+ self._save(record) # Crash from here onward requires inspection, never replay.
245
+ last_client = o.last_client
246
+ try:
247
+ o.set_target(step, record['target_revision'])
248
+ except MeasurementUnavailable:
249
+ if o.fault:
250
+ self._finish(record, 'failed', 'Uncertain target write; owner fault retained', review_required=True)
251
+ return
252
+ record['state'] = 'ramping'
253
+ self._save(record)
254
+ raise
255
+ except Exception as error:
256
+ self._finish(record, 'failed', 'Target write outcome requires inspection: ' + str(error), review_required=True)
257
+ return
258
+ finally:
259
+ o.last_client = last_client # Attached API cannot renew the main controller lease.
260
+ record.update(ramped_target_hz=o.target, target_revision=o.target_revision,
261
+ applied_steps=record['applied_steps'] + 1)
262
+ else:
263
+ o.pi_trial.preempt('foreground_target')
264
+ try:
265
+ record['config_revision'] = save_target(self.laser_file, record['config_revision'], o.target)
266
+ record['saved_target_hz'] = o.target
267
+ except Exception as error:
268
+ self._finish(record, 'failed', 'Applied target but configuration save unresolved: ' + str(error), review_required=True)
269
+ return
270
+ record.update(state='verifying', samples=[], acquisition_fence_s=o.clock(),
271
+ message='Waiting for fresh optical settlement at the confirmed target')
272
+ record.pop('last_verification_acquisition_s', None)
273
+ self._save(record)
274
+
275
+ def _verify(self, record, sample):
276
+ samples = record['samples']
277
+ record['last_verification_acquisition_s'] = sample.acquired_s
278
+ if samples and sample.acquired_s - samples[-1]['acquired_s'] > self.owner.policy['max_sample_age_s'] + DEFAULTS['sample_interval_s']:
279
+ samples.clear()
280
+ error = sample.frequency_hz - self.owner.target
281
+ record.update(frequency_hz=sample.frequency_hz,
282
+ error_hz=sample.frequency_hz - record['target_frequency_hz'],
283
+ updated_unix_s=self.wall_clock())
284
+ tolerance = min(record['tolerance_hz'], self.owner.control['tolerance_hz'])
285
+ if abs(error) > tolerance:
286
+ samples.clear()
287
+ return
288
+ samples.append(dict(frequency_hz=sample.frequency_hz, acquired_s=sample.acquired_s,
289
+ sequence=sample.sequence, error_hz=error))
290
+ if len(samples) < DEFAULTS['verify_samples']:
291
+ return
292
+ if self.owner.target != record['target_frequency_hz']:
293
+ record.update(state='ramping', samples=[], acquisition_fence_s=sample.acquired_s)
294
+ self._save(record)
295
+ return
296
+ with target_file_guard(self.laser_file, blocking=False):
297
+ if not self._configuration_current(record):
298
+ return
299
+ record['verify'] = dict(tolerance_hz=tolerance, samples=deepcopy(samples),
300
+ sample_interval_s=DEFAULTS['sample_interval_s'],
301
+ acquisition_fence_s=record['acquisition_fence_s'])
302
+ self._finish(record, 'settled', 'Confirmed saved target and fresh optical settlement')
303
+
304
+
305
+ def attach(protocol, laser_file, state_directory):
306
+ protocol.owner.target_changes = TargetChanges(protocol, laser_file, state_directory)
307
+
308
+
309
+ def tick(owner):
310
+ manager = getattr(owner, 'target_changes', None)
311
+ if manager is not None:
312
+ manager.tick()
313
+
314
+
315
+ def dispatch(protocol, action, request):
316
+ manager = getattr(protocol.owner, 'target_changes', None)
317
+ try:
318
+ if manager is None:
319
+ raise TargetError(409, 'not_configured', 'DLC target operation has no trusted laser file/store')
320
+ if action == 'target_change_target':
321
+ return manager.get_target()
322
+ if action == 'target_change_start':
323
+ return manager.start(request['body'])
324
+ if action == 'target_change_read':
325
+ return manager.view(manager.find(change_id=request.get('change_id'), request_id=request.get('request_id')))
326
+ if action == 'target_change_cancel':
327
+ return manager.cancel(request['change_id'])
328
+ raise TargetError(400, 'invalid_request', 'Unknown DLC target action')
329
+ except TargetError as error:
330
+ return dict(error=error.payload)
@@ -0,0 +1,44 @@
1
+ """One verified target + calibrated PZT feedforward boundary.
2
+
3
+ The internal DLC current FF stays enabled with its existing coefficient. Unknown
4
+ writes fault the owner; there is no replay or guessed target restoration.
5
+ """
6
+ import math
7
+ import time
8
+ from ldcon.drivers.dlcpro import UncertainDLCWrite
9
+ from ldcon.core.contracts import MeasurementUnavailable
10
+
11
+
12
+ def apply(owner, target):
13
+ o=owner;o._healthy()
14
+ delta_hz=target-o.target
15
+ if not math.isfinite(target) or target<=0 or abs(delta_hz)>o.control['capture_hz']:
16
+ raise ValueError('Target step exceeds the configured capture increment')
17
+ o.read_control()
18
+ before=o.instrument.snapshot(o.channel,o.lease)
19
+ slope=o.policy['axes']['piezo']['frequency_hz_per_unit']
20
+ delta=delta_hz/slope
21
+ output=before['outputs']['piezo'] if 'outputs' in before else before['piezo']['setpoint']
22
+ value=output+delta
23
+ started=o.clock();started_unix=time.time()
24
+ def before_write():
25
+ fresh=o.read_control()
26
+ if abs(fresh.frequency_hz-o.target)>o.control['capture_hz']:
27
+ raise MeasurementUnavailable('Current target left capture before FF write; no write attempted')
28
+ if o.clock()-o.last_pi_write<o.policy['axes']['piezo']['update_interval_s']:
29
+ raise MeasurementUnavailable('PZT update interval not yet elapsed; no write attempted')
30
+ try:
31
+ confirmed=o.instrument.move(o.channel,o.lease,'piezo',value,prewrite=before_write)
32
+ except UncertainDLCWrite:
33
+ o.fault='Target feedforward write outcome uncertain'
34
+ o.audit(dict(event='owner_fault',reason=o.fault));raise
35
+ o.base+=confirmed-output
36
+ o.target=target;o.target_revision+=1
37
+ fence=o.clock();o.last_acquisition=fence
38
+ o.applied_pzt=confirmed
39
+ o.last_pi_write=fence
40
+ result=dict(confirmed=True,target_frequency_hz=target,target_revision=o.target_revision,
41
+ output_before_v=output,output_after_v=confirmed,delta_v=delta,delta_frequency_hz=delta_hz,
42
+ started_s=started,confirmed_s=fence,started_unix_s=started_unix,confirmed_unix_s=time.time(),acquisition_fence_s=fence,ff_scale=1.)
43
+ o.audit(dict(event='target_step',**result))
44
+ return result
@@ -0,0 +1,75 @@
1
+ """Durable DLC target receipts and scalar-only, revision-checked target saves."""
2
+ from contextlib import contextmanager, nullcontext
3
+ import fcntl
4
+ import hashlib
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ import tempfile
9
+
10
+ from ldcon.core.interface_schema import load_mapping
11
+ from ldcon.jobs.lock_target import edit_target_text
12
+
13
+
14
+ def revision(raw):
15
+ return hashlib.sha256(raw).hexdigest()
16
+
17
+
18
+ @contextmanager
19
+ def target_file_guard(path, *, blocking=True):
20
+ """Shared with normal configuration saves; lock is beside the trusted file."""
21
+ path = Path(path)
22
+ with path.with_name('.' + path.name + '.target.lock').open('a+b') as stream:
23
+ fcntl.flock(stream, fcntl.LOCK_EX | (0 if blocking else fcntl.LOCK_NB))
24
+ yield
25
+
26
+
27
+ def configuration_guard(runtime, kind, name):
28
+ """Use alongside runtime.mutex in configuration.save (DLC and other lasers)."""
29
+ from ldcon.core.state_layout import config_file
30
+ from ldcon.core.records import safe_id
31
+ return target_file_guard(config_file(runtime.root, kind, safe_id(name))) if kind == 'lasers' else nullcontext()
32
+
33
+
34
+ def atomic_bytes(path, raw, *, expected=None):
35
+ path = Path(path)
36
+ descriptor, name = tempfile.mkstemp(prefix='.' + path.name, dir=path.parent)
37
+ try:
38
+ with os.fdopen(descriptor, 'wb') as stream:
39
+ os.fchmod(stream.fileno(), 0o660)
40
+ stream.write(raw)
41
+ stream.flush()
42
+ os.fsync(stream.fileno())
43
+ if expected is not None and revision(path.read_bytes()) != expected:
44
+ raise ValueError('Laser configuration changed before publication')
45
+ os.replace(name, path)
46
+ descriptor = os.open(path.parent, os.O_RDONLY | os.O_DIRECTORY)
47
+ try:
48
+ os.fsync(descriptor)
49
+ finally:
50
+ os.close(descriptor)
51
+ finally:
52
+ if os.path.exists(name):
53
+ os.unlink(name)
54
+
55
+
56
+ def read_laser(path):
57
+ path = Path(path)
58
+ if path.is_symlink() or not path.is_file():
59
+ raise ValueError('Trusted laser file must be a regular file')
60
+ raw = path.read_bytes()
61
+ return load_mapping(raw.decode()), revision(raw)
62
+
63
+
64
+ def save_target(path, expected, target):
65
+ """Caller holds target_file_guard from prewrite checks through this save."""
66
+ raw = Path(path).read_bytes()
67
+ if revision(raw) != expected:
68
+ raise ValueError('Laser configuration changed after target acceptance')
69
+ edited = edit_target_text(raw.decode(), target).encode()
70
+ atomic_bytes(path, edited, expected=expected)
71
+ return revision(edited)
72
+
73
+
74
+ def write_record(path, value):
75
+ atomic_bytes(path, (json.dumps(value, allow_nan=False, sort_keys=True) + '\n').encode())
@@ -0,0 +1,72 @@
1
+ """Read-only DLC owner telemetry, independent of any control card lifetime."""
2
+ from copy import deepcopy
3
+ import json
4
+ import math
5
+ import os
6
+ from urllib.request import Request, ProxyHandler, build_opener
7
+
8
+ from ldcon.core.instrument_client import validate_endpoint
9
+
10
+
11
+ def finite(value):
12
+ return type(value) in (float, int) and math.isfinite(value)
13
+
14
+
15
+ class DLCReader:
16
+ def __init__(self, connection, timeout_s=5):
17
+ self.endpoint = validate_endpoint(connection['endpoint'])
18
+ self.laser_id = connection['laser_id']
19
+ self.timeout_s = timeout_s
20
+ self.http = build_opener(ProxyHandler({}))
21
+
22
+ def read(self):
23
+ request = Request(self.endpoint + '/rpc', method='POST',
24
+ data=json.dumps(dict(action='inspect', laser_id=self.laser_id)).encode(),
25
+ headers={'Content-Type': 'application/json'})
26
+ token = os.environ.get('LDCON_INSTRUMENT_TOKEN')
27
+ if token:
28
+ request.add_header('Authorization', 'Bearer ' + token)
29
+ with self.http.open(request, timeout=self.timeout_s) as response:
30
+ result = json.load(response)
31
+ if not result.get('ok'):
32
+ raise RuntimeError('DLC owner telemetry read failed')
33
+ return result['result']
34
+
35
+
36
+ def summarize(result, *, now, max_age_s):
37
+ state = result['snapshot']
38
+ row = {key: deepcopy(state[key]) for key in (
39
+ 'observed_s', 'hardware', 'outputs', 'target_frequency_hz',
40
+ 'target_revision', 'mode', 'pi_enabled', 'integral_v', 'control',
41
+ 'frequency_input', 'hardware_readback', 'open_loop') if key in state}
42
+ row.update(read_ok=True, owner_fault=bool(state.get('fault')),
43
+ owner_busy=result.get('client_claimed') is True)
44
+ if type(state.get('pi_enabled')) is bool:
45
+ row['native_pi_enabled'] = state['pi_enabled']
46
+ emission = state.get('hardware', {}).get('emission')
47
+ if type(emission) is bool:
48
+ row['output_enabled'] = emission
49
+ obs = state.get('observation') or {}
50
+ acquired = obs.get('acquired_unix_s')
51
+ age = now-acquired if finite(acquired) else None
52
+ fresh = finite(age) and 0 <= age <= max_age_s
53
+ frequency = obs.get('frequency_hz')
54
+ valid = fresh and finite(frequency) and frequency > 0
55
+ quality = obs.get('quality') or {}
56
+ optical = (valid and obs.get('quality_valid') is True
57
+ and quality.get('fresh') is True and quality.get('quality_valid') is True)
58
+ row.update(frequency_valid=valid, optical_fresh=fresh, optical_valid=optical,
59
+ observation=dict(quality_valid=optical))
60
+ if finite(age):
61
+ row['sample_age_s'] = age
62
+ if finite(acquired):
63
+ row['acquired_unix_s'] = acquired
64
+ if valid:
65
+ row['frequency_hz'] = frequency
66
+ row['wavelength_nm'] = 299792458e9/frequency
67
+ if optical:
68
+ metrics = {k: v for k, v in (obs.get('metrics') or {}).items() if finite(v)}
69
+ row['observation']['metrics'] = metrics
70
+ if type(quality.get('single_mode')) is bool:
71
+ row['single_mode'] = quality['single_mode']
72
+ return row