Historical Universal Fuzzy Scale¶
The Universal Fuzzy Scale is the historical five-level preset used by
FuzzyRoutines consumers: Min → Low → Med → High → Max. This page preserves
its original coefficients and shows how to reconstruct its classifier with the
modern immutable API. “Universal” is the preset's historical name, not a claim
that these coefficients fit every application. It is not the three-level default
FuzzyScale, nor a set of five equally spaced triangles.
Both panels use the same coefficients. The left panel evaluates the historical facade; the right evaluates the explicit modern construction. The Min curve drops sharply around 0.125; Low, Med and High have flat tops and quadratic shoulders; Max rises between 0.77 and 0.95. The small Min tail remains positive even where it is invisible at this scale.
Cybersecurity and risk levels¶
The scale can turn a normalized score into linguistic levels for cybersecurity
and risk assessment: for example, the risk of an asset, the severity of a
security finding, or the effectiveness of protective controls. First define
how observations or expert assessments produce a score in [0, 1], and what
an increase means. Higher risk and stronger protection have opposite meanings
for decisions, even when both use the labels Min, Low, Med, High and Max.
For an illustrative risk score of 0.75, the historical scale selects High
with membership 1. This means full membership in the High term under this
model; neither number is a probability of an incident. Keep the membership
grades alongside the selected label to expose transitions between levels.
Choose normalization, aggregation and response rules for the application,
validate them against domain evidence, and handle weak coverage explicitly as
shown below. The scale supplies linguistic interpretation of an input score;
it does not by itself measure risk or define an incident-response policy.
The historical definition¶
| Level | Historical family / modern constructor | Historical supportSet |
|---|---|---|
| Min | hyperbolic / Hyperbolic(8, 20, 0) |
[0, 0.23] |
| Low | bell / Bell(0.17, 0.23, 0.34) |
[0.17, 0.40] |
| Med | bell / Bell(0.34, 0.40, 0.60) |
[0.34, 0.66] |
| High | bell / Bell(0.60, 0.66, 0.77) |
[0.60, 0.83] |
| Max | parabolic / SShoulder(0.77, 0.95) |
[0.77, 1] |
Here supportSet is the historical finite centroid integration window.
UniversalFuzzyScale.Fuzzy(x) evaluates each raw mFunction.mju(x) and does
not clip the membership curve to that window. In particular, Min remains
positive beyond 0.23. The modern reconstruction therefore gives every term the
same closed universe [0, 1]; assigning those five windows as term universes
would change behavior and make classification fail for most coordinates.
For normalized coordinates \(0\le x\le1\), define the quadratic rising shoulder:
A bell's right foot is \(c+b-a\); its core is the entire interval \([b,c]\). The scale is not a partition of unity: grades need not sum to one and are not probabilities. The independent formulas above are evaluated in the executable example and plot tests without calling the membership API.
Existing historical code¶
from math import isclose
from fuzzyroutines.FuzzyRoutines import UniversalFuzzyScale
scale = UniversalFuzzyScale()
assert [level["name"] for level in scale.levels] == ["Min", "Low", "Med", "High", "Max"]
grades = {level["name"]: level["fSet"].mFunction.mju(0.2) for level in scale.levels}
assert isclose(grades["Low"], 0.5)
assert scale.Fuzzy(0.2)["name"] == "Low"
assert scale.GetLevelByName("Med")["fSet"].supportSet == (0.34, 0.66)
assert scale.GetLevelByName("Min")["fSet"].mFunction.mju(0.5) > 0
print(grades, scale.Fuzzy(0.2)["name"])
The compatibility facade retains the old names and constructor. This example runs on version 2.0; it does not assert that every numerical bug in 1.0.3 is preserved. In particular, the reviewed centroid corrections are documented in the migration notes.
Explicit construction with the modern API¶
from math import isclose
from fuzzyroutines import (
Bell, ContinuousUniverse, FuzzificationPolicy, Hyperbolic,
LinguisticScale, LinguisticTerm, ScalarFuzzySet, SShoulder,
)
from fuzzyroutines.FuzzyRoutines import UniversalFuzzyScale
universe = ContinuousUniverse(0, 1, leftClosed=True, rightClosed=True)
models = (
("Min", Hyperbolic(8, 20, 0)),
("Low", Bell(0.17, 0.23, 0.34)),
("Med", Bell(0.34, 0.40, 0.60)),
("High", Bell(0.60, 0.66, 0.77)),
("Max", SShoulder(0.77, 0.95)),
)
scale = LinguisticScale(tuple(
LinguisticTerm(name, ScalarFuzzySet(universe, model))
for name, model in models
))
legacy = UniversalFuzzyScale()
policy = FuzzificationPolicy(tiePolicy="last", tieTolerance=0)
for coordinate in (0, 0.125, 0.17, 0.2, 0.37, 0.5, 0.63, 0.81, 0.86, 1):
result = scale.Fuzzify(coordinate, policy)
for item, previous in zip(result.memberships, legacy.levels, strict=True):
assert isclose(item.grade, previous["fSet"].mFunction.mju(coordinate), abs_tol=1e-12)
assert result.selectedTerms[0].name == legacy.Fuzzy(coordinate)["name"]
assert isclose(scale.Fuzzify(0.125).confidence, 0.5)
assert not scale.Fuzzify(0.17, FuzzificationPolicy(minimumConfidence=0.1)).isMatch
assert not scale.Fuzzify(0.125, FuzzificationPolicy(minimumConfidence=0.5)).isMatch
print([(item.term.name, item.grade) for item in scale.Fuzzify(0.2).memberships])
The historical parabolic membership is the modern SShoulder, not a parabola
extending without bounds. Keep the original level order and tiePolicy="last"
for historical winner selection. The default modern policy chooses the first
exact maximum; both APIs agree on the five curves, but policy choices must be
explicit in a migration.
Values, boundaries, and decisions¶
| Coordinate | Result / grade |
|---|---|
| 0 | Min = 1; included left boundary |
| 0.125 | Min = 0.5 |
| 0.17 | Min ≈ 0.002129589894; Low = 0: weak coverage |
| 0.20 | Low = 0.5; Min ≈ 0.000082711220 |
| 0.50 | Med = 1; Min ≈ 9.094947018 × 10⁻¹³ |
| 0.81 | High ≈ 2/9, Max ≈ 8/81; select High |
| 0.86 | Max ≈ 0.5 |
| 1 | Max = 1; included right boundary |
The model intentionally retains weak coverage around the transition from Min
to Low; the graph must not be “fixed” by moving coefficients. At 0.17 the
historical classifier selects Min, while an explicit modern threshold of 0.1
returns no match. minimumConfidence is strict: a maximum grade equal to the
threshold also yields no match. The threshold is a membership requirement,
not a probability of a correct decision.
In exact real arithmetic the adjacent bells tie at 0.37 and 0.63. Floating-point
evaluation need not produce exact equality: at 0.37 the current evaluations are
Low ≈ 0.5000000000000009 and Med ≈ 0.4999999999999991, so historical selection
is Low. Do not assert that a mathematically symmetric midpoint must select
the later level. tieTolerance=0 preserves the exact comparison; setting a
positive tolerance intentionally changes near-tie behavior.
The modern example rejects values outside [0,1]. The historical facade
accepts other finite coordinates (for example −0.1 selects Min and 1.1 selects
Max). Validate/normalize physical inputs before classification; do not silently
clamp them. Historical centroids also require their original per-level
integration windows, rather than integrating every modern term over [0,1].
Reproduce and verify¶
python examples/guide.py --scenario universal-fuzzy-scale
The scenario compares both APIs with independent piecewise formulas on 1001
coordinates 0, 0.001, …, 1, checks selected labels and weak-coverage policy,
and preserves the small positive Min tail. The two plotted panels independently
verify all 401 coordinates per curve, ten curves in total. These finite grids
are regression evidence, not a proof over every real input. See
figure reproduction for the pinned SVG toolchain and
the frozen provenance
for the original coefficients and centroid windows.