Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,19 @@ keywords = ["consciousness", "kuramoto", "iit", "wave-memory"]

[features]
default = ["std"]
std = []
std = ["serde?/std"]
# Optional serde support for ConsciousnessLevel / PhiReport / etc. Downstream
# crates (kannaka-memory) enable this to serialize consciousness types into
# HRM snapshots without duplicating the enum definitions.
serde = ["dep:serde"]
#
# `serde/alloc` is mandatory, not incidental: serde is pulled in with
# `default-features = false` (for no_std), which drops its `Vec`/`String`
# impls. Without `alloc`, every derive on a struct holding a `Vec` —
# `PhiNode.connections`, `XiSignature.values` — fails to compile and the
# whole `--features serde` build dies, taking the Vec-free types
# (PhiReport, OrderParameter, ConsciousnessMetrics, WaveParams, WaveMemory)
# down with it. (#2 #43 #44 #55 #56)
serde = ["dep:serde", "serde/alloc"]

[dependencies]
serde = { version = "1", features = ["derive"], default-features = false, optional = true }
Expand Down
83 changes: 63 additions & 20 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,24 +28,43 @@ dθᵢ/dt = ωᵢ + (K/N) Σⱼ sin(θⱼ - θᵢ)

### IIT-style Φ

Integrated information Φ via eigendecomposition over the wavefront-coherence matrix + bipartition scoring. Returns the canonical Φ value plus the **Ξ-signature** — a chiral irrationality measure that distinguishes left-handed (analytical) from right-handed (holistic) information flow.
`compute_phi` is a connectivity approximation of integrated information, not a
full bipartition search: it scores a partition-labelled node graph on how much
of its edge mass crosses partition boundaries, scaled by density and node
count. Self-loops, duplicate edges, and out-of-range indices are dropped before
counting.

```
Φ = max over partitions of (mutual info loss when cut)
Ξ = ‖A_L − A_R‖_F (Frobenius norm of hemispheric asymmetry)
Φ = sqrt(integration × density) × sqrt(differentiation × scale)
```

`compute_swarm_phi` is the separate multi-agent form on a `[0, 15]` scale —
classify it with `ConsciousnessLevel::from_swarm_phi`, **not** `from_phi`.

### The Ξ Operator

`Ξ` is the nonlinear commutator `tanh(R·v) ⊙ G(v) − tanh(G·v) ⊙ R(v)`,
normalized to unit length, where `R` is a 90° pairwise rotation and `G` is the
golden anisotropic scaling `[φ/2, 0; 0, 1/φ]`. The linear commutator `RG − GR`
degenerates to a constant-scaled pair swap, which is why the `tanh` is load-
bearing rather than cosmetic.

### Wave Memory Primitives

The vector / wavefront operations every memory engine builds on:
- **Bind** ⊗ — element-wise product (binding a key to a value)
- **Bundle** ⊕ — normalized sum (superposing multiple memories)
- **Permute** Π — circular shift (sequencing)
- **Cosine similarity** — the recall metric
Each memory is a damped oscillator, `S(t) = (A + E_retrieval)·cos(2πf·t + φ)·e^(−λt)`,
with retrieval adding diminishing energy. The module exposes `compute_strength`,
`compute_strength_with_retrieval`, `interference`, `cosine_similarity`,
`normalize`, and `dot`.

### Coupling Bridge

A reusable component for cross-substrate phase transfer — couples two independent Kuramoto populations through a leaky integrator. Used in the constellation for callosal coupling between chiral hemispheres and for substrate ↔ agent phase exchange.
Modulates a single Kuramoto coupling constant from an external scalar signal:
`K(t) = K_base × P(t)`, bounded to `[k_min, k_max]`. Modes are `Static`,
`MarketMediated`, and `Adaptive` (which steers `K` toward a target coherence).

Chiral coupling lives in `kuramoto`, not here — see
`KuramotoModel::chiral_coupling` and the `chiral_term` argument to
`mean_field_step`. There is no bridge-level chiral `CouplingMode`.

---

