score-set 2.1.0

A Rust library for building static weighted scoring operator sets
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
# score-set
## License
`score-set` provides small, statically composed primitives for building
weighted scoring functions.

A metric consists of:

- a `Measure<Ctx>` that extracts a raw value from a context;
- a `Map01F32` or `Map01F64` that normalizes that value;
- a weight applied to the normalized score.

The normalized result is returned as `Witnessed<f32, V01>` or
`Witnessed<f64, V01>`. This makes the `[0, 1]` boundary an explicit type-level
fact for downstream code.

## Installation

```toml
[dependencies]
score-set = "2.1.0"
```

## Quick start

The measurement output and map input are connected through associated types.
The mapper must accept exactly the value produced by the measurement.

```rust
use score_set::{Metric64, traits::{EvalF64, Map01F64, Measure, V01, prove_v01_f64}};
use witnessed::{WitnessExt, Witnessed};

struct Context {
    latency_ms: f64,
}

struct Latency;

impl Measure<Context> for Latency {
    type Output = f64;

    fn measure(&self, ctx: &Context) -> Self::Output {
        ctx.latency_ms
    }
}

struct LowerIsBetter {
    limit: f64,
}

impl Map01F64 for LowerIsBetter {
    type Input = f64;

    fn map(&self, value: Self::Input) -> Witnessed<f64, V01> {
        let score = (1.0 - value / self.limit).clamp(0.0, 1.0);

        score.witness().by(prove_v01_f64).expect("score was clamped")
    }
}

let metric = Metric64::new(Latency, LowerIsBetter { limit: 100.0 }, 0.7);
let score = metric.eval(&Context { latency_ms: 40.0 });

assert!((score - 0.42).abs() < 1e-12);
```

`Map01F32` has the same API and returns `Witnessed<f32, V01>`:

```rust
use score_set::traits::{Map01F32, V01, prove_v01_f32};
use witnessed::{WitnessExt, Witnessed};

struct Identity;

impl Map01F32 for Identity {
    type Input = f32;

    fn map(&self, value: Self::Input) -> Witnessed<f32, V01> {
        let score = value.clamp(0.0, 1.0);

        score.witness().by(prove_v01_f32).expect("score was clamped")
    }
}
```

## Dynamic score sets

`DynScoreSet32<Ctx>` and `DynScoreSet64<Ctx>` store heterogeneous metrics
behind `EvalF32<Ctx>` or `EvalF64<Ctx>` trait objects.

```rust
use score_set::{DynScoreSet64, Metric64};

let score_set = DynScoreSet64::<Context>::builder()
    .append(Metric64::new(Latency, LowerIsBetter { limit: 100.0 }, 0.7))
    .build();

let score = score_set.eval(&Context { latency_ms: 40.0 });
```

Use the concrete `Metric32`/`Metric64` types when the metric composition is
known at compile time. Use a dynamic score set when the enabled metrics are
selected at runtime.

## Witnesses

`Measure` returns an ordinary associated `Output`. The witness is produced by
the normalization map:

```rust
pub trait Measure<Ctx: ?Sized> {
    type Output;

    fn measure(&self, ctx: &Ctx) -> Self::Output;
}

pub trait Map01F64 {
    type Input;

    fn map(&self, value: Self::Input) -> Witnessed<f64, V01>;
}
```

`V01` is a marker type representing a value known to be in the normalized
`[0, 1]` range. `Witnessed<T, V01>` is a transparent wrapper and has no
runtime witness field.

When a result is derived from already-witnessed values and rechecking is
unnecessary, `by_unchecked` may be used at an explicitly audited unsafe
boundary. The caller must document why the invariant is preserved.

## Design Rationale

`score-set` models a score as a weighted composition of a measurement and a mapping function.

For a context `x`, a single metric is defined as:

```text
metric(x) = w · g(m(x))
```

where:

* `m` is a measurement;
* `g` maps the measurement result into a normalized score;
* `w` is the metric weight.

