celatim 0.2.2__tar.gz → 0.2.4__tar.gz
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.
- {celatim-0.2.2 → celatim-0.2.4}/PKG-INFO +1 -1
- {celatim-0.2.2 → celatim-0.2.4}/pyproject.toml +1 -1
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/__init__.py +1 -1
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/adapter.py +3 -3
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/analysis/__init__.py +14 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/analysis/crosshost_evidence.py +7 -4
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/analysis/crosshost_metrics.py +46 -19
- celatim-0.2.4/src/celatim/analysis/detector_metrics.py +217 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/assurance/scorecard.py +135 -52
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/catalog.py +2 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/cli.py +10 -1
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/data/mechanisms.jsonl +10 -10
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/detect/replay.py +156 -14
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/detect/rules.py +28 -5
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/detect/scrub.py +3 -3
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/discovery.py +4 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/docs/reviewer-quickstart.md +1 -1
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/docs/scenario-authoring.md +7 -1
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/model.py +9 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/observer.py +1 -1
- celatim-0.2.4/src/celatim/pdu/coap_msg.py +68 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/tcp.py +4 -4
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/figures.py +20 -7
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/macros.py +62 -7
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/tables.py +22 -2
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/detector-replay-corpus-v1.schema.json +48 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/coap_message.py +4 -5
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/message_carrier.py +1 -1
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/packet_path.py +6 -6
- celatim-0.2.2/src/celatim/pdu/coap_msg.py +0 -53
- {celatim-0.2.2 → celatim-0.2.4}/LICENSE +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/README.md +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/analysis/dataset.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/analysis/subliminal_controls.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/api.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/assurance/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/assurance/requirements.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/channel/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/channel/bits.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/channel/codec.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/channel/driver.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/channel/framer.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/channel/registry.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/cli_endpoints.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/crypto_transcript.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/data/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/data/protocol_rates.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/detect/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/detect/stateful.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/docs/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/docs/api-guide.md +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/docs/troubleshooting.md +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/doctor.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/envelope.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/errors.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/evidence.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/evidence_index.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/inspection.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/layout/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/layout/loader.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/metrics/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/metrics/storage.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/metrics/subliminal.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/metrics/timing.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pcap_decode.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/bgp_attr.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/bitfield.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/dns_txt.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/format_carriers.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/http2.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/http2_fields.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/minimal_pdu.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/quic.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/quic_fields.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/rtcp.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/scapy_field.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/scapy_pdu.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/ssh_kex.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/struct_fields.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/tls_fields.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/pdu/ws_frame.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/py.typed +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/detector_rules.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/guidance.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/protocol_rates.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/support_matrix.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/report/windows_capture.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/resources.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/reviewer_bundle.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenario.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/http2-ping-opaque.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/quic-connection-id.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/rtp-rtcp-ext-app.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/subliminal-ctrl/zzz-ecdsa-nonce-128ctrl.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/subliminal-ctrl/zzz-rsa-pss-salt-128ctrl.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/tcp-reserved-bits.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/z-edns0-padding-dnsmasq-dig.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/z-http2-ping-opaque-hyper-h2.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/z-http3-reserved-settings-aioquic.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/z-quic-connection-id-aioquic.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/z-tcp-reserved-bits-afpacket.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zz-ecdsa-nonce-local.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zz-rsa-pss-salt-local.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zzz-bgp-scapy.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zzz-coap-aiocoap.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zzz-dns-null-dnspython.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zzz-dns-txt-dnspython.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zzz-ssh-kexinit-paramiko.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/scenarios/zzz-websocket-websockets.toml +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/carrier-endpoint-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/detector-replay-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/detector-trace-manifest-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/doctor-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/evidence-index-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/evidence-run-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/packet-service-preflight-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/packet-service-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/pcap-decode-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/provider-conformance-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/provider-inventory-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/provider-manifest-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/public-bundle-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/public-bundle-verify-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/public-evidence-index-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/qemu-tap-preflight-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/reviewer-bundle-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/reviewer-bundle-verify-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/scenario-execution-plan-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/scenario-inventory-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/scenario-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/scrub-report-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/support-matrix-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/testbed-requirements-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/timing-sweep-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-error-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-event-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-listener-status-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-listener-stop-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-manifest-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-offer-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-receipt-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-state-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/schemas/transfer-status-v1.schema.json +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/session.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/afpacket.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/bgp_message.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/commands.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/daemon.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/dns.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/dns_message.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/h3.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/http2.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/netns.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/qemu.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/quic.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/requirements.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/ssh_message.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/tcpdump.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/testbed/ws_message.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/timing_sweep.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/__init__.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/carrier.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/cli.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/client.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/conformance.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/direct.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/errors.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/listener.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/models.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/packet_service.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/providers.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/security.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/server.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/source.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/state.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/storage.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transfer/wire.py +0 -0
- {celatim-0.2.2 → celatim-0.2.4}/src/celatim/transports.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: celatim
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.4
|
|
4
4
|
Summary: Typed secure file transfer and reproducible covert-channel measurement for IETF protocol fields.
|
|
5
5
|
Keywords: covert-channels,network-measurement,network-protocols,privacy,steganography
|
|
6
6
|
Author: Michael Bommarito
|
|
@@ -549,12 +549,12 @@ def _paths_for(
|
|
|
549
549
|
kind=AdapterPathKind.COAP_AIOCOAP,
|
|
550
550
|
transport_kind="coap_aiocoap",
|
|
551
551
|
evidence_tier="real_daemon_path",
|
|
552
|
-
claim_status="
|
|
552
|
+
claim_status="local_aiocoap_client_server_elective_option_path",
|
|
553
553
|
required_extras=("iot",),
|
|
554
554
|
records_artifact=True,
|
|
555
555
|
notes=(
|
|
556
556
|
"paired aiocoap client/server CoAP message exchange; covert bytes in "
|
|
557
|
-
"
|
|
557
|
+
"an unknown elective option, aiocoap Message codec validates"
|
|
558
558
|
),
|
|
559
559
|
)
|
|
560
560
|
)
|
|
@@ -871,7 +871,7 @@ def _build_tcp_reserved_bits(symbol: Symbol) -> bytes:
|
|
|
871
871
|
if not isinstance(symbol, int):
|
|
872
872
|
raise TypeError("tcp-reserved-bits: expected int-valued carrier symbol")
|
|
873
873
|
if not 0 <= symbol < (1 << TCP_RESERVED_BITS_WIDTH):
|
|
874
|
-
raise ValueError("tcp-reserved-bits: symbol does not fit in
|
|
874
|
+
raise ValueError("tcp-reserved-bits: symbol does not fit in 3 reserved bits")
|
|
875
875
|
return build_tcp_reserved_bits_segment(symbol)
|
|
876
876
|
|
|
877
877
|
|
|
@@ -18,6 +18,14 @@ from .dataset import (
|
|
|
18
18
|
build_records,
|
|
19
19
|
write_dataset,
|
|
20
20
|
)
|
|
21
|
+
from .detector_metrics import (
|
|
22
|
+
DETECTOR_METRICS_SCHEMA_VERSION,
|
|
23
|
+
DetectorMetricReport,
|
|
24
|
+
LabeledDetectionScore,
|
|
25
|
+
ThresholdMetrics,
|
|
26
|
+
detector_metric_report,
|
|
27
|
+
detector_threshold_metrics,
|
|
28
|
+
)
|
|
21
29
|
from .subliminal_controls import (
|
|
22
30
|
SUBLIMINAL_CONTROL_REPORT_SCHEMA_VERSION,
|
|
23
31
|
build_subliminal_control_report,
|
|
@@ -27,14 +35,20 @@ __all__ = [
|
|
|
27
35
|
"CLAIM_LEDGER_SCHEMA_VERSION",
|
|
28
36
|
"CROSSHOST_PUBLIC_INDEX_SCHEMA_VERSION",
|
|
29
37
|
"DATASET_SCHEMA_VERSION",
|
|
38
|
+
"DETECTOR_METRICS_SCHEMA_VERSION",
|
|
30
39
|
"SUBLIMINAL_CONTROL_REPORT_SCHEMA_VERSION",
|
|
31
40
|
"DatasetRecord",
|
|
41
|
+
"DetectorMetricReport",
|
|
42
|
+
"LabeledDetectionScore",
|
|
43
|
+
"ThresholdMetrics",
|
|
32
44
|
"build_claim_ledger",
|
|
33
45
|
"build_crosshost_public_index",
|
|
34
46
|
"build_manifest",
|
|
35
47
|
"build_records",
|
|
36
48
|
"build_subliminal_control_report",
|
|
37
49
|
"claim_count",
|
|
50
|
+
"detector_metric_report",
|
|
51
|
+
"detector_threshold_metrics",
|
|
38
52
|
"load_claim_ledger",
|
|
39
53
|
"load_crosshost_public_index",
|
|
40
54
|
"write_dataset",
|
|
@@ -334,11 +334,13 @@ def _public_metric_record(record: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
334
334
|
"recovered_bytes": record.get("recovered_bytes"),
|
|
335
335
|
"carrier_units": record.get("carrier_units"),
|
|
336
336
|
"raw_capacity_bits_per_unit": record.get("raw_capacity_bits_per_unit"),
|
|
337
|
-
"
|
|
337
|
+
"packing_efficiency_diagnostic": record.get("packing_efficiency_diagnostic"),
|
|
338
338
|
"method_wire_bytes": record.get("method_wire_bytes"),
|
|
339
339
|
"method_wire_basis": record.get("method_wire_basis"),
|
|
340
|
-
"
|
|
341
|
-
"
|
|
340
|
+
"wire_expansion": record.get("wire_expansion"),
|
|
341
|
+
"useful_payload_ratio": record.get("useful_payload_ratio"),
|
|
342
|
+
"carrier_units_per_payload_byte": record.get("carrier_units_per_payload_byte"),
|
|
343
|
+
"method_wire_bytes_per_carrier_unit": record.get("method_wire_bytes_per_carrier_unit"),
|
|
342
344
|
"vxlan_underlay_bytes_no_fcs": record.get("vxlan_underlay_bytes_no_fcs"),
|
|
343
345
|
"payload_to_vxlan_underlay_ratio": record.get("payload_to_vxlan_underlay_ratio"),
|
|
344
346
|
"timing": {
|
|
@@ -347,7 +349,8 @@ def _public_metric_record(record: Mapping[str, Any]) -> dict[str, Any]:
|
|
|
347
349
|
"measured_window_basis": timing_map.get("measured_window_basis"),
|
|
348
350
|
"observed_unit_rate_hz": timing_map.get("observed_unit_rate_hz"),
|
|
349
351
|
"payload_bytes_per_s": timing_map.get("payload_bytes_per_s"),
|
|
350
|
-
"
|
|
352
|
+
"observed_recovery_rate_bps": timing_map.get("observed_recovery_rate_bps"),
|
|
353
|
+
"native_goodput_bps": timing_map.get("native_goodput_bps"),
|
|
351
354
|
"scheduled_unit_rate_hz": timing_map.get("scheduled_unit_rate_hz"),
|
|
352
355
|
"scheduled_duration_s": timing_map.get("scheduled_duration_s"),
|
|
353
356
|
},
|
|
@@ -7,7 +7,8 @@ from collections.abc import Iterable, Mapping, Sequence
|
|
|
7
7
|
from dataclasses import dataclass
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
|
-
METRICS_SCHEMA_VERSION = "celatim.alice_bob_metrics.
|
|
10
|
+
METRICS_SCHEMA_VERSION = "celatim.alice_bob_metrics.v2"
|
|
11
|
+
NATIVE_GOODPUT_CLAIM_STATUS = "native_protocol_goodput_measured"
|
|
11
12
|
|
|
12
13
|
ETHERNET_HEADER_BYTES = 14
|
|
13
14
|
IPV4_HEADER_BYTES = 20
|
|
@@ -74,6 +75,11 @@ def metric_record(input: MechanismMetricInput) -> dict[str, Any]:
|
|
|
74
75
|
if input.carrier_units is not None and input.raw_capacity_bits is not None
|
|
75
76
|
else None
|
|
76
77
|
)
|
|
78
|
+
observed_recovery_rate_bps = (
|
|
79
|
+
_ratio(input.recovered_bytes * 8, input.measured_window_s)
|
|
80
|
+
if input.recovered_bytes is not None
|
|
81
|
+
else None
|
|
82
|
+
)
|
|
77
83
|
return {
|
|
78
84
|
"mechanism": input.mechanism_id,
|
|
79
85
|
"suite": input.suite,
|
|
@@ -84,13 +90,15 @@ def metric_record(input: MechanismMetricInput) -> dict[str, Any]:
|
|
|
84
90
|
"carrier_units": input.carrier_units,
|
|
85
91
|
"raw_capacity_bits_per_unit": input.raw_capacity_bits,
|
|
86
92
|
"carrier_capacity_bits": carrier_capacity_bits,
|
|
87
|
-
"
|
|
93
|
+
"packing_efficiency_diagnostic": _ratio(payload_bits, carrier_capacity_bits),
|
|
88
94
|
"carrier_wire_bytes": input.carrier_wire_bytes,
|
|
89
95
|
"method_wire_bytes": input.method_wire_bytes,
|
|
90
96
|
"method_wire_basis": input.method_wire_basis,
|
|
91
97
|
"vxlan_underlay_bytes_no_fcs": input.vxlan_underlay_bytes_no_fcs,
|
|
92
|
-
"
|
|
93
|
-
"
|
|
98
|
+
"useful_payload_ratio": _ratio(input.payload_bytes, input.method_wire_bytes),
|
|
99
|
+
"wire_expansion": _ratio(input.method_wire_bytes, input.payload_bytes),
|
|
100
|
+
"carrier_units_per_payload_byte": _ratio(input.carrier_units, input.payload_bytes),
|
|
101
|
+
"method_wire_bytes_per_carrier_unit": _ratio(input.method_wire_bytes, input.carrier_units),
|
|
94
102
|
"payload_to_vxlan_underlay_ratio": _ratio(
|
|
95
103
|
input.payload_bytes, input.vxlan_underlay_bytes_no_fcs
|
|
96
104
|
),
|
|
@@ -102,9 +110,10 @@ def metric_record(input: MechanismMetricInput) -> dict[str, Any]:
|
|
|
102
110
|
"scheduled_duration_s": input.scheduled_duration_s,
|
|
103
111
|
"observed_unit_rate_hz": _ratio(input.carrier_units, input.measured_window_s),
|
|
104
112
|
"payload_bytes_per_s": _ratio(input.recovered_bytes, input.measured_window_s),
|
|
105
|
-
"
|
|
106
|
-
|
|
107
|
-
|
|
113
|
+
"observed_recovery_rate_bps": observed_recovery_rate_bps,
|
|
114
|
+
"native_goodput_bps": (
|
|
115
|
+
observed_recovery_rate_bps
|
|
116
|
+
if input.timing_claim_status == NATIVE_GOODPUT_CLAIM_STATUS
|
|
108
117
|
else None
|
|
109
118
|
),
|
|
110
119
|
"claim_status": input.timing_claim_status,
|
|
@@ -123,7 +132,7 @@ def metrics_summary(
|
|
|
123
132
|
measured = [
|
|
124
133
|
record
|
|
125
134
|
for record in pass_records
|
|
126
|
-
if _nested_number(record, ("timing", "
|
|
135
|
+
if _nested_number(record, ("timing", "observed_recovery_rate_bps")) is not None
|
|
127
136
|
]
|
|
128
137
|
return {
|
|
129
138
|
"schema_version": METRICS_SCHEMA_VERSION,
|
|
@@ -134,23 +143,27 @@ def metrics_summary(
|
|
|
134
143
|
"timing_claim_status_counts": _counts(
|
|
135
144
|
_nested_value(record, ("timing", "claim_status")) for record in records
|
|
136
145
|
),
|
|
137
|
-
"
|
|
138
|
-
|
|
146
|
+
"wire_expansion": _stats(_number(record.get("wire_expansion")) for record in pass_records),
|
|
147
|
+
"useful_payload_ratio": _stats(
|
|
148
|
+
_number(record.get("useful_payload_ratio")) for record in pass_records
|
|
149
|
+
),
|
|
150
|
+
"carrier_units_per_payload_byte": _stats(
|
|
151
|
+
_number(record.get("carrier_units_per_payload_byte")) for record in pass_records
|
|
139
152
|
),
|
|
140
|
-
"
|
|
141
|
-
|
|
153
|
+
"observed_recovery_rate_bps": _stats(
|
|
154
|
+
_nested_number(record, ("timing", "observed_recovery_rate_bps")) for record in measured
|
|
142
155
|
),
|
|
143
|
-
"
|
|
144
|
-
_nested_number(record, ("timing", "
|
|
156
|
+
"native_goodput_bps": _stats(
|
|
157
|
+
_nested_number(record, ("timing", "native_goodput_bps")) for record in measured
|
|
145
158
|
),
|
|
146
159
|
"observed_unit_rate_hz": _stats(
|
|
147
160
|
_nested_number(record, ("timing", "observed_unit_rate_hz")) for record in measured
|
|
148
161
|
),
|
|
149
|
-
"
|
|
150
|
-
measured, ("timing", "
|
|
162
|
+
"fastest_observed_recovery_rate_bps": _extreme(
|
|
163
|
+
measured, ("timing", "observed_recovery_rate_bps"), reverse=True
|
|
151
164
|
),
|
|
152
|
-
"
|
|
153
|
-
measured, ("timing", "
|
|
165
|
+
"slowest_observed_recovery_rate_bps": _extreme(
|
|
166
|
+
measured, ("timing", "observed_recovery_rate_bps"), reverse=False
|
|
154
167
|
),
|
|
155
168
|
}
|
|
156
169
|
|
|
@@ -191,10 +204,13 @@ def _counts(values: Iterable[Any]) -> dict[str, int]:
|
|
|
191
204
|
def _stats(values: Iterable[float | None]) -> dict[str, float | None]:
|
|
192
205
|
series = sorted(value for value in values if value is not None)
|
|
193
206
|
if not series:
|
|
194
|
-
return {"min": None, "median": None, "max": None}
|
|
207
|
+
return {"n": 0, "min": None, "q1": None, "median": None, "q3": None, "max": None}
|
|
195
208
|
return {
|
|
209
|
+
"n": len(series),
|
|
196
210
|
"min": series[0],
|
|
211
|
+
"q1": _quantile(series, 0.25),
|
|
197
212
|
"median": _median(series),
|
|
213
|
+
"q3": _quantile(series, 0.75),
|
|
198
214
|
"max": series[-1],
|
|
199
215
|
}
|
|
200
216
|
|
|
@@ -206,6 +222,16 @@ def _median(series: Sequence[float]) -> float:
|
|
|
206
222
|
return (series[midpoint - 1] + series[midpoint]) / 2.0
|
|
207
223
|
|
|
208
224
|
|
|
225
|
+
def _quantile(series: Sequence[float], probability: float) -> float:
|
|
226
|
+
position = (len(series) - 1) * probability
|
|
227
|
+
lower = math.floor(position)
|
|
228
|
+
upper = math.ceil(position)
|
|
229
|
+
if lower == upper:
|
|
230
|
+
return series[lower]
|
|
231
|
+
fraction = position - lower
|
|
232
|
+
return series[lower] + fraction * (series[upper] - series[lower])
|
|
233
|
+
|
|
234
|
+
|
|
209
235
|
def _extreme(
|
|
210
236
|
records: Sequence[Mapping[str, Any]],
|
|
211
237
|
path: Sequence[str],
|
|
@@ -227,6 +253,7 @@ def _extreme(
|
|
|
227
253
|
|
|
228
254
|
__all__ = [
|
|
229
255
|
"METRICS_SCHEMA_VERSION",
|
|
256
|
+
"NATIVE_GOODPUT_CLAIM_STATUS",
|
|
230
257
|
"MechanismMetricInput",
|
|
231
258
|
"carrier_lengths_from_envelope",
|
|
232
259
|
"metric_record",
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""Threshold-sweep metrics for labeled detector scores."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import math
|
|
6
|
+
from collections.abc import Iterable
|
|
7
|
+
from dataclasses import dataclass
|
|
8
|
+
from itertools import pairwise
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
DETECTOR_METRICS_SCHEMA_VERSION = "celatim.detector_metrics.v1"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@dataclass(frozen=True)
|
|
15
|
+
class LabeledDetectionScore:
|
|
16
|
+
label: bool
|
|
17
|
+
score: float
|
|
18
|
+
|
|
19
|
+
def __post_init__(self) -> None:
|
|
20
|
+
if not math.isfinite(self.score):
|
|
21
|
+
raise ValueError("detector score must be finite")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@dataclass(frozen=True)
|
|
25
|
+
class ThresholdMetrics:
|
|
26
|
+
threshold: float
|
|
27
|
+
true_positive: int
|
|
28
|
+
false_positive: int
|
|
29
|
+
true_negative: int
|
|
30
|
+
false_negative: int
|
|
31
|
+
true_positive_rate: float
|
|
32
|
+
false_positive_rate: float
|
|
33
|
+
precision: float | None
|
|
34
|
+
recall: float
|
|
35
|
+
tpr_wilson95: tuple[float, float]
|
|
36
|
+
fpr_wilson95: tuple[float, float]
|
|
37
|
+
prevalence_adjusted_precision: dict[str, float | None]
|
|
38
|
+
|
|
39
|
+
def to_json(self) -> dict[str, Any]:
|
|
40
|
+
return {
|
|
41
|
+
"threshold": self.threshold,
|
|
42
|
+
"true_positive": self.true_positive,
|
|
43
|
+
"false_positive": self.false_positive,
|
|
44
|
+
"true_negative": self.true_negative,
|
|
45
|
+
"false_negative": self.false_negative,
|
|
46
|
+
"true_positive_rate": self.true_positive_rate,
|
|
47
|
+
"false_positive_rate": self.false_positive_rate,
|
|
48
|
+
"precision": self.precision,
|
|
49
|
+
"recall": self.recall,
|
|
50
|
+
"tpr_wilson95": list(self.tpr_wilson95),
|
|
51
|
+
"fpr_wilson95": list(self.fpr_wilson95),
|
|
52
|
+
"prevalence_adjusted_precision": self.prevalence_adjusted_precision,
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class DetectorMetricReport:
|
|
58
|
+
positive_count: int
|
|
59
|
+
negative_count: int
|
|
60
|
+
roc_auc: float
|
|
61
|
+
average_precision: float
|
|
62
|
+
prevalence_assumptions: tuple[float, ...]
|
|
63
|
+
thresholds: tuple[ThresholdMetrics, ...]
|
|
64
|
+
|
|
65
|
+
def to_json(self) -> dict[str, Any]:
|
|
66
|
+
return {
|
|
67
|
+
"schema_version": DETECTOR_METRICS_SCHEMA_VERSION,
|
|
68
|
+
"positive_count": self.positive_count,
|
|
69
|
+
"negative_count": self.negative_count,
|
|
70
|
+
"roc_auc": self.roc_auc,
|
|
71
|
+
"average_precision": self.average_precision,
|
|
72
|
+
"prevalence_assumptions": list(self.prevalence_assumptions),
|
|
73
|
+
"thresholds": [row.to_json() for row in self.thresholds],
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def detector_metric_report(
|
|
78
|
+
scores: Iterable[LabeledDetectionScore],
|
|
79
|
+
*,
|
|
80
|
+
prevalence_assumptions: tuple[float, ...] = (0.0001, 0.001, 0.01),
|
|
81
|
+
) -> DetectorMetricReport:
|
|
82
|
+
observations = tuple(scores)
|
|
83
|
+
positives = sum(observation.label for observation in observations)
|
|
84
|
+
negatives = len(observations) - positives
|
|
85
|
+
if positives == 0 or negatives == 0:
|
|
86
|
+
raise ValueError("detector evaluation requires positive and negative observations")
|
|
87
|
+
if any(not 0 < prevalence < 1 for prevalence in prevalence_assumptions):
|
|
88
|
+
raise ValueError("prevalence assumptions must be between zero and one")
|
|
89
|
+
unique_scores = sorted({observation.score for observation in observations}, reverse=True)
|
|
90
|
+
thresholds = (math.inf, *unique_scores, -math.inf)
|
|
91
|
+
rows = tuple(
|
|
92
|
+
_threshold_metrics(
|
|
93
|
+
observations,
|
|
94
|
+
threshold,
|
|
95
|
+
prevalence_assumptions=prevalence_assumptions,
|
|
96
|
+
)
|
|
97
|
+
for threshold in thresholds
|
|
98
|
+
)
|
|
99
|
+
roc_points = sorted(
|
|
100
|
+
((row.false_positive_rate, row.true_positive_rate) for row in rows),
|
|
101
|
+
key=lambda point: (point[0], point[1]),
|
|
102
|
+
)
|
|
103
|
+
precision_rows = [row for row in rows if row.precision is not None]
|
|
104
|
+
precision_rows.sort(key=lambda row: row.recall)
|
|
105
|
+
return DetectorMetricReport(
|
|
106
|
+
positive_count=positives,
|
|
107
|
+
negative_count=negatives,
|
|
108
|
+
roc_auc=_trapezoid(roc_points),
|
|
109
|
+
average_precision=_step_average_precision(precision_rows),
|
|
110
|
+
prevalence_assumptions=prevalence_assumptions,
|
|
111
|
+
thresholds=rows,
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def detector_threshold_metrics(
|
|
116
|
+
scores: Iterable[LabeledDetectionScore],
|
|
117
|
+
threshold: float,
|
|
118
|
+
*,
|
|
119
|
+
prevalence_assumptions: tuple[float, ...] = (0.0001, 0.001, 0.01),
|
|
120
|
+
) -> ThresholdMetrics:
|
|
121
|
+
"""Evaluate one externally selected threshold without recalibrating it."""
|
|
122
|
+
|
|
123
|
+
observations = tuple(scores)
|
|
124
|
+
positives = sum(observation.label for observation in observations)
|
|
125
|
+
negatives = len(observations) - positives
|
|
126
|
+
if positives == 0 or negatives == 0:
|
|
127
|
+
raise ValueError("detector evaluation requires positive and negative observations")
|
|
128
|
+
if not math.isfinite(threshold):
|
|
129
|
+
raise ValueError("detector threshold must be finite")
|
|
130
|
+
if any(not 0 < prevalence < 1 for prevalence in prevalence_assumptions):
|
|
131
|
+
raise ValueError("prevalence assumptions must be between zero and one")
|
|
132
|
+
return _threshold_metrics(
|
|
133
|
+
observations,
|
|
134
|
+
threshold,
|
|
135
|
+
prevalence_assumptions=prevalence_assumptions,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _threshold_metrics(
|
|
140
|
+
scores: tuple[LabeledDetectionScore, ...],
|
|
141
|
+
threshold: float,
|
|
142
|
+
*,
|
|
143
|
+
prevalence_assumptions: tuple[float, ...],
|
|
144
|
+
) -> ThresholdMetrics:
|
|
145
|
+
true_positive = sum(item.label and item.score >= threshold for item in scores)
|
|
146
|
+
false_positive = sum(not item.label and item.score >= threshold for item in scores)
|
|
147
|
+
positive_count = sum(item.label for item in scores)
|
|
148
|
+
negative_count = len(scores) - positive_count
|
|
149
|
+
false_negative = positive_count - true_positive
|
|
150
|
+
true_negative = negative_count - false_positive
|
|
151
|
+
tpr = true_positive / positive_count
|
|
152
|
+
fpr = false_positive / negative_count
|
|
153
|
+
predicted_positive = true_positive + false_positive
|
|
154
|
+
precision = true_positive / predicted_positive if predicted_positive else None
|
|
155
|
+
adjusted = {
|
|
156
|
+
f"{prevalence:g}": _prevalence_precision(tpr, fpr, prevalence)
|
|
157
|
+
for prevalence in prevalence_assumptions
|
|
158
|
+
}
|
|
159
|
+
return ThresholdMetrics(
|
|
160
|
+
threshold=threshold,
|
|
161
|
+
true_positive=true_positive,
|
|
162
|
+
false_positive=false_positive,
|
|
163
|
+
true_negative=true_negative,
|
|
164
|
+
false_negative=false_negative,
|
|
165
|
+
true_positive_rate=tpr,
|
|
166
|
+
false_positive_rate=fpr,
|
|
167
|
+
precision=precision,
|
|
168
|
+
recall=tpr,
|
|
169
|
+
tpr_wilson95=_wilson95(true_positive, positive_count),
|
|
170
|
+
fpr_wilson95=_wilson95(false_positive, negative_count),
|
|
171
|
+
prevalence_adjusted_precision=adjusted,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _prevalence_precision(tpr: float, fpr: float, prevalence: float) -> float | None:
|
|
176
|
+
denominator = tpr * prevalence + fpr * (1 - prevalence)
|
|
177
|
+
return tpr * prevalence / denominator if denominator else None
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _wilson95(successes: int, trials: int) -> tuple[float, float]:
|
|
181
|
+
z = 1.959963984540054
|
|
182
|
+
proportion = successes / trials
|
|
183
|
+
denominator = 1 + z * z / trials
|
|
184
|
+
center = (proportion + z * z / (2 * trials)) / denominator
|
|
185
|
+
margin = (
|
|
186
|
+
z
|
|
187
|
+
* math.sqrt(proportion * (1 - proportion) / trials + z * z / (4 * trials * trials))
|
|
188
|
+
/ denominator
|
|
189
|
+
)
|
|
190
|
+
return max(0.0, center - margin), min(1.0, center + margin)
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _trapezoid(points: list[tuple[float, float]]) -> float:
|
|
194
|
+
area = 0.0
|
|
195
|
+
for left, right in pairwise(points):
|
|
196
|
+
area += (right[0] - left[0]) * (left[1] + right[1]) / 2
|
|
197
|
+
return area
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
def _step_average_precision(rows: list[ThresholdMetrics]) -> float:
|
|
201
|
+
area = 0.0
|
|
202
|
+
previous_recall = 0.0
|
|
203
|
+
for row in rows:
|
|
204
|
+
if row.recall > previous_recall and row.precision is not None:
|
|
205
|
+
area += (row.recall - previous_recall) * row.precision
|
|
206
|
+
previous_recall = row.recall
|
|
207
|
+
return area
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
__all__ = [
|
|
211
|
+
"DETECTOR_METRICS_SCHEMA_VERSION",
|
|
212
|
+
"DetectorMetricReport",
|
|
213
|
+
"LabeledDetectionScore",
|
|
214
|
+
"ThresholdMetrics",
|
|
215
|
+
"detector_metric_report",
|
|
216
|
+
"detector_threshold_metrics",
|
|
217
|
+
]
|