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,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)
|