A complete score set evaluates multiple metrics and sums their contributions:

```text
score(x) = Σ(i=1..n) w_i · g_i(m_i(x))
```

The library needs to support two different use cases:

1. a fixed, predefined metric set used by most applications;
2. a runtime-configurable metric set, typically constructed from protobuf configuration.

These use cases have different implementation requirements and are therefore represented by separate execution paths.

## Static metric composition

When the metric set is known at compile time, metrics can be represented directly through generic composition:

```rust
pub struct Metric<M, G> {
    measure: M,
    map: G,
    weight: f64,
}
```

A statically defined score set can contain concrete metric types:

```rust
pub struct DefaultScoreSet {
    latency: Metric<Latency, Cauchy>,
    cpu: Metric<CpuUsage, Linear>,
    similarity: Metric<Similarity, Sigmoid>,
}
```

Its evaluation can be written as a direct expression:

```rust
impl Eval<Context> for DefaultScoreSet {
    #[inline]
    fn eval(&self, ctx: &Context) -> f64 {
        self.latency.eval(ctx)
            + self.cpu.eval(ctx)
            + self.similarity.eval(ctx)
    }
}
```

This representation allows Rust to monomorphize the complete evaluation path. It requires no runtime type selection and permits inlining across the measurement, mapping, and aggregation layers.

This is the preferred representation for predefined score sets.

## Runtime-configurable metric composition

A runtime configuration may select an arbitrary subset of available metrics.

For example, one configuration may select:

```text
LatencyCauchy
CpuLinear
```

while another may select:

```text
LatencyIdentity
SimilaritySigmoid
MemoryLinear
```

The concrete generic types of these score sets are different. A function that constructs a score set from runtime data must nevertheless return one stable Rust type.

This creates a fundamental distinction between compile-time and runtime composition.

A compile-time composition may have a type such as:

```text
Append<
    Append<Zero, LatencyCauchy>,
    CpuLinear
>
```

However, a runtime configuration may produce any of the following:

```text
Zero
Append<Zero, LatencyCauchy>
Append<Zero, CpuLinear>
Append<Append<Zero, LatencyCauchy>, CpuLinear>
```

These are different concrete Rust types.

Return-position `impl Trait` does not unify them. It hides one concrete type selected at compile time; it does not represent several types selected by runtime data.

In general, the following three properties cannot be obtained simultaneously
in ordinary ahead-of-time Rust:

```text
runtime-selected structure
+ one concrete static type
+ no enumeration of all structures
```

A runtime-configurable implementation therefore requires a common representation.

## Alternatives considered

Several representations were considered.

### Fixed complete metric set

All possible metrics can be stored in one fixed structure, with disabled metrics assigned zero weight.

This provides a single static type and avoids dynamic dispatch. However, disabled metrics may still require evaluation, and expensive measurements may be repeated unnecessarily.

This approach is appropriate when:

* the complete metric set is small;
* most metrics are usually enabled;
* individual measurements are inexpensive.

It is less suitable for sparse runtime configurations.

### Enumeration of all metric combinations

Every possible runtime subset can be represented as a separate enum variant.

For `N` independently optional metrics, the number of possible subsets is
`2^N`.

For example:

| Optional metrics | Possible subsets |
| ---------------: | ---------------: |
|                2 |                4 |
|                4 |               16 |
|                8 |              256 |
|               16 |           65,536 |

This representation can provide near-static runtime performance, but its code size and compile-time cost grow exponentially.

It is not suitable as a general-purpose library strategy.

### Cartesian-product enum

If the sets of measurements and mappings are closed, the library can generate one enum variant for each supported pair:

```rust
pub enum MetricOp {
    LatencyIdentity(Metric<Latency, Identity>),
    LatencyCauchy(Metric<Latency, Cauchy>),
    CpuLinear(Metric<CpuUsage, Linear>),
}
```

A runtime score set can then be represented as:

```rust
pub struct ScoreSet {
    metrics: Box<[MetricOp]>,
}
```

