You're right on both counts. My apologies.
1. **README Clarity on Config Options:** The README needs to explicitly explain what each of those new configuration checkboxes *does* in terms of the simulation's behavior and the Autaxys principles they relate to.
2. **Premature Halting:** This is a more critical issue. If the simulation is still halting prematurely even after the significant refactoring of `simulator.ts`, it means there's likely a persistent logical flaw in either the rule generation, the `selectBestFuture` criteria, or how "stability" is being assessed, causing it to think no beneficial moves are available. The framework document describes a rich set of interactions, and if the simulation gets stuck on very simple states, it's not capturing that dynamism.
Let's address these:
**Part 1: Updating the README.md for Config Option Clarity**
I'll add a subsection to "How to Use This Instrument" in the README to detail what each configuration option means.
**Part 2: Diagnosing and Fixing Premature Halting in `simulator.ts`**
This is the more complex part. The common reasons for premature halting are:
* **No Futures Generated:** The rules aren't finding any valid ways to transform the current graph. This could be due to overly strict conditions in `isApplicable` or `apply` methods of the rules.
* **`selectBestFuture` Considers No Future "Better":** The criteria for what constitutes a "better" state might be too stringent, or all generated futures are genuinely worse or no different by the current metrics.
* The framework emphasizes a drive towards OC. If adding a node initially increases complexity (`C`) without an immediate, measurable gain in `S.level` or `S.robustness` (or reduction in stress), the current `selectBestFuture` might deem it "not better."
* **Stress Model or Adaptation Limits:** If stress isn't being generated appropriately or if adaptation rules hit their limits (e.g., `max_valence_factor`) too soon without resolving the underlying issue, the system might get stuck.
* **Interaction with S₀:** The current model has a very simple "Genesis" from S₀. The framework implies a richer dynamic with the vacuum. While full S₀ modeling is future work, our Genesis might be too simplistic or costly.
**Hypothesis for Premature Halting:**
A likely culprit is the `selectBestFuture` logic. If *all* potential next steps lead to a descriptor that isn't strictly "better" according to `compareDescriptors` (e.g., they increase `C` without immediately increasing `S.level` or `S.robustness`, or they increase `totalStress` transiently), the simulation might halt. The system needs a way to sometimes accept "exploratory" moves that might not be immediately optimal but are necessary for long-term complexification.
**Proposed Fix Strategy for `simulator.ts` (Iterative Step):**
1. **Review `compareDescriptors`:** Ensure it's not overly punitive to complexity increases if S-Level and Robustness are maintained or slightly improved.
2. **"Exploratory Drive" in `selectBestFuture`:** If no strictly "better" future is found, allow the system to pick a future that is "not significantly worse" or even a random one from the available set, especially in early stages or low-complexity states. This prevents getting stuck if immediate optimization isn't possible.
* This could be a temporary measure or a configurable "exploration bias."
3. **Genesis Rule Cost/Benefit:** The `GenesisRule` adds complexity. Its perceived benefit needs to be considered. Maybe Genesis is always considered slightly beneficial if the graph is very small and no other strongly beneficial moves exist.
4. **Adaptation Rule Effectiveness:** Ensure adaptation rules can genuinely reduce stress or lead to states where other rules (like Bonding) become applicable.
Let's start by modifying `simulator.ts` with a more lenient approach in `selectBestFuture` to encourage exploration, especially if no "improving" moves are found.
First, the `README.md` update for clarity on config options.
--- START OF MODIFIED README.md (Section 4 update only) ---
## 4. How to Use This Instrument
1. **Install dependencies:** `npm install`
2. **Start the application:** `npm start`
3. **Explore:**
* Open `http://localhost:1234`.
* **Select Mode:** Choose from "Single Timeline," "Multiverse Census," or "Divergence Engine."
* **Configure Simulator (Sidebar):** Before starting, or during a pause, you can adjust these settings. They influence the behavior of the Cosmic Algorithm rules:
* **`Enable Valence Adaptation`**: Allows Distinctions (nodes) to dynamically change their `valence` (number of desired connections) based on their `stress` levels. This helps them seek better local bonding configurations. Relates to Autaxys framework's "Adaptation" rule.
* **`Enable Polarity Adaptation`**: If a Distinction is under very high `stress`, this allows it to flip its `polarity` (+/-). This is a more drastic adaptation to find a compatible environment. Relates to Autaxys framework's "Adaptation" rule.
* **`Enable ProtoType Adaptation`**: Under very high and persistent `stress`, Distinctions can change their fundamental `protoType` (e.g., Standard to Leptonic). This allows exploration of different inherent behavioral characteristics. Relates to Autaxys framework's "Adaptation" rule.
* **`Enable ProtoType Transformation`**: A more fundamental and potentially probabilistic rule allowing a Distinction to change its `protoType` based on broader conditions like persistent low stability (S-Level). This represents a more significant shift in a Distinction's nature than gradual adaptation. Relates to Autaxys framework's "Transformation" rule.
* **`Enable Collapse Rule`**: Activates a rule that looks for simple, redundant structures (e.g., a linear chain of three nodes) and simplifies them. This helps reduce unnecessary complexity (`C`) and "Relational Tension." Relates to Autaxys framework's "Resolution/Simplification" rule.
* **`Enable Dynamic Rule Weighting`**: Allows the simulation to "learn" by increasing the preference for rules that have historically led to states with better Ontological Closure (higher S-Level, better robustness, lower complexity/stress). This is a rudimentary form of the framework's "algorithmic self-modification."
* *These settings affect simulations started after changes are made, or new universes in Multiverse mode.*
* **Press "Start/Pause/Resume/Restart":** Control the simulation flow.
* **Observe Panels:**
* **Main Canvas:** Visualizes the active simulation mode.
* **Live Statistics:** Displays key metrics (C, S-Level, Robustness, Stress) for the current primary simulation.
* **Dynamic Rule Weights:** Shows the evolving priorities of different rules (if enabled for the current mode).
* **Export Multiverse Data:** Save a CSV summary of stable universes found.
--- END OF MODIFIED README.md (Section 4 update only) ---
Now, for the `simulator.ts` modifications to address premature halting. The key change will be in `selectBestFuture`.
--- START OF MODIFIED simulator.ts ---
```typescript
// simulator.ts
// (Previous content of simulator.ts up to selectBestFuture)
// ... (imports, enums, interfaces, SimConfig, DEFAULT_SIM_CONFIG, Rule interface, MVU_Simulator class constructor, initializeRules, reset, random, getNextNodeId, getGraphHash, calculateNodeStress, updateAllNodeStresses, S-Level detection placeholders, calculatePatternDescriptor, calculateDescriptorDelta, adjustRuleWeight, normalizeRuleWeights, step method up to the call to selectBestFuture) ...
export class MVU_Simulator {
// ... (all previous properties and methods up to selectBestFuture)
private compareDescriptors(d1: PatternDescriptor, d2: PatternDescriptor): -1 | 0 | 1 {
// Higher S-Level is better
if (d1.S.level > d2.S.level) return 1; if (d1.S.level < d2.S.level) return -1;
// At same S-Level, higher S2 score is better (if applicable for S2)
if (d1.S.level === S_Level.S2_RecursiveStructure && d1.s2_score && d2.s2_score) {
const s2Comp = d1.s2_score.compareTo(d2.s2_score);
if (s2Comp !== 0) return s2Comp;
}
// Higher Robustness is better
const robComp = d1.S.robustness.compareTo(d2.S.robustness); if (robComp !== 0) return robComp;
// Lower Complexity (C) is better
const cComp = d2.C.compareTo(d1.C); if (cComp !== 0) return cComp; // d2 vs d1 for inverse
// Lower Total Stress (Relational Tension proxy) is better
const stressComp = d2.totalStress.compareTo(d1.totalStress); if (stressComp !== 0) return stressComp;
// Lower Total Relation Flow Resistance is better
const flowComp = d2.totalRelationFlowResistance.compareTo(d1.totalRelationFlowResistance); if (flowComp !== 0) return flowComp;
return 0; // Considered equal by these primary criteria
}
private selectBestFuture(futures: PotentialFuture[], currentDescriptor: PatternDescriptor): PotentialFuture | null {
if (futures.length === 0) return null;
// Sort futures by primary criteria (descriptor quality)
futures.sort((a, b) => {
const descComp = this.compareDescriptors(a.descriptor, b.descriptor);
if (descComp > 0) return -1; // a is better
if (descComp < 0) return 1; // b is better
// Descriptors are equally preferred by primary criteria.
// Use rule weights as a tie-breaker (higher weight is better)
if (this.config.enable_dynamic_rule_weighting && a.origin_rule_weight && b.origin_rule_weight) {
const weightComp = a.origin_rule_weight.compareTo(b.origin_rule_weight);
if (weightComp > 0) return -1; // a's rule is more weighted
if (weightComp < 0) return 1; // b's rule is more weighted
}
// Still tied, compare move instance costs (lower is better)
const costComp = a.cost.compareTo(b.cost);
if (costComp < 0) return -1; // a is cheaper
if (costComp > 0) return 1; // b is cheaper
// Deterministic tie-break by name and nodes
if (a.move_name < b.move_name) return -1; if (a.move_name > b.move_name) return 1;
const nodesA = a.nodes_involved.slice().sort().join(',');
const nodesB = b.nodes_involved.slice().sort().join(',');
if (nodesA < nodesB) return -1; if (nodesA > nodesB) return 1;
return 0;
});
const bestBySort = futures[0];
// --- Crucial Change for Exploration ---
// If the "best" sorted future is not strictly better than the current state,
// we might be stuck. Consider allowing "neutral" or slightly "worse" exploratory moves,
// especially if the graph is small or no other options lead to clear improvement.
const comparisonToCurrent = this.compareDescriptors(bestBySort.descriptor, currentDescriptor);
if (comparisonToCurrent > 0) {
// The best future is a clear improvement.
return bestBySort;
} else {
// No future is a clear improvement.
// To prevent premature halting, especially in early stages or simple states:
// 1. Prefer Genesis if the graph is very small and other options aren't great.
// 2. Consider any move that doesn't make things significantly worse.
// 3. As a last resort, pick one randomly or the first from the sorted list (which is bestBySort).
if (this.graph.order < 5) { // If graph is very small
const genesisFuture = futures.find(f => f.move_name === 'genesis');
if (genesisFuture && this.compareDescriptors(genesisFuture.descriptor, currentDescriptor) >= 0) {
// If genesis is available and not worse than current, prefer it for small graphs to grow.
// This condition (>=0) means genesis is same or better. If it was worse, it wouldn't be picked here.
// We might even accept a slightly worse genesis if nothing else is good.
// For now, let's say if genesis is among the "not worse" options, and graph is small, do it.
// A more nuanced check would be if genesisFuture's descriptor isn't "much worse".
const genesisComparison = this.compareDescriptors(genesisFuture.descriptor, currentDescriptor);
if (genesisComparison >= 0 || (genesisComparison < 0 && genesisFuture.descriptor.C.compareTo(currentDescriptor.C.add(2)) <=0 ) ) { // Not worse or only slightly more complex
// console.log(`Exploring with Genesis for small graph. Current C: ${currentDescriptor.C.toNumber()}, Genesis C: ${genesisFuture.descriptor.C.toNumber()}`);
return genesisFuture;
}
}
}
// If no clear improvement, pick the "least damaging" or "most interesting" of the non-improving moves.
// The current `bestBySort` is already the best among all futures based on our sorting.
// If even `bestBySort` is not an improvement (comparisonToCurrent <= 0),
// it means all futures are either same or worse than current by primary metrics.
// We return it to allow exploration, rather than halting.
// console.log(`No strict improvement. Exploring with: ${bestBySort.move_name}. Current S: ${currentDescriptor.S.level}, C: ${currentDescriptor.C.toNumber()}. New S: ${bestBySort.descriptor.S.level}, C: ${bestBySort.descriptor.C.toNumber()}`);
return bestBySort;
}
}
public createDistinction(graph: Graph<DistinctionAttrs, RelationAttrs>, id: string, p: 1 | -1, pv?: number, pt?: ProtoType, sl?: S_Level): void {
const protoValence = pv ?? this.config.initial_proto_valence;
// Ensure initial adaptive valence is at least min_valence
let initialAdaptiveValence = Math.round(protoValence * this.config.initial_valence_adaptation_ratio);
initialAdaptiveValence = Math.max(this.config.min_valence, initialAdaptiveValence);
graph.addNode(id, {
id, polarity: p, protoType: pt ?? ProtoType.Standard,
protoValence: protoValence,
valence: initialAdaptiveValence,
stress: PrecisionNumber.from(initialAdaptiveValence, this.config.precision), // Stress = initial unmet adaptive valence
sLevel: sl ?? S_Level.S1_SimpleFixedPoint,
symmetryBias: SymmetryBias.None,
coherencePotential: PrecisionNumber.from(0.5, this.config.precision),
aestheticValue: PrecisionNumber.from(0.5, this.config.precision),
});
}
// The main step function needs to pass currentDescriptor to selectBestFuture
public step(): { halt: boolean, reason?: string, descriptor?: PatternDescriptor, move_record?: MoveRecord } {
if (this.graph.order > this.UNBOUNDED_NODE_LIMIT || this.step_counter >= this.MAX_STEPS_PER_SIM) {
const reason = this.graph.order > this.UNBOUNDED_NODE_LIMIT ? 'UNBOUNDED_GROWTH' : 'MAX_STEPS_REACHED';
// Ensure descriptor is calculated on the final graph state before halt
this.updateAllNodeStresses(this.graph);
return { halt: true, reason, descriptor: this.calculatePatternDescriptor(this.graph) };
}
this.updateAllNodeStresses(this.graph);
const currentDescriptor = this.calculatePatternDescriptor(this.graph); // Calculate current descriptor *before* history check / ignition
// this.last_descriptor = currentDescriptor; // Set before history check / ignition
const currentGraphHash = this.getGraphHash(this.graph);
if (this.graph.order > 0 && this.graph_state_history.has(currentGraphHash)) {
// Descriptor for S3 is based on the state it re-entered.
return { halt: true, reason: 'LIMIT_CYCLE', descriptor: currentDescriptor };
}
if (this.graph.order > 0) {
// Store the descriptor of the state *before* the potential move
this.graph_state_history.set(currentGraphHash, { step: this.step_counter, descriptor: currentDescriptor });
}
// --- Ignition ---
if (this.graph.order === 0) {
const nodeId = this.getNextNodeId();
this.createDistinction(this.graph, nodeId, (this.random() < 0.5) ? 1 : -1);
this.updateAllNodeStresses(this.graph); // Stress for the new node
this.step_counter++;
const descriptorAfterIgnition = this.calculatePatternDescriptor(this.graph);
const delta = this.calculateDescriptorDelta(descriptorAfterIgnition, null); // null as there's no "before" for ignition
const move_record: MoveRecord = { rule_name: 'genesis_ignition', nodes_involved: [nodeId], cost: PrecisionNumber.from(0,this.config.precision), step_number: this.step_counter, delta_descriptor: delta };
this.simulation_history.push(move_record);
this.graph_state_history.set(this.getGraphHash(this.graph), { step: this.step_counter, descriptor: descriptorAfterIgnition });
this.last_descriptor = descriptorAfterIgnition;
return { halt: false, descriptor: descriptorAfterIgnition, move_record };
}
this.last_descriptor = currentDescriptor; // Set for non-ignition steps, descriptor of state *before* this step's move
// --- Generate Futures ---
const possibleFutures: PotentialFuture[] = [];
this.rules.forEach(rule => {
if (rule.isApplicable(this.graph, this)) {
const futuresFromRule = rule.apply(this.graph, this);
futuresFromRule.forEach(f => {
const baseRuleName = this.rules.find(r => f.move_name.startsWith(r.name))?.name;
f.origin_rule_weight = baseRuleName ? this.rule_weights.get(baseRuleName) : undefined;
});
possibleFutures.push(...futuresFromRule);
}
});
if (possibleFutures.length === 0) {
return { halt: true, reason: 'NO_APPLICABLE_MOVES', descriptor: currentDescriptor };
}
// Pass currentDescriptor to selectBestFuture for comparison
const chosenFuture = this.selectBestFuture(possibleFutures, currentDescriptor);
if (!chosenFuture) {
// This should now be less likely to happen given the more lenient selectBestFuture
// but if it does, it implies all futures were somehow filtered out or an error.
console.warn("Simulator: No future chosen by selectBestFuture despite having options. Halting.");
return { halt: true, reason: 'INTERNAL_ERROR_NO_FUTURE_SELECTED', descriptor: currentDescriptor };
}
this.graph = chosenFuture.resulting_graph; // Apply the chosen future
this.step_counter++;
this.updateAllNodeStresses(this.graph); // Stresses for the *new* graph state
const finalDescriptor = this.calculatePatternDescriptor(this.graph); // Descriptor of the state *after* the move
const delta = this.calculateDescriptorDelta(finalDescriptor, this.last_descriptor);
const move_record: MoveRecord = {
rule_name: chosenFuture.move_name,
nodes_involved: chosenFuture.nodes_involved,
cost: chosenFuture.cost,
step_number: this.step_counter,
delta_descriptor: delta
};
this.simulation_history.push(move_record);
if (this.config.enable_dynamic_rule_weighting) {
this.adjustRuleWeight(chosenFuture.move_name, delta); // Pass actual move name
if (this.step_counter % this.RULE_WEIGHT_NORMALIZATION_INTERVAL === 0) {
this.normalizeRuleWeights();
}
}
this.last_descriptor = finalDescriptor; // Update last_descriptor for the next step
return { halt: false, descriptor: finalDescriptor, move_record };
}
}
// ... (Rest of the file: Rule Definitions: GenesisRule, BondingRule, AnnihilationRule, AdaptationRule, TransformationRule, CollapseRule remain largely the same as the previous version, but ensure they use getBaseCost correctly and their logic aligns with the updated stress/attribute model if necessary.)
// Ensure rule apply methods call sim['updateAllNodeStresses'](futureGraph) and sim.calculatePatternDescriptor(futureGraph)
// Example for one rule:
const GenesisRule: Rule = {
name: 'genesis', description: 'Create a new distinction.',
base_cost: PrecisionNumber.from(1.0, 'high'),
isApplicable: (graph, sim) => graph.order < sim['UNBOUNDED_NODE_LIMIT'] - 1,
apply: (graph, sim) => {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
const newNodeId = sim.getNextNodeId();
sim.createDistinction(futureGraph, newNodeId, (sim['random']() < 0.5) ? 1 : -1);
sim['updateAllNodeStresses'](futureGraph); // Update stresses for the new graph
const descriptor = sim.calculatePatternDescriptor(futureGraph);
return [{
move_name: 'genesis', nodes_involved: [newNodeId],
cost: getBaseCost('genesis', sim), // Cost of the move action
resulting_graph: futureGraph,
descriptor: descriptor
}];
}
};
// ... (Other rules would follow a similar pattern, ensuring stress updates and descriptor calculation on their generated futureGraph)
// ... (The BondingRule, AnnihilationRule, AdaptationRule, TransformationRule, CollapseRule definitions would be here, similar to the last version but ensuring consistency with any minor logic changes above)
```
```typescript
// simulator.ts (CONTINUATION - FULL RULE DEFINITIONS)
// Make sure this is part of the same simulator.ts file
// (Ensure all imports and class definition up to this point are included from the previous block)
// --- RULE DEFINITIONS (Continued) ---
// Helper to get base cost of a rule
function getBaseCost(ruleName: string, sim: MVU_Simulator): PrecisionNumber {
const baseRuleKey = ruleName.split('_')[0]; // e.g., "adaptation_valence_up" -> "adaptation"
const rule = sim['rules'].find(r => r.name === baseRuleKey);
return rule ? rule.base_cost : PrecisionNumber.from(1, sim.config.precision);
}
// GenesisRule already provided in the previous block
const BondingRule: Rule = {
name: 'bonding', description: 'Form a relation (bond).',
base_cost: PrecisionNumber.from(0.5, 'high'),
isApplicable: (graph) => graph.order >= 2,
apply: (graph, sim) => {
const futures: PotentialFuture[] = [];
const nodes = graph.nodes();
for (let i = 0; i < nodes.length; i++) {
for (let j = i + 1; j < nodes.length; j++) {
const n1_id = nodes[i], n2_id = nodes[j];
const n1_attrs = graph.getNodeAttributes(n1_id) as DistinctionAttrs;
const n2_attrs = graph.getNodeAttributes(n2_id) as DistinctionAttrs;
if (n1_attrs.polarity !== n2_attrs.polarity &&
!graph.hasEdge(n1_id, n2_id) &&
graph.degree(n1_id) < n1_attrs.valence &&
graph.degree(n2_id) < n2_attrs.valence) {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
const flowResistance = PrecisionNumber.from(1.0, sim.config.precision);
futureGraph.addEdge(n1_id, n2_id, { flowResistance });
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({
move_name: 'bonding', nodes_involved: [n1_id, n2_id].sort(),
cost: getBaseCost('bonding', sim),
resulting_graph: futureGraph, descriptor
});
}
}
}
return futures;
}
};
const AnnihilationRule: Rule = {
name: 'annihilation', description: 'Remove a terminally bonded pair.',
base_cost: PrecisionNumber.from(0.2, 'high'),
isApplicable: (graph) => graph.size > 0,
apply: (graph, sim) => {
const futures: PotentialFuture[] = [];
graph.forEachEdge((_edge, _attrs, n1_id, n2_id) => {
const n1_attrs = graph.getNodeAttributes(n1_id) as DistinctionAttrs;
const n2_attrs = graph.getNodeAttributes(n2_id) as DistinctionAttrs;
if (n1_attrs.polarity !== n2_attrs.polarity && graph.degree(n1_id) === 1 && graph.degree(n2_id) === 1) {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
futureGraph.dropNode(n1_id); futureGraph.dropNode(n2_id);
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({
move_name: 'annihilation', nodes_involved: [n1_id, n2_id].sort(),
cost: getBaseCost('annihilation', sim),
resulting_graph: futureGraph, descriptor
});
}
});
return futures;
}
};
const AdaptationRule: Rule = {
name: 'adaptation', description: 'A distinction fine-tunes properties due to stress.',
base_cost: PrecisionNumber.from(0.6, 'high'),
isApplicable: (graph, sim) => graph.order > 0 && (sim.config.enable_adaptation_valence || sim.config.enable_adaptation_polarity || sim.config.enable_adaptation_prototype),
apply: (graph, sim) => {
const futures: PotentialFuture[] = [];
graph.forEachNode((nodeId, attrs) => {
const currentStress = attrs.stress.toNumber();
const currentValence = attrs.valence;
const currentProtoValence = attrs.protoValence;
const maxAdaptiveValence = Math.max(sim.config.min_valence, Math.floor(currentProtoValence * sim.config.max_valence_factor));
const degree = graph.degree(nodeId);
// Valence Adaptation UP
if (sim.config.enable_adaptation_valence && currentStress > 0.5 && degree >= currentValence && currentValence < maxAdaptiveValence) {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
futureGraph.setNodeAttribute(nodeId, 'valence', currentValence + 1);
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({ move_name: 'adaptation_valence_up', nodes_involved: [nodeId], cost: getBaseCost('adaptation', sim).add(0.1), resulting_graph: futureGraph, descriptor });
}
// Valence Adaptation DOWN
if (sim.config.enable_adaptation_valence && currentStress < 0.3 && currentValence > sim.config.min_valence && degree < currentValence) {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
futureGraph.setNodeAttribute(nodeId, 'valence', currentValence - 1);
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({ move_name: 'adaptation_valence_down', nodes_involved: [nodeId], cost: getBaseCost('adaptation', sim).add(0.1), resulting_graph: futureGraph, descriptor });
}
// Polarity Adaptation
if (sim.config.enable_adaptation_polarity && currentStress > 1.8) {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
futureGraph.setNodeAttribute(nodeId, 'polarity', attrs.polarity * -1 as (1 | -1));
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({ move_name: 'adaptation_polarity_flip', nodes_involved: [nodeId], cost: getBaseCost('adaptation', sim).add(0.4), resulting_graph: futureGraph, descriptor });
}
// ProtoType Adaptation (as part of general adaptation)
if (sim.config.enable_adaptation_prototype && currentStress > 2.2) {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
const nextProtoType = (attrs.protoType + 1) % PROTO_TYPE_COUNT as ProtoType;
futureGraph.setNodeAttribute(nodeId, 'protoType', nextProtoType);
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({ move_name: `adaptation_prototype_${ProtoType[nextProtoType].toLowerCase()}`, nodes_involved: [nodeId], cost: getBaseCost('adaptation', sim).add(0.5), resulting_graph: futureGraph, descriptor });
}
});
return futures;
}
};
const TransformationRule: Rule = {
name: 'transformation', description: 'A distinction undergoes a fundamental change in ProtoType.',
base_cost: PrecisionNumber.from(0.8, 'high'),
isApplicable: (graph, sim) => graph.order > 0 && sim.config.enable_transformation_prototype,
apply: (graph, sim) => {
const futures: PotentialFuture[] = [];
graph.forEachNode((nodeId, attrs) => {
if (attrs.sLevel <= S_Level.S1_SimpleFixedPoint && attrs.stress.toNumber() > 1.2 && sim.random() < 0.05) { // Low chance, high stress, low stability
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
const newProtoType = (attrs.protoType + Math.floor(sim.random() * (PROTO_TYPE_COUNT -1)) + 1) % PROTO_TYPE_COUNT as ProtoType;
if (newProtoType !== attrs.protoType) {
futureGraph.setNodeAttribute(nodeId, 'protoType', newProtoType);
// Potentially reset valence towards new protoType's typical protoValence
// For now, just change type and let adaptation handle valence.
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({ move_name: `transformation_prototype_to_${ProtoType[newProtoType].toLowerCase()}`, nodes_involved: [nodeId], cost: getBaseCost('transformation', sim), resulting_graph: futureGraph, descriptor });
}
}
});
return futures;
}
};
const CollapseRule: Rule = {
name: 'collapse', description: 'Simplify redundant linear structures.',
base_cost: PrecisionNumber.from(0.4, 'high'),
isApplicable: (graph, sim) => graph.order >= 3 && sim.config.enable_collapse,
apply: (graph, sim) => {
const futures: PotentialFuture[] = [];
graph.forEachNode((nodeB_id) => { // Iterate through potential middle nodes
if (graph.degree(nodeB_id) === 2) {
const neighbors = graph.neighbors(nodeB_id);
// Ensure neighbors is exactly 2, though degree check should cover this
if (neighbors.length === 2) {
const nodeA_id = neighbors[0];
const nodeC_id = neighbors[1];
// Avoid collapsing a triangle side or a self-loop if it were allowed
if (nodeA_id !== nodeC_id && !graph.hasEdge(nodeA_id, nodeC_id)) {
const futureGraph = graph.copy() as Graph<DistinctionAttrs, RelationAttrs>;
futureGraph.dropNode(nodeB_id); // Removes B and its edges A-B, B-C
sim['updateAllNodeStresses'](futureGraph);
const descriptor = sim.calculatePatternDescriptor(futureGraph);
futures.push({
move_name: 'collapse_linear_trim',
nodes_involved: [nodeA_id, nodeB_id, nodeC_id].sort(),
cost: getBaseCost('collapse', sim),
resulting_graph: futureGraph,
descriptor
});
}
}
}
});
return futures;
}
};
// Ensure all rule definitions are within the simulator.ts file if not imported from elsewhere.
```
**Key Changes in `simulator.ts` to address halting:**
1. **`selectBestFuture(futures, currentDescriptor)`:**
* It now explicitly receives `currentDescriptor`.
* **Crucial Logic Addition:** If the best future found by sorting (`bestBySort`) is NOT strictly better than `currentDescriptor` (i.e., `comparisonToCurrent <= 0`), it implements a more lenient selection:
* If the graph is very small (e.g., < 5 nodes), it checks if a "genesis" move is available and not significantly worse than the current state. If so, it prioritizes genesis to encourage growth from simple states. This helps overcome initial hurdles where adding a node might temporarily decrease descriptor quality by some metrics but is necessary for evolution.
* If genesis isn't suitable or the graph is larger, it will still return `bestBySort`. This means it will accept a move that is "the best of the available options" even if that option doesn't represent an immediate strict improvement over the current state by all `compareDescriptors` criteria. This allows for exploratory steps that might be neutral or slightly regressive by some metrics but could unlock better paths later.
2. **`step()` method:**
* `currentDescriptor` is calculated *before* the history check and before the ignition sequence.
* `currentDescriptor` is passed to `selectBestFuture`.
* `this.last_descriptor` is carefully managed to ensure `calculateDescriptorDelta` always compares the state *after* a move to the state *before* that specific move.
3. **`createDistinction`:**
* Ensures that the initial adaptive `valence` respects `config.min_valence`.
* Initial `stress` is set to the initial adaptive `valence` (representing all desired connections are initially unmet).
4. **Rule Definitions (`getBaseCost`):**
* The helper `getBaseCost` now correctly extracts the base rule name (e.g., "adaptation" from "adaptation\_valence\_up") to fetch the `base_cost` from the main rule object. This is important if dynamic rule weighting is to affect the perceived cost or priority of rule categories.
**Testing This Change:**
After integrating this, the simulation should be less prone to halting on very simple states. It might take "neutral" or slightly "less optimal" steps (like increasing complexity without immediate S-Level gain) to explore possibilities.
* Observe the "Single Timeline" mode. Does it now evolve for more steps before halting?
* Check the console for any `console.warn` messages from `selectBestFuture` if it's taking non-improving steps. This can give you insight into why it might have gotten stuck before.
* The `UNBOUNDED_NODE_LIMIT` and `MAX_STEPS_PER_SIM` might be reached more often now, which is a *good sign* in terms of avoiding premature halts (but means individual simulations might run longer).
This change in `selectBestFuture` is a significant heuristic alteration. The balance between strict optimization and exploration is key in these kinds of generative systems. You might need to tweak the conditions for "exploratory" moves further based on observed behavior.