Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions library/src/iqb/calculator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Module that implements calculating IQB scores."""

import copy
from pprint import pprint

from .config import IQB_CONFIG
Expand All @@ -13,21 +14,30 @@ def __init__(self, config=None, name=None):
Initialize a new instance of IQBCalculator.

Parameters:
config (str): The file with the configuration of the IQB formula parameters. If "None" (default), it gets the parameters from the IQB_CONFIG dict.
config (dict | str): If "None" (default), use IQB_CONFIG. A dict is used
as in-memory configuration. A string is currently treated as a
configuration file path and is not implemented.
name (str): [Optional] name for the IQBCalculator instance.
"""
self.set_config(config)
self.name = name

def set_config(self, config):
"""Sets up configuration parameters. If "None" (default), it gets the parameters from the IQB_CONFIG dict."""
"""Set up configuration parameters."""
if config is None:
self.config = IQB_CONFIG
else:
elif isinstance(config, dict):
# Keep calculator behavior isolated from external mutations.
self.config = copy.deepcopy(config)
elif isinstance(config, str):
# TODO: load config data from file (json, yaml, or other format) as a dict
raise NotImplementedError(
"method for reading from configuration file other than the default not implemented"
)
else:
raise TypeError(
f"config must be None, dict, or str, got: {type(config).__name__}"
)

def print_config(self):
"""
Expand All @@ -36,27 +46,27 @@ def print_config(self):
"""
# TODO: to be updated
print("### IQB formula weights and thresholds")
pprint(IQB_CONFIG)
pprint(self.config)
print()

print("### Use cases")
for uc in IQB_CONFIG["use cases"]:
for uc in self.config["use cases"]:
print(f"\t{uc}")
print()

print("### Network requirements")
for uc in IQB_CONFIG["use cases"]:
for nr in IQB_CONFIG["use cases"][uc]["network requirements"]:
for uc in self.config["use cases"]:
for nr in self.config["use cases"][uc]["network requirements"]:
print(f"\t{nr}")
break
print()

print("### Weights & Thresholds")
print("\tUse case\t \tNetwork requirement \tWeight \tThreshold min")
for uc in IQB_CONFIG["use cases"]:
for nr in IQB_CONFIG["use cases"][uc]["network requirements"]:
nr_w = IQB_CONFIG["use cases"][uc]["network requirements"][nr]["w"]
nr_th = IQB_CONFIG["use cases"][uc]["network requirements"][nr]["threshold min"]
for uc in self.config["use cases"]:
for nr in self.config["use cases"][uc]["network requirements"]:
nr_w = self.config["use cases"][uc]["network requirements"][nr]["w"]
nr_th = self.config["use cases"][uc]["network requirements"][nr]["threshold min"]
print(f"\t{uc:20} \t{nr:20} \t{nr_w} \t{nr_th}")
print()

Expand Down
64 changes: 64 additions & 0 deletions library/tests/iqb/calculator_test.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
"""Tests for the IQBCalculator score calculation module."""

import copy

import pytest

from iqb import IQB_CONFIG, IQBCalculator
Expand All @@ -23,6 +25,22 @@ def test_init_uses_default_config(self):
iqb = IQBCalculator()
assert iqb.config == IQB_CONFIG

def test_init_accepts_dict_config(self):
"""Test that IQBCalculator accepts an in-memory config dict."""
custom_config = copy.deepcopy(IQB_CONFIG)
custom_config["use cases"]["web browsing"]["network requirements"][
"download_throughput_mbps"
]["threshold min"] = 500

iqb = IQBCalculator(config=custom_config)

assert (
iqb.config["use cases"]["web browsing"]["network requirements"][
"download_throughput_mbps"
]["threshold min"]
== 500
)

def test_init_with_invalid_config_raises_error(self):
"""Test that providing a non-existent config file raises NotImplementedError."""
with pytest.raises(NotImplementedError):
Expand Down Expand Up @@ -127,6 +145,36 @@ def test_calculate_iqb_score_consistency(self):
score2 = iqb.calculate_iqb_score()
assert score1 == score2

def test_calculate_iqb_score_changes_with_custom_thresholds(self):
"""Test that stricter thresholds lower the IQB score for the same data."""
sample_data = {
"m-lab": {
"download_throughput_mbps": 15,
"upload_throughput_mbps": 20,
"latency_ms": 75,
"packet_loss": 0.007,
}
}

default_score = IQBCalculator().calculate_iqb_score(data=sample_data)

strict_config = copy.deepcopy(IQB_CONFIG)
for use_case in strict_config["use cases"].values():
use_case["network requirements"]["download_throughput_mbps"]["threshold min"] = (
500
)
use_case["network requirements"]["upload_throughput_mbps"]["threshold min"] = (
500
)
use_case["network requirements"]["latency_ms"]["threshold min"] = 5
use_case["network requirements"]["packet_loss"]["threshold min"] = 0.001

strict_score = IQBCalculator(config=strict_config).calculate_iqb_score(
data=sample_data
)

assert strict_score < default_score


class TestIQBCalculatorConfig:
"""Tests for IQBCalculator configuration."""
Expand Down Expand Up @@ -179,8 +227,24 @@ def test_set_config_with_none(self):
iqb.set_config(None)
assert iqb.config == IQB_CONFIG

def test_set_config_with_dict(self):
"""Test that set_config accepts a dict."""
iqb = IQBCalculator()
custom_config = copy.deepcopy(IQB_CONFIG)
custom_config["use cases"]["gaming"]["w"] = 2

iqb.set_config(custom_config)

assert iqb.config["use cases"]["gaming"]["w"] == 2

def test_set_config_with_file_raises_error(self):
"""Test that set_config raises error for file paths."""
iqb = IQBCalculator()
with pytest.raises(NotImplementedError):
iqb.set_config("some_file.json")

def test_set_config_with_invalid_type_raises_type_error(self):
"""Test that set_config rejects unsupported config types."""
iqb = IQBCalculator()
with pytest.raises(TypeError):
iqb.set_config(123)
9 changes: 6 additions & 3 deletions prototype/Home.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

import streamlit as st
from session_state import initialize_app_state
from utils.calculation_utils import build_data_for_calculate
from utils.calculation_utils import (
build_data_for_calculate,
calculate_iqb_score_with_custom_settings,
)
from visualizations.sunburst_data import (
prepare_complete_hierarchy_sunburst_data,
prepare_requirements_sunburst_data,
Expand Down Expand Up @@ -69,8 +72,8 @@ def main():
with right_col:
try:
data_for_calculation = build_data_for_calculate(state)
iqb_score = state.iqb.calculate_iqb_score(
data=data_for_calculation, print_details=False
iqb_score = calculate_iqb_score_with_custom_settings(
state, data=data_for_calculation, print_details=False
)

tab1, tab2, tab3 = st.tabs(["Requirements", "Use Cases", "Full Hierarchy"])
Expand Down
Loading