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}