Each metric evaluation performs one enum dispatch, after which the concrete measurement and mapping types are known.

This avoids trait objects and preserves static dispatch inside each enum branch.
However, the generated representation grows with the Cartesian product
`|M| × |G|`, where `M` is the measurement set and `G` is the mapping set.

Adding a new measurement or mapping expands the generated enum and its conversion logic.

### JIT compilation

A runtime configuration could be translated into an intermediate representation and compiled into a specialized native function.

Conceptually:

```text
protobuf configuration
    -> score plan
    -> JIT intermediate representation
    -> native scoring function
```

This can provide runtime-selected composition without per-metric dispatch.

However, it introduces substantial engineering requirements:

* executable-memory management;
* ABI boundaries between generated code and Rust;
* unsafe function-pointer handling;
* platform-specific testing;
* lifetime management for compiled code and metric state;
* integration of user-defined measurements;
* runtime compilation overhead.

For the expected number and cost of metrics, this complexity is not justified.

### Dynamic dispatch

The simplest runtime representation is a heterogeneous collection of evaluators:

```rust
pub trait Eval<Ctx> {
    fn eval(&self, ctx: &Ctx) -> f64;
}

pub struct DynScoreSet<Ctx> {
    metrics: Box<[
        Box<dyn Eval<Ctx> + Send + Sync>
    ]>,
}
```

Concrete metrics remain generic:

```rust
pub struct Metric<M, G> {
    measure: M,
    map: G,
    weight: f64,
}
```

and implement the common evaluation interface:

```rust
impl<Ctx, M, G> Eval<Ctx> for Metric<M, G>
where
    M: Measure<Ctx>,
    G: Map01,
{
    fn eval(&self, ctx: &Ctx) -> f64 {
        self.weight
            * self.map.map(
                self.measure.measure(ctx),
            )
    }
}
```

Runtime configuration constructs only the enabled metrics:

```rust
let mut metrics = Vec::new();

if let Some(config) = proto.latency_cauchy {
    metrics.push(Box::new(
        Metric::<Latency, Cauchy>::compile(config)?,
    ));
}

if let Some(config) = proto.cpu_linear {
    metrics.push(Box::new(
        Metric::<CpuUsage, Linear>::compile(config)?,
    ));
}
```

The dynamic boundary exists only between the score set and each concrete metric:

```text
DynScoreSet
    -> dyn Eval
    -> Metric<M, G>
```

Inside `Metric<M, G>::eval`, both the measurement type and mapping type remain concrete and monomorphized.

The runtime cost is one indirect call per enabled metric. In exchange, the representation provides:

* arbitrary runtime metric subsets;
* execution of enabled metrics only;
* a stable return type;
* straightforward ownership and lifetime management;
* no generated Cartesian-product enum;
* no exponential type expansion;
* simple addition of new measurements and mappings.

## Selected design

The library uses two execution paths.

### Default path

The default metric set is represented as a concrete static type.

```text
default configuration
    -> static score set
    -> monomorphized evaluation
```

This path is intended for the common case and provides:

* static dispatch;
* direct aggregation;
* full inlining opportunities;
* no runtime metric-selection overhead.

### Custom path

User-defined runtime configurations are represented by `DynScoreSet`.

```text
custom configuration
    -> concrete Metric<M, G> values
    -> Box<dyn Eval<Ctx>>
    -> dynamic score set
```

This path provides runtime flexibility while keeping the implementation small and maintainable.

The expected workload is dominated by the default configuration. Therefore, the static path optimizes the common case, while the dynamic path handles uncommon custom configurations without imposing additional complexity on the entire library.

## Execution-path selection

The execution mode can be selected during initialization:

```rust
match custom_config {
    None => {
        let score_set = DefaultScoreSet::compile()?;
        run_service(score_set).await
    }

    Some(config) => {
        let score_set = DynScoreSet::compile(config)?;
        run_service(score_set).await
    }
}
```

The service itself remains generic:

