rti-ethics-engine 1.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- rti_ethics_engine/__init__.py +73 -0
- rti_ethics_engine/config.py +169 -0
- rti_ethics_engine/core.py +184 -0
- rti_ethics_engine/dimensions.py +373 -0
- rti_ethics_engine/metrics.py +52 -0
- rti_ethics_engine/results.py +386 -0
- rti_ethics_engine-1.0.0.dist-info/METADATA +198 -0
- rti_ethics_engine-1.0.0.dist-info/RECORD +10 -0
- rti_ethics_engine-1.0.0.dist-info/WHEEL +4 -0
- rti_ethics_engine-1.0.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTI Ethics Engine - Production AI Ethics Framework for RTI Applications
|
|
3
|
+
India's first granular 9-dimension AI ethics evaluator (36 metrics)
|
|
4
|
+
|
|
5
|
+
Quick Start:
|
|
6
|
+
from rti_ethics_engine import evaluate
|
|
7
|
+
|
|
8
|
+
metrics = {
|
|
9
|
+
"fairness": {"spd": 0.02, "di": 1.0, "eop": 0.05, "aod": 0.03},
|
|
10
|
+
"legal": {"lawfulness": 1.0, "cvr": 0.98},
|
|
11
|
+
# ... other dimensions
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
result = evaluate(metrics)
|
|
15
|
+
print(result.final_label) # "ETHICAL"
|
|
16
|
+
print(result.s_llm) # 0.92
|
|
17
|
+
|
|
18
|
+
# Granular access
|
|
19
|
+
print(result.get_l1_metric("fairness", "spd")) # 0.02
|
|
20
|
+
print(result.get_failed_dims()) # []
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from .core import EthicsEngine
|
|
24
|
+
from .results import EthicsResult, DimensionResult, RiskBand, FinalLabel
|
|
25
|
+
from .config import (
|
|
26
|
+
L1_THRESHOLDS,
|
|
27
|
+
DIMENSION_WEIGHTS,
|
|
28
|
+
RISK_THRESHOLDS,
|
|
29
|
+
DIMENSION_INFO,
|
|
30
|
+
METRIC_CATALOG
|
|
31
|
+
)
|
|
32
|
+
from .metrics import build_default_metrics
|
|
33
|
+
|
|
34
|
+
__version__ = "1.0.0"
|
|
35
|
+
__author__ = "RTI Ethics Engine Team"
|
|
36
|
+
__license__ = "MIT"
|
|
37
|
+
|
|
38
|
+
__all__ = [
|
|
39
|
+
# Core classes
|
|
40
|
+
"EthicsEngine",
|
|
41
|
+
"EthicsResult",
|
|
42
|
+
"DimensionResult",
|
|
43
|
+
# Enums
|
|
44
|
+
"RiskBand",
|
|
45
|
+
"FinalLabel",
|
|
46
|
+
# Config
|
|
47
|
+
"L1_THRESHOLDS",
|
|
48
|
+
"DIMENSION_WEIGHTS",
|
|
49
|
+
"RISK_THRESHOLDS",
|
|
50
|
+
"DIMENSION_INFO",
|
|
51
|
+
"METRIC_CATALOG",
|
|
52
|
+
# Helpers
|
|
53
|
+
"evaluate",
|
|
54
|
+
"build_default_metrics",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def evaluate(metrics: dict) -> EthicsResult:
|
|
59
|
+
"""
|
|
60
|
+
Quick evaluation entrypoint.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
metrics: Dict of dimension -> metrics mapping.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
EthicsResult with complete evaluation and granular accessors.
|
|
67
|
+
|
|
68
|
+
Example:
|
|
69
|
+
result = evaluate({"fairness": {"spd": 0.02}, "legal": {"lawfulness": 1.0}})
|
|
70
|
+
print(result.final_label) # "ETHICAL"
|
|
71
|
+
"""
|
|
72
|
+
engine = EthicsEngine()
|
|
73
|
+
return engine.run_protocol(metrics)
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTI Ethics Engine - Configuration Module
|
|
3
|
+
Centralized thresholds and weights for all 36 metrics.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Dict, Any
|
|
7
|
+
|
|
8
|
+
# ================== L1 THRESHOLDS (Hard Gates) ==================
|
|
9
|
+
# These are the strict mathematical gates that must pass for L1 approval.
|
|
10
|
+
# Violation of ANY threshold results in L1 FAIL for that dimension.
|
|
11
|
+
|
|
12
|
+
L1_THRESHOLDS: Dict[str, Dict[str, float]] = {
|
|
13
|
+
"fairness": {
|
|
14
|
+
"spd": 0.1, # Statistical Parity Difference: |SPD| <= 0.1
|
|
15
|
+
"di_min": 0.8, # Disparate Impact: 0.8 <= DI <= 1.25
|
|
16
|
+
"di_max": 1.25,
|
|
17
|
+
"eop": 0.1, # Equal Opportunity Difference: |EOP| <= 0.1
|
|
18
|
+
"aod": 0.1 # Average Odds Difference: |AOD| <= 0.1
|
|
19
|
+
},
|
|
20
|
+
"drift": {
|
|
21
|
+
"psi": 0.2, # Population Stability Index: PSI <= 0.2
|
|
22
|
+
"ks": 0.05 # Kolmogorov-Smirnov statistic: KS <= 0.05
|
|
23
|
+
},
|
|
24
|
+
"environmental": {
|
|
25
|
+
"inf_cost_max": 0.05, # Max inference cost (USD)
|
|
26
|
+
"gpu_hours_max": 0.1 # Max GPU hours per query
|
|
27
|
+
},
|
|
28
|
+
"explainability": {
|
|
29
|
+
"confidence_min": 0.6, # Minimum model confidence
|
|
30
|
+
"fi_min": 0.1 # Minimum feature importance coverage
|
|
31
|
+
},
|
|
32
|
+
"ux": {
|
|
33
|
+
"tcr_min": 0.85, # Task Completion Rate: TCR >= 0.85
|
|
34
|
+
"ftr_max": 0.15 # Frustration Rate: FTR <= 0.15
|
|
35
|
+
},
|
|
36
|
+
"legal": {
|
|
37
|
+
"lawfulness": 1.0, # Must have legal basis (binary)
|
|
38
|
+
"cvr_min": 0.95 # Consent Validity Rate: CVR >= 0.95
|
|
39
|
+
},
|
|
40
|
+
"performance": {
|
|
41
|
+
"accuracy_min": 0.8, # Minimum accuracy
|
|
42
|
+
"recall_min": 0.75 # Minimum recall
|
|
43
|
+
},
|
|
44
|
+
"monitoring": {
|
|
45
|
+
"alc_min": 0.95, # Audit Log Completeness: ALC >= 0.95
|
|
46
|
+
"sla_min": 0.99 # SLA Compliance: SLA >= 0.99
|
|
47
|
+
},
|
|
48
|
+
"security": {
|
|
49
|
+
"adv_acc_min": 0.7, # Adversarial Accuracy: AdvAcc >= 0.7
|
|
50
|
+
"pisr_max": 0.05 # Prompt Injection Success Rate: PISR <= 0.05
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ================== DIMENSION WEIGHTS (Step 904) ==================
|
|
55
|
+
# Authoritative Policy Weights - NO NORMALIZATION (Sum = 1.24)
|
|
56
|
+
# These reflect the relative importance of each dimension in RTI context.
|
|
57
|
+
|
|
58
|
+
DIMENSION_WEIGHTS: Dict[str, float] = {
|
|
59
|
+
"legal": 0.20,
|
|
60
|
+
"fairness": 0.20,
|
|
61
|
+
"explainability": 0.16,
|
|
62
|
+
"security": 0.16,
|
|
63
|
+
"monitoring": 0.16,
|
|
64
|
+
"performance": 0.12,
|
|
65
|
+
"ux": 0.12,
|
|
66
|
+
"drift": 0.08,
|
|
67
|
+
"environmental": 0.04
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
# ================== RISK THRESHOLDS ==================
|
|
71
|
+
# Tau values for categorical decision boundaries
|
|
72
|
+
|
|
73
|
+
RISK_THRESHOLDS: Dict[str, float] = {
|
|
74
|
+
"tau1": 0.3, # Below this: LOW risk
|
|
75
|
+
"tau2": 0.7 # Above this: HIGH risk (0.3-0.7 = MEDIUM)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
# ================== DIMENSION METADATA ==================
|
|
79
|
+
# Human-readable names and descriptions for each dimension
|
|
80
|
+
|
|
81
|
+
DIMENSION_INFO: Dict[str, Dict[str, str]] = {
|
|
82
|
+
"legal": {
|
|
83
|
+
"name": "Legal Compliance",
|
|
84
|
+
"description": "Statutory alignment with the RTI Act 2005."
|
|
85
|
+
},
|
|
86
|
+
"fairness": {
|
|
87
|
+
"name": "Bias & Fairness",
|
|
88
|
+
"description": "Ensures equitable outcomes across demographics."
|
|
89
|
+
},
|
|
90
|
+
"explainability": {
|
|
91
|
+
"name": "Explainability & Transparency",
|
|
92
|
+
"description": "Ensures system reasoning is interpretable."
|
|
93
|
+
},
|
|
94
|
+
"security": {
|
|
95
|
+
"name": "Security & Robustness",
|
|
96
|
+
"description": "Resistance to prompt injection and poisoning."
|
|
97
|
+
},
|
|
98
|
+
"monitoring": {
|
|
99
|
+
"name": "Monitoring & Compliance",
|
|
100
|
+
"description": "Integrity of the audit trail."
|
|
101
|
+
},
|
|
102
|
+
"performance": {
|
|
103
|
+
"name": "Model Performance",
|
|
104
|
+
"description": "Core statistical accuracy and recall."
|
|
105
|
+
},
|
|
106
|
+
"ux": {
|
|
107
|
+
"name": "Human-AI UX",
|
|
108
|
+
"description": "Evaluates user friction and reading complexity."
|
|
109
|
+
},
|
|
110
|
+
"drift": {
|
|
111
|
+
"name": "Data & Model Drift",
|
|
112
|
+
"description": "Detects performance degradation over time."
|
|
113
|
+
},
|
|
114
|
+
"environmental": {
|
|
115
|
+
"name": "Environmental & Cost",
|
|
116
|
+
"description": "Monitors carbon footprint and compute efficiency."
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
# ================== METRIC CATALOG ==================
|
|
121
|
+
# Complete catalog of all 36 metrics organized by layer and dimension
|
|
122
|
+
|
|
123
|
+
METRIC_CATALOG: Dict[str, Dict[str, list]] = {
|
|
124
|
+
"fairness": {
|
|
125
|
+
"l1": ["spd", "di", "eop", "aod"],
|
|
126
|
+
"l2": ["fnrd", "pred_parity", "refusal_disp"],
|
|
127
|
+
"l3": ["counterfactual", "mi_leakage"]
|
|
128
|
+
},
|
|
129
|
+
"drift": {
|
|
130
|
+
"l1": ["psi", "ks"],
|
|
131
|
+
"l2": ["pred_drift", "conf_drift"],
|
|
132
|
+
"l3": ["emb_drift"]
|
|
133
|
+
},
|
|
134
|
+
"environmental": {
|
|
135
|
+
"l1": ["inf_cost", "gpu_hours"],
|
|
136
|
+
"l2": ["compute_intensity", "hardware_util"],
|
|
137
|
+
"l3": ["cur"]
|
|
138
|
+
},
|
|
139
|
+
"explainability": {
|
|
140
|
+
"l1": ["confidence", "fi"],
|
|
141
|
+
"l2": ["counterfactual_dist"],
|
|
142
|
+
"l3": ["exp_stability"]
|
|
143
|
+
},
|
|
144
|
+
"ux": {
|
|
145
|
+
"l1": ["tcr", "ftr"],
|
|
146
|
+
"l2": ["dtc", "cognitive_load"],
|
|
147
|
+
"l3": ["readability_fk"]
|
|
148
|
+
},
|
|
149
|
+
"legal": {
|
|
150
|
+
"l1": ["lawfulness", "cvr"],
|
|
151
|
+
"l2": ["proportionality"],
|
|
152
|
+
"l3": ["dpia"]
|
|
153
|
+
},
|
|
154
|
+
"performance": {
|
|
155
|
+
"l1": ["accuracy", "recall"],
|
|
156
|
+
"l2": ["group_f1"],
|
|
157
|
+
"l3": ["acc_lt"]
|
|
158
|
+
},
|
|
159
|
+
"monitoring": {
|
|
160
|
+
"l1": ["alc", "sla"],
|
|
161
|
+
"l2": ["pvr"],
|
|
162
|
+
"l3": ["irf"]
|
|
163
|
+
},
|
|
164
|
+
"security": {
|
|
165
|
+
"l1": ["adv_acc", "pisr"],
|
|
166
|
+
"l2": ["mir"],
|
|
167
|
+
"l3": ["pdr"]
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""
|
|
2
|
+
RTI Ethics Engine - Core Orchestrator
|
|
3
|
+
Main EthicsEngine class that coordinates evaluation across all dimensions.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from datetime import datetime
|
|
7
|
+
from typing import Dict, Any
|
|
8
|
+
|
|
9
|
+
from .config import DIMENSION_WEIGHTS, RISK_THRESHOLDS, DIMENSION_INFO
|
|
10
|
+
from .dimensions import create_all_dimensions
|
|
11
|
+
from .results import EthicsResult, DimensionResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EthicsEngine:
|
|
15
|
+
"""
|
|
16
|
+
RTI Ethics Engine - Production AI Ethics Evaluator
|
|
17
|
+
|
|
18
|
+
Implements a 3-layer evaluation protocol:
|
|
19
|
+
- Layer 1 (L1): Hard mathematical gates (pass/fail)
|
|
20
|
+
- Layer 2 (L2): Weighted risk scoring [0, 1]
|
|
21
|
+
- Layer 3 (L3): Forensic evidence bundle for CIC auditors
|
|
22
|
+
|
|
23
|
+
Example usage:
|
|
24
|
+
engine = EthicsEngine()
|
|
25
|
+
result = engine.run_protocol(metrics)
|
|
26
|
+
|
|
27
|
+
# Access results
|
|
28
|
+
print(result.final_label) # "ETHICAL"
|
|
29
|
+
print(result.get_l1_metric("fairness", "spd")) # 0.023
|
|
30
|
+
print(result.get_failed_dims()) # []
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self):
|
|
34
|
+
"""Initialize the ethics engine with all 9 dimensions."""
|
|
35
|
+
self.dimensions = create_all_dimensions()
|
|
36
|
+
self.weights = DIMENSION_WEIGHTS
|
|
37
|
+
|
|
38
|
+
def run_protocol(self, all_metrics: Dict[str, Dict[str, Any]]) -> EthicsResult:
|
|
39
|
+
"""
|
|
40
|
+
Execute the complete 3-layer ethics evaluation protocol.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
all_metrics: Dict of dimension -> metrics mapping.
|
|
44
|
+
Example: {
|
|
45
|
+
"fairness": {"spd": 0.02, "di": 1.0, "eop": 0.05, "aod": 0.03},
|
|
46
|
+
"legal": {"lawfulness": 1.0, "cvr": 0.98},
|
|
47
|
+
...
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
EthicsResult with complete evaluation data and granular accessors.
|
|
52
|
+
"""
|
|
53
|
+
# Initialize tracking
|
|
54
|
+
total_weighted_risk = 0.0
|
|
55
|
+
risk_vector: Dict[str, float] = {}
|
|
56
|
+
audit_trail: list = []
|
|
57
|
+
l1_failed_dims: list = []
|
|
58
|
+
dimension_results: Dict[str, DimensionResult] = {}
|
|
59
|
+
layer3_explanations: Dict[str, Any] = {}
|
|
60
|
+
|
|
61
|
+
# Generate audit ID
|
|
62
|
+
audit_id = f"RTI-{datetime.now().strftime('%Y%j%H%M%S')}"
|
|
63
|
+
|
|
64
|
+
# STEP 1: Evaluate ALL dimensions (NO early return)
|
|
65
|
+
for key, dim in self.dimensions.items():
|
|
66
|
+
m = all_metrics.get(key, {})
|
|
67
|
+
|
|
68
|
+
# Layer 1: Hard gate evaluation
|
|
69
|
+
l1_status, l1_reasons = dim.evaluate_layer1(m)
|
|
70
|
+
|
|
71
|
+
# L1 FAIL -> L2 = 1.0 MAX RISK (hard gate enforcement)
|
|
72
|
+
if l1_status == "FAIL":
|
|
73
|
+
l2_risk = 1.0
|
|
74
|
+
l2_indicators = ["L1_HARD_GATE_VIOLATION"]
|
|
75
|
+
l1_failed_dims.append(dim.name)
|
|
76
|
+
audit_trail.append(f"L1 {dim.name}: ❌ FAIL -> L2=1.00")
|
|
77
|
+
else:
|
|
78
|
+
# Layer 2: Risk scoring
|
|
79
|
+
l2_risk, l2_indicators = dim.evaluate_layer2(m)
|
|
80
|
+
audit_trail.append(f"L1 {dim.name}: ✅ PASS -> L2={l2_risk:.2f}")
|
|
81
|
+
|
|
82
|
+
# Layer 3: Forensic evidence
|
|
83
|
+
l3_evidence = dim.get_layer3_evidence(
|
|
84
|
+
m, (l1_status, l1_reasons), (l2_risk, l2_indicators)
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Store dimension result
|
|
88
|
+
dimension_results[key] = DimensionResult(
|
|
89
|
+
key=key,
|
|
90
|
+
name=dim.name,
|
|
91
|
+
l1_status=l1_status,
|
|
92
|
+
l1_reasons=l1_reasons,
|
|
93
|
+
l2_risk=l2_risk,
|
|
94
|
+
l2_indicators=l2_indicators,
|
|
95
|
+
l3_evidence=l3_evidence,
|
|
96
|
+
raw_metrics=m
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# Weighted risk aggregation
|
|
100
|
+
total_weighted_risk += l2_risk * self.weights[key]
|
|
101
|
+
risk_vector[dim.name] = round(l2_risk, 4)
|
|
102
|
+
layer3_explanations[key] = l3_evidence
|
|
103
|
+
|
|
104
|
+
# STEP 2: Calculate S_LLM (LLM Ethics Score)
|
|
105
|
+
s_llm = 1.0 - total_weighted_risk
|
|
106
|
+
|
|
107
|
+
# STEP 3: Determine final decision based on weighted risk
|
|
108
|
+
final_label, risk_band, required_action = self._determine_decision(
|
|
109
|
+
total_weighted_risk, l1_failed_dims, dimension_results
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Determine overall L1 status
|
|
113
|
+
layer1_status = "FAIL" if l1_failed_dims else "PASS"
|
|
114
|
+
|
|
115
|
+
# Build and return EthicsResult
|
|
116
|
+
return EthicsResult(
|
|
117
|
+
final_label=final_label,
|
|
118
|
+
layer1_status=layer1_status,
|
|
119
|
+
layer2_risk_score=round(total_weighted_risk, 4),
|
|
120
|
+
s_llm=round(s_llm, 4),
|
|
121
|
+
risk_band=risk_band,
|
|
122
|
+
required_action=required_action,
|
|
123
|
+
audit_id=audit_id,
|
|
124
|
+
risk_vector=risk_vector,
|
|
125
|
+
dimension_results=dimension_results,
|
|
126
|
+
layer3_evidence={
|
|
127
|
+
"audit_trail": audit_trail,
|
|
128
|
+
"explanations": layer3_explanations
|
|
129
|
+
}
|
|
130
|
+
)
|
|
131
|
+
|
|
132
|
+
def _determine_decision(
|
|
133
|
+
self,
|
|
134
|
+
total_risk: float,
|
|
135
|
+
l1_failed: list,
|
|
136
|
+
dimension_results: Dict[str, DimensionResult]
|
|
137
|
+
) -> tuple:
|
|
138
|
+
"""
|
|
139
|
+
Determine final label, risk band, and required action.
|
|
140
|
+
|
|
141
|
+
Decision logic:
|
|
142
|
+
1. If all L1 fail AND high risk -> CRITICAL / BLOCK_AND_PURGE
|
|
143
|
+
2. If risk > tau2 (0.7) -> UNETHICAL / HIGH / BLOCK_AND_REVIEW
|
|
144
|
+
3. If any L1 fail -> CONDITIONALLY_ETHICAL / MEDIUM / REVIEW_FAILED_DIMS
|
|
145
|
+
4. If risk > tau1 (0.3) -> CONDITIONALLY_ETHICAL / MEDIUM / HUMAN_REVIEW
|
|
146
|
+
5. Otherwise -> ETHICAL / LOW / NONE
|
|
147
|
+
"""
|
|
148
|
+
tau1 = RISK_THRESHOLDS["tau1"]
|
|
149
|
+
tau2 = RISK_THRESHOLDS["tau2"]
|
|
150
|
+
|
|
151
|
+
if total_risk > tau2:
|
|
152
|
+
# High aggregate risk (>= 0.70)
|
|
153
|
+
all_failed = all(v.l1_status == "FAIL" for v in dimension_results.values())
|
|
154
|
+
if all_failed:
|
|
155
|
+
return "UNETHICAL", "CRITICAL", "BLOCK_AND_PURGE"
|
|
156
|
+
return "UNETHICAL", "HIGH", "BLOCK_AND_REVIEW"
|
|
157
|
+
|
|
158
|
+
elif l1_failed:
|
|
159
|
+
# At least one dimension failed L1, but total risk is manageable
|
|
160
|
+
return "CONDITIONALLY_ETHICAL", "MEDIUM", f"REVIEW_FAILED_DIMS ({', '.join(l1_failed)})"
|
|
161
|
+
|
|
162
|
+
elif total_risk > tau1:
|
|
163
|
+
# All L1 pass, but moderate aggregate risk (> 0.30)
|
|
164
|
+
return "CONDITIONALLY_ETHICAL", "MEDIUM", "HUMAN_REVIEW (Risk)"
|
|
165
|
+
|
|
166
|
+
else:
|
|
167
|
+
# Low risk (< 0.30) and all L1 pass
|
|
168
|
+
return "ETHICAL", "LOW", "NONE"
|
|
169
|
+
|
|
170
|
+
def evaluate(self, all_metrics: Dict[str, Dict[str, Any]]) -> EthicsResult:
|
|
171
|
+
"""Alias for run_protocol() for convenience."""
|
|
172
|
+
return self.run_protocol(all_metrics)
|
|
173
|
+
|
|
174
|
+
def get_dimension_names(self) -> list:
|
|
175
|
+
"""Get list of all dimension names."""
|
|
176
|
+
return [dim.name for dim in self.dimensions.values()]
|
|
177
|
+
|
|
178
|
+
def get_dimension_keys(self) -> list:
|
|
179
|
+
"""Get list of all dimension keys."""
|
|
180
|
+
return list(self.dimensions.keys())
|
|
181
|
+
|
|
182
|
+
def get_weights(self) -> Dict[str, float]:
|
|
183
|
+
"""Get dimension weights."""
|
|
184
|
+
return self.weights.copy()
|