-module(viva_math@attractor). -compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]). -define(FILEPATH, "src/viva_math/attractor.gleam"). -export([emotional_attractors/0, nearest/2, basin_weights/3, analyze/3, classify_emotion/1, attractor_pull/3, weighted_pull/4, ou_mean_reversion/4, in_basin/3, nearby_attractors/3, blend_attractors/3, create/4, dominant_dimension/1]). -export_type([attractor/0, attractor_result/0]). -if(?OTP_RELEASE >= 27). -define(MODULEDOC(Str), -moduledoc(Str)). -define(DOC(Str), -doc(Str)). -else. -define(MODULEDOC(Str), -compile([])). -define(DOC(Str), -compile([])). -endif. ?MODULEDOC( " Attractor dynamics for emotional states.\n" "\n" " Based on Mehrabian's PAD model (1996) and dynamical systems theory.\n" " Emotions form attractors in PAD space - stable states the system tends toward.\n" "\n" " The 8 basic emotions correspond to the octants of the PAD cube:\n" " - Joy: (+P, +A, +D)\n" " - Sadness: (-P, -A, -D)\n" " - Anger: (-P, +A, +D)\n" " - Fear: (-P, +A, -D)\n" " - Surprise: (+P, +A, -D) or (-P, +A, -D)\n" " - Disgust: (-P, -A, +D)\n" " - Trust: (+P, -A, +D)\n" " - Anticipation: (+P, +A, +D)\n" "\n" " References:\n" " - Mehrabian (1996) \"Pleasure-arousal-dominance: A general framework\"\n" " - Russell (2003) \"Core affect and the psychological construction of emotion\"\n" ). -type attractor() :: {attractor, binary(), viva_math@vector:vec3()}. -type attractor_result() :: {attractor_result, attractor(), float(), list({attractor(), float()})}. -file("src/viva_math/attractor.gleam", 44). ?DOC( " The 8 basic emotional attractors (Mehrabian octants).\n" " Values from empirical research on emotion self-reports.\n" ). -spec emotional_attractors() -> list(attractor()). emotional_attractors() -> [{attractor, <<"joy"/utf8>>, {vec3, 0.76, 0.48, 0.35}}, {attractor, <<"excitement"/utf8>>, {vec3, 0.62, 0.75, 0.38}}, {attractor, <<"trust"/utf8>>, {vec3, 0.58, -0.23, 0.42}}, {attractor, <<"serenity"/utf8>>, {vec3, 0.45, -0.42, 0.21}}, {attractor, <<"sadness"/utf8>>, {vec3, -0.63, -0.27, -0.33}}, {attractor, <<"fear"/utf8>>, {vec3, -0.64, 0.6, -0.43}}, {attractor, <<"anger"/utf8>>, {vec3, -0.51, 0.59, 0.25}}, {attractor, <<"disgust"/utf8>>, {vec3, -0.6, 0.35, 0.11}}]. -file("src/viva_math/attractor.gleam", 60). ?DOC(" Find the nearest attractor to a given point.\n"). -spec nearest(viva_math@vector:vec3(), list(attractor())) -> {ok, attractor()} | {error, nil}. nearest(Point, Attractors) -> case Attractors of [] -> {error, nil}; [First | Rest] -> Initial = {First, viva_math@vector:distance(Point, erlang:element(3, First))}, Result = gleam@list:fold( Rest, Initial, fun(Acc, Attr) -> Dist = viva_math@vector:distance( Point, erlang:element(3, Attr) ), case Dist < erlang:element(2, Acc) of true -> {Attr, Dist}; false -> Acc end end ), {ok, erlang:element(1, Result)} end. -file("src/viva_math/attractor.gleam", 91). ?DOC( " Calculate influence weights for all attractors using softmax of negative distances.\n" "\n" " CORRECTED per DeepSeek R1 validation:\n" " w_i = exp(-γ × d_i) / Σ_j exp(-γ × d_j)\n" "\n" " Where γ = 1/temperature (higher temp = softer weights, lower temp = sharper).\n" " This is more numerically stable than 1/d and matches Boltzmann distribution.\n" "\n" " Closer attractors have higher weights. The temperature parameter controls\n" " how \"sharp\" the weighting is (lower temp = more weight on nearest).\n" ). -spec basin_weights(viva_math@vector:vec3(), list(attractor()), float()) -> list({attractor(), float()}). basin_weights(Point, Attractors, Temperature) -> case Attractors of [] -> []; _ -> Gamma = case Temperature =< +0.0 of true -> 1.0; false -> case Temperature of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> 1.0 / Gleam@denominator end end, Neg_gamma_distances = gleam@list:map( Attractors, fun(Attr) -> Dist = viva_math@vector:distance( Point, erlang:element(3, Attr) ), {Attr, +0.0 - (Gamma * Dist)} end ), Max_val = gleam@list:fold( Neg_gamma_distances, -1000.0, fun(Acc, Pair) -> gleam@float:max(Acc, erlang:element(2, Pair)) end ), Exps = gleam@list:map( Neg_gamma_distances, fun(Pair@1) -> {erlang:element(1, Pair@1), gleam_community@maths:exponential( erlang:element(2, Pair@1) - Max_val )} end ), Sum = gleam@list:fold( Exps, +0.0, fun(Acc@1, Pair@2) -> Acc@1 + erlang:element(2, Pair@2) end ), case Sum =:= +0.0 of true -> gleam@list:map(Attractors, fun(A) -> {A, +0.0} end); false -> gleam@list:map( Exps, fun(Pair@3) -> {erlang:element(1, Pair@3), case Sum of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator@1 -> erlang:element( 2, Pair@3 ) / Gleam@denominator@1 end} end ) end end. -file("src/viva_math/attractor.gleam", 134). ?DOC(" Comprehensive attractor analysis for a point.\n"). -spec analyze(viva_math@vector:vec3(), list(attractor()), float()) -> {ok, attractor_result()} | {error, nil}. analyze(Point, Attractors, Temperature) -> case nearest(Point, Attractors) of {error, nil} -> {error, nil}; {ok, Near} -> Dist = viva_math@vector:distance(Point, erlang:element(3, Near)), Weights = basin_weights(Point, Attractors, Temperature), {ok, {attractor_result, Near, Dist, Weights}} end. -file("src/viva_math/attractor.gleam", 150). ?DOC(" Classify emotional state by nearest attractor name.\n"). -spec classify_emotion(viva_math@vector:vec3()) -> binary(). classify_emotion(Point) -> case nearest(Point, emotional_attractors()) of {ok, Attr} -> erlang:element(2, Attr); {error, nil} -> <<"neutral"/utf8>> end. -file("src/viva_math/attractor.gleam", 161). ?DOC( " Compute attractor pull - force vector toward nearest attractor.\n" "\n" " The pull strength increases with distance from attractor (spring-like).\n" " strength parameter controls overall force magnitude.\n" ). -spec attractor_pull(viva_math@vector:vec3(), attractor(), float()) -> viva_math@vector:vec3(). attractor_pull(Point, Attractor, Strength) -> Diff = viva_math@vector:sub(erlang:element(3, Attractor), Point), Dist = viva_math@vector:length(Diff), case Dist =:= +0.0 of true -> viva_math@vector:zero(); false -> Normalized = viva_math@vector:scale(Diff, case Dist of +0.0 -> +0.0; -0.0 -> -0.0; Gleam@denominator -> 1.0 / Gleam@denominator end), viva_math@vector:scale(Normalized, Strength * Dist) end. -file("src/viva_math/attractor.gleam", 181). ?DOC( " Compute weighted pull from all attractors.\n" "\n" " Each attractor pulls proportionally to its basin weight.\n" ). -spec weighted_pull( viva_math@vector:vec3(), list(attractor()), float(), float() ) -> viva_math@vector:vec3(). weighted_pull(Point, Attractors, Strength, Temperature) -> Weights = basin_weights(Point, Attractors, Temperature), gleam@list:fold( Weights, viva_math@vector:zero(), fun(Acc, Pair) -> {Attr, Weight} = Pair, Pull = attractor_pull(Point, Attr, Strength * Weight), viva_math@vector:add(Acc, Pull) end ). -file("src/viva_math/attractor.gleam", 201). ?DOC( " Ornstein-Uhlenbeck mean reversion toward attractor.\n" "\n" " dx = theta * (attractor - x) * dt\n" "\n" " This is the deterministic part of O-U process.\n" " theta controls reversion speed (higher = faster return to attractor).\n" ). -spec ou_mean_reversion( viva_math@vector:vec3(), viva_math@vector:vec3(), float(), float() ) -> viva_math@vector:vec3(). ou_mean_reversion(Current, Attractor, Theta, Dt) -> Diff = viva_math@vector:sub(Attractor, Current), Delta = viva_math@vector:scale(Diff, Theta * Dt), viva_math@vector:add(Current, Delta). -file("src/viva_math/attractor.gleam", 215). ?DOC( " Check if point is in basin of an attractor.\n" "\n" " A point is \"in\" a basin if that attractor has the highest weight.\n" ). -spec in_basin(viva_math@vector:vec3(), attractor(), list(attractor())) -> boolean(). in_basin(Point, Attractor, All) -> case nearest(Point, All) of {ok, Near} -> erlang:element(2, Near) =:= erlang:element(2, Attractor); {error, nil} -> false end. -file("src/viva_math/attractor.gleam", 227). ?DOC(" Find all attractors within a given distance.\n"). -spec nearby_attractors(viva_math@vector:vec3(), list(attractor()), float()) -> list(attractor()). nearby_attractors(Point, Attractors, Radius) -> gleam@list:filter( Attractors, fun(Attr) -> viva_math@vector:distance(Point, erlang:element(3, Attr)) =< Radius end ). -file("src/viva_math/attractor.gleam", 240). ?DOC( " Interpolate between two attractors based on a blend factor.\n" "\n" " t=0 gives first attractor, t=1 gives second.\n" ). -spec blend_attractors(attractor(), attractor(), float()) -> attractor(). blend_attractors(A, B, T) -> Pos = viva_math@vector:lerp(erlang:element(3, A), erlang:element(3, B), T), Name = <<<<(erlang:element(2, A))/binary, "_"/utf8>>/binary, (erlang:element(2, B))/binary>>, {attractor, Name, Pos}. -file("src/viva_math/attractor.gleam", 247). ?DOC(" Create a custom attractor from name and PAD values.\n"). -spec create(binary(), float(), float(), float()) -> attractor(). create(Name, Pleasure, Arousal, Dominance) -> {attractor, Name, viva_math@vector:pad(Pleasure, Arousal, Dominance)}. -file("src/viva_math/attractor.gleam", 257). ?DOC(" Get the dominant emotion component (P, A, or D) for an attractor.\n"). -spec dominant_dimension(attractor()) -> binary(). dominant_dimension(Attractor) -> P = gleam@float:absolute_value( erlang:element(2, erlang:element(3, Attractor)) ), A = gleam@float:absolute_value( erlang:element(3, erlang:element(3, Attractor)) ), D = gleam@float:absolute_value( erlang:element(4, erlang:element(3, Attractor)) ), case (P >= A) andalso (P >= D) of true -> <<"pleasure"/utf8>>; false -> case A >= D of true -> <<"arousal"/utf8>>; false -> <<"dominance"/utf8>> end end.