Current section
Files
Jump to
Current section
Files
src/viva_math@attractor.erl
-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.60, -0.43}},
{attractor, <<"anger"/utf8>>, {vec3, -0.51, 0.59, 0.25}},
{attractor, <<"disgust"/utf8>>, {vec3, -0.60, 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", 223).
?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", 236).
?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", 243).
?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", 248).
?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.