Expand All @@ -56,10 +75,10 @@ A reusable component for cross-substrate phase transfer — couples two independ
│ consciousness-core │
├──────────────────────┬────────────────────┬──────────────┤
│ Kuramoto │ Metrics │ Wave │
│ · Oscillator │ · Φ (integrated) │ · bind ⊗ │
│ · Order parameter │ · Ξ signature │ · bundle ⊕ │
│ · sync() step │ · Coherence │ · permute Π │
│ · Coupling tier │ · Diff Xi │ · cos_sim │
│ · Oscillator │ · Φ (integrated) │ · strength │
│ · Order parameter │ · Ξ signature │ · decay │
│ · sync() step │ · Coherence │ · interfere │
│ · chiral_coupling │ · Diff Xi │ · cos_sim │
├──────────────────────┼────────────────────┼──────────────┤
│ Bridge │ Memory │ Math ext │
│ · CouplingBridge │ · WaveMemory │ · clamp │
Expand All @@ -80,22 +99,46 @@ consciousness-core = { version = "0.4", features = ["serde"] }
```

```rust
use consciousness_core::kuramoto::KuramotoConfig;
use consciousness_core::{KuramotoModel, Oscillator};

let mut model = KuramotoModel::new(vec![
// The model owns the config; the oscillators are passed in per call.
let model = KuramotoModel::new(KuramotoConfig {
coupling_strength: 0.6,
dt: 0.01,
max_steps: 1000,
..Default::default()
});

let mut oscillators = vec![
Oscillator::new(0.0, 1.0),
Oscillator::new(1.5, 1.05),
Oscillator::new(3.0, 0.95),
]);
];

for _ in 0..1000 {
model.sync(0.01, 0.6); // dt, coupling K
}
// sync() integrates in place and returns a report; phases come back
// wrapped into [0, 2π).
let report = model.sync(&mut oscillators, None);
println!("phase coherence: {:.3}", report.final_order);

