""" Generic entropy metrics for WorldSmithAI. This module computes Shannon entropy over arbitrary world object attributes. It deliberately avoids hardcoded species, ecosystem, market, civilization, or research-domain assumptions. A category can be any DSL-defined value reachable by path: - type - state.current_goal - memory.active_options.strategy.option_id - metadata.faction - state.status - memory.latest_behavior - amount bucket labels supplied through custom counts Example: metric = EntropyMetric(collection="agents", value_path="type") result = metric.compute(world) print(result.entropy) print(result.normalized_entropy) Weighted resource entropy: metric = EntropyMetric( collection="resources", value_path="type", weight_path="amount", ) result = metric(world) Future extensibility: - Add conditional entropy between two paths. - Add mutual information between agent attributes. - Add rolling entropy windows. - Add spatial entropy over grid cells or regions. - Add behavior entropy from action histories. - Feed normalized entropy into interestingness and God-Agent metrics. """ from __future__ import annotations import copy import logging import math from collections.abc import Iterable, Mapping, MutableSequence, Sequence from dataclasses import dataclass, field from enum import Enum from numbers import Real from types import MappingProxyType from typing import TYPE_CHECKING, Any, ClassVar import numpy as np if TYPE_CHECKING: from core.world import World logger = logging.getLogger(__name__) _MISSING = object() _EPSILON = 1.0e-12 class EntropyCollection(str, Enum): """Collections over which entropy may be computed.""" AGENTS = "agents" RESOURCES = "resources" BOTH = "both" class EntropyLogBase(str, Enum): """Common logarithm bases for entropy.""" NATURAL = "e" BASE_2 = "2" BASE_10 = "10" @dataclass(frozen=True) class EntropyResult: """Result produced by entropy metric computation. Attributes: metric_name: Stable metric name. collection: World collection analyzed. value_path: Path used to extract category values. weight_path: Optional path used to weight observations. base: Logarithm base used for entropy. item_count: Number of included objects. total_weight: Sum of non-negative observation weights. outcome_count: Number of distinct outcomes. counts: Outcome weights keyed by outcome label. probabilities: Outcome probabilities keyed by outcome label. entropy: Shannon entropy. max_entropy: Maximum entropy possible with this number of outcomes. normalized_entropy: Entropy divided by maximum entropy. effective_number: Perplexity, equal to base ** entropy. redundancy: One minus normalized entropy. dominant_outcome: Most probable outcome label. dominance: Probability of the dominant outcome. step: Optional world step. metadata: Additional JSON-compatible details. """ metric_name: str collection: str value_path: str weight_path: str | None base: str item_count: int total_weight: float outcome_count: int counts: Mapping[str, float] = field(default_factory=dict) probabilities: Mapping[str, float] = field(default_factory=dict) entropy: float = 0.0 max_entropy: float = 0.0 normalized_entropy: float = 0.0 effective_number: float = 0.0 redundancy: float = 0.0 dominant_outcome: str | None = None dominance: float = 0.0 step: int | None = None metadata: Mapping[str, Any] = field(default_factory=dict) @property def is_empty(self) -> bool: """Return whether no observations contributed to entropy.""" return self.item_count == 0 or self.total_weight <= 0.0 or self.outcome_count == 0 @property def is_degenerate(self) -> bool: """Return whether all probability mass belongs to one outcome.""" return self.outcome_count <= 1 or self.entropy <= _EPSILON def to_dict(self) -> dict[str, Any]: """Return a JSON-friendly representation of the entropy result.""" return { "metric_name": self.metric_name, "collection": self.collection, "value_path": self.value_path, "weight_path": self.weight_path, "base": self.base, "item_count": self.item_count, "total_weight": self.total_weight, "outcome_count": self.outcome_count, "counts": copy.deepcopy(dict(self.counts)), "probabilities": copy.deepcopy(dict(self.probabilities)), "entropy": self.entropy, "max_entropy": self.max_entropy, "normalized_entropy": self.normalized_entropy, "effective_number": self.effective_number, "redundancy": self.redundancy, "dominant_outcome": self.dominant_outcome, "dominance": self.dominance, "is_empty": self.is_empty, "is_degenerate": self.is_degenerate, "step": self.step, "metadata": copy.deepcopy(dict(self.metadata)), } @dataclass class EntropyMetric: """Compute Shannon entropy over a generic world collection. By default, this computes entropy over alive agents grouped by ``type``. The same class can compute entropy over resources, goals, statuses, strategies, factions, memory categories, or any DSL-defined path. Examples: Agent type entropy: metric = EntropyMetric() result = metric.compute(world) Resource entropy weighted by amount: metric = EntropyMetric( collection="resources", value_path="type", weight_path="amount", ) result = metric.compute(world) Current-goal entropy: metric = EntropyMetric( collection="agents", value_path="state.current_goal", include_missing=True, ) result = metric.compute(world) """ name: ClassVar[str] = "entropy" collection: EntropyCollection | str = EntropyCollection.AGENTS value_path: str = "type" weight_path: str | None = None base: EntropyLogBase | str | float = EntropyLogBase.NATURAL alive_only: bool = True include_missing: bool = False missing_label: str = "unknown" include_values: tuple[str, ...] = () exclude_values: tuple[str, ...] = () case_sensitive: bool = True strip_labels: bool = True include_collection_prefix: bool = False minimum_weight: float = 0.0 metadata: Mapping[str, Any] = field(default_factory=dict) def compute(self, world: World) -> EntropyResult: """Compute entropy for the configured world collection. Args: world: Runtime world object. Returns: ``EntropyResult`` containing entropy and distribution statistics. """ collection = _normalize_collection(self.collection) items = _iter_world_items(world, collection) counts, included_count, skipped_count = self._counts(items, collection) result = entropy_from_counts( counts, metric_name=self.name, collection=collection.value, value_path=self.value_path, weight_path=self.weight_path, base=self.base, item_count=included_count, step=_world_step(world), metadata={ **copy.deepcopy(dict(self.metadata)), "skipped_count": skipped_count, "alive_only": self.alive_only, "include_missing": self.include_missing, "minimum_weight": self.minimum_weight, }, ) logger.debug( "Computed entropy over %s path %s: entropy=%.6f normalized=%.6f", collection.value, self.value_path, result.entropy, result.normalized_entropy, ) return result def __call__(self, world: World) -> EntropyResult: """Compute entropy, allowing metric instances to be called directly.""" return self.compute(world) def _counts( self, items: Sequence[Any], collection: EntropyCollection, ) -> tuple[dict[str, float], int, int]: """Return outcome weights, included item count, and skipped item count.""" counts: dict[str, float] = {} included_count = 0 skipped_count = 0 include_values = { _normalize_label(value, case_sensitive=self.case_sensitive, strip=True) for value in self.include_values } exclude_values = { _normalize_label(value, case_sensitive=self.case_sensitive, strip=True) for value in self.exclude_values } for item in items: if self.alive_only and _is_agent_like(item) and not _is_alive(item): skipped_count += 1 continue label = self._label_for(item, collection) if label is None: skipped_count += 1 continue normalized_label = _normalize_label( label, case_sensitive=self.case_sensitive, strip=self.strip_labels, ) if include_values and normalized_label not in include_values: skipped_count += 1 continue if exclude_values and normalized_label in exclude_values: skipped_count += 1 continue weight = self._weight_for(item) if weight <= max(0.0, float(self.minimum_weight)): skipped_count += 1 continue counts[normalized_label] = counts.get(normalized_label, 0.0) + weight included_count += 1 return counts, included_count, skipped_count def _label_for(self, item: Any, collection: EntropyCollection) -> str | None: """Return the categorical label for an item.""" raw_label = _read_path(item, self.value_path, _MISSING) if raw_label is _MISSING or raw_label is None or str(raw_label) == "": if not self.include_missing: return None raw_label = self.missing_label label = _stable_label(raw_label) if self.include_collection_prefix and collection is EntropyCollection.BOTH: return f"{_item_collection_name(item)}:{label}" return label def _weight_for(self, item: Any) -> float: """Return the non-negative observation weight for an item.""" if self.weight_path is None: return 1.0 raw_weight = _read_path(item, self.weight_path, 0.0) if not _is_number(raw_weight): return 0.0 return max(0.0, float(raw_weight)) @dataclass class EntropyTracker: """Track entropy results over time. This is a lightweight helper for simulations that want entropy curves before the full metrics subsystem is introduced. """ metric: EntropyMetric = field(default_factory=EntropyMetric) max_history: int = 1000 history: list[EntropyResult] = field(default_factory=list) def update(self, world: World) -> EntropyResult: """Compute entropy for the current world and append it to history.""" result = self.metric.compute(world) _append_bounded(self.history, result, self.max_history) return result def latest(self) -> EntropyResult | None: """Return the latest entropy result, if any.""" if not self.history: return None return self.history[-1] def to_series(self, value_key: str = "entropy") -> list[dict[str, Any]]: """Return a JSON-friendly time series for one result field. Args: value_key: Name of an ``EntropyResult`` attribute to extract. Returns: List of dictionaries with ``index``, ``step``, and ``value``. """ series: list[dict[str, Any]] = [] for index, result in enumerate(self.history): value = getattr(result, value_key, None) if not _is_number(value): continue series.append( { "index": index, "step": result.step, "value": float(value), } ) return series def to_dict(self) -> dict[str, Any]: """Return a JSON-friendly tracker representation.""" return { "metric": { "name": self.metric.name, "collection": _normalize_collection(self.metric.collection).value, "value_path": self.metric.value_path, "weight_path": self.metric.weight_path, "base": _base_label(self.metric.base), }, "max_history": self.max_history, "history": [result.to_dict() for result in self.history], } def compute_entropy( world: World, *, collection: EntropyCollection | str = EntropyCollection.AGENTS, value_path: str = "type", weight_path: str | None = None, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, alive_only: bool = True, include_missing: bool = False, ) -> EntropyResult: """Convenience function for computing generic world entropy.""" metric = EntropyMetric( collection=collection, value_path=value_path, weight_path=weight_path, base=base, alive_only=alive_only, include_missing=include_missing, ) return metric.compute(world) def compute_agent_type_entropy( world: World, *, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, alive_only: bool = True, include_missing: bool = False, ) -> EntropyResult: """Compute entropy over agent ``type`` labels.""" return compute_entropy( world, collection=EntropyCollection.AGENTS, value_path="type", base=base, alive_only=alive_only, include_missing=include_missing, ) def compute_resource_type_entropy( world: World, *, weight_by_amount: bool = False, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, include_missing: bool = False, ) -> EntropyResult: """Compute entropy over resource ``type`` labels.""" return compute_entropy( world, collection=EntropyCollection.RESOURCES, value_path="type", weight_path="amount" if weight_by_amount else None, base=base, alive_only=False, include_missing=include_missing, ) def entropy_from_counts( counts: Mapping[str, float], *, metric_name: str = "entropy", collection: str = "custom", value_path: str = "value", weight_path: str | None = None, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, item_count: int | None = None, step: int | None = None, metadata: Mapping[str, Any] | None = None, ) -> EntropyResult: """Compute entropy statistics from outcome counts or weights. Args: counts: Mapping from outcome label to non-negative count or weight. metric_name: Name stored in the result. collection: Collection label stored in the result. value_path: Value path stored in the result. weight_path: Optional weight path stored in the result. base: Logarithm base. item_count: Optional number of contributing items. step: Optional world step. metadata: Optional result metadata. Returns: ``EntropyResult`` with entropy and distribution statistics. """ cleaned_counts = { str(label): max(0.0, float(weight)) for label, weight in counts.items() if _is_number(weight) and float(weight) > 0.0 } sorted_counts = dict(sorted(cleaned_counts.items(), key=lambda item: item[0])) total_weight = float(sum(sorted_counts.values())) outcome_count = len(sorted_counts) if total_weight <= _EPSILON or outcome_count == 0: return EntropyResult( metric_name=metric_name, collection=collection, value_path=value_path, weight_path=weight_path, base=_base_label(base), item_count=0 if item_count is None else int(item_count), total_weight=0.0, outcome_count=0, counts={}, probabilities={}, step=step, metadata=metadata or {}, ) probabilities = { label: weight / total_weight for label, weight in sorted_counts.items() } entropy_result = entropy_from_probabilities( probabilities, metric_name=metric_name, collection=collection, value_path=value_path, weight_path=weight_path, base=base, counts=sorted_counts, item_count=outcome_count if item_count is None else int(item_count), total_weight=total_weight, step=step, metadata=metadata, ) return entropy_result def entropy_from_probabilities( probabilities: Mapping[str, float], *, metric_name: str = "entropy", collection: str = "custom", value_path: str = "value", weight_path: str | None = None, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, counts: Mapping[str, float] | None = None, item_count: int | None = None, total_weight: float | None = None, step: int | None = None, metadata: Mapping[str, Any] | None = None, ) -> EntropyResult: """Compute entropy statistics from probabilities. Probabilities are normalized defensively, so callers may pass values that sum approximately but not exactly to one. """ cleaned_probabilities = { str(label): max(0.0, float(probability)) for label, probability in probabilities.items() if _is_number(probability) and float(probability) > 0.0 } probability_sum = float(sum(cleaned_probabilities.values())) if probability_sum <= _EPSILON: return EntropyResult( metric_name=metric_name, collection=collection, value_path=value_path, weight_path=weight_path, base=_base_label(base), item_count=0 if item_count is None else int(item_count), total_weight=0.0 if total_weight is None else float(total_weight), outcome_count=0, counts={}, probabilities={}, step=step, metadata=metadata or {}, ) normalized_probabilities = { label: probability / probability_sum for label, probability in sorted(cleaned_probabilities.items(), key=lambda item: item[0]) } probability_values = np.asarray(list(normalized_probabilities.values()), dtype=float) log_base = _base_value(base) entropy = _shannon_entropy_array(probability_values, log_base=log_base) outcome_count = len(normalized_probabilities) max_entropy = _log(outcome_count, log_base) if outcome_count > 1 else 0.0 normalized_entropy = entropy / max_entropy if max_entropy > _EPSILON else 0.0 normalized_entropy = min(max(float(normalized_entropy), 0.0), 1.0) effective_number = float(log_base**entropy) if log_base > 0 and log_base != 1.0 else math.exp(entropy) redundancy = 1.0 - normalized_entropy dominant_outcome, dominance = max( normalized_probabilities.items(), key=lambda item: (item[1], item[0]), ) return EntropyResult( metric_name=metric_name, collection=collection, value_path=value_path, weight_path=weight_path, base=_base_label(base), item_count=outcome_count if item_count is None else int(item_count), total_weight=float(probability_sum if total_weight is None else total_weight), outcome_count=outcome_count, counts={} if counts is None else copy.deepcopy(dict(counts)), probabilities=normalized_probabilities, entropy=float(entropy), max_entropy=float(max_entropy), normalized_entropy=float(normalized_entropy), effective_number=float(effective_number), redundancy=float(redundancy), dominant_outcome=dominant_outcome, dominance=float(dominance), step=step, metadata=metadata or {}, ) def shannon_entropy( probabilities: Sequence[float], *, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, normalize: bool = True, ) -> float: """Compute Shannon entropy from a sequence of probabilities. Args: probabilities: Non-negative probability-like values. base: Logarithm base. Use ``"e"``, ``"2"``, ``"10"``, or a positive numeric base not equal to one. normalize: Whether to normalize values to sum to one. Returns: Shannon entropy. """ array = np.asarray(probabilities, dtype=float).reshape(-1) array = np.nan_to_num(array, nan=0.0, posinf=0.0, neginf=0.0) array = np.clip(array, 0.0, None) if normalize: total = float(np.sum(array)) if total <= _EPSILON: return 0.0 array = array / total return _shannon_entropy_array(array, log_base=_base_value(base)) def normalized_entropy( probabilities: Sequence[float], *, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, ) -> float: """Compute Shannon entropy normalized to the interval [0, 1].""" array = np.asarray(probabilities, dtype=float).reshape(-1) array = np.nan_to_num(array, nan=0.0, posinf=0.0, neginf=0.0) array = np.clip(array, 0.0, None) total = float(np.sum(array)) if total <= _EPSILON: return 0.0 positive_count = int(np.sum(array > 0.0)) if positive_count <= 1: return 0.0 entropy = shannon_entropy(array, base=base, normalize=True) maximum = _log(positive_count, _base_value(base)) if maximum <= _EPSILON: return 0.0 return min(max(float(entropy / maximum), 0.0), 1.0) def effective_number( probabilities: Sequence[float], *, base: EntropyLogBase | str | float = EntropyLogBase.NATURAL, ) -> float: """Return entropy effective number, also known as perplexity.""" entropy = shannon_entropy(probabilities, base=base, normalize=True) base_value = _base_value(base) if base_value > 0 and base_value != 1.0: return float(base_value**entropy) return float(math.exp(entropy)) def _normalize_collection(value: EntropyCollection | str) -> EntropyCollection: """Normalize an entropy collection value.""" if isinstance(value, EntropyCollection): return value return EntropyCollection(str(value)) def _normalize_base(value: EntropyLogBase | str | float) -> EntropyLogBase | float: """Normalize a log-base value.""" if isinstance(value, EntropyLogBase): return value if isinstance(value, str): lowered = value.strip().lower() if lowered in {"e", "natural", "nat", "ln"}: return EntropyLogBase.NATURAL if lowered in {"2", "base2", "bits", "bit"}: return EntropyLogBase.BASE_2 if lowered in {"10", "base10"}: return EntropyLogBase.BASE_10 numeric_base = float(lowered) else: numeric_base = float(value) if numeric_base <= 0.0 or math.isclose(numeric_base, 1.0): raise ValueError("Entropy log base must be positive and not equal to 1") return numeric_base def _base_value(value: EntropyLogBase | str | float) -> float: """Return numeric logarithm base.""" normalized = _normalize_base(value) if normalized is EntropyLogBase.NATURAL: return math.e if normalized is EntropyLogBase.BASE_2: return 2.0 if normalized is EntropyLogBase.BASE_10: return 10.0 return float(normalized) def _base_label(value: EntropyLogBase | str | float) -> str: """Return a stable label for a logarithm base.""" normalized = _normalize_base(value) if isinstance(normalized, EntropyLogBase): return normalized.value numeric = float(normalized) if numeric.is_integer(): return str(int(numeric)) return str(numeric) def _log(value: float, log_base: float) -> float: """Return logarithm with a configured base.""" if value <= 0.0: return 0.0 if math.isclose(log_base, math.e): return math.log(value) return math.log(value) / math.log(log_base) def _shannon_entropy_array(probabilities: np.ndarray, *, log_base: float) -> float: """Compute Shannon entropy from a cleaned probability array.""" positive = probabilities[probabilities > 0.0] if positive.size == 0: return 0.0 logs = np.log(positive) if not math.isclose(log_base, math.e): logs = logs / math.log(log_base) return float(-np.sum(positive * logs)) def _world_step(world: World) -> int | None: """Return the current world step if available.""" value = getattr(world, "step_count", None) if isinstance(value, Real) and not isinstance(value, bool): return int(value) return None def _is_number(value: Any) -> bool: """Return whether a value is a real numeric scalar, excluding booleans.""" return isinstance(value, (Real, np.integer, np.floating)) and not isinstance(value, bool) def _is_alive(item: Any) -> bool: """Return whether an item is alive when it exposes an ``alive`` field.""" return bool(getattr(item, "alive", True)) def _is_agent_like(item: Any) -> bool: """Return whether an item looks like a runtime agent.""" return hasattr(item, "alive") or hasattr(item, "behaviors") or hasattr(item, "policy") def _item_collection_name(item: Any) -> str: """Return a best-effort collection name for an item.""" if _is_agent_like(item): return EntropyCollection.AGENTS.value if hasattr(item, "amount"): return EntropyCollection.RESOURCES.value return "items" def _iter_world_items(world: World, collection: EntropyCollection) -> tuple[Any, ...]: """Return world items for a configured entropy collection.""" if collection is EntropyCollection.AGENTS: return _iter_collection(getattr(world, "agents", ())) if collection is EntropyCollection.RESOURCES: return _iter_collection(getattr(world, "resources", ())) agents = _iter_collection(getattr(world, "agents", ())) resources = _iter_collection(getattr(world, "resources", ())) return agents + resources def _iter_collection(raw_collection: Any) -> tuple[Any, ...]: """Return items from a mapping-backed or sequence-backed collection.""" if raw_collection is None: return () if isinstance(raw_collection, Mapping): values = raw_collection.values() elif isinstance(raw_collection, Iterable) and not isinstance(raw_collection, (str, bytes)): values = raw_collection else: values = (raw_collection,) return tuple(item for item in values if item is not None) def _split_path(path: str) -> tuple[str, ...]: """Split a dot-separated path into components.""" return tuple(part for part in str(path).split(".") if part) def _get_mapping_path(container: Mapping[str, Any], path: str, default: Any = _MISSING) -> Any: """Read a nested mapping value using dot notation.""" parts = _split_path(path) if not parts: return default current: Any = container for part in parts: if not isinstance(current, Mapping) or part not in current: return default current = current[part] return current def _get_object_path(root: Any, path: str, default: Any = _MISSING) -> Any: """Read nested values from mappings, sequences, or object attributes.""" parts = _split_path(path) if not parts: return root current: Any = root for part in parts: if isinstance(current, Mapping): if part not in current: return default current = current[part] continue if isinstance(current, Sequence) and not isinstance(current, (str, bytes)) and part.isdigit(): index = int(part) if index >= len(current): return default current = current[index] continue if not hasattr(current, part): return default current = getattr(current, part) return current def _read_path(item: Any, path: str, default: Any = _MISSING) -> Any: """Read a path from an item. Supported special prefixes: - ``state.foo`` - ``state:foo`` - ``memory.foo`` - ``memory:foo`` - ``metadata.foo`` - ``metadata:foo`` Without a prefix, object attributes and mapping keys are read directly. """ normalized_path = str(path) if normalized_path.startswith("state."): state = getattr(item, "state", {}) return _get_mapping_path( state if isinstance(state, Mapping) else {}, normalized_path.removeprefix("state."), default, ) if normalized_path.startswith("state:"): state = getattr(item, "state", {}) return _get_mapping_path( state if isinstance(state, Mapping) else {}, normalized_path.removeprefix("state:"), default, ) if normalized_path.startswith("memory."): memory = getattr(item, "memory", {}) return _get_mapping_path( memory if isinstance(memory, Mapping) else {}, normalized_path.removeprefix("memory."), default, ) if normalized_path.startswith("memory:"): memory = getattr(item, "memory", {}) return _get_mapping_path( memory if isinstance(memory, Mapping) else {}, normalized_path.removeprefix("memory:"), default, ) if normalized_path.startswith("metadata."): metadata = getattr(item, "metadata", {}) return _get_mapping_path( metadata if isinstance(metadata, Mapping) else {}, normalized_path.removeprefix("metadata."), default, ) if normalized_path.startswith("metadata:"): metadata = getattr(item, "metadata", {}) return _get_mapping_path( metadata if isinstance(metadata, Mapping) else {}, normalized_path.removeprefix("metadata:"), default, ) return _get_object_path(item, normalized_path, default) def _stable_label(value: Any) -> str: """Return a stable string label for an outcome value.""" if value is None: return "none" if isinstance(value, bool): return "true" if value else "false" if _is_number(value): numeric_value = float(value) if numeric_value.is_integer(): return str(int(numeric_value)) return str(numeric_value) if isinstance(value, Mapping): parts = [f"{key}={_stable_label(value[key])}" for key in sorted(value.keys(), key=str)] return "{" + ",".join(parts) + "}" if isinstance(value, Sequence) and not isinstance(value, (str, bytes)): return "[" + ",".join(_stable_label(item) for item in value) + "]" return str(value) def _normalize_label(value: Any, *, case_sensitive: bool, strip: bool) -> str: """Normalize an outcome label for deterministic comparisons.""" label = _stable_label(value) if strip: label = label.strip() if not case_sensitive: label = label.lower() return label def _append_bounded(items: MutableSequence[Any], value: Any, max_items: int) -> None: """Append an item while enforcing an optional maximum history length.""" items.append(value) if max_items > 0 and len(items) > max_items: del items[: len(items) - max_items] METRIC_REGISTRY: Mapping[str, type[EntropyMetric]] = MappingProxyType( { EntropyMetric.name: EntropyMetric, } ) __all__ = [ "EntropyCollection", "EntropyLogBase", "EntropyMetric", "EntropyResult", "EntropyTracker", "METRIC_REGISTRY", "compute_agent_type_entropy", "compute_entropy", "compute_resource_type_entropy", "effective_number", "entropy_from_counts", "entropy_from_probabilities", "normalized_entropy", "shannon_entropy", ]