diff --git a/docs/decisions/ADR-0208-environmental-sensorium-loop.md b/docs/decisions/ADR-0208-environmental-sensorium-loop.md new file mode 100644 index 00000000..51d288cb --- /dev/null +++ b/docs/decisions/ADR-0208-environmental-sensorium-loop.md @@ -0,0 +1,65 @@ +# ADR-0208: Environmental Sensorium Loop + +**Status:** Proposed +**Date:** 2026-06-04 +**Domains:** `sensorium/environment/`, `sensorium/compiler/`, `sensorium/*`, future sensorimotor compilers +**Depends on:** ADR-0013, ADR-0180, ADR-0181, ADR-0197, ADR-0198 + +## Decision + +CORE will represent a moment of environmental evidence as an `ObservationFrame`: +a deterministic bundle of already-compiled afferent `CompilationUnitLike` +deltas. The frame is not a fusion layer, not a shared embedding space, and not a +mutable world model. + +```text +environment + -> modality compilers + -> compiled afferent units + -> ObservationFrame + -> Delta-CRDT merge + -> field / recall / cognition + -> governed efferent decode + -> action result / proprioception re-enters as afferent evidence +``` + +## Contract + +`ObservationFrame` contains: + +```text +frame_id +monotonic_tick +source_clock +units: tuple[CompilationUnitLike, ...] +causal_parent_ids +environment_sha256 +trace_hash +``` + +Rules: + +- Units are canonicalized by `merge_key` and exact duplicates deduplicate. +- Trace records contain hashes and provenance only, never raw pixels, PCM, or + actuator payloads. +- Audio chunks, vision tiles, text turns, and future proprioceptive feedback + remain native compilation units. +- Motor commands and action traces are efferent; they are not observation units. +- Action outcomes re-enter through afferent sensorimotor/proprioceptive + compilers. + +## Consequences + +This closes the architectural gap between independent modality compilers and an +embodied environment loop without inventing late fusion. Cross-modal coherence +is recovered after merge through exact manifold recall and field resonance. The +hot path stays local and deterministic; fleet/offline aggregation remains a +proposal/review path, not runtime truth. + +## Proof Obligations + +- Same afferent units in any arrival order produce the same frame trace hash. +- Unsafe raw payloads fail before entering frame traces. +- Efferent action records fail if passed as observation units. +- Sensorimotor feedback can enter as afferent evidence without enabling motor + emission. diff --git a/docs/decisions/ADR-0209-sensorimotor-feedback-contract.md b/docs/decisions/ADR-0209-sensorimotor-feedback-contract.md new file mode 100644 index 00000000..b7d83124 --- /dev/null +++ b/docs/decisions/ADR-0209-sensorimotor-feedback-contract.md @@ -0,0 +1,70 @@ +# ADR-0209: Sensorimotor Feedback Is Afferent + +**Status:** Proposed +**Date:** 2026-06-04 +**Domains:** `sensorium/sensorimotor/`, `sensorium/protocol.py`, future robotics integrations +**Depends on:** ADR-0013, ADR-0198, ADR-0208 + +## Decision + +CORE will treat proprioception, tactile/contact state, actuator state feedback, +and action result evidence as **afferent sensorimotor input**. Motor commands +remain **efferent** and are governed separately by `EfferentGate`. + +```text +proprioception / contact / actuator feedback + -> sensorimotor compiler + -> SensorimotorCompilationUnit + -> ObservationFrame + +field action intent + -> EfferentGate + AuthorityToken + -> governed decode / refusal + -> environment effect + -> result feedback re-enters as sensorimotor input +``` + +## Contract + +The v1 afferent signal is quantized and replayable: + +```text +ProprioceptiveSignal + pose_q + velocity_q + force_torque_q + contact_q + actuator_state_q + source_sha256 + canonical_sha256 +``` + +The compiler emits: + +```text +SensorimotorIR +SensorimotorCompilationUnit +ContentAddressedDelta +``` + +No decoder, trajectory executor, actuator driver, robot interface, tool call, or +skill invocation is introduced by this contract. + +## Consequences + +This reserves the correct robotics shape without making unsafe action emission +look like ordinary perception. A robot can later close the loop through +environment orchestration, but the two halves remain type-separated: + +- sensorimotor feedback is evidence; +- motor command is authorized action; +- action results become new evidence only after they re-enter through an + afferent compiler. + +## Proof Obligations + +- Same canonical proprioceptive signal produces identical unit and merge key. +- IR replay reproduces the projection. +- Sensorimotor deltas merge idempotently. +- Sensorimotor compiler exposes no decode path. +- Trace records contain no command or trajectory payload. diff --git a/evals/vision_sensorium/__init__.py b/evals/vision_sensorium/__init__.py new file mode 100644 index 00000000..6e93276a --- /dev/null +++ b/evals/vision_sensorium/__init__.py @@ -0,0 +1,2 @@ +"""Deterministic vision compiler eval lane.""" + diff --git a/evals/vision_sensorium/expected_ir.jsonl b/evals/vision_sensorium/expected_ir.jsonl new file mode 100644 index 00000000..3662c8f5 --- /dev/null +++ b/evals/vision_sensorium/expected_ir.jsonl @@ -0,0 +1,9 @@ +{"canonical_sha256": "2539e3b2ac17082d709bf08d8b93b80ea3d6d8104bb18f93a3c3579a27272770", "event_type_counts": {"orient.edge_energy": 5, "region.chroma": 5, "region.flat": 5, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "flat_gray", "unit_count": 5, "unit_ir_sha256": ["1805976d658a70c473365ba1825ce51230bb527a149ae07f0aa1fd007db6975b", "76c82a15ea0f67cd612679c04025b5a91adf8349e8e2e0e909d3b15bfd4b1250", "f593b65fee41df238fe74ede402f6695b3c2975b2b3d3156d6f8b47844ef5ac2", "2e6e3d938cdb22a8bcab977d54353ee944ce4b589f8024ba3beafdeb182fefac", "5df381dd3a8ae41050f344393b79756e395e3da63e3b1cf3a2fce05ee1e93a44"]} +{"canonical_sha256": "de941bccf3aab2c76c4c513a6b7745eba39b299ee708d7ee1c1f02c81af558b1", "event_type_counts": {"orient.edge_energy": 5, "region.chroma": 5, "region.contrast": 1, "region.flat": 4, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "vertical_edge", "unit_count": 5, "unit_ir_sha256": ["c0bb3e2c3606df5d9f430d8c4b87a279bb2068df8534e1ae2e2cf2417b75523a", "3264a01a8291126b02c29dbaffa9c8f1994e7c035c1ca8f2d4ff82913be57b3c", "dfca4c49efa1041140fb48959136cf93485ad91b53c47a4813705563068221d4", "406af11b33f6aec73b398e3daf283aaf3271c4374895484582997c1fcf9cc888", "a41ec8ce38f9b96c1487da31551c078120d87ae5c65a1c6ab7498413aa98f2d5"]} +{"canonical_sha256": "81ebbbf4e07009bba424159810e06274bb453a1cfa4221187c14287db77d6f0b", "event_type_counts": {"contour.closure": 2, "orient.edge_energy": 5, "region.blob": 1, "region.chroma": 5, "region.contrast": 2, "region.corner": 2, "region.flat": 3, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "corner_block", "unit_count": 5, "unit_ir_sha256": ["8559deef8b525dc9b9658f2608d71034fbeb4f72198c3655618df92995f12426", "3264a01a8291126b02c29dbaffa9c8f1994e7c035c1ca8f2d4ff82913be57b3c", "3c75d4ac5655de36c25ac42f52b6abe8ee795bad6494c24f706758031ddbd554", "406af11b33f6aec73b398e3daf283aaf3271c4374895484582997c1fcf9cc888", "3594727932503362bb78893ca8b22b2841e60f7d05590f3b5f885a171dcf0539"]} +{"canonical_sha256": "3d6bd7f67919d37eb9a3f2222c9c91dac6b47df5a904edf50c67072016c45ca0", "event_type_counts": {"contour.closure": 5, "orient.edge_energy": 5, "region.blob": 5, "region.chroma": 5, "region.contrast": 5, "region.corner": 5, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "center_blob", "unit_count": 5, "unit_ir_sha256": ["f85e1a041b6d1b295000e52b57fdfdfbaaf5480567e9944f11a198516276d0e8", "a783fcd737812546c82e8f840b5987bbf93f427dba0e5a1c98b273520349a97f", "e38b05b3418a97cd19ece411d90cb5af0a836c7a35214ce96e62813c0d81cf3e", "c65af4c1c9ec7d85ea8673b08a71d8f7005ea538f6aab96ef44a42dd84928af7", "2c49e77da998e90693aa25f41d495c1cff369e5ba7e49805bf16acf9290dc44a"]} +{"canonical_sha256": "f5ac89e012cc68167c370552ef3e3058d8b1ef0758e4e4b8943cb88e2b787bef", "event_type_counts": {"contour.closure": 5, "orient.edge_energy": 5, "region.chroma": 5, "region.contrast": 5, "region.corner": 5, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "checker_texture", "unit_count": 5, "unit_ir_sha256": ["250503203675980bfb964ea8ba5b8dca48bd4b394b9dcfb92a83578858f2e9e3", "255a2331c646ff561851de1f852e0850439b9d2dbdf3828a07ccb30f23fd9946", "2f76219af0dded3845f28400ec731db0b7ca94b47a75cea109b7aa9c468130b0", "e13b7ea49cd608d717c1f8f19df5805f8e3b3d079420a088808e46eb48379d6d", "2c5084229cece80f8fdea81f056847e67dfdfea5b287eb7234380a46a2da66c9"]} +{"canonical_sha256": "a91aa64832aa6d75e5631b54de97b5d620def288b2cce898f09535922dcb6b1e", "event_type_counts": {"orient.edge_energy": 5, "region.chroma": 5, "region.contrast": 5, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "contrast_ramp", "unit_count": 5, "unit_ir_sha256": ["bf5b935e984489baa72563f26b18a8106deda4e8be1ed84bf211397a228624ab", "14f0dc93ee144bc57264873248a3f6bd137c06cc0fd14d99bc7d9aa494a705e8", "ad165740c173f9599ea40b2cdfb23bb3d961aaa04c06744d7cabf035398f561a", "356fd042b284604030dbe81965df66a88dc4ad5b7a8d721013f2c039c9080fc5", "a9b48462e0c9133cfe994eec25a5629656013436bcbeb307b82a9200467b3355"]} +{"canonical_sha256": "211bf8d52098a28d425fa604a60f2c4a2d1e09ad9451f23b08b9cd34de6c70b9", "event_type_counts": {"orient.edge_energy": 5, "region.chroma": 5, "region.contrast": 1, "region.flat": 4, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "chroma_split", "unit_count": 5, "unit_ir_sha256": ["093e51212fcf40dd10d3085906772d20a9eac52a93895ad0b8698bf08b65ed7a", "61991cf7ad68437449727e2dcd11d47828962a177683b4fcee78fc71449342ea", "294368f2a968e4b07718a32bae1b0c0b3691d136f96cbea8c20b08a00063e571", "99ee396623df3187039256625aa23ff56fb95056f74a639380d4434dd3919e25", "d63532072fcafb030a0483a4df69d9100ff7e90c24b9f7cc73d9dc0bd6d84354"]} +{"canonical_sha256": "8022d2e39e825baae1e516e13ee44cdb8aee7960a4cf29e3267ae8c3f177fa7c", "event_type_counts": {"orient.edge_energy": 5, "region.blob": 5, "region.chroma": 5, "region.contrast": 5, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "salient_spot", "unit_count": 5, "unit_ir_sha256": ["6adb1086d482d152c6f13e1e04cd90e3d06c03bd2ebca6a9460a9dc27b8ef649", "fbf6301c26f35fbb1c2775b2aa7e679872af555c33769155c70051a312c687c1", "90965d7e5597559fd92315a54a463b4f0980269c40f18d4de4279059c89c993e", "451a0b869e8c7ac483fed4b9909187ab590eb8ad771d3fdb0888a6034809cf10", "be45a2c65e350585b627903bdca98be2fc2a069d8461b1d2ceee6081a3f90eff"]} +{"canonical_sha256": "280d4b12edb93ff93573e059af9ffcc8a3dcb5aad8d7de62e4403754205b8cb8", "event_type_counts": {"contour.closure": 5, "orient.edge_energy": 5, "region.blob": 5, "region.chroma": 5, "region.contrast": 5, "region.corner": 5, "salient.figure_ground": 5, "texture.regularity": 5}, "id": "contour_box", "unit_count": 5, "unit_ir_sha256": ["a586f1d7aa07f157fd3b2b1c7c85b4585fc77aa9bbb64f8515db3e5f1f99c38c", "f7c65d09469fcb2fe6bf90f399eb97607dfb0e634accd85117c6f8e90a0480b5", "118716ded59c86aa2eef07ec31e174c8c5d630454ba0a1e5becd0abfe8aa6af0", "b04d9b186b13fc06c73f5928daf8224b2a0fdd3b75558ec9b7fdb8c7d0903bff", "05981a687df449108ce2382a2e1c230008d900876ca85474c420d4c9c798e6bb"]} diff --git a/evals/vision_sensorium/expected_projection.json b/evals/vision_sensorium/expected_projection.json new file mode 100644 index 00000000..a8800511 --- /dev/null +++ b/evals/vision_sensorium/expected_projection.json @@ -0,0 +1,1910 @@ +{ + "center_blob": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "c9f6be88d2195568008169ddaf9276ab1e7c04e6a436b391dff8982e340a86e2", + "reference_versor": [ + 0.7212908864021301, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.22993063926696777, + 0.28105300664901733, + -0.0994982197880745, + 0.0, + 0.2861078083515167, + 0.10908539593219757, + 0.0, + 0.4886484742164612, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07379721105098724, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "f7a7b5d61b660a0587d0d0597ab336802678ebed3f593311a3a15a704ede77f6", + "reference_versor": [ + 0.7137175798416138, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.23288056254386902, + 0.3016986846923828, + -0.11369837075471878, + 0.0, + 0.2876531481742859, + 0.121232770383358, + 0.0, + 0.48103663325309753, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.05988730117678642, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "f6fe672860535e72813f48eabb71451e665006ad15eedc29ea269ebd7d39a6dd", + "reference_versor": [ + 0.714083731174469, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.23674994707107544, + 0.25574952363967896, + -0.08872875571250916, + 0.0, + 0.30179908871650696, + 0.11222591251134872, + 0.0, + 0.49935540556907654, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08786418288946152, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "6806826abca74444ff19c19936f6f9f2daad98f4d761c20cd7a5daa531d2edfd", + "reference_versor": [ + 0.708467960357666, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.24927087128162384, + 0.23284192383289337, + -0.08159071207046509, + 0.0, + 0.3044765293598175, + 0.10835052281618118, + 0.0, + 0.5088029503822327, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.10834471881389618, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "a8592b5861a0936bfef666ba0bb9b6e7a6054bb5fb832fae876d4a8d4e4dfd44", + "reference_versor": [ + 0.68705153465271, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2564890682697296, + 0.2548616826534271, + -0.10417802631855011, + 0.0, + 0.34822073578834534, + 0.153333842754364, + 0.0, + 0.4862462282180786, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07184464484453201, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "checker_texture": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "6b6f9b0a609399bb279cca3aacb51175e63f0ce18dba978935b87b99aa414217", + "reference_versor": [ + 0.6248517632484436, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1170668974518776, + 0.42825159430503845, + -0.1736692190170288, + 0.0, + -0.043557774275541306, + -0.05491085723042488, + 0.0, + 0.5928910970687866, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.16081930696964264, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "e74b34ab819930dca8b86a20825bad6e9a1ea7226cb359245a91cf294cc862b7", + "reference_versor": [ + 0.6215673089027405, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.11467999219894409, + 0.4315762519836426, + -0.17744232714176178, + 0.0, + -0.04680028185248375, + -0.054124463349580765, + 0.0, + 0.5932230949401855, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.16039133071899414, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "e74b34ab819930dca8b86a20825bad6e9a1ea7226cb359245a91cf294cc862b7", + "reference_versor": [ + 0.6215673089027405, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.11467999219894409, + 0.4315762519836426, + -0.17744232714176178, + 0.0, + -0.04680028185248375, + -0.054124463349580765, + 0.0, + 0.5932230949401855, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.16039133071899414, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "70ed50ba5bce79948ad5a0a193506770793c0b4bd0872d154a06c729cc0051ee", + "reference_versor": [ + 0.6183041930198669, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.11224329471588135, + 0.43484756350517273, + -0.18123266100883484, + 0.0, + -0.05006176978349686, + -0.053319212049245834, + 0.0, + 0.5935000777244568, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15991313755512238, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "32f8d4556f8be89a40a6b58bf412a8a2f37235e12c1bd846602d3cdb00a307fa", + "reference_versor": [ + 0.6096251010894775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.10807454586029053, + 0.42911839485168457, + -0.20176906883716583, + 0.0, + -0.05130501836538315, + -0.04779261350631714, + 0.0, + 0.601798415184021, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.15730905532836914, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "chroma_split": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "3f8e18a57724faef1f1aa8806f6921048c32ab06c1441f948a93131deab071e9", + "reference_versor": [ + 0.9328449368476868, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.061626777052879333, + 0.1599825769662857, + 0.10688996315002441, + 0.0, + 0.21482662856578827, + 0.2048368752002716, + 0.0, + 0.02826949954032898, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.008645984344184399, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "5bc02dd8cc628934c3c496482f3e15a18ef5944fd9590c8ef4c024f773215639", + "reference_versor": [ + 0.9332165122032166, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.06954992562532425, + 0.16192080080509186, + 0.10793732106685638, + 0.0, + 0.19371992349624634, + 0.21914201974868774, + 0.0, + 0.025741232559084892, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.013698581606149673, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "02d350a1a872fa44fb1fcee9055c076e63ba8e79be90cbcdb0345027baa70415", + "reference_versor": [ + 0.9307920932769775, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.06422202289104462, + 0.1637021005153656, + 0.10903730243444443, + 0.0, + 0.2169293314218521, + 0.20703764259815216, + 0.0, + 0.028451580554246902, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.009037344716489315, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "75b67f7c5a599b6c3414d13a36b83b4a1763e6557693d8a11dc8be61f0810b4d", + "reference_versor": [ + 0.9311608672142029, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.07224651426076889, + 0.16557352244853973, + 0.11002202332019806, + 0.0, + 0.19584769010543823, + 0.22129780054092407, + 0.0, + 0.025879444554448128, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.014201412908732891, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "85f925b6572aaf4c87dd238b43406fd23d3ee873fb1fdf61b572e75ca1ba62e4", + "reference_versor": [ + 0.90505051612854, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.18364593386650085, + 0.26070690155029297, + 0.07877114415168762, + 0.0, + 0.1774432212114334, + 0.19950531423091888, + 0.0, + 0.008235796354711056, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.04035413637757301, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "contour_box": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "2b90afe51e18ad83aa41a8165cae0a8085e5409d99eb908b4882c2a2af50c380", + "reference_versor": [ + 0.731372058391571, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.27545440196990967, + 0.3026246130466461, + -0.08372723311185837, + 0.0, + 0.2976676821708679, + 0.11165980249643326, + 0.0, + 0.42780613899230957, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08084419369697571, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "dfbe12a9e08b53456108134a1f2d8949d223697c6a00b5cba4704322bcc58589", + "reference_versor": [ + 0.7274520397186279, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2745327651500702, + 0.332455039024353, + -0.09895174950361252, + 0.0, + 0.2872651219367981, + 0.1192575991153717, + 0.0, + 0.4174816608428955, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.06397565454244614, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "5203c357767dd24e062e88a464be884bd8c68eb6251572c72467f3d5e5c3a633", + "reference_versor": [ + 0.7277149558067322, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2787332236766815, + 0.28001731634140015, + -0.07389294356107712, + 0.0, + 0.30575117468833923, + 0.1087116152048111, + 0.0, + 0.44085636734962463, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.09598171710968018, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "ea5d2ef52f58b97cd57e5189141e389b44a1a6aaf366e39da522bb8523dc4965", + "reference_versor": [ + 0.7230232954025269, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2817756235599518, + 0.2574843168258667, + -0.06408781558275223, + 0.0, + 0.31329771876335144, + 0.10540775209665298, + 0.0, + 0.45358994603157043, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.11146415770053864, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "99440aa795e098c9231fb9cafbcfdb75eefb8d76810ac7f15e68b0d7fffb8c04", + "reference_versor": [ + 0.7049604654312134, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.29689455032348633, + 0.27952641248703003, + -0.08820090442895889, + 0.0, + 0.34412288665771484, + 0.1403273642063141, + 0.0, + 0.42910394072532654, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.08202086389064789, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "contrast_ramp": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "80eb3c6e8fea1b7b801f982b7ab4551037ce065a7eed78a7535a79a34f2e7237", + "reference_versor": [ + 0.9188927412033081, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.1850241869688034, + 0.27348843216896057, + 0.09391507506370544, + 0.0, + 0.1761963814496994, + 0.08036729693412781, + 0.0, + -0.014312044717371464, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.008793304674327374, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "761de998a1aa7f4acdeade7f6b42138f126172ca2e2117b04035a07d2afdaf42", + "reference_versor": [ + 0.9168206453323364, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.18665002286434174, + 0.27721700072288513, + 0.09584319591522217, + 0.0, + 0.17742927372455597, + 0.08230089396238327, + 0.0, + -0.014962718822062016, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.00938308984041214, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "761de998a1aa7f4acdeade7f6b42138f126172ca2e2117b04035a07d2afdaf42", + "reference_versor": [ + 0.9168206453323364, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.18665002286434174, + 0.27721700072288513, + 0.09584319591522217, + 0.0, + 0.17742927372455597, + 0.08230089396238327, + 0.0, + -0.014962718822062016, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.00938308984041214, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "0ebff4fa54507b1a6eede3f3055199d311d95501d259a901bde07c24647e4528", + "reference_versor": [ + 0.9147282838821411, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.18825772404670715, + 0.28093811869621277, + 0.09775388240814209, + 0.0, + 0.17864015698432922, + 0.08421752601861954, + 0.0, + -0.015632228925824165, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.009992072358727455, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "b1c9188dad16df0381ebf1f266d402dcfa544916a66a5df33890737838c5561a", + "reference_versor": [ + 0.906849205493927, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25073498487472534, + 0.28477826714515686, + 0.0892772302031517, + 0.0, + 0.1391879767179489, + 0.075070820748806, + 0.0, + -0.020835790783166885, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.01563268154859543, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "corner_block": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "077319d988348d665b87972ce908f4516148894c77bbffbca107fe4f732d2d54", + "reference_versor": [ + 0.7113939523696899, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.29716992378234863, + 0.32128944993019104, + -0.08222510665655136, + 0.0, + 0.3254401981830597, + 0.12220872193574905, + 0.0, + 0.41058507561683655, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0787043571472168, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "bc4a7935acc698ede5c053c77997c0ece34b4476343fbceb891aee09b5ef3c0d", + "reference_versor": [ + 0.9451851844787598, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.04979962110519409, + 0.16432426869869232, + 0.1153317391872406, + 0.0, + 0.23035979270935059, + 0.1031208261847496, + 0.0, + 0.0056336685083806515, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010477413423359394, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "bc4a7935acc698ede5c053c77997c0ece34b4476343fbceb891aee09b5ef3c0d", + "reference_versor": [ + 0.9451851844787598, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.04979962110519409, + 0.16432426869869232, + 0.1153317391872406, + 0.0, + 0.23035979270935059, + 0.1031208261847496, + 0.0, + 0.0056336685083806515, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010477413423359394, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "4faf3122f33492c4228e661de1b8baa3017c0d8adbe45cf307479b512ca1fda8", + "reference_versor": [ + 0.9433317184448242, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.05208340287208557, + 0.16809768974781036, + 0.11773113906383514, + 0.0, + 0.2324216514825821, + 0.10551494359970093, + 0.0, + 0.005598478019237518, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.01051383651793003, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "08910392c5cce8eec02eb81188b3945c604319e4f92e6faca7209fe7df29f496", + "reference_versor": [ + 0.7879268527030945, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.14135272800922394, + 0.35130274295806885, + -0.050423331558704376, + 0.0, + 0.07898887246847153, + -0.03835470974445343, + 0.0, + 0.4651922583580017, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.09550052136182785, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "flat_gray": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "7710ecb00af0248b4cbf70b8e2d8dbb730225e0a3433268fc60c1342e5bfd52c", + "reference_versor": [ + 0.961235523223877, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0591854527592659, + 0.15662622451782227, + 0.11337296664714813, + 0.0, + 0.15798725187778473, + 0.10084988921880722, + 0.0, + -0.0017689305823296309, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0020921514369547367, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "6e9c9e26a61c13f42bb5df14bf6b9c7c5aa8823fe5bf1b826b5a5d4de9671f0a", + "reference_versor": [ + 0.9595705270767212, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.061753131449222565, + 0.16021554172039032, + 0.11579000949859619, + 0.0, + 0.1602034866809845, + 0.10325585305690765, + 0.0, + -0.0019676347728818655, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0019646957516670227, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "6e9c9e26a61c13f42bb5df14bf6b9c7c5aa8823fe5bf1b826b5a5d4de9671f0a", + "reference_versor": [ + 0.9595705270767212, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.061753131449222565, + 0.16021554172039032, + 0.11579000949859619, + 0.0, + 0.1602034866809845, + 0.10325585305690765, + 0.0, + -0.0019676347728818655, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0019646957516670227, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "8748536384e2c0b730cfb71c1549bc3a9e0f5f4064d9ec82b400aef43a9b074e", + "reference_versor": [ + 0.9578737020492554, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.06430831551551819, + 0.16381072998046875, + 0.11818558722734451, + 0.0, + 0.1623961180448532, + 0.10564089566469193, + 0.0, + -0.0021788522135466337, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0018245088867843151, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "8748536384e2c0b730cfb71c1549bc3a9e0f5f4064d9ec82b400aef43a9b074e", + "reference_versor": [ + 0.9578737020492554, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.06430831551551819, + 0.16381072998046875, + 0.11818558722734451, + 0.0, + 0.1623961180448532, + 0.10564089566469193, + 0.0, + -0.0021788522135466337, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0018245088867843151, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "salient_spot": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "ed4dd24f8316a8a4c3939bc949d4f86702df0ce831ac3b6ad3fa6d4d2f9f116d", + "reference_versor": [ + 0.8522048592567444, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.25386008620262146, + 0.20516905188560486, + 0.10805278271436691, + 0.0, + 0.38535547256469727, + 0.06900189071893692, + 0.0, + -0.04360314458608627, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.019258959218859673, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "5335b0d1fe27044c1676a72da84cb3a42586adfbd9414e1e07eca8f6c23e95cd", + "reference_versor": [ + 0.8441712260246277, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2578524947166443, + 0.24128659069538116, + 0.11144081503152847, + 0.0, + 0.37770169973373413, + 0.06926342099905014, + 0.0, + -0.05080514773726463, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.014545409008860588, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "1e3b900e6d41e53f43feb46289b966f19d8610649f0c2a98970ac5f253779bf6", + "reference_versor": [ + 0.8520291447639465, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2563367486000061, + 0.1730870008468628, + 0.10843462496995926, + 0.0, + 0.3991835415363312, + 0.07160088419914246, + 0.0, + -0.03937813267111778, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.024410048499703407, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "fe1bbbeeeac06ef53dec6da70d1bd5de7f3edb34f038077954b18de3acc2571e", + "reference_versor": [ + 0.8506535291671753, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.258583664894104, + 0.14090722799301147, + 0.1085110753774643, + 0.0, + 0.4126696288585663, + 0.07413756847381592, + 0.0, + -0.034858208149671555, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.02976410649716854, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "a84e7e96b7baec65d1efe09e3f16864a67e0ebc411132304a7fc5042e11fb243", + "reference_versor": [ + 0.841992974281311, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.26262515783309937, + 0.1892092227935791, + 0.11087167263031006, + 0.0, + 0.40774378180503845, + 0.07197483628988266, + 0.0, + -0.044619329273700714, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.023599693551659584, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ], + "vertical_edge": [ + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "e38b28228f075769bc69b8169b414a695c59927b45bdd8f3e1732302a451569b", + "reference_versor": [ + 0.9497386813163757, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.049469634890556335, + 0.1599607765674591, + 0.11303132027387619, + 0.0, + 0.216633602976799, + 0.1007685735821724, + 0.0, + 0.004418587777763605, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009040328674018383, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 0 + }, + "projection_sha256": "bc4a7935acc698ede5c053c77997c0ece34b4476343fbceb891aee09b5ef3c0d", + "reference_versor": [ + 0.9451851844787598, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.04979962110519409, + 0.16432426869869232, + 0.1153317391872406, + 0.0, + 0.23035979270935059, + 0.1031208261847496, + 0.0, + 0.0056336685083806515, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.010477413423359394, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 0, + "tile_row": 1 + }, + "projection_sha256": "1330c88907645d7f6e5791afba820075f69688d846240525e469795811bf8f55", + "reference_versor": [ + 0.9479408860206604, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.05181238427758217, + 0.16370077431201935, + 0.1154516264796257, + 0.0, + 0.21874357759952545, + 0.10318219661712646, + 0.0, + 0.00436779297888279, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.009061327204108238, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 0, + "tile_col": 1, + "tile_row": 1 + }, + "projection_sha256": "4faf3122f33492c4228e661de1b8baa3017c0d8adbe45cf307479b512ca1fda8", + "reference_versor": [ + 0.9433317184448242, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.05208340287208557, + 0.16809768974781036, + 0.11773113906383514, + 0.0, + 0.2324216514825821, + 0.10551494359970093, + 0.0, + 0.005598478019237518, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.01051383651793003, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + { + "coord": { + "scale_level": 1, + "tile_col": 0, + "tile_row": 0 + }, + "projection_sha256": "4186b3767ff9ff699ac87fabe2ce35a61d6d5f04333371aeaad9610843223bdd", + "reference_versor": [ + 0.8961490988731384, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.2666640281677246, + 0.2941581904888153, + 0.10004779696464539, + 0.0, + 0.1545407623052597, + 0.06793244183063507, + 0.0, + -0.024824930354952812, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + -0.0124324606731534, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + } + ] +} diff --git a/evals/vision_sensorium/fixtures.json b/evals/vision_sensorium/fixtures.json new file mode 100644 index 00000000..334f3a68 --- /dev/null +++ b/evals/vision_sensorium/fixtures.json @@ -0,0 +1,24 @@ +{ + "size": 32, + "comment": "Deterministic visual synthesis specs. Fixtures are designed with predictable measured facts so the gate grades lexer/parser semantics as well as determinism.", + "fixtures": [ + {"id": "flat_gray", "kind": "flat", "rgb": [0.5, 0.5, 0.5], + "expect": "flat low-contrast field"}, + {"id": "vertical_edge", "kind": "edge", "orientation": "vertical", + "expect": "hard oriented edge and contrast"}, + {"id": "corner_block", "kind": "corner", + "expect": "corner/junction response"}, + {"id": "center_blob", "kind": "blob", + "expect": "center blob / region onset"}, + {"id": "checker_texture", "kind": "checker", "period": 4, + "expect": "high-frequency texture"}, + {"id": "contrast_ramp", "kind": "ramp", + "expect": "luminance contrast gradient"}, + {"id": "chroma_split", "kind": "chroma_split", + "expect": "strong chroma regime"}, + {"id": "salient_spot", "kind": "salient_spot", + "expect": "salient figure on ground"}, + {"id": "contour_box", "kind": "contour_box", + "expect": "closed contour-like border"} + ] +} diff --git a/evals/vision_sensorium/generate_expected.py b/evals/vision_sensorium/generate_expected.py new file mode 100644 index 00000000..08561642 --- /dev/null +++ b/evals/vision_sensorium/generate_expected.py @@ -0,0 +1,68 @@ +"""Regenerate frozen expected artifacts for the vision eval lane.""" + +from __future__ import annotations + +import json +from collections import Counter +from pathlib import Path + +from evals.vision_sensorium.synth import synthesize +from sensorium.vision import VisionCompiler, canonicalize_image +from sensorium.vision.types import VisionIR + +_HERE = Path(__file__).resolve().parent + + +def event_type_counts(ir: VisionIR) -> dict[str, int]: + events = ( + *ir.regions, + *ir.contour_arcs, + *ir.orient_events, + *ir.texture_atoms, + *ir.salient_events, + *ir.content_anchors, + ) + return dict(sorted(Counter(e.event_type for e in events).items())) + + +def main() -> None: + spec = json.loads((_HERE / "fixtures.json").read_text()) + compiler = VisionCompiler() + ir_lines: list[str] = [] + projection: dict[str, list[dict]] = {} + + for fx in spec["fixtures"]: + image = canonicalize_image(synthesize(fx), size=int(spec["size"])) + units = compiler.compile_image(image) + counts = Counter() + for unit in units: + counts.update(event_type_counts(unit.vision_ir)) + ir_lines.append(json.dumps({ + "id": fx["id"], + "canonical_sha256": image.canonical_sha256, + "unit_count": len(units), + "unit_ir_sha256": [unit.ir_sha256 for unit in units], + "event_type_counts": dict(sorted(counts.items())), + }, sort_keys=True)) + projection[fx["id"]] = [ + { + "coord": { + "scale_level": unit.coord.scale_level, + "tile_row": unit.coord.tile_row, + "tile_col": unit.coord.tile_col, + }, + "projection_sha256": unit.projection_sha256, + "reference_versor": [float(x) for x in unit.versor.tolist()], + } + for unit in units + ] + + (_HERE / "expected_ir.jsonl").write_text("\n".join(ir_lines) + "\n") + (_HERE / "expected_projection.json").write_text( + json.dumps(projection, indent=2, sort_keys=True) + "\n" + ) + print(f"wrote expected artifacts for {len(spec['fixtures'])} fixtures") + + +if __name__ == "__main__": + main() diff --git a/evals/vision_sensorium/synth.py b/evals/vision_sensorium/synth.py new file mode 100644 index 00000000..fac681cd --- /dev/null +++ b/evals/vision_sensorium/synth.py @@ -0,0 +1,66 @@ +"""Deterministic synthetic image fixtures for vision_core_v1.""" + +from __future__ import annotations + +import numpy as np + +SIZE = 32 + + +def _flat(rgb: list[float], size: int) -> np.ndarray: + out = np.zeros((size, size, 3), dtype=np.float32) + out[:, :, :] = np.asarray(rgb, dtype=np.float32) + return out + + +def synthesize(spec: dict) -> np.ndarray: + """Return a float32 RGB image for a fixture spec.""" + size = int(spec.get("size", SIZE)) + kind = spec["kind"] + if kind == "flat": + return _flat(list(spec.get("rgb", [0.5, 0.5, 0.5])), size) + if kind == "edge": + out = _flat([0.15, 0.15, 0.15], size) + if spec.get("orientation") == "horizontal": + out[size // 2:, :, :] = 0.9 + else: + out[:, size // 2:, :] = 0.9 + return out + if kind == "corner": + out = _flat([0.1, 0.1, 0.1], size) + out[4:16, 4:7, :] = 0.95 + out[4:7, 4:16, :] = 0.95 + out[11:16, 11:16, :] = 0.75 + return out + if kind == "blob": + out = _flat([0.2, 0.2, 0.2], size) + yy, xx = np.mgrid[:size, :size] + mask = (xx - size / 2) ** 2 + (yy - size / 2) ** 2 <= (size / 5) ** 2 + out[mask, :] = 0.95 + return out.astype(np.float32) + if kind == "checker": + period = int(spec.get("period", 4)) + yy, xx = np.mgrid[:size, :size] + mask = ((xx // period) + (yy // period)) % 2 + out = np.repeat(mask[:, :, None].astype(np.float32), 3, axis=2) + return out + if kind == "ramp": + x = np.linspace(0.0, 1.0, size, dtype=np.float32) + ramp = np.repeat(x[None, :, None], size, axis=0) + return np.repeat(ramp, 3, axis=2) + if kind == "chroma_split": + out = _flat([0.1, 0.1, 0.8], size) + out[:, size // 2:, :] = np.asarray([0.9, 0.15, 0.1], dtype=np.float32) + return out + if kind == "salient_spot": + out = _flat([0.45, 0.45, 0.45], size) + out[size // 2 - 3:size // 2 + 3, size // 2 - 3:size // 2 + 3, :] = 1.0 + return out + if kind == "contour_box": + out = _flat([0.2, 0.2, 0.2], size) + out[7:25, 7:11, :] = 1.0 + out[7:25, 21:25, :] = 1.0 + out[7:11, 7:25, :] = 1.0 + out[21:25, 7:25, :] = 1.0 + return out + raise ValueError(f"unknown vision fixture kind: {kind!r}") diff --git a/sensorium/environment/__init__.py b/sensorium/environment/__init__.py new file mode 100644 index 00000000..d077e09f --- /dev/null +++ b/sensorium/environment/__init__.py @@ -0,0 +1,5 @@ +"""Environmental observation contracts for sensorium units.""" + +from sensorium.environment.frame import ObservationFrame, build_observation_frame + +__all__ = ["ObservationFrame", "build_observation_frame"] diff --git a/sensorium/environment/frame.py b/sensorium/environment/frame.py new file mode 100644 index 00000000..1a6efea8 --- /dev/null +++ b/sensorium/environment/frame.py @@ -0,0 +1,107 @@ +"""Deterministic environmental observation frames. + +An ObservationFrame is a traceable bundle of already-compiled afferent units. +It is not a fusion layer and it never accepts efferent action commands as +observation evidence. +""" + +from __future__ import annotations + +from dataclasses import dataclass +from typing import Iterable + +import numpy as np + +from sensorium.audio.checksum import sha256_json +from sensorium.compiler.protocol import CompilationUnitLike, MergeKey + +_UNSAFE_ATTRS = ("pixels", "samples", "pcm", "waveform", "raw_bytes", "action_trace") + + +def _reject_unsafe_unit(unit: CompilationUnitLike) -> None: + if bool(getattr(unit, "efferent", False)): + raise ValueError("efferent action traces are not afferent observation units") + if str(getattr(unit, "pack_id", "")).startswith("motor"): + raise ValueError("motor/efferent packs are not observation units") + for attr in _UNSAFE_ATTRS: + if hasattr(unit, attr): + value = getattr(unit, attr) + if isinstance(value, (np.ndarray, bytes, bytearray)): + raise TypeError(f"unsafe observation payload on unit: {attr}") + + +def _unit_record(unit: CompilationUnitLike) -> dict[str, object]: + _reject_unsafe_unit(unit) + return { + "merge_key": list(unit.merge_key), + "canonical_sha256": unit.canonical_sha256, + "ir_sha256": unit.ir_sha256, + "pack_id": unit.pack_id, + "pack_manifest_sha256": unit.pack_manifest_sha256, + "projection_sha256": unit.projection_sha256, + "versor_condition": float(unit.versor_condition), + } + + +def _canonical_units(units: Iterable[CompilationUnitLike]) -> tuple[CompilationUnitLike, ...]: + ordered = sorted(tuple(units), key=lambda u: u.merge_key) + deduped: list[CompilationUnitLike] = [] + last_key: MergeKey | None = None + for unit in ordered: + if unit.merge_key != last_key: + deduped.append(unit) + last_key = unit.merge_key + return tuple(deduped) + + +@dataclass(frozen=True, slots=True) +class ObservationFrame: + """A deterministic environmental slice over afferent compiled units.""" + + frame_id: str + monotonic_tick: int + source_clock: str + units: tuple[CompilationUnitLike, ...] + causal_parent_ids: tuple[str, ...] + environment_sha256: str + trace_hash: str + + +def build_observation_frame( + *, + monotonic_tick: int, + source_clock: str, + units: Iterable[CompilationUnitLike], + causal_parent_ids: tuple[str, ...] = (), +) -> ObservationFrame: + if monotonic_tick < 0: + raise ValueError("monotonic_tick must be non-negative") + canonical_units = _canonical_units(units) + unit_records = [_unit_record(unit) for unit in canonical_units] + env_payload = { + "monotonic_tick": int(monotonic_tick), + "source_clock": str(source_clock), + "causal_parent_ids": list(causal_parent_ids), + "unit_records": unit_records, + } + environment_sha256 = sha256_json(env_payload) + trace_hash = sha256_json({ + "kind": "ObservationFrame", + "environment_sha256": environment_sha256, + "unit_merge_keys": [record["merge_key"] for record in unit_records], + }) + frame_id = sha256_json({ + "kind": "ObservationFrame.id", + "monotonic_tick": int(monotonic_tick), + "source_clock": str(source_clock), + "trace_hash": trace_hash, + }) + return ObservationFrame( + frame_id=frame_id, + monotonic_tick=int(monotonic_tick), + source_clock=str(source_clock), + units=canonical_units, + causal_parent_ids=tuple(causal_parent_ids), + environment_sha256=environment_sha256, + trace_hash=trace_hash, + ) diff --git a/sensorium/protocol.py b/sensorium/protocol.py index ff34496f..e60062ac 100644 --- a/sensorium/protocol.py +++ b/sensorium/protocol.py @@ -42,6 +42,7 @@ class Modality(str, Enum): TEXT = "text" VISION = "vision" AUDIO = "audio" + SENSORIMOTOR = "sensorimotor" MOTOR = "motor" diff --git a/sensorium/sensorimotor/__init__.py b/sensorium/sensorimotor/__init__.py new file mode 100644 index 00000000..c1f9e304 --- /dev/null +++ b/sensorium/sensorimotor/__init__.py @@ -0,0 +1,30 @@ +"""Afferent sensorimotor / proprioceptive compiler contract.""" + +from sensorium.sensorimotor.arena import ( + SensorimotorArena, + SensorimotorDelta, + merge_sensorimotor_deltas, + sensorimotor_merge_trace_hash, +) +from sensorium.sensorimotor.compiler import SensorimotorCompiler, canonicalize_proprioception +from sensorium.sensorimotor.trace import sensorimotor_evidence_trace +from sensorium.sensorimotor.types import ( + ProprioceptiveSignal, + SensorimotorCompilationUnit, + SensorimotorEvent, + SensorimotorIR, +) + +__all__ = [ + "ProprioceptiveSignal", + "SensorimotorArena", + "SensorimotorCompilationUnit", + "SensorimotorCompiler", + "SensorimotorDelta", + "SensorimotorEvent", + "SensorimotorIR", + "canonicalize_proprioception", + "merge_sensorimotor_deltas", + "sensorimotor_evidence_trace", + "sensorimotor_merge_trace_hash", +] diff --git a/sensorium/sensorimotor/arena.py b/sensorium/sensorimotor/arena.py new file mode 100644 index 00000000..20701414 --- /dev/null +++ b/sensorium/sensorimotor/arena.py @@ -0,0 +1,60 @@ +"""Sensorimotor CRDT wrappers.""" + +from __future__ import annotations + +from dataclasses import dataclass + +from sensorium.compiler.arena import LocalArena +from sensorium.compiler.delta import ContentAddressedDelta, merge_deltas +from sensorium.compiler.trace import merge_trace_hash +from sensorium.sensorimotor.trace import sensorimotor_evidence_trace +from sensorium.sensorimotor.types import SensorimotorCompilationUnit + + +@dataclass(frozen=True, slots=True) +class SensorimotorDelta: + _inner: ContentAddressedDelta[SensorimotorCompilationUnit] + + @classmethod + def from_units( + cls, + units: tuple[SensorimotorCompilationUnit, ...] | list[SensorimotorCompilationUnit], + ) -> "SensorimotorDelta": + return cls(ContentAddressedDelta.from_units(units)) + + @property + def units(self) -> tuple[SensorimotorCompilationUnit, ...]: + return self._inner.units + + def join(self, other: "SensorimotorDelta") -> "SensorimotorDelta": + return SensorimotorDelta(self._inner.join(other._inner)) + + @property + def merge_keys(self) -> tuple[tuple[str, str, str], ...]: + return self._inner.merge_keys + + def __len__(self) -> int: + return len(self._inner) + + +class SensorimotorArena: + __slots__ = ("_arena",) + + def __init__(self) -> None: + self._arena: LocalArena[SensorimotorCompilationUnit] = LocalArena() + + def push(self, unit: SensorimotorCompilationUnit) -> None: + self._arena.push(unit) + + def snapshot(self) -> SensorimotorDelta: + return SensorimotorDelta(self._arena.snapshot()) + + +def merge_sensorimotor_deltas( + deltas: list[SensorimotorDelta] | tuple[SensorimotorDelta, ...], +) -> SensorimotorDelta: + return SensorimotorDelta(merge_deltas(delta._inner for delta in deltas)) + + +def sensorimotor_merge_trace_hash(delta: SensorimotorDelta) -> str: + return merge_trace_hash(delta._inner, sensorimotor_evidence_trace) diff --git a/sensorium/sensorimotor/compiler.py b/sensorium/sensorimotor/compiler.py new file mode 100644 index 00000000..5f43a095 --- /dev/null +++ b/sensorium/sensorimotor/compiler.py @@ -0,0 +1,166 @@ +"""Deterministic afferent sensorimotor compiler.""" + +from __future__ import annotations + +import math + +import numpy as np + +from algebra.cl41 import geometric_product +from algebra.versor import unitize_versor, versor_condition +from sensorium.audio.checksum import sha256_array, sha256_json +from sensorium.sensorimotor.types import ( + ProprioceptiveSignal, + SensorimotorCompilationUnit, + SensorimotorEvent, + SensorimotorIR, +) + +CL41_DIM = 32 +VERSOR_CONDITION_MAX = 1e-6 +THETA_STEP = math.pi / 512.0 +_EVENT_ORDER = ( + "proprio.pose", + "proprio.velocity", + "haptic.force_torque", + "haptic.contact", + "actuator.state", +) +_BLADE_BY_EVENT = { + "proprio.pose": 6, + "proprio.velocity": 7, + "haptic.force_torque": 8, + "haptic.contact": 10, + "actuator.state": 11, +} +_BASE_BY_EVENT = { + "proprio.pose": 48, + "proprio.velocity": 64, + "haptic.force_torque": 80, + "haptic.contact": 96, + "actuator.state": 112, +} + + +def canonicalize_proprioception( + *, + pose_q: tuple[int, ...] = (), + velocity_q: tuple[int, ...] = (), + force_torque_q: tuple[int, ...] = (), + contact_q: tuple[int, ...] = (), + actuator_state_q: tuple[int, ...] = (), + source_id: str = "", +) -> ProprioceptiveSignal: + payload = { + "pose_q": list(pose_q), + "velocity_q": list(velocity_q), + "force_torque_q": list(force_torque_q), + "contact_q": list(contact_q), + "actuator_state_q": list(actuator_state_q), + "source_id": source_id, + } + source_sha256 = sha256_json(payload) + canonical_payload = {k: payload[k] for k in payload if k != "source_id"} + return ProprioceptiveSignal( + pose_q=tuple(int(v) for v in pose_q), + velocity_q=tuple(int(v) for v in velocity_q), + force_torque_q=tuple(int(v) for v in force_torque_q), + contact_q=tuple(int(v) for v in contact_q), + actuator_state_q=tuple(int(v) for v in actuator_state_q), + source_sha256=source_sha256, + canonical_sha256=sha256_json(canonical_payload), + ) + + +def _event(event_type: str, values: tuple[int, ...]) -> SensorimotorEvent: + attrs = tuple((f"q{idx}", int(value)) for idx, value in enumerate(values)) + return SensorimotorEvent(event_type, attrs, ()) + + +def _parse(signal: ProprioceptiveSignal) -> SensorimotorIR: + events = ( + _event("proprio.pose", signal.pose_q), + _event("proprio.velocity", signal.velocity_q), + _event("haptic.force_torque", signal.force_torque_q), + _event("haptic.contact", signal.contact_q), + _event("actuator.state", signal.actuator_state_q), + ) + payload = [ + { + "event_type": ev.event_type, + "attrs": [list(pair) for pair in ev.attrs], + "evidence_ids": list(ev.evidence_ids), + } + for ev in events + ] + return SensorimotorIR(events, sha256_json({"events": payload})) + + +def _build_rotor(blade_index: int, theta_q: int) -> np.ndarray: + out = np.zeros(CL41_DIM, dtype=np.float64) + half = (theta_q * THETA_STEP) / 2.0 + out[0] = math.cos(half) + out[blade_index] = math.sin(half) + return out + + +def _theta_q(event: SensorimotorEvent) -> int: + total = sum(abs(int(value)) for _, value in event.attrs if isinstance(value, int)) + return max(0, min(768, _BASE_BY_EVENT[event.event_type] + total)) + + +def compile_events(events: tuple[SensorimotorEvent, ...]) -> tuple[np.ndarray, float]: + rank = {name: idx for idx, name in enumerate(_EVENT_ORDER)} + v = np.zeros(CL41_DIM, dtype=np.float64) + v[0] = 1.0 + for event in sorted(events, key=lambda ev: rank[ev.event_type]): + r = _build_rotor(_BLADE_BY_EVENT[event.event_type], _theta_q(event)) + v = geometric_product(v, r) + v = unitize_versor(v) + vc = float(versor_condition(v)) + if vc >= VERSOR_CONDITION_MAX: + raise ValueError( + f"sensorimotor compilation failed versor check: {vc:.3e} >= {VERSOR_CONDITION_MAX:.0e}" + ) + return v.astype(np.float32), vc + + +class SensorimotorCompiler: + """Compiler for afferent proprioceptive feedback only.""" + + modality = "sensorimotor" + + def __init__(self, pack_id: str = "sensorimotor_core_v1") -> None: + self._pack_id = pack_id + self._manifest_sha256 = sha256_json({ + "pack_id": pack_id, + "basis_version": "sensorimotor-basis-v1", + "events": list(_EVENT_ORDER), + }) + + def compile_signal(self, signal: ProprioceptiveSignal) -> SensorimotorCompilationUnit: + ir = _parse(signal) + versor, vc = compile_events(ir.events) + return SensorimotorCompilationUnit( + canonical_sha256=signal.canonical_sha256, + ir_sha256=ir.ir_sha256, + pack_id=self._pack_id, + pack_manifest_sha256=self._manifest_sha256, + projection_sha256=sha256_array(versor), + versor=versor, + versor_condition=vc, + sensorimotor_ir=ir, + ) + + def compile_ir(self, ir: SensorimotorIR) -> SensorimotorCompilationUnit: + versor, vc = compile_events(ir.events) + return SensorimotorCompilationUnit( + canonical_sha256="", + ir_sha256=ir.ir_sha256, + pack_id=self._pack_id, + pack_manifest_sha256=self._manifest_sha256, + projection_sha256=sha256_array(versor), + versor=versor, + versor_condition=vc, + sensorimotor_ir=ir, + ) diff --git a/sensorium/sensorimotor/trace.py b/sensorium/sensorimotor/trace.py new file mode 100644 index 00000000..51a008ee --- /dev/null +++ b/sensorium/sensorimotor/trace.py @@ -0,0 +1,18 @@ +"""Trace-safe sensorimotor evidence.""" + +from __future__ import annotations + +from sensorium.sensorimotor.types import SensorimotorCompilationUnit + + +def sensorimotor_evidence_trace(unit: SensorimotorCompilationUnit) -> dict[str, object]: + return { + "modality": "sensorimotor", + "pack_id": unit.pack_id, + "canonical_sha256": unit.canonical_sha256, + "ir_sha256": unit.ir_sha256, + "pack_manifest_sha256": unit.pack_manifest_sha256, + "projection_sha256": unit.projection_sha256, + "merge_key": list(unit.merge_key), + "versor_condition": unit.versor_condition, + } diff --git a/sensorium/sensorimotor/types.py b/sensorium/sensorimotor/types.py new file mode 100644 index 00000000..1235ef9a --- /dev/null +++ b/sensorium/sensorimotor/types.py @@ -0,0 +1,47 @@ +"""Typed sensorimotor IR for afferent proprioceptive feedback.""" + +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np + + +@dataclass(frozen=True, slots=True) +class ProprioceptiveSignal: + pose_q: tuple[int, ...] + velocity_q: tuple[int, ...] + force_torque_q: tuple[int, ...] + contact_q: tuple[int, ...] + actuator_state_q: tuple[int, ...] + source_sha256: str + canonical_sha256: str + + +@dataclass(frozen=True, slots=True) +class SensorimotorEvent: + event_type: str + attrs: tuple[tuple[str, int | str], ...] + evidence_ids: tuple[str, ...] + + +@dataclass(frozen=True, slots=True) +class SensorimotorIR: + events: tuple[SensorimotorEvent, ...] + ir_sha256: str + + +@dataclass(frozen=True, slots=True) +class SensorimotorCompilationUnit: + canonical_sha256: str + ir_sha256: str + pack_id: str + pack_manifest_sha256: str + projection_sha256: str + versor: np.ndarray + versor_condition: float + sensorimotor_ir: SensorimotorIR + + @property + def merge_key(self) -> tuple[str, str, str]: + return (self.canonical_sha256, self.ir_sha256, self.projection_sha256) diff --git a/sensorium/vision/lexer.py b/sensorium/vision/lexer.py index 002eaf11..e75ac097 100644 --- a/sensorium/vision/lexer.py +++ b/sensorium/vision/lexer.py @@ -36,7 +36,7 @@ def lex_tile(signal: VisionTileSignal) -> tuple[VisualEvent, ...]: angle = float(np.arctan2(np.mean(gy), np.mean(gx)) + np.pi) orient_q = int(np.floor((angle / (2.0 * np.pi)) * 16.0)) % 16 edge_q = _bin(energy_mean, max_value=0.5) - corner_q = _bin(float(np.mean(np.abs(gx * gy))), max_value=0.25) + corner_q = _bin(float(np.mean(np.abs(gx * gy))), max_value=0.02) center = luma[luma.shape[0] // 4: 3 * luma.shape[0] // 4, luma.shape[1] // 4: 3 * luma.shape[1] // 4] blob_q = _bin(abs(float(np.mean(center)) - mean), max_value=0.5) texture_q = _bin(float(np.mean(np.abs(energy - energy_mean))), max_value=0.5) diff --git a/tests/test_observation_frame_contract.py b/tests/test_observation_frame_contract.py new file mode 100644 index 00000000..bc5f2cd9 --- /dev/null +++ b/tests/test_observation_frame_contract.py @@ -0,0 +1,75 @@ +from __future__ import annotations + +from dataclasses import dataclass + +import numpy as np +import pytest + +from sensorium.environment import build_observation_frame + + +@dataclass(frozen=True, slots=True) +class _Unit: + canonical_sha256: str + ir_sha256: str + pack_id: str + pack_manifest_sha256: str + projection_sha256: str + versor: np.ndarray + versor_condition: float = 0.0 + + @property + def merge_key(self) -> tuple[str, str, str]: + return (self.canonical_sha256, self.ir_sha256, self.projection_sha256) + + +def _unit(name: str, pack_id: str) -> _Unit: + v = np.zeros(32, dtype=np.float32) + v[0] = 1.0 + return _Unit(name, f"ir-{name}", pack_id, "manifest", f"proj-{name}", v) + + +def test_observation_frame_is_order_invariant_and_deduped(): + audio = _unit("a", "audio_core_v1") + vision = _unit("v", "vision_core_v1") + text = _unit("t", "en") + f1 = build_observation_frame(monotonic_tick=7, source_clock="local", units=[audio, vision, text, audio]) + f2 = build_observation_frame(monotonic_tick=7, source_clock="local", units=[text, audio, vision]) + assert f1.trace_hash == f2.trace_hash + assert f1.environment_sha256 == f2.environment_sha256 + assert len(f1.units) == 3 + assert tuple(unit.merge_key for unit in f1.units) == tuple(sorted(unit.merge_key for unit in f1.units)) + + +def test_mixed_units_remain_content_addressed(): + frame = build_observation_frame( + monotonic_tick=1, + source_clock="edge", + causal_parent_ids=("parent",), + units=[_unit("audio", "audio_core_v1"), _unit("vision", "vision_core_v1")], + ) + assert frame.frame_id + assert frame.causal_parent_ids == ("parent",) + assert frame.units[0].merge_key < frame.units[1].merge_key + + +def test_unsafe_payloads_are_rejected_from_frame_trace(): + @dataclass(frozen=True, slots=True) + class BadUnit(_Unit): + samples: bytes = b"pcm" + + with pytest.raises(TypeError, match="unsafe observation payload"): + build_observation_frame(monotonic_tick=0, source_clock="local", units=[BadUnit( + "a", "ir-a", "audio_core_v1", "manifest", "proj-a", np.zeros(32, dtype=np.float32) + )]) + + +def test_efferent_action_trace_is_not_an_afferent_unit(): + @dataclass(frozen=True, slots=True) + class ActionUnit(_Unit): + efferent: bool = True + + with pytest.raises(ValueError, match="efferent"): + build_observation_frame(monotonic_tick=0, source_clock="local", units=[ActionUnit( + "m", "ir-m", "motor_test", "manifest", "proj-m", np.zeros(32, dtype=np.float32) + )]) diff --git a/tests/test_sensorimotor_contract.py b/tests/test_sensorimotor_contract.py new file mode 100644 index 00000000..d53cfcb1 --- /dev/null +++ b/tests/test_sensorimotor_contract.py @@ -0,0 +1,73 @@ +from __future__ import annotations + +import numpy as np + +from sensorium.protocol import Modality +from sensorium.sensorimotor import ( + SensorimotorCompiler, + SensorimotorDelta, + canonicalize_proprioception, + merge_sensorimotor_deltas, + sensorimotor_evidence_trace, + sensorimotor_merge_trace_hash, +) + + +def _signal(): + return canonicalize_proprioception( + pose_q=(10, -4, 3), + velocity_q=(1, 0, -1), + force_torque_q=(2, 3, 5), + contact_q=(1, 0, 1, 0), + actuator_state_q=(7, 8), + source_id="test-sensor", + ) + + +def test_sensorimotor_is_afferent_modality_label(): + assert Modality.SENSORIMOTOR.value == "sensorimotor" + + +def test_same_proprioceptive_signal_produces_identical_unit(): + compiler = SensorimotorCompiler() + u1 = compiler.compile_signal(_signal()) + u2 = compiler.compile_signal(_signal()) + assert np.array_equal(u1.versor, u2.versor) + assert u1.merge_key == u2.merge_key + assert u1.versor.shape == (32,) + assert u1.versor.dtype == np.float32 + assert u1.versor_condition < 1e-6 + + +def test_sensorimotor_ir_replay_is_deterministic(): + compiler = SensorimotorCompiler() + unit = compiler.compile_signal(_signal()) + replay = compiler.compile_ir(unit.sensorimotor_ir) + assert np.array_equal(unit.versor, replay.versor) + assert unit.ir_sha256 == replay.ir_sha256 + assert unit.projection_sha256 == replay.projection_sha256 + + +def test_sensorimotor_delta_merge_is_idempotent(): + compiler = SensorimotorCompiler() + unit = compiler.compile_signal(_signal()) + delta = SensorimotorDelta.from_units([unit]) + merged = merge_sensorimotor_deltas([delta, delta]) + assert merged.merge_keys == delta.merge_keys + assert sensorimotor_merge_trace_hash(merged) == sensorimotor_merge_trace_hash(delta) + + +def test_sensorimotor_trace_has_no_actuator_command_payload(): + unit = SensorimotorCompiler().compile_signal(_signal()) + trace = sensorimotor_evidence_trace(unit) + assert trace["modality"] == "sensorimotor" + assert "command" not in trace + assert "trajectory" not in trace + for value in trace.values(): + assert not isinstance(value, (np.ndarray, bytes, bytearray)) + + +def test_sensorimotor_compiler_exposes_no_decode_path(): + compiler = SensorimotorCompiler() + assert not hasattr(compiler, "decode") + assert not hasattr(compiler, "decode_batch") diff --git a/tests/test_vision_eval_gates.py b/tests/test_vision_eval_gates.py new file mode 100644 index 00000000..a3ae93a4 --- /dev/null +++ b/tests/test_vision_eval_gates.py @@ -0,0 +1,127 @@ +"""Vision compiler eval gate table.""" + +from __future__ import annotations + +import json +from collections import Counter +from pathlib import Path + +import numpy as np +import pytest + +from evals.vision_sensorium.synth import synthesize +from sensorium.adapters.vision import make_vision_pack +from sensorium.registry import ModalityRegistry +from sensorium.vision import VisionCompiler, canonicalize_image, vision_evidence_trace +from sensorium.vision.grid import iter_tile_signals +from sensorium.vision.types import VisionIR + +_EVAL_DIR = Path("evals/vision_sensorium") +TOL = 1e-6 + + +def _load_fixtures() -> list[dict]: + return json.loads((_EVAL_DIR / "fixtures.json").read_text())["fixtures"] + + +def _load_expected_ir() -> dict[str, dict]: + out: dict[str, dict] = {} + for line in (_EVAL_DIR / "expected_ir.jsonl").read_text().splitlines(): + if line.strip(): + row = json.loads(line) + out[row["id"]] = row + return out + + +def _load_expected_projection() -> dict[str, list[dict]]: + return json.loads((_EVAL_DIR / "expected_projection.json").read_text()) + + +def _event_type_counts(ir: VisionIR) -> dict[str, int]: + events = ( + *ir.regions, + *ir.contour_arcs, + *ir.orient_events, + *ir.texture_atoms, + *ir.salient_events, + *ir.content_anchors, + ) + return dict(sorted(Counter(e.event_type for e in events).items())) + + +FIXTURES = _load_fixtures() +EXPECTED_IR = _load_expected_ir() +EXPECTED_PROJ = _load_expected_projection() +IDS = [fx["id"] for fx in FIXTURES] + + +@pytest.fixture(scope="module") +def compiler() -> VisionCompiler: + return VisionCompiler() + + +@pytest.mark.parametrize("fx", FIXTURES, ids=IDS) +def test_vision_gate_table(fx, compiler): + image = canonicalize_image(synthesize(fx)) + units = compiler.compile_image(image) + signals_by_coord = {signal.coord: signal for signal in iter_tile_signals(image)} + fid = fx["id"] + + assert image.canonical_sha256 == EXPECTED_IR[fid]["canonical_sha256"] + assert len(units) == EXPECTED_IR[fid]["unit_count"] + + counts = Counter() + for idx, unit in enumerate(units): + assert unit.versor.shape == (32,) + assert unit.versor.dtype == np.float32 + assert unit.versor_condition < TOL + + again = compiler.compile_tile(signals_by_coord[unit.coord]) + assert np.array_equal(unit.versor, again.versor) + assert unit.merge_key == again.merge_key + + replay = compiler.compile_ir(unit.vision_ir) + assert np.array_equal(unit.versor, replay.versor) + assert unit.ir_sha256 == replay.ir_sha256 == EXPECTED_IR[fid]["unit_ir_sha256"][idx] + + counts.update(_event_type_counts(unit.vision_ir)) + reference = np.asarray(EXPECTED_PROJ[fid][idx]["reference_versor"], dtype=np.float32) + assert unit.projection_sha256 == EXPECTED_PROJ[fid][idx]["projection_sha256"] + assert np.allclose(unit.versor, reference, atol=TOL) + + assert dict(sorted(counts.items())) == EXPECTED_IR[fid]["event_type_counts"] + + +@pytest.mark.parametrize("fx", FIXTURES, ids=IDS) +def test_vision_trace_hygiene_no_pixels(fx, compiler): + image = canonicalize_image(synthesize(fx)) + for unit in compiler.compile_image(image): + trace = vision_evidence_trace(unit) + assert "pixels" not in trace + for value in trace.values(): + assert not isinstance(value, (np.ndarray, bytes, bytearray)) + + +def test_vision_gate_closure_refuses_projection(): + image = canonicalize_image(synthesize(FIXTURES[1])) + tile = iter_tile_signals(image)[0] + reg = ModalityRegistry() + reg.mount(make_vision_pack("vision_core_v1"), sample=tile) + with pytest.raises(RuntimeError, match="gate is not engaged"): + reg.project("vision_core_v1", tile) + + +def test_semantic_expectations_match_designed_fixtures(): + required = { + "flat_gray": {"region.flat"}, + "vertical_edge": {"region.contrast", "orient.edge_energy"}, + "corner_block": {"region.corner"}, + "center_blob": {"region.blob"}, + "checker_texture": {"texture.regularity"}, + "contrast_ramp": {"region.contrast"}, + "chroma_split": {"region.chroma"}, + "salient_spot": {"salient.figure_ground"}, + "contour_box": {"contour.closure"}, + } + for fid, event_types in required.items(): + assert event_types <= set(EXPECTED_IR[fid]["event_type_counts"]), fid