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.
- ldcon/__init__.py +302 -0
- ldcon/_moved.py +49 -0
- ldcon/agent_tools.py +4 -0
- ldcon/agents/__init__.py +1 -0
- ldcon/agents/adjustment_guidance.py +73 -0
- ldcon/agents/adjustment_quality.py +399 -0
- ldcon/agents/agent_adapters.py +709 -0
- ldcon/agents/agent_background.py +307 -0
- ldcon/agents/agent_bundle.py +75 -0
- ldcon/agents/agent_cards.py +171 -0
- ldcon/agents/agent_config_access.py +22 -0
- ldcon/agents/agent_configuration.py +224 -0
- ldcon/agents/agent_configuration_policy.py +67 -0
- ldcon/agents/agent_connect.py +93 -0
- ldcon/agents/agent_device_parameters.py +41 -0
- ldcon/agents/agent_execution.py +77 -0
- ldcon/agents/agent_history.py +112 -0
- ldcon/agents/agent_inbox_watch.py +121 -0
- ldcon/agents/agent_interactive.py +386 -0
- ldcon/agents/agent_interactive_hooks.py +381 -0
- ldcon/agents/agent_language.py +48 -0
- ldcon/agents/agent_log_context.py +308 -0
- ldcon/agents/agent_log_read.py +250 -0
- ldcon/agents/agent_log_watch.py +27 -0
- ldcon/agents/agent_metrics.py +111 -0
- ldcon/agents/agent_monitor.py +272 -0
- ldcon/agents/agent_opencode_http.py +270 -0
- ldcon/agents/agent_opencode_mcp.py +64 -0
- ldcon/agents/agent_owner_status.py +216 -0
- ldcon/agents/agent_profiles.py +186 -0
- ldcon/agents/agent_providers.py +53 -0
- ldcon/agents/agent_repairs.py +105 -0
- ldcon/agents/agent_routing.py +90 -0
- ldcon/agents/agent_runtime.py +70 -0
- ldcon/agents/agent_sandbox.py +141 -0
- ldcon/agents/agent_startup_notice.py +142 -0
- ldcon/agents/agent_task.py +336 -0
- ldcon/agents/agent_tools.py +488 -0
- ldcon/agents/agent_window.py +70 -0
- ldcon/agents/agent_workspace.py +340 -0
- ldcon/agents/card_fallback.py +284 -0
- ldcon/agents/dlc_pi_adjustment.py +250 -0
- ldcon/agents/dlc_pi_plan.py +24 -0
- ldcon/agents/dlc_pi_response_plan.py +26 -0
- ldcon/agents/failure_policy.py +473 -0
- ldcon/agents/fallback_service.py +146 -0
- ldcon/agents/fallback_targets.py +96 -0
- ldcon/agents/gateway_role.py +31 -0
- ldcon/agents/manual_adjustment_grant.py +66 -0
- ldcon/agents/measurement_grant.py +95 -0
- ldcon/agents/monitor_quality_binding.py +33 -0
- ldcon/agents/notification_policy.py +346 -0
- ldcon/agents/operating_mode_client.py +99 -0
- ldcon/agents/report_pdf.py +105 -0
- ldcon/agents/resident_adjustment.py +572 -0
- ldcon/agents/resident_agent.py +438 -0
- ldcon/agents/resident_quality_apply.py +233 -0
- ldcon/agents/task_decisions.py +218 -0
- ldcon/agents/teams_card.py +100 -0
- ldcon/agents/teams_destinations.py +157 -0
- ldcon/cld_owner.py +4 -0
- ldcon/cli.py +19 -0
- ldcon/core/__init__.py +1 -0
- ldcon/core/adjustment_plan.py +47 -0
- ldcon/core/adjustment_registry.py +100 -0
- ldcon/core/agent_authority.py +903 -0
- ldcon/core/card_admission.py +15 -0
- ldcon/core/card_definition.py +14 -0
- ldcon/core/card_reporting.py +238 -0
- ldcon/core/configuration.py +357 -0
- ldcon/core/configuration_transaction.py +225 -0
- ldcon/core/contracts.py +86 -0
- ldcon/core/current_settings.py +86 -0
- ldcon/core/deck_yaml.py +45 -0
- ldcon/core/device_conditions.py +133 -0
- ldcon/core/execution_context.py +3 -0
- ldcon/core/instrument_client.py +190 -0
- ldcon/core/interface_cards.py +520 -0
- ldcon/core/interface_schema.py +640 -0
- ldcon/core/laser_assets.py +238 -0
- ldcon/core/metric_quantities.py +38 -0
- ldcon/core/modulation.py +107 -0
- ldcon/core/operation_registry.py +195 -0
- ldcon/core/paths.py +26 -0
- ldcon/core/period_context.py +118 -0
- ldcon/core/records.py +4 -0
- ldcon/core/result_reservations.py +469 -0
- ldcon/core/site_bindings.py +25 -0
- ldcon/core/state_layout.py +70 -0
- ldcon/core/target_policy.py +79 -0
- ldcon/core/telegraf.py +106 -0
- ldcon/core/telemetry.py +130 -0
- ldcon/core/temperature_settings.py +12 -0
- ldcon/dashboard_metrics.py +4 -0
- ldcon/device_registry.py +4 -0
- ldcon/drivers/__init__.py +1 -0
- ldcon/drivers/cld1015.py +296 -0
- ldcon/drivers/cld1015_config.py +31 -0
- ldcon/drivers/dlcpro.py +177 -0
- ldcon/drivers/dlcpro_analog.py +33 -0
- ldcon/drivers/dlcpro_console.py +133 -0
- ldcon/drivers/dlcpro_priority.py +46 -0
- ldcon/drivers/ecdl.py +334 -0
- ldcon/drivers/esp32_servo.py +187 -0
- ldcon/drivers/fizeau.py +114 -0
- ldcon/drivers/frequency_sweep.py +69 -0
- ldcon/drivers/influx_wavelength.py +70 -0
- ldcon/drivers/metric_sources.py +82 -0
- ldcon/drivers/native_dither_telemetry.py +50 -0
- ldcon/drivers/rigol_scope.py +126 -0
- ldcon/drivers/siglent.py +418 -0
- ldcon/drivers/staircase.py +155 -0
- ldcon/drivers/wsu_udp.py +164 -0
- ldcon/esp32_time_telemetry.py +4 -0
- ldcon/example_config.py +41 -0
- ldcon/fallback_service.py +4 -0
- ldcon/instrument_owner.py +4 -0
- ldcon/instrument_service.py +4 -0
- ldcon/interface_worker.py +4 -0
- ldcon/job_service.py +4 -0
- ldcon/jobs/__init__.py +1 -0
- ldcon/jobs/adaptive_pid.py +234 -0
- ldcon/jobs/adaptive_window_session.py +922 -0
- ldcon/jobs/adjustment_continuation.py +284 -0
- ldcon/jobs/axis_frequency.py +219 -0
- ldcon/jobs/axis_frequency_report.py +86 -0
- ldcon/jobs/background_parameter_session.py +142 -0
- ldcon/jobs/background_pid_session.py +447 -0
- ldcon/jobs/branch_maps.py +71 -0
- ldcon/jobs/branch_target_migration.py +33 -0
- ldcon/jobs/card_runtime.py +157 -0
- ldcon/jobs/change_measure.py +293 -0
- ldcon/jobs/change_measure_adapter.py +360 -0
- ldcon/jobs/change_measure_dlc.py +94 -0
- ldcon/jobs/change_measure_parameters.py +222 -0
- ldcon/jobs/change_measure_target.py +135 -0
- ldcon/jobs/current_cards.py +159 -0
- ldcon/jobs/current_devices.py +71 -0
- ldcon/jobs/dashboard_api.py +61 -0
- ldcon/jobs/device_parameters.py +426 -0
- ldcon/jobs/dlc_pi_measurement.py +151 -0
- ldcon/jobs/dlcpro_awg_analysis.py +77 -0
- ldcon/jobs/dlcpro_awg_card.py +116 -0
- ldcon/jobs/dlcpro_cards.py +240 -0
- ldcon/jobs/ecdl_branch_hop.py +175 -0
- ldcon/jobs/ecdl_branch_recovery.py +176 -0
- ldcon/jobs/ecdl_devices.py +306 -0
- ldcon/jobs/ecdl_locked_sweep_proxy.py +526 -0
- ldcon/jobs/ecdl_mapped_recovery.py +6 -0
- ldcon/jobs/ecdl_mode_map.py +469 -0
- ldcon/jobs/ecdl_mode_recovery.py +20 -0
- ldcon/jobs/ecdl_online.py +158 -0
- ldcon/jobs/ecdl_operating_point.py +403 -0
- ldcon/jobs/ecdl_pi_commission.py +161 -0
- ldcon/jobs/ecdl_recovery.py +25 -0
- ldcon/jobs/ecdl_recovery_plan.py +90 -0
- ldcon/jobs/ecdl_recovery_test.py +59 -0
- ldcon/jobs/ecdl_relock.py +7 -0
- ldcon/jobs/ecdl_response_study.py +369 -0
- ldcon/jobs/ecdl_steady.py +343 -0
- ldcon/jobs/ecdl_thermal_context.py +242 -0
- ldcon/jobs/ecdl_window_center.py +734 -0
- ldcon/jobs/emission_card.py +102 -0
- ldcon/jobs/fringe_sweep_reference.py +89 -0
- ldcon/jobs/interface_devices.py +122 -0
- ldcon/jobs/interface_worker.py +471 -0
- ldcon/jobs/job_service.py +125 -0
- ldcon/jobs/laser_live.py +270 -0
- ldcon/jobs/learned_settling.py +450 -0
- ldcon/jobs/lock_target.py +461 -0
- ldcon/jobs/locked_triangle.py +36 -0
- ldcon/jobs/measurement_acquisition.py +51 -0
- ldcon/jobs/measurement_analysis.py +59 -0
- ldcon/jobs/measurement_execution.py +40 -0
- ldcon/jobs/measurement_notice.py +78 -0
- ldcon/jobs/measurement_presets.py +52 -0
- ldcon/jobs/measurement_report.py +76 -0
- ldcon/jobs/measurement_statistics.py +22 -0
- ldcon/jobs/metric_cards.py +34 -0
- ldcon/jobs/metric_interface.py +100 -0
- ldcon/jobs/modulation_sweep.py +274 -0
- ldcon/jobs/native_background_session.py +91 -0
- ldcon/jobs/native_current_acquire.py +143 -0
- ldcon/jobs/native_current_card.py +328 -0
- ldcon/jobs/native_current_online.py +129 -0
- ldcon/jobs/native_ff_proxy.py +1361 -0
- ldcon/jobs/native_ff_sweep.py +802 -0
- ldcon/jobs/online_pid.py +178 -0
- ldcon/jobs/operating_point.py +1399 -0
- ldcon/jobs/operating_point_proxy.py +236 -0
- ldcon/jobs/operation_execution.py +126 -0
- ldcon/jobs/operation_history.py +419 -0
- ldcon/jobs/optical_broker.py +23 -0
- ldcon/jobs/pi_algorithms.py +30 -0
- ldcon/jobs/pi_response.py +239 -0
- ldcon/jobs/progress.py +63 -0
- ldcon/jobs/quality_baseline.py +411 -0
- ldcon/jobs/recovery_decisions.py +131 -0
- ldcon/jobs/recovery_history.py +152 -0
- ldcon/jobs/recovery_procedure.py +285 -0
- ldcon/jobs/recovery_reference.py +123 -0
- ldcon/jobs/recovery_stationary.py +138 -0
- ldcon/jobs/response_algorithms.py +47 -0
- ldcon/jobs/response_comparison.py +62 -0
- ldcon/jobs/response_record.py +90 -0
- ldcon/jobs/runtime_parameters.py +434 -0
- ldcon/jobs/settling_profile.py +140 -0
- ldcon/jobs/shot_analysis.py +1300 -0
- ldcon/jobs/simulation.py +127 -0
- ldcon/jobs/slow_control.py +208 -0
- ldcon/jobs/slow_current.py +88 -0
- ldcon/jobs/study_adoption.py +122 -0
- ldcon/jobs/study_execution.py +182 -0
- ldcon/jobs/sweep_ff_analysis.py +320 -0
- ldcon/jobs/sweep_ff_optimization.py +202 -0
- ldcon/jobs/sweep_ff_plan.py +166 -0
- ldcon/jobs/sweep_ff_report.py +254 -0
- ldcon/jobs/sweep_ff_transport.py +326 -0
- ldcon/jobs/sweep_tracking.py +424 -0
- ldcon/jobs/sweep_verdict.py +288 -0
- ldcon/jobs/target_response.py +638 -0
- ldcon/jobs/target_response_analysis.py +65 -0
- ldcon/jobs/target_response_control.py +215 -0
- ldcon/jobs/target_response_decisions.py +152 -0
- ldcon/jobs/target_response_experiment.py +110 -0
- ldcon/jobs/target_response_feedforward.py +130 -0
- ldcon/jobs/target_response_search.py +225 -0
- ldcon/jobs/target_response_shift.py +475 -0
- ldcon/jobs/target_response_thermal.py +113 -0
- ldcon/jobs/target_response_thermal_experiment.py +120 -0
- ldcon/jobs/target_sequence.py +166 -0
- ldcon/jobs/temperature_identification.py +86 -0
- ldcon/jobs/wavelength_history.py +17 -0
- ldcon/jobs/window_actions.py +641 -0
- ldcon/jobs/window_center.py +784 -0
- ldcon/jobs/window_control.py +130 -0
- ldcon/jobs/window_decisions.py +105 -0
- ldcon/jobs/window_sampling.py +66 -0
- ldcon/jobs/workflow_artifacts.py +73 -0
- ldcon/migration/__init__.py +810 -0
- ldcon/migration/migration_decks.py +239 -0
- ldcon/migration/migration_readiness.py +397 -0
- ldcon/migration/migration_relocation.py +283 -0
- ldcon/monitor_owner.py +4 -0
- ldcon/owner_telemetry.py +4 -0
- ldcon/owners/__init__.py +1 -0
- ldcon/owners/adc_log_threshold.py +209 -0
- ldcon/owners/background_grants.py +44 -0
- ldcon/owners/cld_instrument_owner.py +96 -0
- ldcon/owners/cld_owner.py +706 -0
- ldcon/owners/cld_projection.py +194 -0
- ldcon/owners/cld_system_owner.py +276 -0
- ldcon/owners/current_owner.py +68 -0
- ldcon/owners/device_registry.py +229 -0
- ldcon/owners/dlcpro_awg_plan.py +115 -0
- ldcon/owners/dlcpro_awg_port.py +98 -0
- ldcon/owners/dlcpro_awg_protocol.py +190 -0
- ldcon/owners/dlcpro_awg_return.py +63 -0
- ldcon/owners/dlcpro_awg_sweep.py +437 -0
- ldcon/owners/dlcpro_awg_timing.py +35 -0
- ldcon/owners/dlcpro_awg_tracking.py +175 -0
- ldcon/owners/dlcpro_awg_tracking_guard.py +87 -0
- ldcon/owners/dlcpro_awg_tracking_report.py +49 -0
- ldcon/owners/dlcpro_awg_zero.py +82 -0
- ldcon/owners/dlcpro_deployment.py +66 -0
- ldcon/owners/dlcpro_ff.py +64 -0
- ldcon/owners/dlcpro_ff_adjustment.py +159 -0
- ldcon/owners/dlcpro_ff_delta_adjustment.py +194 -0
- ldcon/owners/dlcpro_frequency_window.py +61 -0
- ldcon/owners/dlcpro_observation.py +62 -0
- ldcon/owners/dlcpro_owner.py +830 -0
- ldcon/owners/dlcpro_pi_protocol.py +44 -0
- ldcon/owners/dlcpro_pi_trial.py +161 -0
- ldcon/owners/dlcpro_readback.py +50 -0
- ldcon/owners/dlcpro_service.py +203 -0
- ldcon/owners/dlcpro_target_changes.py +330 -0
- ldcon/owners/dlcpro_target_step.py +44 -0
- ldcon/owners/dlcpro_target_store.py +75 -0
- ldcon/owners/dlcpro_telemetry.py +72 -0
- ldcon/owners/ecdl_background_parameters.py +248 -0
- ldcon/owners/ecdl_background_pid.py +184 -0
- ldcon/owners/ecdl_hot_control.py +255 -0
- ldcon/owners/ecdl_independent_pid.py +95 -0
- ldcon/owners/ecdl_locked_study.py +122 -0
- ldcon/owners/ecdl_locked_sweep.py +1875 -0
- ldcon/owners/ecdl_native_pid.py +181 -0
- ldcon/owners/ecdl_optical.py +37 -0
- ldcon/owners/ecdl_owner.py +1058 -0
- ldcon/owners/ecdl_policy_reload.py +494 -0
- ldcon/owners/ecdl_prelock.py +612 -0
- ldcon/owners/ecdl_projection.py +636 -0
- ldcon/owners/ecdl_steady_owner.py +1859 -0
- ldcon/owners/ecdl_steady_policy.py +212 -0
- ldcon/owners/ecdl_target_ff.py +214 -0
- ldcon/owners/ecdl_target_settling.py +163 -0
- ldcon/owners/ecdl_tec_pair.py +88 -0
- ldcon/owners/ecdl_thermal_study.py +251 -0
- ldcon/owners/emission.py +365 -0
- ldcon/owners/emission_adapters.py +268 -0
- ldcon/owners/esp32_native_client.py +335 -0
- ldcon/owners/esp32_native_owner.py +2166 -0
- ldcon/owners/esp32_time_telemetry.py +253 -0
- ldcon/owners/esp32_udp_gate.py +136 -0
- ldcon/owners/external_sweep_guard.py +44 -0
- ldcon/owners/flexible_native_trial.py +172 -0
- ldcon/owners/fringe_peaks.py +92 -0
- ldcon/owners/full_data_log.py +133 -0
- ldcon/owners/instrument_identity.py +125 -0
- ldcon/owners/instrument_owner.py +1041 -0
- ldcon/owners/instrument_projection.py +104 -0
- ldcon/owners/instrument_service.py +638 -0
- ldcon/owners/migration_physical_lock.py +162 -0
- ldcon/owners/monitor_owner.py +322 -0
- ldcon/owners/native_assembly.py +37 -0
- ldcon/owners/native_assembly_host.py +260 -0
- ldcon/owners/native_background.py +233 -0
- ldcon/owners/native_current_tuning.py +251 -0
- ldcon/owners/native_ff_proof.py +433 -0
- ldcon/owners/native_instrument_owner.py +161 -0
- ldcon/owners/native_modulation_limits.py +45 -0
- ldcon/owners/native_policy_reload.py +186 -0
- ldcon/owners/native_policy_safety.py +213 -0
- ldcon/owners/native_policy_update.py +473 -0
- ldcon/owners/native_system_owner.py +420 -0
- ldcon/owners/operating_mode_owner.py +128 -0
- ldcon/owners/owner_projection.py +530 -0
- ldcon/owners/owner_telemetry.py +166 -0
- ldcon/owners/pi_handover.py +154 -0
- ldcon/owners/projection_transport.py +143 -0
- ldcon/owners/remote_instruments.py +323 -0
- ldcon/owners/sdg_client.py +122 -0
- ldcon/owners/sdg_owner.py +468 -0
- ldcon/owners/tec_rate.py +29 -0
- ldcon/owners/wsu_udp_monitor.py +113 -0
- ldcon/report_pdf.py +4 -0
- ldcon/resources/agent-model-options.json +142 -0
- ldcon/resources/agent_contract/adjustment.txt +45 -0
- ldcon/resources/agent_contract/common.txt +42 -0
- ldcon/resources/agent_contract/diagnosis.txt +11 -0
- ldcon/resources/agent_contract/research.txt +20 -0
- ldcon/resources/agent_contract/response_optimization.txt +13 -0
- ldcon/resources/examples/README.html +10 -0
- ldcon/resources/examples/assignments/sr1092.yaml +100 -0
- ldcon/resources/examples/assignments/sr422.yaml +677 -0
- ldcon/resources/examples/assignments/thf685.yaml +7 -0
- ldcon/resources/examples/interface-decks/dfb-dbr-ldc-tec.yaml +430 -0
- ldcon/resources/examples/interface-decks/ecdl-dlcpro.yaml +288 -0
- ldcon/resources/examples/interface-decks/ecdl-pzt-ldc-tec.yaml +698 -0
- ldcon/resources/examples/lasers/sr1092.yaml +267 -0
- ldcon/resources/examples/lasers/sr422.yaml +403 -0
- ldcon/resources/examples/lasers/thf685.yaml +158 -0
- ldcon/resources/report_template/ldconreport.cls +42 -0
- ldcon/resources/report_template/report.tex +47 -0
- ldcon/web/__init__.py +1 -0
- ldcon/web/agent_approval_api.py +126 -0
- ldcon/web/agent_settings_api.py +54 -0
- ldcon/web/agent_task_api.py +211 -0
- ldcon/web/control_readback.py +63 -0
- ldcon/web/dashboard_activity.py +109 -0
- ldcon/web/dashboard_controls_revision.py +29 -0
- ldcon/web/dashboard_metrics.py +146 -0
- ldcon/web/dashboard_snapshot.py +163 -0
- ldcon/web/dashboard_state.py +104 -0
- ldcon/web/dashboard_wavelength.py +150 -0
- ldcon/web/device_parameters_api.py +52 -0
- ldcon/web/dither_quality.py +46 -0
- ldcon/web/dlcpro_target_api.py +91 -0
- ldcon/web/dlcpro_target_query.py +23 -0
- ldcon/web/failure_api.py +62 -0
- ldcon/web/gateway_lan.py +208 -0
- ldcon/web/grafana_activity.py +84 -0
- ldcon/web/grafana_dashboard.py +492 -0
- ldcon/web/grafana_dlcpro.py +158 -0
- ldcon/web/grafana_quality.py +167 -0
- ldcon/web/interface_projection.py +27 -0
- ldcon/web/job_registry.py +68 -0
- ldcon/web/job_report.py +122 -0
- ldcon/web/laser_operations.py +445 -0
- ldcon/web/lock_target_api.py +554 -0
- ldcon/web/lock_target_queue.py +212 -0
- ldcon/web/log_archive.py +191 -0
- ldcon/web/metric_query_profiles.py +378 -0
- ldcon/web/online_position.py +26 -0
- ldcon/web/operating_mode_api.py +72 -0
- ldcon/web/operation_api.py +34 -0
- ldcon/web/operation_observation.py +57 -0
- ldcon/web/operations_settings.py +92 -0
- ldcon/web/quality_threshold_metrics.py +64 -0
- ldcon/web/shot_analysis_api.py +136 -0
- ldcon/web/sweep_api.py +1186 -0
- ldcon/web/sweep_guard.py +45 -0
- ldcon/web/target_reference_panel.py +38 -0
- ldcon/web/teams_destinations_api.py +81 -0
- ldcon/web/web_fleet.py +482 -0
- ldcon/web/web_runtime.py +811 -0
- ldcon/web/web_server.py +402 -0
- ldcon/web/window_api.py +28 -0
- ldcon/web_assets/agent-settings.css +4 -0
- ldcon/web_assets/agent-settings.html +53 -0
- ldcon/web_assets/agent-settings.js +270 -0
- ldcon/web_assets/app.css +9 -0
- ldcon/web_assets/app.js +802 -0
- ldcon/web_assets/card-browser.js +160 -0
- ldcon/web_assets/card-guide.js +9 -0
- ldcon/web_assets/cards/acquire.metrics.html +82 -0
- ldcon/web_assets/cards/agent.history_review.html +13 -0
- ldcon/web_assets/cards/agent.log_watch.html +1 -0
- ldcon/web_assets/cards/agent.monitor.html +13 -0
- ldcon/web_assets/cards/agent.request.html +1 -0
- ldcon/web_assets/cards/agent.resident.html +1 -0
- ldcon/web_assets/cards/agent.task.html +17 -0
- ldcon/web_assets/cards/commission.ecdl_pi.html +1 -0
- ldcon/web_assets/cards/identify.axis_frequency.html +1 -0
- ldcon/web_assets/cards/identify.current_response.html +1 -0
- ldcon/web_assets/cards/laser.emission.html +8 -0
- ldcon/web_assets/cards/measure.batch.html +1 -0
- ldcon/web_assets/cards/measure.change.html +1 -0
- ldcon/web_assets/cards/measure.target_response.html +1 -0
- ldcon/web_assets/cards/monitor.wavelength.html +1 -0
- ldcon/web_assets/cards/notify.teams.html +1 -0
- ldcon/web_assets/cards/optimize.settling_criteria.html +1 -0
- ldcon/web_assets/cards/optimize.target_feedforward.html +1 -0
- ldcon/web_assets/cards/optimize.target_step.html +1 -0
- ldcon/web_assets/cards/optimize.target_thermal.html +1 -0
- ldcon/web_assets/cards/plan.ecdl_recovery.html +14 -0
- ldcon/web_assets/cards/prepare.ecdl_recovery.html +18 -0
- ldcon/web_assets/cards/recover.ecdl_mode_hop.html +12 -0
- ldcon/web_assets/cards/report.grafana_dashboard.html +22 -0
- ldcon/web_assets/cards/servo.current_pid.html +1 -0
- ldcon/web_assets/cards/servo.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/servo.ecdl_steady_lock.html +11 -0
- ldcon/web_assets/cards/servo.native_current.html +1 -0
- ldcon/web_assets/cards/study.ecdl_current_response.html +1 -0
- ldcon/web_assets/cards/study.ecdl_locked_current_map.html +1 -0
- ldcon/web_assets/cards/study.ecdl_mode_map.html +6 -0
- ldcon/web_assets/cards/study.ecdl_thermal_context.html +5 -0
- ldcon/web_assets/cards/study.target_response.html +1 -0
- ldcon/web_assets/cards/sweep.current_modulation.html +1 -0
- ldcon/web_assets/cards/sweep.dlcpro_frequency.html +1 -0
- ldcon/web_assets/cards/sweep.servo_modulation.html +1 -0
- ldcon/web_assets/cards/test.ecdl_recovery.html +4 -0
- ldcon/web_assets/cards/tune.ecdl_initial_point.html +1 -0
- ldcon/web_assets/cards/tune.ecdl_operating_point.html +13 -0
- ldcon/web_assets/cards/tune.ecdl_window_center.html +7 -0
- ldcon/web_assets/cards/tune.native_current.html +1 -0
- ldcon/web_assets/cards/tune.pid_coordinate_search.html +1 -0
- ldcon/web_assets/cards/tune.pid_factor_reduction.html +1 -0
- ldcon/web_assets/cards/tune.pid_online.html +1 -0
- ldcon/web_assets/cards/tune.temperature_once.html +18 -0
- ldcon/web_assets/committing-input.js +35 -0
- ldcon/web_assets/configuration-editor.js +194 -0
- ldcon/web_assets/dashboard-state.js +39 -0
- ldcon/web_assets/failures.html +1 -0
- ldcon/web_assets/failures.js +19 -0
- ldcon/web_assets/fallback.css +3 -0
- ldcon/web_assets/fallback.html +1 -0
- ldcon/web_assets/fallback.js +21 -0
- ldcon/web_assets/fleet-settings.js +20 -0
- ldcon/web_assets/fleet.css +9 -0
- ldcon/web_assets/fleet.html +8 -0
- ldcon/web_assets/fleet.js +130 -0
- ldcon/web_assets/gateway-presentation.js +27 -0
- ldcon/web_assets/grafana-help.js +31 -0
- ldcon/web_assets/help.html +22 -0
- ldcon/web_assets/i18n.js +279 -0
- ldcon/web_assets/index.html +14 -0
- ldcon/web_assets/laser-workspace.js +279 -0
- ldcon/web_assets/modulation.html +1 -0
- ldcon/web_assets/modulation.js +14 -0
- ldcon/web_assets/reservations.html +16 -0
- ldcon/web_assets/reservations.js +77 -0
- ldcon/web_assets/teams-settings.js +91 -0
- ldcon/web_assets/window-actions.js +37 -0
- ldcon/web_assets/workspace.css +99 -0
- ldcon/web_fleet.py +4 -0
- ldcon/web_server.py +4 -0
- ldcon-0.2.0.dist-info/METADATA +85 -0
- ldcon-0.2.0.dist-info/RECORD +482 -0
- ldcon-0.2.0.dist-info/WHEEL +5 -0
- ldcon-0.2.0.dist-info/entry_points.txt +5 -0
- ldcon-0.2.0.dist-info/licenses/LICENSE +21 -0
- 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
|