```rust
async fn run_service<E>(
    score_set: E,
) -> Result<(), Error>
where
    E: Eval<Context> + Send + Sync + 'static,
{
    // Service implementation.
}
```

The compiler only needs to instantiate the service for the two top-level score-set types:

```text
run_service::<DefaultScoreSet>
run_service::<DynScoreSet>
```

It does not need to instantiate the service for every possible metric subset.

## Design principle

The selected design does not attempt to force compile-time and runtime composition into one representation.

Instead, it uses the representation appropriate to each case:

```text
predefined configuration -> static composition
runtime configuration    -> dynamic composition
```

Dynamic dispatch is limited to the boundary where runtime heterogeneity must be represented. The internal implementation of each concrete metric remains generic and statically typed.

This keeps the common path fully static while allowing the configurable path to remain direct, extensible, and maintainable.

## Application integration guidance

The library does not require applications to use separate static and dynamic execution paths.

It provides the building blocks needed for both forms of composition:

* concrete generic metrics such as `Metric<M, G>`;
* a common `Eval<Ctx>` interface;
* a dynamically composed score set for runtime-defined configurations.

Applications may choose the representation that best matches their workload.

### Static application-defined score sets

When an application has a predefined metric set, it may define a concrete score-set type directly:

```rust
pub struct DefaultScoreSet {
    latency: Metric<Latency, Cauchy>,
    cpu: Metric<CpuUsage, Linear>,
    similarity: Metric<Similarity, Sigmoid>,
}
```

Its evaluation can be expressed through direct aggregation:

```rust
impl Eval<Context> for DefaultScoreSet {
    #[inline]
    fn eval(&self, ctx: &Context) -> f64 {
        self.latency.eval(ctx)
            + self.cpu.eval(ctx)
            + self.similarity.eval(ctx)
    }
}
```

Because the complete structure is known at compile time, Rust may monomorphize and inline the evaluation path.

This type is application-defined. It is not a special execution mode required or managed by the library.

### Runtime-configurable score sets

When a metric set is selected from runtime data, the application may construct a `DynScoreSet`:

```text
runtime configuration
    -> concrete Metric<M, G> values
    -> Box<dyn Eval<Ctx>>
    -> DynScoreSet
```

This representation supports arbitrary runtime-selected metric subsets while preserving concrete generic implementations inside each metric.

The dynamic boundary is limited to the collection of heterogeneous metrics:

```text
DynScoreSet
    -> dyn Eval<Ctx>
    -> Metric<M, G>
```

Inside each concrete `Metric<M, G>`, the measurement and mapping types remain statically known.

### Optional application-level specialization

Applications whose workload is dominated by one predefined configuration may choose to use a static type for that common case and `DynScoreSet` only for runtime overrides.

For example:

```rust
match custom_config {
    None => {
        let score_set = DefaultScoreSet::new()?;
        run_service(score_set).await
    }

    Some(config) => {
        let score_set = DynScoreSet::compile(config)?;
        run_service(score_set).await
    }
}
```

The service can remain generic:

```rust
async fn run_service<E>(
    score_set: E,
) -> Result<(), Error>
where
    E: Eval<Context> + Send + Sync + 'static,
{
    // Service implementation.
}
```

In this architecture, the compiler instantiates the service for the application-defined top-level score-set types:

```text
run_service::<DefaultScoreSet>
run_service::<DynScoreSet>
```

It does not instantiate the service for every possible runtime metric subset.

This split is an application optimization, not a requirement of `score-set`.

## Representation principle

Compile-time and runtime composition have different representation requirements:

```text
compile-time-known composition -> concrete generic type
runtime-selected composition   -> type-erased heterogeneous collection
```

The library supports both representations without requiring applications to expose both.

Applications may use only concrete score sets, only `DynScoreSet`, or a combination of the two.

Dynamic dispatch is introduced only when runtime heterogeneity must be represented. Concrete metric implementations remain generic and statically typed.

## License

Licensed under either of:

- Apache License, Version 2.0
- MIT License