frame_plugins/influence.rs
1// SPDX-License-Identifier: MPL-2.0
2//
3// Part of Auguth Labs open-source softwares.
4// Built for the Substrate framework.
5//
6// This Source Code Form is subject to the terms of the Mozilla Public
7// License, v. 2.0. If a copy of the MPL was not distributed with this
8// file, You can obtain one at https://mozilla.org/MPL/2.0/.
9//
10// Copyright (c) 2026 Auguth Labs (OPC) Pvt Ltd, India
11
12// ===============================================================================
13// `````````````````````````````` INFLUENCE PLUGINS ``````````````````````````````
14// ===============================================================================
15
16//! Provides a suite of **pluggable influence models** to transform raw input values
17//! (e.g., stake, contribution, vote weight, or score) into computed influence metrics
18//! used by election systems, reputation engines, and governance mechanisms.
19//!
20//! ## Why Influence, Not Raw Values
21//!
22//! Raw values (e.g., total stake, number of votes, or token balances) alone often do
23//! not capture the **relative importance**, **fairness**, or **risk-adjusted weight**
24//! of participants. Influence allows the system to:
25//! - Normalize inputs so that extreme values do not dominate outcomes.
26//! - Apply non-linear scaling to reward incremental contributions more fairly.
27//! - Implement thresholds, caps, or decay functions to manage governance risk.
28//! - Adjust voting power or rewards dynamically without changing the underlying raw
29//! assets.
30//!
31//! By computing influence, the system abstracts raw contributions into **comparable
32//! metrics** that can be safely and consistently used in elections, scoring systems,
33//! and reward distribution.
34//!
35//! ## Purpose
36//!
37//! Influence models enable flexible, runtime-configurable strategies for calculating
38//! how much "power" or "weight" an input carries. By swapping models or adjusting
39//! their parameters, the system can adapt to different fairness, risk, or proportionality
40//! requirements.
41//!
42//! ## Key Concepts
43//!
44//! - **Input (`x`)**: Typically represents the resource, stake, vote, or contribution
45//! that is being converted to influence.
46//! - **Output (`f(x)`)**: The computed influence value used by election or scoring
47//! algorithms.
48//! - **Context**: Optional runtime parameters or configurations that guide how the
49//! model behaves.
50//!
51//! ## Usage
52//!
53//! Each model is implemented as a [`plugin_model!`] and can be applied dynamically
54//! in elections, staking, reputation, or governance systems. Context parameters allow
55//! fine-tuning without changing the underlying logic.
56//!
57//! Example usage scenarios:
58//! - Flat election systems: compute influence from author stake or backers.
59//! - Reputation systems: convert contributions to normalized influence scores.
60//! - Governance voting: implement thresholds, caps, or diminishing returns to improve fairness.
61
62// ===============================================================================
63// ``````````````````````````````````` IMPORTS ```````````````````````````````````
64// ===============================================================================
65
66// --- FRAME Suite ---
67use frame_suite::{
68 fixedpoint::{FixedForInteger, FixedOp, IntegerToFixed, FixedSignedCast,},
69 plugin_model,
70};
71
72// --- Substrate primitives ---
73use sp_runtime::{
74 traits::{Bounded, CheckedDiv, One, Zero},
75 FixedI128, FixedPointNumber, Saturating,
76};
77
78// ===============================================================================
79// `````````````````````````````` LINEAR-INFLUENCE ```````````````````````````````
80// ===============================================================================
81
82plugin_model! {
83
84 /// Provides a **linear influence model** where output equals input.
85 ///
86 /// `f(x) = x`
87 ///
88 /// - `x`: input value (e.g., vote weight, token amount)
89 ///
90 /// ## Characteristics
91 /// - Direct proportionality between input and output.
92 /// - Simplest and most intuitive model; no transformation applied.
93 /// - Useful as a **baseline model** or for **linear scoring systems**.
94 ///
95 /// ## Reference
96 /// - Foundational in statistics and physics.
97 /// - Used in **linear regression**, **trend estimation**, and **baseline economic models**.
98 /// - https://en.wikipedia.org/wiki/Linear_function_(calculus)
99 name: pub LinearModel,
100 input: Input,
101 bounds: [Input: Clone],
102 /// Linear model implementation without needing external context.
103 ///
104 /// Used when influence is directly proportional to the input.
105 /// Always returns the input value unmodified.
106 compute: |input, _context| {
107 input.clone()
108 }
109}
110
111// ===============================================================================
112// ````````````````````````````` QUADRATIC-INFLUENCE `````````````````````````````
113// ===============================================================================
114
115plugin_model! {
116
117 /// Provides a **quadratic (square-root) influence model** that compresses large inputs.
118 ///
119 /// `f(x) = sqrt(x)`
120 ///
121 /// - `x`: input value (e.g., score, weight, or stake)
122 ///
123 /// ## Characteristics
124 /// - **Non-linear scaling**: grows rapidly at small values, slows for larger inputs.
125 /// - **Compression effect**: prevents large inputs from dominating influence.
126 /// - **Handles negative inputs** by clamping to zero (no imaginary numbers).
127 ///
128 /// ## Applications
129 /// - Voting power normalization
130 /// - Contribution weighting in participatory systems
131 ///
132 /// ## References
133 /// - https://en.wikipedia.org/wiki/Square_root_voting_system
134 /// - Signal scaling and information compression
135 name: pub QuadraticModel,
136 input: Input,
137 bounds: [
138 Input: IntegerToFixed + Zero,
139 <Input as FixedForInteger>::FixedPoint: FixedOp + PartialOrd
140 ],
141 /// Quadratic model implementation without external context.
142 compute: |input, _context| {
143 let x = Input::to_fixed(&input);
144 match <<Input as FixedForInteger>::FixedPoint as FixedOp>::fixed_sqrt(&x) {
145 Some(sqrt) => Input::from_fixed(&sqrt),
146 None => Input::zero(),
147 }
148 }
149}
150
151// ===============================================================================
152// ```````````````````````````` LOGARITHMIC-INFLUENCE ````````````````````````````
153// ===============================================================================
154
155plugin_model! {
156
157 /// Provides a **logarithmic influence model** with diminishing returns.
158 ///
159 /// `f(x) = log(x)`
160 ///
161 /// - `x`: input value (e.g., contribution, stake, or score)
162 ///
163 /// ## Characteristics
164 /// - **Diminishing returns**: large gains initially, with decreasing marginal impact.
165 /// - Natural inverse of exponential growth.
166 /// - Helps **limit influence concentration** in large-scale systems.
167 /// - For `x <= 0` (undefined domain) values are clamped to zero.
168 ///
169 /// ## Applications
170 /// - Human perception modeling (e.g., sound, brightness)
171 /// - Information theory and utility modeling
172 ///
173 /// ## Reference
174 /// - https://en.wikipedia.org/wiki/Weber%E2%80%93Fechner_law
175 /// - https://en.wikipedia.org/wiki/Logarithmic_scale
176 name: pub LogarithmicModel,
177 input: Input,
178 bounds: [
179 Input: IntegerToFixed + Zero,
180 <Input as FixedForInteger>::FixedPoint: FixedOp
181 ],
182 /// Logarithmic model implementation without external context.
183 ///
184 /// Models diminishing influence growth.
185 /// If the input is very small or zero, fixed_ln must handle domain restrictions safely.
186 compute: |input, _context| {
187 let x = Input::to_fixed(&input);
188 // fixed_ln returns None for x <= 0 (undefined domain); map to zero,
189 // which is the natural sentinel for "no influence" in this system.
190 match FixedOp::fixed_ln(&x) {
191 Some(ln) => Input::from_fixed(&ln),
192 None => Input::zero(),
193 }
194 }
195}
196
197// ===============================================================================
198// ````````````````````````````` THRESHOLD-INFLUENCE `````````````````````````````
199// ===============================================================================
200
201/// Configuration: the threshold value to activate influence.
202pub struct ThresholdModelConfig<T> {
203 pub threshold: T,
204}
205
206plugin_model! {
207
208 /// Provides a **threshold-based influence model** that enforces minimum eligibility.
209 ///
210 /// ```text
211 /// f(x) = {
212 /// x, if x >= threshold
213 /// 0, otherwise
214 /// }
215 /// ```
216 ///
217 /// - `x`: input value (e.g., score, stake, weight)
218 /// - `threshold`: minimum required input for influence
219 ///
220 /// ## Characteristics
221 /// - Enforces **minimum participation or eligibility**.
222 /// - Filters out noise or spam contributions.
223 ///
224 /// ## Applications
225 /// - Eligibility filters in voting or staking
226 /// - Activity thresholds in DAOs and moderation systems
227 ///
228 /// ## Reference
229 /// - Widely used in economics, game theory, and governance rule sets
230 name: pub ThresholdModel,
231 input: Input,
232 context: ThresholdModelConfig<Input>,
233 bounds: [
234 Input: PartialOrd + Zero + Clone,
235 ],
236 /// If input >= threshold, pass it through; otherwise return zero/default.
237 compute: |input, context| {
238 match input >= context.threshold {
239 true => input.clone(),
240 false => Input::zero()
241 }
242 }
243}
244
245// ===============================================================================
246// `````````````````````````````` SIGMOID-INFLUENCE ``````````````````````````````
247// ===============================================================================
248
249/// Configuration for the SigmoidModel.
250/// Parameters define the maximum output and the growth phase range for the curve.
251pub struct SigmoidModelConfig<F>
252where
253 F: FixedPointNumber,
254{
255 /// `L` - Maximum possible output of the sigmoid curve.
256 /// This is the upper bound the curve approaches but never exceeds.
257 /// Example: If L = 100, the curve will asymptotically approach 100.
258 pub max_output: F,
259
260 /// `alpha` - Starting fraction of `max_output` for the growth phase.
261 /// Example: `alpha` = 0.10 means growth phase starts when output = 10% of L.
262 /// If L = 100, this means growth starts at output = 10.
263 pub start_frac: F,
264
265 /// `beta` - Ending fraction of `max_output` for the growth phase.
266 /// Example: `beta` = 0.90 means growth phase ends when output = 90% of L.
267 /// If L = 100, this means growth ends at output = 90.
268 pub end_frac: F,
269
270 /// `x_alpha` - Input value (stake, score, etc.) at which output = alpha * L.
271 /// Marks the *start point* of the rapid growth region on the curve.
272 /// Example: If x_alpha = 50 and alpha = 0.10, then at stake = 50 the output is 10% of L.
273 pub start_x: F,
274
275 /// `x_beta` - Input value at which output = beta * L.
276 /// Marks the *end point* of the rapid growth region on the curve.
277 /// Example: If x_beta = 80 and beta = 0.90, then at stake = 80 the output is 90% of L.
278 pub end_x: F,
279}
280
281plugin_model! {
282
283 /// Provides a sigmoid (logistic) influence model with a configurable growth phase.
284 ///
285 /// ```text
286 /// f(x) = L / (1 + e^(-k * (x - x0)))
287 /// ```
288 ///
289 /// You do not set `k` or `x0` directly. Instead you describe the shape of the
290 /// curve using five intuitive parameters, and the model derives `k` and `x0`
291 /// from them:
292 ///
293 /// - `L` : the maximum output the curve can ever reach
294 /// - `alpha` : what fraction of `L` marks the start of the growth phase (e.g. 0.1 = 10%)
295 /// - `beta` : what fraction of `L` marks the end of the growth phase (e.g. 0.9 = 90%)
296 /// - `x_alpha` : the input value where output first reaches `alpha * L`
297 /// - `x_beta` : the input value where output reaches `beta * L`
298 ///
299 /// From those, the model computes:
300 ///
301 /// - `w = x_beta - x_alpha` (growth width)
302 /// - `k = [ ln(beta / (1 - beta)) - ln(alpha / (1 - alpha)) ] / w` (growth rate)
303 /// - `x0 = x_alpha - (1 / k) * ln(alpha / (1 - alpha))` (midpoint)
304 ///
305 /// ## Signed Arithmetic
306 ///
307 /// Even when the context `FixedPoint` is unsigned, all intermediate steps
308 /// (logit, k, x0, the exponent) are computed in concrete `FixedI128` via
309 /// `FixedSignedCast`. This is necessary because `logit(alpha)` is negative
310 /// for any `alpha < 0.5`, and the exponent `-k * (x - x0)` is negative for
311 /// all `x > x0`. Unsigned arithmetic would silently clamp both to zero,
312 /// producing a completely wrong curve.
313 ///
314 /// ## Precision Note
315 ///
316 /// The `ln -> k -> x0 -> exp` chain accumulates a small amount of rounding
317 /// error across four fixed-point operations. The practical effect is that `x0`
318 /// lands fractionally above its exact value, shifting the curve slightly to the
319 /// right. At the definition points `x_alpha` and `x_beta` the logit cancels
320 /// cleanly and the output is exact. At all other points including the midpoint,
321 /// the output may be 1 integer unit below the ideal value -- for example,
322 /// `f(x_beta)` may yield `89` instead of `90` when `L = 100`.
323 ///
324 /// - Exact: `f(80) = 90.000000000`
325 /// - Fixed-point: `f(80) = 89.999999...` -> truncates to `89`
326 ///
327 /// This is expected and inconsequential for integer influence scores.
328 ///
329 /// ## Guard Conditions (returns zero)
330 ///
331 /// - `alpha <= 0` or `alpha >= 1`
332 /// - `beta <= 0` or `beta >= 1`
333 /// - `x_beta <= x_alpha` (degenerate or inverted growth window)
334 /// - `k == 0` (flat curve, midpoint is undefined)
335 name: pub SigmoidModel,
336 input: Input,
337 others: [FixedPoint],
338 context: SigmoidModelConfig<FixedPoint>,
339 bounds: [
340 Input: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint> + Zero,
341 FixedPoint: FixedPointNumber + FixedSignedCast<Signed = FixedI128>,
342 ],
343 compute: |input, context| {
344 let zero_fp = FixedPoint::zero();
345 let one_fp = FixedPoint::one();
346 let zero_s = FixedI128::zero();
347
348 let l = context.max_output;
349 let alpha = context.start_frac;
350 let beta = context.end_frac;
351 let x_alpha = context.start_x;
352 let x_beta = context.end_x;
353
354 // Guard: alpha and beta must be strictly in (0, 1).
355 if alpha <= zero_fp || alpha >= one_fp || beta <= zero_fp || beta >= one_fp {
356 return Input::zero();
357 }
358
359 // Growth width w = x_beta - x_alpha; must be > 0.
360 // Compute in signed space so subtraction is safe even for unsigned FixedPoint.
361 let x_alpha_s: FixedI128 = FixedSignedCast::saturated_into(x_alpha);
362 let x_beta_s: FixedI128 = FixedSignedCast::saturated_into(x_beta);
363 let w_s: FixedI128 = x_beta_s.saturating_sub(x_alpha_s);
364 if w_s <= zero_s {
365 return Input::zero();
366 }
367
368 // logit(p) = ln(p / (1-p))
369 //
370 // ratio = p/(1-p) is computed in FixedPoint space (always > 0 since 0 < p < 1).
371 // Then promoted to FixedI128 for fixed_ln, which can return a negative result.
372 // FixedI128::fixed_ln is a CONCRETE call - no generic FixedOp bound needed.
373 let logit = |p: FixedPoint| -> Option<FixedI128> {
374 let denom = one_fp.saturating_sub(p); // 1 - p (> 0 since p < 1)
375 let ratio = p.checked_div(&denom)?; // p/(1-p) > 0
376 let ratio_s: FixedI128 = FixedSignedCast::saturated_into(ratio);
377 FixedI128::fixed_ln(&ratio_s) // concrete, can be negative
378 };
379
380 let logit_alpha: FixedI128 = match logit(alpha) {
381 Some(v) => v,
382 None => return Input::zero(),
383 };
384 let logit_beta: FixedI128 = match logit(beta) {
385 Some(v) => v,
386 None => return Input::zero(),
387 };
388
389 // k = (logit(beta) - logit(alpha)) / w - always > 0 when beta > alpha.
390 let k_num: FixedI128 = logit_beta.saturating_sub(logit_alpha);
391 let k: FixedI128 = match k_num.checked_div(&w_s) {
392 Some(v) => v,
393 None => return Input::zero(),
394 };
395 if k == zero_s {
396 return Input::zero();
397 }
398
399 // x0 = x_alpha - logit(alpha) / k
400 // For alpha < 0.5: logit(alpha) < 0, so -logit(alpha)/k > 0, meaning x0 > x_alpha.
401 let logit_alpha_over_k: FixedI128 = match logit_alpha.checked_div(&k) {
402 Some(v) => v,
403 None => return Input::zero(),
404 };
405 let x0: FixedI128 = x_alpha_s.saturating_sub(logit_alpha_over_k);
406
407 // f(x) = L / (1 + e^(-k * (x - x0)))
408 // All computation in FixedI128 to handle negative exponent argument correctly.
409 let x_s: FixedI128 = FixedSignedCast::saturated_into(Input::to_fixed(&input));
410 let delta: FixedI128 = x_s.saturating_sub(x0);
411 let k_delta: FixedI128 = k.saturating_mul(delta);
412 // This negation requires signed arithmetic. In unsigned space this would clamp to 0.
413 let neg_k_delta: FixedI128 = zero_s.saturating_sub(k_delta);
414
415 // Concrete FixedI128::fixed_exp - no generic bound needed.
416 let exp_val: FixedI128 = FixedI128::fixed_exp(&neg_k_delta)
417 .unwrap_or(FixedI128::max_value());
418
419 let denom: FixedI128 = FixedI128::one().saturating_add(exp_val);
420
421 let l_s: FixedI128 = FixedSignedCast::saturated_into(l);
422 let output_s: FixedI128 = l_s.checked_div(&denom).unwrap_or(zero_s);
423
424 // Project back to FixedPoint (always >= 0 since L >= 0 and denom >= 1).
425 let output_fp: FixedPoint = FixedSignedCast::saturated_from(output_s);
426 Input::from_fixed(&output_fp)
427 }
428}
429
430// ===============================================================================
431// ```````````````````````````` EXPONENTIAL-INFLUENCE ````````````````````````````
432// ===============================================================================
433
434/// Configuration for Exponential Model
435///
436/// - `growth_rate`: Determines how steeply the value grows.
437/// - A higher value leads to faster exponential increase.
438pub struct ExponentialModelConfig<F>
439where
440 F: FixedPointNumber,
441{
442 pub growth_rate: F,
443}
444
445plugin_model! {
446
447 /// Provides an exponential influence model with rapid growth.
448 ///
449 /// `f(x) = e^(k * x)`
450 ///
451 /// - `x`: input value (e.g., vote weight, reputation)
452 /// - `k`: growth rate (positive for exponential growth)
453 /// - `e`: Euler's number (~2.718)
454 ///
455 /// ## Characteristics
456 /// - Growth rate is proportional to the current value.
457 /// - Models **compound growth**, **population increase**, and **epidemics**.
458 /// - Overflows are saturated to max_value.
459 ///
460 /// ## Applications
461 /// - Incentive amplification systems
462 /// - Growth modeling in economics and networks
463 ///
464 /// ## References
465 /// - https://en.wikipedia.org/wiki/Exponential_growth
466 name: pub ExponentialModel,
467 input: Input,
468 others: [FixedPoint],
469 context: ExponentialModelConfig<FixedPoint>,
470 bounds: [
471 Input: IntegerToFixed + FixedForInteger<FixedPoint = FixedPoint> + PartialOrd,
472 FixedPoint: FixedPointNumber + FixedOp,
473 ],
474 compute: |input, context| {
475 // Convert the generic input to the context type (e.g., FixedU128)
476 let x = input.to_fixed();
477 // Apply growth rate: k * x
478 let kx = context.growth_rate.saturating_mul(x);
479 // Compute e^(k * x)
480 let result = FixedOp::fixed_exp(&kx).unwrap_or(FixedPoint::max_value());
481 Input::from_fixed(&result)
482 }
483}
484
485// ===============================================================================
486// ``````````````````````````````` BINARY-INFLUENCE ``````````````````````````````
487// ===============================================================================
488
489/// Binary model configuration
490///
491/// - `pass_threshold`: Minimum input required to be considered a pass
492/// - `pass_value`: Output when input passes threshold
493/// - `fail_value`: Output when input is below threshold
494pub struct BinaryModelConfig<T> {
495 pub pass_threshold: T,
496 pub pass_value: T,
497 pub fail_value: T,
498}
499
500plugin_model! {
501
502 /// Provides a **binary influence model** that maps input to one of two fixed outputs.
503 ///
504 /// ```text
505 /// f(x) = pass_value if x >= threshold
506 /// fail_value otherwise
507 /// ```
508 ///
509 /// - `x`: input value (e.g., vote weight, signal score, approval rating)
510 /// - `threshold`: the boundary that separates pass from fail
511 /// - `pass_value`: output when input meets or exceeds the threshold
512 /// - `fail_value`: output when input falls below the threshold
513 ///
514 /// ## Characteristics
515 /// - All-or-nothing output; no partial or proportional influence.
516 /// - The threshold is inclusive - `x == threshold` is a pass.
517 ///
518 /// ## Applications
519 /// - Quorum checks in voting systems
520 /// - On/off feature activation in governance
521 /// - Eligibility gates in staking or reputation systems
522 ///
523 /// ## References:
524 /// - [Binary decision rule](https://en.wikipedia.org/wiki/Decision_rule)
525 /// - [Threshold logic](https://en.wikipedia.org/wiki/Threshold_logic)
526 name: pub BinaryModel,
527 input: Input,
528 context: BinaryModelConfig<Input>,
529 bounds: [
530 Input: Copy + PartialOrd,
531 ],
532 compute: |input, context| {
533 let outcome = input >= context.pass_threshold;
534 match outcome {
535 true => context.pass_value,
536 false => context.fail_value
537 }
538 }
539}
540
541// ===============================================================================
542// ````````````````````````````` CAPPED-LINEAR-INFLUENCE `````````````````````````
543// ===============================================================================
544
545/// Configuration for the `CappedLinearModel`
546///
547/// - `max_influence`: the upper bound of the influence, no matter how large the input is
548pub struct CappedLinearModelConfig<T> {
549 pub max_influence: T,
550}
551
552plugin_model! {
553
554 /// Provides a capped linear influence model with an upper bound.
555 ///
556 /// `f(x) = min(x, max_influence)`
557 ///
558 /// - `x`: input value (e.g., stake, score)
559 /// - `max_influence`: maximum allowed influence
560 ///
561 /// ## Characteristics
562 /// - Grows linearly until reaching a fixed cap.
563 /// - Prevents outliers or large inputs from dominating.
564 ///
565 /// ## Applications
566 /// - Capped voting power
567 /// - Anti-sybil systems
568 /// - Influence throttling in distributed systems
569 ///
570 /// ## References
571 /// - https://en.wikipedia.org/wiki/Quadratic_voting
572 /// - https://en.wikipedia.org/wiki/Reputation_system
573 name: pub CappedLinearModel,
574 input: Input,
575 context: CappedLinearModelConfig<Input>,
576 bounds: [
577 Input: Copy + PartialOrd,
578 ],
579 compute: |input, context| {
580 let result = input > context.max_influence;
581 match result {
582 true => context.max_influence,
583 false => input
584 }
585 }
586}
587
588// ===============================================================================
589// ```````````````````````` INFLUENCE MODELS PLUGIN TESTS ````````````````````````
590// ===============================================================================
591
592#[cfg(test)]
593mod tests {
594
595 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
596 // ``````````````````````````````````` IMPORTS ```````````````````````````````````
597 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
598
599 // --- Local crate imports ---
600 use super::*;
601
602 // --- FRAME Suite ---
603 use frame_suite::plugin_test;
604
605 // --- Substrate primitives ---
606 use sp_runtime::{FixedI128, FixedU128};
607
608 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
609 // `````````````````````````````` LINEAR-INFLUENCE ```````````````````````````````
610 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
611
612 plugin_test! {
613 model: LinearModel,
614 input: u64,
615 cases: {
616 (linear_model_unsigned_zero, 0, 0),
617 (linear_model_unsigned_single_digit, 6, 6),
618 (linear_model_unsigned_double_digit, 42, 42),
619 (linear_model_unsigned_large_value, 1000, 1000),
620 (linear_model_unsigned_max_u64, u64::MAX, u64::MAX)
621
622 }
623 }
624
625 plugin_test! {
626 model: LinearModel,
627 input: i64,
628 cases: {
629 (linear_model_signed_negative_value, -55, -55),
630 (linear_model_signed_positive_value, 100, 100),
631 (linear_model_signed_min, i64::MIN, i64::MIN),
632 (linear_model_signed_max, i64::MAX, i64::MAX)
633
634 }
635 }
636
637 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
638 // ````````````````````````````` QUADRATIC-INFLUENCE `````````````````````````````
639 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
640
641 plugin_test! {
642 model: QuadraticModel,
643 input: u64,
644 cases: {
645 (quadratic_model_unsigned_one, 1, 1),
646 (quadratic_model_unsigned_zero, 0, 0),
647 (quadratic_model_unsigned_perfect_sqr_single, 9, 3),
648 (quadratic_model_unsigned_perfect_sqr_double, 81, 9),
649 (quadratic_model_unsigned_perfect_sqr_triple, 225, 15),
650 (quadratic_model_unsigned_imperfect_sqr_single, 5, 2),
651 (quadratic_model_unsigned_imperfect_sqr_double, 61, 7),
652 (quadratic_model_unsigned_imperfect_sqr_triple, 230, 15),
653 // sqrt(10_000) = 100
654 (quadratic_model_unsigned_perfect_large, 10_000, 100),
655 // sqrt(u64::MAX) ~= 4_294_967_295 (2^32 - 1)
656 (quadratic_model_unsigned_max, u64::MAX, 4_294_967_295),
657
658 }
659 }
660
661 plugin_test! {
662 model: QuadraticModel,
663 input: i64,
664 cases: {
665 (quadratic_model_signed_negative_one, -1, 0),
666 (quadratic_model_signed_negative_sigle, -4, 0),
667 (quadratic_model_signed_negative_double, -64, 0),
668 (quadratic_model_signed_positive_sigle, 4, 2),
669 (quadratic_model_signed_positive_double, 64, 8),
670 (quadratic_model_signed_max, i64::MAX, 3_037_000_499),
671 }
672 }
673
674 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
675 // ```````````````````````````` LOGARITHMIC-INFLUENCE ````````````````````````````
676 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
677
678 plugin_test! {
679 model: LogarithmicModel,
680 input: u64,
681 cases: {
682 (logarithmic_model_unsigned_one, 1, 0),
683 (logarithmic_model_unsigned_small_value, 3, 1),
684 (logarithmic_model_unsigned_single_digit, 9, 2),
685 (logarithmic_model_unsigned_double_digit, 10, 2),
686 (logarithmic_model_unsigned_large_value, 1_000_000, 13),
687 (logarithmic_model_unsigned_max, u64::MAX, 44),
688 // ln(2) ~= 0.693 -> truncates to 0 for integer output
689 (logarithmic_model_unsigned_two_truncates_to_zero, 2, 0),
690 // ln(e) = 1 -> 1
691 (logarithmic_model_unsigned_e_approx, 3, 1),
692 }
693 }
694
695 plugin_test! {
696 model: LogarithmicModel,
697 input: i64,
698 cases: {
699 (log_signed_negative, -10, 0),
700 (log_signed_zero, 0, 0),
701 (log_signed_one, 1, 0),
702 (log_signed_two, 2, 0),
703 (log_signed_three, 3, 1),
704 (log_signed_small, 9, 2),
705 (log_signed_ten, 10, 2),
706 (log_signed_large, 1_000_000, 13),
707 (log_signed_min, i64::MIN, 0),
708 (log_signed_max, i64::MAX, 43),
709 }
710 }
711
712 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
713 // ````````````````````````````` THRESHOLD-INFLUENCE `````````````````````````````
714 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
715
716 plugin_test! {
717 model: ThresholdModel,
718 input: u64,
719 output: u64,
720 context: ThresholdModelConfig<u64>,
721 value: ThresholdModelConfig {
722 threshold : 100
723 },
724 cases: {
725 (threshold_model_unsigned_below_threshold, 99, 0),
726 (threshold_model_unsigned_above_threshold, 105, 105),
727 (threshold_model_unsigned_equal_to_threshold, 100, 100),
728 }
729 }
730
731 plugin_test! {
732 model: ThresholdModel,
733 input: i64,
734 output: i64,
735 context: ThresholdModelConfig<i64>,
736 value: ThresholdModelConfig {
737 threshold : -50
738 },
739 cases: {
740 (threshold_model_signed_below_negative_threshold, -51, 0),
741 (threshold_model_signed_above_negative_threshold, -25, -25),
742 (threshold_model_signed_equal_to_negative_threshold, -50, -50),
743 (threshold_model_signed_positive_input, 1, 1),
744 }
745 }
746
747 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
748 // `````````````````````````````` SIGMOID-INFLUENCE ``````````````````````````````
749 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
750
751 // Wide curve: L=100, alpha=0.1 @ x=20, beta=0.9 @ x=80.
752 // k ~= 0.073, x0 = 50 (symmetric since alpha = 1 - beta).
753 plugin_test! {
754 model: SigmoidModel,
755 input: u64,
756 output: u64,
757 context: SigmoidModelConfig<FixedU128>,
758 value: SigmoidModelConfig {
759 max_output: FixedU128::from_inner(100_000_000_000_000_000_000), // 100.0
760 start_frac: FixedU128::from_inner(100_000_000_000_000_000), // 0.1
761 end_frac: FixedU128::from_inner(900_000_000_000_000_000), // 0.9
762 start_x: FixedU128::from_inner(20_000_000_000_000_000_000), // 20.0
763 end_x: FixedU128::from_inner(80_000_000_000_000_000_000), // 80.0
764 },
765 cases: {
766 // deep in the lower tail -- sigmoid never reaches 0, f(0) ~= 2.5 -> 2
767 (sigmoid_model_zero_input, 0, 2),
768 // x_alpha is a definition point, output is exactly alpha * L = 10
769 (sigmoid_model_at_start, 20, 10),
770 // fixed-point rounding shifts x0 slightly, so f(50) = 49.999... -> 49
771 (sigmoid_model_midpoint, 50, 49),
772 // same rounding at x_beta: f(80) = 89.999... -> 89 instead of 90
773 (sigmoid_model_at_end, 80, 89),
774 // deep in the upper tail, f(100) ~= 97.5 -> 97
775 (sigmoid_model_high_input, 100, 97),
776 }
777 }
778
779 // Steep curve: L=200, alpha=0.1 @ x=45, beta=0.9 @ x=55.
780 // k ~= 0.439, x0 = 50 (symmetric). Growth happens over just 10 units.
781 plugin_test! {
782 model: SigmoidModel,
783 input: i64,
784 output: i64,
785 context: SigmoidModelConfig<FixedI128>,
786 value: SigmoidModelConfig {
787 max_output: FixedI128::from_inner(200_000_000_000_000_000_000), // 200.0
788 start_frac: FixedI128::from_inner(100_000_000_000_000_000), // 0.1
789 end_frac: FixedI128::from_inner(900_000_000_000_000_000), // 0.9
790 start_x: FixedI128::from_inner(45_000_000_000_000_000_000), // 45.0
791 end_x: FixedI128::from_inner(55_000_000_000_000_000_000), // 55.0
792 },
793 cases: {
794 // x=40 is 5 below x_alpha, still in the tail -- f(40) ~= 2.4 -> 2,
795 // not 20; alpha*L is only guaranteed at x_alpha itself, not before it
796 (sigmoid_steep_below_start, 40, 2),
797 // x_alpha is a definition point, output is exactly alpha * L = 20
798 (sigmoid_steep_at_start, 45, 20),
799 // fixed-point rounding: f(50) = 99.999... -> 99 instead of 100
800 (sigmoid_steep_midpoint, 50, 99),
801 // same rounding at x_beta: f(55) = 179.999... -> 179 instead of 180
802 (sigmoid_steep_at_end, 55, 179),
803 // x=60 is 5 above x_beta, deep in the upper tail -- f(60) ~= 197.6 -> 197
804 (sigmoid_steep_above_end, 60, 197),
805 }
806 }
807
808 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
809 // ```````````````````````````` EXPONENTIAL-INFLUENCE ````````````````````````````
810 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
811
812 plugin_test! {
813 model: ExponentialModel,
814 input: u64,
815 output: u64,
816 context: ExponentialModelConfig<FixedU128>,
817 value: ExponentialModelConfig {
818 growth_rate: FixedU128::saturating_from_integer(1) // 1.0
819 },
820 cases: {
821 (exponential_model_unsigned_zero_input, 0, 1), // e^(1.0 * 0) = e^0 = 1
822 (exponential_model_unsigned_one_input, 1, 2), // e^(1.0 * 1) = e^1 ~= 2.718 -> 2
823 (exponential_model_unsigned_two_input, 2, 7), // e^(1.0 * 2) = e^2 ~= 7.389 -> 7
824 (exponential_model_unsigned_three_input, 3, 20), // e^(1.0 * 3) = e^3 ~= 20.085 -> 20
825 (exponential_model_unsigned_five_input, 5, 148), // e^(1.0 * 5) = e^5 ~= 148.413 -> 148
826 }
827 }
828
829 plugin_test! {
830 model: ExponentialModel,
831 input: i64,
832 output: i64,
833 context: ExponentialModelConfig<FixedI128>,
834 value: ExponentialModelConfig {
835 growth_rate: FixedI128::saturating_from_integer(1) // 1.0
836 },
837 cases: {
838 (exponential_model_signed_zero, 0, 1), // e^0 = 1
839 (exponential_model_signed_positive, 1, 2), // e^1 ~= 2.718 -> 2
840 (exponential_model_signed_negative, -1, 0), // e^(-1) ~= 0.367 -> 0
841 (exponential_model_signed_negative_two, -2, 0), // e^(-2) ~= 0.135 -> truncates to 0
842 (exponential_model_signed_large_negative, -5, 0), // e^(-5) ~= 0.0067 -> 0
843 (exponential_model_signed_two, 2, 7), // e^2 ~= 7.389 -> 7
844 }
845 }
846
847 //------ ExponentialModel with smaller growth rate
848 plugin_test! {
849 model: ExponentialModel,
850 input: u64,
851 output: u64,
852 context: ExponentialModelConfig<FixedU128>,
853 value: ExponentialModelConfig {
854 growth_rate: FixedU128::saturating_from_rational(1, 2) // 0.5
855 },
856 cases: {
857 (exponential_model_small_rate_zero, 0, 1), // e^(0.5 * 0) = 1
858 (exponential_model_small_rate_one, 1, 1), // e^(0.5 * 1) ~= 1.648 -> 1
859 (exponential_model_small_rate_two, 2, 2), // e^(0.5 * 2) = e^1 ~= 2.718 -> 2
860 (exponential_model_small_rate_four, 4, 7), // e^(0.5 * 4) = e^2 ~= 7.389 -> 7
861 (exponential_model_small_rate_ten, 10, 148), // e^(0.5 * 10) = e^5 ~= 148.413 -> 148
862 }
863 }
864
865 //------ ExponentialModel with high growth rate
866 plugin_test! {
867 model: ExponentialModel,
868 input: u64,
869 output: u64,
870 context: ExponentialModelConfig<FixedU128>,
871 value: ExponentialModelConfig {
872 growth_rate: FixedU128::saturating_from_integer(2) // 2.0
873 },
874 cases: {
875 (exponential_model_high_rate_zero, 0, 1), // e^(2.0 * 0) = 1
876 (exponential_model_high_rate_one, 1, 7), // e^(2.0 * 1) = e^2 ~= 7.389 -> 7
877 (exponential_model_high_rate_two, 2, 54), // e^(2.0 * 2) = e^4 ~= 54.598 -> 54
878 (exponential_model_high_rate_three, 3, 403), // e^(2.0 * 3) = e^6 ~= 403.428 -> 403
879 }
880 }
881
882 // --- ExponentialModel: k = 0 -> e^0 = 1 for all inputs ---
883 plugin_test! {
884 model: ExponentialModel,
885 input: u64,
886 output: u64,
887 context: ExponentialModelConfig<FixedU128>,
888 value: ExponentialModelConfig {
889 growth_rate: FixedU128::zero()
890 },
891 cases: {
892 (exponential_model_zero_rate_zero_input, 0, 1),
893 (exponential_model_zero_rate_large_input, 1_000_000, 1),
894 }
895 }
896
897 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
898 // ``````````````````````````````` BINARY-INFLUENCE ``````````````````````````````
899 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
900
901 plugin_test! {
902 model: BinaryModel,
903 input: u64,
904 output: u64,
905 context: BinaryModelConfig<u64>,
906 value: BinaryModelConfig {
907 pass_threshold: 100,
908 pass_value: 1,
909 fail_value: 0
910 },
911 cases: {
912 (binary_model_unsigned_above_threshold, 101, 1),
913 (binary_model_unsigned_below_threshold, 99, 0),
914 (binary_model_unsigned_equal_to_threshold, 100, 1),
915 }
916 }
917
918 plugin_test! {
919 model: BinaryModel,
920 input: i64,
921 output: i64,
922 context: BinaryModelConfig<i64>,
923 value: BinaryModelConfig {
924 pass_threshold: 50,
925 pass_value: 1,
926 fail_value: -1
927 },
928 cases: {
929 (binary_model_signed_abv_threshold, 51, 1),
930 (binary_model_signed_blw_threshold, 49, -1),
931 (binary_model_signed_eql_to_threshold, 50, 1),
932 }
933 }
934
935 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
936 // ````````````````````````````` CAPPED-LINEAR-INFLUENCE `````````````````````````
937 // ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
938
939 plugin_test! {
940 model: CappedLinearModel,
941 input: u64,
942 output: u64,
943 context: CappedLinearModelConfig<u64>,
944 value: CappedLinearModelConfig {
945 max_influence: 100
946 },
947 cases: {
948 (capped_linear_unsigned_model_above_cap, 150, 100),
949 (capped_linear_unsigned_model_below_cap, 99, 99),
950 (capped_linear_unsigned_model_equal_to_cap, 100, 100),
951 }
952 }
953
954 plugin_test! {
955 model: CappedLinearModel,
956 input: i64,
957 output: i64,
958 context: CappedLinearModelConfig<i64>,
959 value: CappedLinearModelConfig {
960 max_influence: -50
961 },
962 cases: {
963 (capped_linear_model_signed_above_negative_cap, -75, -75),
964 (capped_linear_model_signed_below_negative_cap, -35, -50),
965 (capped_linear_model_signed_equal_to_negative_cap, -50, -50),
966 }
967 }
968}