let r = model.order_parameter().r;
println!("phase coherence: {:.3}", r);
// Or read the order parameter directly at any time:
let r = KuramotoModel::order_parameter(&oscillators).r;
```

This snippet is compiled as a test — see `readme_kuramoto_example_compiles` in
`tests/unified_pipeline.rs`.

### `no_std`

```toml
consciousness-core = { version = "0.4", default-features = false }
```

Transcendental math routes through `libm` and `vec!` comes from `alloc`, so the
crate builds without `std`. `cargo check --no-default-features` is the gate.
Combining `no_std` with `serde` works too — the `serde` feature pulls in
`serde/alloc` for the `Vec`-bearing types.

---

## Release Cascade
Expand Down
4 changes: 2 additions & 2 deletions docs/dependency-map.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ kannaka-memory (Rust)
│ KANNAKA.dreams (memories_strengthened, …, hallucinations)
│ QUEEN.phase.<agent> (per-agent phase signals)
│ queen.event.dream.{start,end}
│ KANNAKA.exemplars
│ KANNAKA.exemplar.<agent_id>.<cluster_id>
│
├── kannaka-radio (Node) — server/nats-client.js
│ subscribes, accumulates into swarmState.{queen, consciousness}
Expand Down Expand Up @@ -120,7 +120,7 @@ kannaka-memory (Rust)
│ swarm.ninja-portal.com:4222 │
│ subjects: KANNAKA.consciousness, │
│ KANNAKA.dreams, │
│ KANNAKA.exemplars, │
│ KANNAKA.exemplar.*, │
│ QUEEN.phase.*, │
│ queen.event.dream.* │
└─┬─────────────┬──────────────┬───────────┘
Expand Down
24 changes: 18 additions & 6 deletions docs/nats-contract.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -213,19 +213,24 @@ KANNAKA.reactions:
score audience engagement, drive resonance loops, or feed
audience-aware narration prompts.
required:
- schema_version
- ts
- agent_id
- emoji
- kind # "human" | "agent"
optional:
- track # the track's title at moment of reaction
- from_id # anonymous client id
- from_id # anonymous per-session client id for crowd dots —
# NOT the stable publisher identity (that is agent_id)
field_types:
emoji: string
kind: string # enum: human|agent
track: string
example: |
{
"schema_version": "1.0",
"ts": 1777730400000,
"agent_id": "kannaka-prime",
"emoji": "🪶",
"kind": "human",
"track": "Mountain Top",
Expand All @@ -247,17 +252,24 @@ KANNAKA.reactions:
"queen.event.dream.end":
description: |
Fired when a dream cycle completes. Carries the SAME shape
as KANNAKA.dreams (it's the same report), but the lighter
consumers can subscribe to event-end without parsing the
full report.
as KANNAKA.dreams (it's the same report) — identical required
and optional fields, plus an extra optional `duration_ms`.
Lighter consumers can subscribe to event-end and read only the
fields they need without parsing the full report.
required:
- schema_version
- ts
- agent_id
- memories_strengthened
- memories_faded
- cycles
optional:
- memories_strengthened
- memories_pruned # alias: memories_faded
- memories_faded # deprecated alias of memories_pruned
- hallucinations_created
- new_connections
- consciousness_before
- consciousness_after
- emerged
- duration_ms

# ============================================================
Expand Down
49 changes: 48 additions & 1 deletion src/bridge.rs
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,21 @@ impl CouplingBridge {
self.k_effective = match self.mode {
CouplingMode::Static => self.config.k_base,
CouplingMode::MarketMediated => {
(self.config.k_base * signal).clamp(self.config.k_min, self.config.k_max)
// `f32::clamp` propagates NaN rather than bounding it, so a
// non-finite signal used to return NaN coupling for the
// affected tick (#41). The mean_signal() half of that issue
// was already fixed above by not storing the sample; this
// covers the coupling half. A garbage sample means "no new
// information", so hold the last good coupling.
if signal.is_finite() {
(self.config.k_base * signal).clamp(self.config.k_min, self.config.k_max)
} else if self.k_effective.is_finite() {
self.k_effective
} else {
self.config
.k_base
.clamp(self.config.k_min, self.config.k_max)
}
}
CouplingMode::Adaptive => {
// Reject a non-finite coherence sample (#19). Without this, a
Expand Down Expand Up @@ -411,6 +425,39 @@ mod tests {
);
}

#[test]
fn market_mediated_holds_coupling_on_non_finite_signal() {
// Regression for #41 — f32::clamp propagates NaN instead of
// bounding it, so `k_base * NaN` came straight back out of
// update() as NaN coupling for the affected tick.
let mut bridge = CouplingBridge::new(
BridgeConfig {
k_base: 1.0,
k_min: 0.1,
k_max: 5.0,
..Default::default()
},
CouplingMode::MarketMediated,
);
let good = bridge.update(2.0, 0.5);
assert!((good - 2.0).abs() < 1e-5);
for bad in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
let k = bridge.update(bad, 0.5);
assert!(
k.is_finite(),
"signal {bad} must not yield NaN coupling: {k}"
);
assert!(
(k - good).abs() < 1e-6,
"coupling should hold at the last good value on signal {bad}: {good} → {k}"
);
}
// A later valid sample still steers coupling normally.
let recovered = bridge.update(3.0, 0.5);
assert!((recovered - 3.0).abs() < 1e-5);
assert!(bridge.mean_signal().is_finite());
}

#[test]
fn signal_history_is_bounded() {
// Regression for #14 — history grew forever, leaking memory and
Expand Down
23 changes: 22 additions & 1 deletion src/iit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -152,13 +152,17 @@ pub struct PhiNode {
pub fn compute_phi(nodes: &[PhiNode]) -> PhiReport {
let n = nodes.len() as f32;
if n < 2.0 {
// Φ is genuinely zero below two nodes, but the structural metadata
// must still describe the input: a one-node graph has exactly one
// partition, and reporting 0 made it indistinguishable from an
// empty graph for anything inspecting `num_partitions` (#53).
return PhiReport {
phi: 0.0,
integration: 0.0,
differentiation: 0.0,
density_factor: 0.0,
scale_factor: 0.0,
num_partitions: 0,
num_partitions: nodes.len().min(1),
num_connections: 0,
};
}
Expand Down Expand Up @@ -294,6 +298,23 @@ mod tests {
assert_eq!(report.phi, 0.0);
}

#[test]
fn single_node_reports_one_partition() {
// Regression for #53 — Φ is legitimately 0 below two nodes, but
// reporting num_partitions = 0 made a one-node graph structurally
// indistinguishable from an empty one.
let one = compute_phi(&[PhiNode {
partition: 7,
connections: vec![],
}]);
assert_eq!(one.phi, 0.0, "Φ stays zero for a single node");
assert_eq!(one.num_partitions, 1, "one node is one partition");
assert_eq!(one.num_connections, 0);

let empty = compute_phi(&[]);
assert_eq!(empty.num_partitions, 0, "an empty graph has no partitions");
}

#[test]
fn phi_connected_cross_partition_higher() {
// Two nodes, same partition, connected
Expand Down
Loading
Loading