Appendix B: The Learner Manifold — Code Walkthrough


This appendix provides Python implementations of the core computational objects in the geometric framework. All code is designed for reproducibility and requires only NumPy, SciPy, and scikit-learn.

B.1 Learner State Representation

import numpy as np
from dataclasses import dataclass

@dataclass
class LearnerState:
    """A point on the 6-dimensional learner manifold."""
    d1_domain_knowledge: float    # [0, 1]
    d2_procedural_skill: float    # [0, 1]
    d3_metacognition: float       # [0, 1]
    d4_motivation: float          # [0, 1]
    d5_transfer: float            # [0, 1]
    d6_creativity: float          # [0, 1]

    def to_vector(self) -> np.ndarray:
        return np.array([
            self.d1_domain_knowledge,
            self.d2_procedural_skill,
            self.d3_metacognition,
            self.d4_motivation,
            self.d5_transfer,
            self.d6_creativity
        ])

# Alex and Ethan from Chapter 1
alex = LearnerState(0.45, 0.85, 0.70, 0.80, 0.75, 0.65)
ethan = LearnerState(0.80, 0.50, 0.40, 0.55, 0.40, 0.35)

B.2 GPA Contraction and Irrecoverability Demonstration

def gpa_contraction(state: LearnerState,
                    weights: np.ndarray = None) -> float:
    """Contract a 6D learner state to a scalar GPA.

    Default weights reflect typical exam-dominated assessment:
    heavy on d1, moderate on d2, negligible on d3-d6.
    """
    if weights is None:
        weights = np.array([0.60, 0.25, 0.05, 0.05, 0.03, 0.02])
    return float(weights @ state.to_vector())

def demonstrate_irrecoverability(n_samples: int = 10000,
                                 target_gpa: float = 0.60,
                                 tolerance: float = 0.01):
    """Generate random learner states that produce the same GPA.

    Returns states in the level set G^{-1}(target_gpa +/- tolerance).
    """
    weights = np.array([0.60, 0.25, 0.05, 0.05, 0.03, 0.02])
    states = np.random.uniform(0, 1, size=(n_samples, 6))
    gpas = states @ weights
    mask = np.abs(gpas - target_gpa) < tolerance
    level_set = states[mask]
    return level_set  # Many distinct states, same GPA

B.3 Metric Estimation from Learning Curves

from scipy.optimize import curve_fit

def power_law(practice, a, b, c):
    """Power law learning curve: performance = c - a * practice^(-b)."""
    return c - a * np.power(practice + 1, -b)

def estimate_local_curvature(practice_data: np.ndarray,
                              performance_data: np.ndarray) -> float:
    """Estimate local curvature from learning curve shape.

    Higher curvature = steeper initial curve = harder transition.
    """
    popt, _ = curve_fit(power_law, practice_data, performance_data,
                        p0=[0.5, 0.5, 1.0], maxfev=5000)
    a, b, c = popt
    # Curvature proxy: the exponent b (higher = steeper = harder)
    return b

B.4 Educational Bond Index Computation

def compute_bond_index(actual_paths: np.ndarray,
                       geodesic_paths: np.ndarray,
                       sigma_inv: np.ndarray) -> float:
    """Compute the Educational Bond Index.

    Parameters
    ----------
    actual_paths : array of shape (n_students, n_timesteps, 6)
        Observed learning trajectories.
    geodesic_paths : array of shape (n_students, n_timesteps, 6)
        Computed optimal trajectories.
    sigma_inv : array of shape (6, 6)
        Inverse covariance matrix (precision matrix).

    Returns
    -------
    float
        Expected learning deviation (Bond Index).
    """
    n_students, n_steps, d = actual_paths.shape
    total_deviation = 0.0
    for s in range(n_students):
        path_deviation = 0.0
        for t in range(n_steps):
            diff = actual_paths[s, t] - geodesic_paths[s, t]
            # Mahalanobis distance at each timestep
            path_deviation += np.sqrt(diff @ sigma_inv @ diff)
        total_deviation += path_deviation / n_steps
    return total_deviation / n_students

B.5 Gauge Violation Tensor Calculation

def compute_gauge_violation_tensor(
    scores_original: np.ndarray,
    scores_transformed: np.ndarray,
    transformation_labels: list
) -> dict:
    """Compute gauge violation tensor V_ij.

    Parameters
    ----------
    scores_original : array of shape (n_students, n_score_dims)
    scores_transformed : array of shape (n_transforms, n_students, n_score_dims)
    transformation_labels : list of transformation names

    Returns
    -------
    dict mapping transformation name to mean score change vector
    """
    V = {}
    for i, label in enumerate(transformation_labels):
        diff = scores_transformed[i] - scores_original
        V[label] = np.mean(diff, axis=0)  # Mean change per score dimension
    return V

B.6 Trajectory Clustering for Profile Identification

from sklearn.cluster import KMeans
from sklearn.preprocessing import StandardScaler

def cluster_learner_trajectories(
    trajectory_features: np.ndarray,
    n_clusters: int = 4
) -> tuple:
    """Cluster students by trajectory features to identify learner profiles.

    Parameters
    ----------
    trajectory_features : array of shape (n_students, n_features)
        Features extracted from LMS interaction data.
    n_clusters : int
        Number of learner profiles to identify.

    Returns
    -------
    labels : array of cluster assignments
    centers : array of cluster centers (profile prototypes)
    """
    scaler = StandardScaler()
    X_scaled = scaler.fit_transform(trajectory_features)
    kmeans = KMeans(n_clusters=n_clusters, random_state=42, n_init=10)
    labels = kmeans.fit_predict(X_scaled)
    centers = scaler.inverse_transform(kmeans.cluster_centers_)
    return labels, centers

All code is available in the companion repository. For full documentation and extended examples, see the project website.