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,142 @@
1
+ """Finite baseline/candidate comparison using the shared background lifecycle."""
2
+ import copy
3
+ import math
4
+ import statistics
5
+ from pathlib import Path
6
+
7
+ from experiment_core.records import atomic_json
8
+ from ldcon.core.adjustment_registry import parameters,number
9
+ from ldcon.jobs.background_pid_session import BackgroundPIDSession,_Refused
10
+ from ldcon.jobs.adaptive_window_session import finite
11
+ from ldcon.jobs.measurement_statistics import numeric_summary
12
+
13
+
14
+ def summarize(rows,objective,center):
15
+ errors=[r['error_hz'] for r in rows];dac=[r['outputs']['piezo'] for r in rows]
16
+ measured=numeric_summary(errors)
17
+ if objective=='frequency_rms_hz':score=measured['rms']
18
+ elif objective=='pzt_midrange_rms_count':score=math.sqrt(statistics.mean((v-center)**2 for v in dac))
19
+ else:score=sum(abs(b-a) for a,b in zip(dac,dac[1:]))/(rows[-1]['acquired_s']-rows[0]['acquired_s'])
20
+ cadence=[r.get('native_interval_last_s',r.get('native_interval_s')) for r in rows]
21
+ cadence=[v for v in cadence if finite(v) and v>0]
22
+ tec=[r['outputs']['temperature'] for r in rows]
23
+ return dict(score=score,samples=len(rows),start=rows[0]['acquired_s'],end=rows[-1]['acquired_s'],
24
+ source_interval_s=statistics.median(cadence) if cadence else None,
25
+ observation_interval_s=statistics.median(b['acquired_s']-a['acquired_s'] for a,b in zip(rows,rows[1:])),
26
+ tec_excursion_v=max(tec)-min(tec),frequency_rms_hz=measured['rms'],
27
+ statistics={'frequency_error_hz':measured})
28
+
29
+
30
+ class BackgroundParameterSession(BackgroundPIDSession):
31
+ def __init__(self,directory,lock,*,family,adjustment_parameters,**kwargs):
32
+ self.family=family;self.rows=[];self.baseline=None;self.comparison=None;self.phase='baseline';self.last_source=None;self.last_native_seq=None
33
+ super().__init__(directory,lock,pid_parameters=adjustment_parameters,**kwargs)
34
+
35
+ def _parameters(self,raw,lock):
36
+ if not callable(getattr(lock,'background_parameters_target',None)):
37
+ raise ValueError('Owner adapter lacks runtime parameter epochs')
38
+ return parameters(raw,self.family,require_candidate=True)
39
+
40
+ def _start(self):
41
+ self._fence()
42
+ info=self.lock.response_info()
43
+ if self.expected_system_id and (info.get('owner_context') or {}).get('system_id')!=self.expected_system_id:
44
+ raise _Refused('Owner belongs to another laser')
45
+ status=self.lock.background_status()
46
+ if not self._reference_before_begin(info,status):return
47
+ identity=dict(self.background,controller_job_id=self.directory.parents[2].name)
48
+ identity={k:identity[k] for k in ('monitor_id','generation','grant_revision','controller_job_id')}
49
+ result=self._effect('begin',lambda:self.lock.background_begin(family=self.family,parameters=self.p,
50
+ identity=identity,expected_generation=status['generation'],
51
+ expected_operating_mode_revision=self.background.get('expected_operating_mode_revision'),poll_interval_s=self.poll_interval_s,
52
+ handoff_timeout_s=self.background['handoff_timeout_s']))
53
+ self.epoch=result['epoch'];self.active=True
54
+ self._remember_epoch(self.epoch)
55
+ if not self._reference_after_begin():return
56
+ self.center=self.epoch['checkpoint']['pzt_midrange_dac']
57
+ self.skip_row=True
58
+ self.phase_started=self.clock();self.rows=[]
59
+ atomic_json(self.directory/'checkpoint.json',self.epoch)
60
+ self._status('measuring',phase=self.phase,parameters=self.epoch['checkpoint']['parameters'])
61
+
62
+ def _sample(self,row):
63
+ if row is None:return
64
+ stamp=row.get('acquired_s');source=row.get('source_acquired_unix_s') or stamp
65
+ # Never consume the row which preceded owner admission/candidate commit.
66
+ if not finite(stamp) or not finite(source) or self.last_source is not None and source<=self.last_source:return
67
+ self.last_source=source
68
+ seq=row.get('native_input_seq')
69
+ previous_native=self.last_native_seq
70
+ self.last_native_seq=seq if type(seq) is int else None
71
+ if row.get('native_input_stale') is not False or type(seq) is not int or (previous_native is not None and seq<=previous_native):
72
+ self.rows=[];self._status('waiting_for_measurement',reason='Fresh advancing native input required');return
73
+ if self.skip_row:
74
+ self.skip_row=False;return
75
+ if self.clock()-self.phase_started<self.p['settle_s']:return
76
+ if row.get('control_ready_passed') is not True or row.get('adjustment_ready_passed', row.get('entry_gate_passed')) is not True or row.get('target_ramp_active') or row.get('state')!='locked':
77
+ self.rows=[];self._status('waiting_for_measurement',reason='Fresh unchanged lock context required');return
78
+ try:
79
+ number(row['error_hz'],'error')
80
+ for key in ('piezo','temperature','current'):number(row['outputs'][key],key)
81
+ except (ValueError,KeyError,TypeError):
82
+ self.rows=[];return
83
+ if self.rows and stamp<=self.rows[-1]['acquired_s']:return
84
+ if self.rows and stamp-self.rows[-1]['acquired_s']>self.p['max_sample_gap_s']:
85
+ self.rows=[]
86
+ item={k:copy.deepcopy(row[k]) for k in ('acquired_s','error_hz','outputs')}
87
+ for key in ('native_interval_last_s','native_interval_s','supervision_sequence'):
88
+ if key in row:item[key]=row[key]
89
+ self.rows.append(item)
90
+ if len(self.rows)>100000:raise _Refused('Comparison evidence budget exceeded')
91
+ self._status('measuring',phase=self.phase,samples=len(self.rows),candidate=self.p['candidate'])
92
+ if len(self.rows)<self.p['min_samples'] or stamp-self.rows[0]['acquired_s']<self.p['window_s']:return
93
+ # A 100 s candidate must not be adopted before its slow loop has acted.
94
+ # Four planner cycles cover at least two TEC/FF opportunities; actual
95
+ # thermal motion is still independently required for FF identification.
96
+ first,last=self.rows[0].get('supervision_sequence'),self.rows[-1].get('supervision_sequence')
97
+ if type(first) is not int or type(last) is not int or last-first<4:return
98
+ result=summarize(self.rows,self.p['objective'],self.center)
99
+ result['supervision_cycles']=last-first
100
+ atomic_json(self.directory/(self.phase+'-measurements.json'),dict(summary=result,rows=self.rows))
101
+ atomic_json(self.directory/('segment-%03d-%s-measurements.json'%(self.segment,self.phase)),dict(summary=result,rows=self.rows))
102
+ if self.phase=='baseline':
103
+ self.baseline=result;self._fence()
104
+ receipt=self._effect('candidate',lambda:self.lock.background_parameters_target(self.epoch['token']))
105
+ if receipt.get('epoch',{}).get('phase')!='candidate':raise _Refused('Owner did not accept candidate')
106
+ self.phase='candidate';self.phase_started=self.clock();self.rows=[]
107
+ return
108
+ self.comparison=dict(objective=self.p['objective'],baseline=self.baseline,candidate=result)
109
+ a,b=self.baseline['source_interval_s'],result['source_interval_s']
110
+ if a is None or b is None or abs(b-a)>a*self.p['maximum_cadence_fraction']:
111
+ self.preempt('incomparable_source_cadence');return
112
+ a,b=self.baseline['observation_interval_s'],result['observation_interval_s']
113
+ if self.p['objective']=='pzt_activity_count_per_s' and abs(b-a)>a*self.p['maximum_cadence_fraction']:
114
+ self.preempt('incomparable_source_cadence');return
115
+ if min(self.baseline['tec_excursion_v'],result['tec_excursion_v'])<self.p['minimum_tec_excursion_v']:
116
+ self.preempt('insufficient_thermal_excitation');return
117
+ if result['score']>=self.baseline['score']*(1-self.p['minimum_improvement_fraction']):
118
+ self.preempt('not_improved');return
119
+ # Improvement in a chosen auxiliary objective cannot hide worse frequency locking.
120
+ if result['frequency_rms_hz']>self.baseline['frequency_rms_hz']*(1+self.p['minimum_improvement_fraction']):
121
+ self.preempt('frequency_regressed');return
122
+ self._fence()
123
+ receipt=self._effect('adopt',lambda:self.lock.background_adopt(self.epoch['token']))
124
+ self._completed(receipt,'succeeded')
125
+
126
+ def _completed(self,receipt,status):
127
+ if status=='cancelled' and self.returning in ('not_improved','insufficient_thermal_excitation','incomparable_source_cadence','frequency_regressed'):
128
+ status='unchanged'
129
+ super()._completed(receipt,status)
130
+ path=self.directory/'results'/(self.family+'.json')
131
+ import json
132
+ result=json.loads(path.read_text())
133
+ result['comparison']=self.comparison
134
+ atomic_json(path,result)
135
+ atomic_json(self.directory/('segment-%03d-%s-result.json'%(self.segment,self.family)),result)
136
+ if self.continuation is not None:
137
+ self.continuation['evidence']=result
138
+ atomic_json(self.directory/('segment-%03d-outcome.json'%self.segment),self.continuation)
139
+ self._status(self.continuation['state'])
140
+ # Details stay with the job; no notifications or external upload here.
141
+ from html import escape
142
+ (self.directory/'report.html').write_text('<!doctype html><html lang="en"><meta charset="utf-8"><title>Runtime adjustment</title><h1>'+escape(self.family)+' comparison</h1><pre>'+escape(json.dumps(result,indent=2))+'</pre></html>')
@@ -0,0 +1,447 @@
1
+ """Finite existing OnlinePIDTuner composition inside a background owner epoch.
2
+
3
+ The model chooses an admitted strategy or a typed candidate within owner bounds. Native gain transitions
4
+ and restoration stay in the owner, independent of model/provider availability.
5
+ """
6
+ import copy
7
+ import math
8
+ import statistics
9
+ from pathlib import Path
10
+ import time
11
+
12
+ from experiment_core.records import Journal,atomic_json,digest,safe_id
13
+ from ldcon.core.contracts import DeviceError,MeasurementUnavailable
14
+ from ldcon.core.instrument_client import BrokerError
15
+ from ldcon.jobs.online_pid import OnlinePIDTuner,parameters
16
+ from ldcon.jobs.adaptive_window_session import bounded_json,finite
17
+ from ldcon.jobs.adjustment_continuation import AdjustmentContinuation,RECOVERABLE
18
+ from ldcon.jobs.measurement_statistics import numeric_summary
19
+
20
+
21
+ def background_parameters(card, overrides, *, require_candidate=False):
22
+ """Shared background schema; candidate values never widen physical policy."""
23
+ source={**card['parameters'],**overrides}
24
+ if source.get('strategy')!='validated_candidates':
25
+ return parameters(card,overrides)
26
+ proposed=source.pop('candidate',None)
27
+ source['strategy']='factor_reduction'
28
+ result=parameters({'parameters':{}},source)
29
+ if result['save_settings']:
30
+ raise ValueError('Validated background candidates must be runtime-only')
31
+ result['strategy']='validated_candidates'
32
+ if proposed is not None:
33
+ from ldcon.owners.flexible_native_trial import candidate
34
+ result['candidate']=candidate(proposed)
35
+ elif require_candidate:
36
+ raise ValueError('A typed native PI candidate is required at admission')
37
+ return result
38
+
39
+
40
+ class _SessionTuner(OnlinePIDTuner):
41
+ def _metrics(self,rows,actuator):
42
+ result=super()._metrics(rows,actuator)
43
+ measured=numeric_summary(r['error'] for _,r in rows)
44
+ result.update(rms=measured['rms'],mean=measured['mean'],statistics={'frequency_error_hz':measured})
45
+ s=actuator.session
46
+ atomic_json(s.directory/('segment-%03d-%s-%03d-measurements.json'%(s.segment,self.phase,self.trial_index)),
47
+ dict(summary=result,rows=rows,pid=actuator.pid))
48
+ return result
49
+
50
+ def _complete(self,status,pid,reason):
51
+ pid.session.comparison_reason=reason
52
+ return super()._complete(status,pid,reason)
53
+
54
+
55
+ class _CandidateTuner(_SessionTuner):
56
+ """One explicit, owner-proved candidate using existing finite comparison."""
57
+ def tick(self,now,row,pid,actuator):
58
+ settling=False
59
+ if self.active and self.phase=='trial' and not self.finishing and not self.finalizing:
60
+ ready=self._ready(pid)
61
+ if not ready:
62
+ self.candidate_ready_at=None
63
+ self.candidate_measuring=False
64
+ elif self.candidate_ready_at is None:
65
+ # This tick is a fresh accepted acquisition, after every owner
66
+ # configuration step. Transition time is not settling time.
67
+ self.candidate_ready_at=now+self.p['settle_s']
68
+ settling=not ready or now<self.candidate_ready_at
69
+ if not self.candidate_measuring:
70
+ self.rows=[]
71
+ self.window_started=now
72
+ if not settling:self.candidate_measuring=True
73
+ # Keep the shared cancellation, deadline, eligibility and asynchronous
74
+ # restoration checks active even while discarding settling samples.
75
+ super().tick(now,row,pid,actuator)
76
+ if settling and self.active and self.phase=='trial' and not self.finishing:
77
+ # window_started was reset above, so the positive measurement
78
+ # window cannot complete on this temporary row.
79
+ self.rows=[]
80
+ if self.candidate_ready_at is not None:
81
+ self._status('running',settling=True,
82
+ settle_remaining_s=max(0.,self.candidate_ready_at-now),pid=pid.pid)
83
+
84
+ def _healthy(self,row):
85
+ # Quality/display tolerances are not an optimization objective. The
86
+ # adapter has already enforced fresh input and physical owner bounds.
87
+ return not row['limited'] and math.isfinite(row['error'])
88
+
89
+ def _comparison_inconclusive(self,metrics,row):
90
+ return metrics['comparison_blocks'] < 6 or metrics['resolved_within_window_drift']
91
+
92
+ def _accept(self,metrics,old,row):
93
+ uncertainty=self.p['confidence_multiplier']*math.hypot(old['mse_standard_error'],metrics['mse_standard_error'])
94
+ improvement=old['mse']-metrics['mse']
95
+ accepted=(improvement>max(uncertainty,old['mse']*self.p['minimum_improvement'])
96
+ and metrics['peak']<=old['peak']*1.1
97
+ and metrics['activity_per_s']<=max(old['activity_per_s']*2,old['activity_per_s']+1)
98
+ and metrics['integral_saturation_fraction']<=old['integral_saturation_fraction'])
99
+ self.comparison_session.comparison=dict(baseline=old,trial=metrics,accepted=accepted,
100
+ uncertainty_margin_hz_squared=uncertainty,lock_tolerance_used=False,
101
+ limitation='Six temporal blocks with HAC inflation; finite empirical comparison, not a calibrated confidence guarantee or global optimum')
102
+ return accepted
103
+
104
+ def _metrics(self, rows, actuator):
105
+ result=super()._metrics(rows,actuator)
106
+ # Six temporal blocks, not N independent controller samples. This
107
+ # conservative comparison retains target bias in the squared loss.
108
+ start,end=rows[0][0],rows[-1][0]
109
+ duration=max(end-start,1e-12)
110
+ blocks=[[] for _ in range(6)]
111
+ signed=[[] for _ in range(6)]
112
+ for stamp,row in rows:
113
+ index=min(5,int(6*(stamp-start)/duration))
114
+ blocks[index].append(row['error']**2);signed[index].append(row['error'])
115
+ means=[statistics.mean(b) for b in blocks if len(b)>=2]
116
+ signed_means=[statistics.mean(b) for b in signed if len(b)>=2]
117
+ variance=statistics.variance(means) if len(means)>1 else 0.
118
+ if len(means)>2:
119
+ centre=statistics.mean(means)
120
+ variance+=2*sum(max(0.,sum((a-centre)*(b-centre) for a,b in zip(means,means[lag:]))/len(means))
121
+ for lag in (1,2))
122
+ resolved=False
123
+ if len(signed_means)==6:
124
+ a,b=signed_means[:3],signed_means[3:]
125
+ se=math.sqrt((statistics.variance(a)+statistics.variance(b))/3)
126
+ resolved=abs(statistics.mean(a)-statistics.mean(b))>4*max(se,math.ulp(max(map(abs,signed_means))))
127
+ result.update(mse=statistics.mean(r['error']**2 for _,r in rows),
128
+ comparison_blocks=len(means),
129
+ mse_standard_error=math.sqrt(variance/len(means)) if means else 0.,
130
+ resolved_within_window_drift=resolved,
131
+ activity_per_s=sum(abs(b['output']-a['output']) for (_,a),(_,b) in zip(rows,rows[1:]))/duration,
132
+ objective='target-centred MSE; block uncertainty, peak, actuator activity and saturation constraints',
133
+ lock_tolerance_used=False)
134
+ result['score']=result['mse']
135
+ result['integral_saturation_fraction']=sum(bool(r.get('integral_saturated')) for _,r in rows)/len(rows)
136
+ result['maximum_integral_usage']=max(r['integral_usage'] for _,r in rows)
137
+ s=actuator.session
138
+ self.comparison_session=s
139
+ atomic_json(s.directory/('segment-%03d-%s-%03d-measurements.json'%(s.segment,self.phase,self.trial_index)),
140
+ dict(summary=result,rows=rows,pid=actuator.pid))
141
+ return result
142
+
143
+ def _finalize(self, pid, actuator):
144
+ # This path can increase or decrease P, I and integral contribution.
145
+ self._finish('succeeded' if self.accepted else 'unchanged',pid,
146
+ 'Accepted an owner-validated PI/integral candidate' if self.accepted
147
+ else 'No reliable improvement; original settings retained')
148
+
149
+ def _trial(self,pid):
150
+ from ldcon.owners.flexible_native_trial import pid_from_candidate
151
+ pid.retune(pid_from_candidate(self.p['candidate']))
152
+ self.phase='trial';self.rows=[];self.window_started=self.clock()
153
+ self.candidate_ready_at=None;self.candidate_measuring=False
154
+ self._status('running',candidate=self.p['candidate'],pid=pid.pid)
155
+
156
+
157
+ class _Refused(ValueError):
158
+ pass
159
+
160
+
161
+ def changed_adoption(last, family):
162
+ import math
163
+ """Compare the actual completed owner readback with this epoch's checkpoint."""
164
+ if not isinstance(last,dict) or last.get('state')!='adopted':return False
165
+ receipt,checkpoint=last.get('receipt') or {},last.get('checkpoint') or {}
166
+ if receipt.get('confirmed') is not True or receipt.get('response_ended') is not True:return False
167
+ if family=='pi':
168
+ actual=receipt.get('actual_native') or {}
169
+ keys=('pid_kp','pid_ki','pid_kd','pid_integral_min','pid_integral_max')
170
+ if any(not finite(actual.get(k)) or not finite(checkpoint.get(k)) for k in keys):return False
171
+ # Same tolerance as both owner completion readback checks.
172
+ return any(not math.isclose(actual[k],checkpoint[k],rel_tol=1e-8,abs_tol=1e-12) for k in keys)
173
+ if family in ('slow_loop','feedforward'):
174
+ actual=receipt.get('actual_parameters');original=checkpoint.get('parameters')
175
+ keys={'tec_gain','tec_dt_cap_s','supervision_interval_s','piezo_center_dac','current_per_temperature'}
176
+ if (not isinstance(actual,dict) or not isinstance(original,dict) or
177
+ set(actual)!=keys or set(original)!=keys):return False
178
+ if any(value is not None and not finite(value) for row in (actual,original) for value in row.values()):return False
179
+ return actual!=original # Owner runtime parameter installation uses exact values.
180
+ return False
181
+
182
+
183
+ class _PIDPort:
184
+ def __init__(self,session,state):
185
+ self.session=session
186
+ self.update(state)
187
+
188
+ def update(self,state):
189
+ self.state=copy.deepcopy(state)
190
+ self.pid=copy.deepcopy(state['pid']);self.config=copy.deepcopy(state['config'])
191
+
192
+ def ready(self):return self.state['ready']
193
+
194
+ def retune(self,pid):
195
+ self.session._fence()
196
+ result=self.session._effect('target',lambda:self.session.lock.background_pid_target(self.session.epoch['token'],pid))
197
+ self.update(result['pid_tuning'])
198
+
199
+ def save_pid(self,pid):
200
+ raise _Refused('Persistent background PI adoption is unavailable')
201
+
202
+ def finish(self,status):
203
+ self.session._fence()
204
+ result=self.session._effect('finish',lambda:self.session.lock.background_pid_finish(self.session.epoch['token'],status))
205
+ self.session._completed(result,status)
206
+
207
+
208
+ class BackgroundPIDSession(AdjustmentContinuation):
209
+ family='pi'
210
+
211
+ def _parameters(self, raw, lock):
212
+ p=background_parameters({'parameters':{}},raw,require_candidate=True)
213
+ if p['strategy']=='validated_candidates' and not getattr(lock,'supports_validated_candidates',False):
214
+ raise ValueError('This owner adapter does not support validated PI/integral candidates')
215
+ if p['strategy'] not in ('factor_reduction','validated_candidates') or p['save_settings']:
216
+ raise ValueError('Background PI requires a supported bounded strategy and save_settings=false')
217
+ return p
218
+
219
+ def __init__(self,directory,lock,*,pid_parameters,background,emit=lambda *a:None,
220
+ clock=time.monotonic,wall_clock=time.time,expected_system_id=None,
221
+ foreground_probe=lambda:False,poll_interval_s=1.,continuation=False):
222
+ self._continuation_init(continuation)
223
+ self.p=self._parameters(pid_parameters,lock)
224
+ if (not isinstance(background,dict) or not {'monitor_id','generation','handoff_timeout_s'}<=set(background)
225
+ or set(background)-{'monitor_id','generation','handoff_timeout_s','grant_revision','expected_operating_mode_revision'}
226
+ or not all(isinstance(background.get(k),str) and background[k] for k in ('monitor_id','generation'))
227
+ or not finite(background['handoff_timeout_s']) or not 0<background['handoff_timeout_s']<=600):
228
+ raise ValueError('Background PI identity and explicit handoff deadline are required')
229
+ self.directory=Path(directory)/'adaptive-window'
230
+ for name in ('inbox','claimed','results'):
231
+ path=self.directory/name
232
+ if path.is_symlink():raise ValueError('Background PI directories must not be symlinks')
233
+ path.mkdir(parents=True,exist_ok=True)
234
+ self.lock,self.emit,self.clock,self.wall_clock=lock,emit,clock,wall_clock
235
+ self.background=copy.deepcopy(background);self.expected_system_id=expected_system_id
236
+ self.foreground_probe,self.poll_interval_s=foreground_probe,poll_interval_s
237
+ self.active=False;self.blocked=False;self.write_unknown=False;self.effect_revision=0
238
+ self.epoch=None;self.returning=None;self.return_deadline=None;self.last_restoration=None
239
+ self.cancel_ids=[]
240
+ self.deadline=clock()+self.p['timeout_s'];self.last_stamp=None;self.port=None;self.tuner=None
241
+ self.journal=Journal(self.directory/'execution.sqlite3')
242
+ admission=dict(family=self.family,parameters=self.p,background=background,continuation=continuation)
243
+ self.blocked=not self.journal.admit('session','adaptive',admission)
244
+ self.decision_revision=digest([admission,str(self.directory),wall_clock()])
245
+ if not self.blocked:self.journal.transition('session',0,'running',{'kind':'attached_pi'})
246
+ self._status('reconciliation_required' if self.blocked else 'idle')
247
+
248
+ def _status(self,state,**extra):
249
+ if self._continuation_waiting():state=self.continuation['state']
250
+ atomic_json(self.directory/'status.json',dict(family=self.family,state=state,active=self.active,
251
+ background=self.background,epoch=self.epoch,returning=self.returning,runtime_only=True,
252
+ decision_revision=self.decision_revision,expires_unix_s=self.wall_clock()+max(0.,self.deadline-self.clock()),
253
+ **{'segment':self.segment,'continuation':self.continuation,**extra}))
254
+
255
+ def _publish(self,value):
256
+ atomic_json(self.directory/'pid-progress.json',value)
257
+ self.emit('background_pid_progress',value)
258
+ if not self.blocked:self._status('tuning',progress=value)
259
+
260
+ def _effect(self,kind,call):
261
+ self.effect_revision+=1
262
+ identifier='effect.%d'%self.effect_revision
263
+ self.journal.admit(identifier,'adaptive',dict(kind=kind,epoch=self.epoch))
264
+ self.journal.transition(identifier,0,'running',{'kind':'intent'})
265
+ try:receipt=call()
266
+ except (MeasurementUnavailable,BrokerError) as error:
267
+ if not self.lock.uncertain_output and (isinstance(error,MeasurementUnavailable) or error.code in ('range','configuration')):
268
+ self.journal.transition(identifier,1,'inconclusive',{'kind':'prewrite_refusal'},{'reason':str(error)})
269
+ if isinstance(error,MeasurementUnavailable):raise
270
+ raise _Refused(str(error)) from error
271
+ self.write_unknown=self.blocked=True
272
+ self.journal.transition(identifier,1,'uncertain',{'kind':'unknown'},{'reason':str(error)})
273
+ raise
274
+ except Exception as error:
275
+ self.write_unknown=self.blocked=True
276
+ self.journal.transition(identifier,1,'uncertain',{'kind':'unknown'},{'reason':str(error)})
277
+ raise
278
+ self.journal.transition(identifier,1,'succeeded',{'kind':'confirmed'},receipt)
279
+ return receipt
280
+
281
+ def _fence(self):
282
+ if self.foreground_probe() or self.returning:
283
+ self.preempt('foreground')
284
+ raise _Refused('Foreground activity fenced the PI proposal')
285
+
286
+ def _start(self):
287
+ self._fence()
288
+ info=self.lock.response_info()
289
+ if self.expected_system_id and (info.get('owner_context') or {}).get('system_id')!=self.expected_system_id:
290
+ raise _Refused('Owner belongs to another laser')
291
+ status=self.lock.background_status()
292
+ if not self._reference_before_begin(info,status):return
293
+ result=self._effect('begin',lambda:self.lock.background_begin(family='pi',parameters=self.p,
294
+ expected_generation=status['generation'],
295
+ expected_operating_mode_revision=self.background.get('expected_operating_mode_revision'),poll_interval_s=self.poll_interval_s,
296
+ handoff_timeout_s=self.background['handoff_timeout_s'],
297
+ identity={k:self.background[k] for k in ('monitor_id','generation')}))
298
+ self.epoch=result['epoch'];self.active=True
299
+ self._remember_epoch(self.epoch)
300
+ if not self._reference_after_begin():return
301
+ atomic_json(self.directory/'checkpoint.json',self.epoch)
302
+ self.port=_PIDPort(self,result['pid_tuning'])
303
+ pending=[dict(request_id=self.directory.parent.name,card_id='background-pi',parameters=self.p)]
304
+ tuner_class=_CandidateTuner if self.p['strategy']=='validated_candidates' else _SessionTuner
305
+ if self.p['strategy']=='validated_candidates':
306
+ # The comparator's loop count is one; gain_factors do not select or scale this candidate.
307
+ pending[0]['parameters']=dict(self.p,gain_factors=[.5])
308
+ self.tuner=tuner_class(lambda:pending.pop() if pending else None,self._publish,self.emit,self.clock)
309
+
310
+ def _completed(self,receipt,status):
311
+ last=receipt.get('last') or {}
312
+ expected='adopted' if status=='succeeded' else 'restored'
313
+ if receipt.get('epoch') is not None or last.get('token')!=self.epoch['token'] or last.get('state')!=expected:
314
+ self.write_unknown=self.blocked=True
315
+ raise DeviceError('Owner did not confirm background PI completion')
316
+ self.active=False;self.blocked=True
317
+ if expected=='restored':self.last_restoration=copy.deepcopy(receipt)
318
+ reason=self.returning or ('not_improved' if status=='unchanged' else 'comparison_inconclusive' if status=='failed' else None)
319
+ result=dict(state=status,family=self.family,runtime_only=True,owner_receipt=receipt,
320
+ reason=reason,comparison_reason=getattr(self,'comparison_reason',None),
321
+ comparison=getattr(self,'comparison',None),
322
+ restart_behavior='Not saved; next arm follows configured parameters and retained-reference rules')
323
+ atomic_json(self.directory/'results'/(self.family+'.json'),result)
324
+ atomic_json(self.directory/('segment-%03d-%s-result.json'%(self.segment,self.family)),result)
325
+ for identifier in self.cancel_ids:
326
+ atomic_json(self.directory/'results'/(identifier+'.json'),dict(result,id=identifier))
327
+ self.emit('background_'+self.family+'_result',result)
328
+ if status == 'succeeded' and changed_adoption(last,self.family):
329
+ self.emit('quality_adjustment_accepted', dict(family=self.family,
330
+ action_id=last['token'], accepted_unix_s=self.wall_clock()))
331
+ self.epoch=None;self.returning=None
332
+ if reason in RECOVERABLE and self._interrupt(reason,evidence=result):return
333
+ old=self.journal.get('session')
334
+ if old['state']=='running':
335
+ self.journal.transition('session',old['sequence'],'succeeded' if status in ('succeeded','unchanged') else 'cancelled',
336
+ {'kind':'owner_confirmed_pi_completion'},result)
337
+ self.epoch=None;self.returning=None
338
+ self._status(status,result=result)
339
+
340
+ def preempt(self,reason='foreground'):
341
+ if self._continuation_waiting():
342
+ self._end_continuation(reason);return
343
+ if self.blocked or self.returning:return
344
+ self.returning=reason
345
+ self.return_deadline=(None if self.epoch and self.epoch.get('completion_mode')=='verified_handover'
346
+ else self.clock()+self.background['handoff_timeout_s'])
347
+ self.decision_revision=digest([self.decision_revision,'revoked',reason])
348
+ self._status('returning_before_foreground' if reason=='foreground' else 'stopping_restoring')
349
+ if not self.active:
350
+ self.blocked=True;self._status(reason)
351
+
352
+ def _restore(self):
353
+ if self.return_deadline is not None and self.clock()>self.return_deadline:
354
+ self.blocked=True;self._status('reconciliation_required',reason='PI restoration deadline exceeded');return
355
+ try:receipt=self._effect('restore',lambda:self.lock.background_restore(self.epoch['token'],self.returning))
356
+ except (MeasurementUnavailable,_Refused) as error:
357
+ self._status('stopping_restoring',reason=str(error));return
358
+ epoch=receipt.get('epoch')
359
+ if epoch and epoch.get('state')=='reconciliation_required':
360
+ self.blocked=True;self._status('reconciliation_required',reason=epoch.get('reason'));return
361
+ if epoch is None:self._completed(receipt,'cancelled')
362
+
363
+ def check_deadlines(self):
364
+ if self._check_continuation():return
365
+ if self.blocked:return
366
+ if self.active:
367
+ status=self.lock.background_status()
368
+ if status.get('owner_fault') or self.lock.uncertain_output:
369
+ self.write_unknown=self.blocked=True
370
+ raise DeviceError('Background PI owner state requires reconciliation')
371
+ epoch=status.get('epoch')
372
+ last=status.get('last') or {}
373
+ if epoch is None and last.get('token')==self.epoch['token'] and last.get('state')=='restored':
374
+ self._completed(status,'cancelled');return
375
+ if epoch and epoch.get('state')!='active':self.preempt('foreground' if status.get('reservations') else epoch.get('reason','owner_fenced'))
376
+ if self.returning:self._restore();return
377
+ if self.clock()>=self.deadline:
378
+ self.preempt('experiment_timeout')
379
+
380
+ def poll(self,*,busy=False,row=None):
381
+ if self._poll_continuation():return
382
+ self.check_deadlines()
383
+ if self.blocked or self.returning or self._continuation_waiting():return
384
+ for path in sorted((self.directory/'inbox').glob('*.json')):
385
+ try:
386
+ safe_id(path.stem)
387
+ command=bounded_json(path)
388
+ valid=isinstance(command,dict) and command.get('id')==path.stem and set(command)<={'id','action','based_on'}
389
+ claimed=self.directory/'claimed'/path.name
390
+ if claimed.exists():path.unlink();continue
391
+ path.replace(claimed)
392
+ if valid and command.get('action')=='cancel':
393
+ self.cancel_ids.append(command['id'])
394
+ atomic_json(self.directory/'results'/path.name,dict(id=command['id'],state='stopping_restoring'))
395
+ self.preempt('cancelled');return
396
+ raise ValueError('PI trials run the admitted comparison autonomously; only cancellation is accepted')
397
+ except (ValueError,TypeError,OSError) as error:
398
+ self.emit('background_pid_command_rejected',dict(reason=str(error)))
399
+ # No command can gain authority by breaking this optional inbox.
400
+ self.preempt('invalid_command');return
401
+ try:
402
+ if not self.active:
403
+ if busy:return
404
+ self._start()
405
+ if not self.active or self.returning or self.blocked:return
406
+ self._sample(row)
407
+ except MeasurementUnavailable as error:
408
+ self._status('waiting_for_measurement',reason=str(error))
409
+ except _Refused as error:
410
+ if self.active:self.preempt('prewrite_rejected')
411
+ else:self.blocked=True;self._status('rejected',reason=str(error))
412
+
413
+ def _sample(self,row):
414
+ if row is None or not row.get('pid_tuning'):return
415
+ self.port.update(row['pid_tuning'])
416
+ stamp=row.get('acquired_s')
417
+ if not finite(stamp) or self.last_stamp is not None and stamp<=self.last_stamp:return
418
+ if self.last_stamp is not None and stamp-self.last_stamp>self.port.config['max_sample_gap_s']:
419
+ self.tuner.restart_window('Accepted optical acquisition gap; acquire a complete new comparison')
420
+ self.last_stamp=stamp
421
+ tolerance=row.get('tolerance_hz')
422
+ if not finite(tolerance) or tolerance<=0:raise _Refused('Parent lock tolerance is required for PI comparison')
423
+ data=dict(error=row['error_hz'],filtered_error=row['error_hz'],output=row['outputs']['piezo'],
424
+ limited=not row['pid_tuning']['eligible'],control_deadband=tolerance,requested_tolerance=tolerance)
425
+ if self.p['strategy']=='validated_candidates':
426
+ observation=row['pid_tuning'].get('integral_observation',{})
427
+ # Only the supported adapter's fresh, in-bound raw-integral sample
428
+ # can lift the legacy saturation veto. Unknown evidence still blocks.
429
+ valid=(observation.get('valid') is True and finite(observation.get('usage'))
430
+ and 0<=observation['usage']<=1+1e-9)
431
+ data.update(limited=not valid,integral_usage=observation.get('usage'),
432
+ integral_saturated=observation.get('saturated') is True)
433
+ self.tuner.tick(self.clock(),data,self.port,self.port)
434
+
435
+ def before_recovery(self):
436
+ if self.continuation_enabled:return self._before_recovery('branch_lost')
437
+ if not self.blocked:self.close('branch_lost')
438
+
439
+ def close(self,reason='cancelled'):
440
+ if self._continuation_waiting():
441
+ self._end_continuation(reason);return
442
+ if self.write_unknown or self.lock.uncertain_output:
443
+ self.blocked=True;self._status('reconciliation_required');return
444
+ if reason in ('parent_stopped','branch_lost') and self.active:
445
+ self.lock.background_abandon(reason);self.active=False;self.blocked=True
446
+ self._status(reason,disposition='parent_safety_or_recovery_owns_outputs');return
447
+ self.preempt(reason)