Skip to content

Commit 2638d17

Browse files
authored
Investigate unused params (#575)
* Add reporting of usage of Updatables * Adapt tests to new error messages * Add tests and improve message for un-updated ParamsMap * Remove accidentally added import * Make pylint ignore line in test * Bump version number for new release
1 parent ccfa196 commit 2638d17

File tree

6 files changed

+205
-24
lines changed

6 files changed

+205
-24
lines changed

firecrown/parameters.py

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,21 @@
77
from __future__ import annotations
88

99
import copy
10+
from dataclasses import dataclass
1011
import warnings
1112
from collections.abc import Iterable, Iterator, Sequence
1213

1314

15+
@dataclass
16+
class UpdatableUsageRecord:
17+
"""Dataclass to record the usage of parameters by an Updatable object."""
18+
19+
cls: str
20+
prefix: str | None
21+
sampler_params: list[str]
22+
internal_params: list[str]
23+
24+
1425
def parameter_get_full_name(prefix: None | str, param: str) -> str:
1526
"""Form a full parameter name from the given (optional) prefix and name.
1627
@@ -78,6 +89,7 @@ def __init__(self, *args, **kwargs) -> None:
7889

7990
self.lower_case: bool = False
8091
self.used_keys: set[str] = set()
92+
self.usages: list[UpdatableUsageRecord] = []
8193

8294
def __getitem__(self, key: str) -> float:
8395
"""Return the value for the given key.
@@ -220,15 +232,65 @@ def keys(self) -> set[str]:
220232
"""
221233
return set(self.params.keys())
222234

235+
def record_usage(
236+
self,
237+
cls: type,
238+
prefix: str | None,
239+
sampler_params: list[str],
240+
internal_params: list[str],
241+
) -> None:
242+
"""Record the usage of parameters based on the provided lists.
243+
244+
This method marks parameters as used based on the provided lists
245+
of sampler and internal parameters.
246+
247+
:param cls: type of the updatable being recorded.
248+
:param prefix: optional prefix for the parameters
249+
:param sampler_params: list of sampler parameter names
250+
:param internal_params: list of internal parameter names
251+
"""
252+
self.usages.append(
253+
UpdatableUsageRecord(
254+
cls=cls.__name__,
255+
prefix=prefix,
256+
sampler_params=sampler_params,
257+
internal_params=internal_params,
258+
)
259+
)
260+
261+
def report_usages(self) -> str:
262+
"""Generate a report of parameter usages by Updatable objects.
263+
264+
:return: A formatted string report of parameter usages.
265+
"""
266+
if not self.usages:
267+
return "No Updatables have been updated."
268+
report_lines = ["Parameter usage report:"]
269+
for usage in self.usages:
270+
report_lines.append(f"Updatable class: {usage.cls}, Prefix: {usage.prefix}")
271+
if usage.sampler_params:
272+
report_lines.append(
273+
f" Sampler parameters used: {', '.join(usage.sampler_params)}"
274+
)
275+
if usage.internal_params:
276+
report_lines.append(
277+
f" Internal parameters used: {', '.join(usage.internal_params)}"
278+
)
279+
return "\n".join(report_lines)
280+
223281

224282
def handle_unused_params(params: ParamsMap, raise_on_unused: bool = False):
225283
"""Check for unused keys in the parameters map."""
226284
unused_keys = params.get_unused_keys()
227285
if unused_keys:
228286
message = (
229-
f"Unused keys in parameters: {sorted(unused_keys)}. "
230-
"This may indicate a problem with the parameter mapping."
287+
f"Unused keys in parameters: {sorted(unused_keys)}.\n"
288+
"This may indicate a missing statistic or systematic.\n"
289+
"You may also have a modeling mismatch, e.g. you have specified "
290+
"sampling of neutrino masses but did not configure CAMB to use "
291+
"massive neutrinos.\n"
231292
)
293+
message += params.report_usages()
232294
if raise_on_unused:
233295
raise ValueError(message)
234296

firecrown/updatable.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,12 @@ def update(self, params: ParamsMap) -> None:
193193
f"update, but {','.join(internal_params)} was specified."
194194
)
195195

196+
params.record_usage(
197+
type(self),
198+
self.parameter_prefix,
199+
[sp.name for sp in self._sampler_parameters],
200+
list(self._internal_parameters.keys()),
201+
)
196202
for parameter in self._sampler_parameters:
197203
try:
198204
value = params.get_from_full_name(parameter.fullname)

firecrown/version.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@
88

99
FIRECROWN_MAJOR = 1
1010
FIRECROWN_MINOR = 14
11-
FIRECROWN_PATCH = "0a0"
11+
FIRECROWN_PATCH = 0
1212
__version__ = f"{FIRECROWN_MAJOR}.{FIRECROWN_MINOR}.{FIRECROWN_PATCH}"

tests/test_parameters.py

Lines changed: 112 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -437,20 +437,14 @@ def test_handle_unused_params():
437437
# All parameters are unused, should raise a warning
438438
with pytest.warns(
439439
UserWarning,
440-
match=re.escape(
441-
"Unused keys in parameters: ['a', 'b', 'c']. "
442-
"This may indicate a problem with the parameter mapping."
443-
),
440+
match=re.escape("Unused keys in parameters: ['a', 'b', 'c']."),
444441
):
445442
handle_unused_params(params=params, raise_on_unused=False)
446443

447444
# All parameters are unused, should raise an error
448445
with pytest.raises(
449446
ValueError,
450-
match=re.escape(
451-
"Unused keys in parameters: ['a', 'b', 'c']. "
452-
"This may indicate a problem with the parameter mapping."
453-
),
447+
match=re.escape("Unused keys in parameters: ['a', 'b', 'c']."),
454448
):
455449
handle_unused_params(params=params, raise_on_unused=True)
456450

@@ -461,24 +455,126 @@ def test_handle_unused_params():
461455
# Now 'c' is unused, should raise a warning
462456
with pytest.warns(
463457
UserWarning,
464-
match=re.escape(
465-
"Unused keys in parameters: ['c']. "
466-
"This may indicate a problem with the parameter mapping."
467-
),
458+
match=re.escape("Unused keys in parameters: ['c']."),
468459
):
469460
handle_unused_params(params=params, raise_on_unused=False)
470461

471462
# Now 'c' is unused, should raise an error
472463
with pytest.raises(
473464
ValueError,
474-
match=re.escape(
475-
"Unused keys in parameters: ['c']. "
476-
"This may indicate a problem with the parameter mapping."
477-
),
465+
match=re.escape("Unused keys in parameters: ['c']."),
478466
):
479467
handle_unused_params(params=params, raise_on_unused=True)
480468

481469

470+
def test_report_empty_usage():
471+
"""Test that report_usages generates correct report for no usage of Updatable"""
472+
# Test case 1: Empty usages list
473+
params = ParamsMap({"a": 1.0})
474+
report = params.report_usages()
475+
assert report == "No Updatables have been updated."
476+
477+
478+
def test_report_usages():
479+
"""Test that report_usages generates correct reports for various scenarios."""
480+
# Test case 2: Usage with only sampler parameters
481+
params = ParamsMap({"param1": 1.0, "param2": 2.0})
482+
params.record_usage(
483+
cls=ParamsMap,
484+
prefix="test",
485+
sampler_params=["param1", "param2"],
486+
internal_params=[],
487+
)
488+
report = params.report_usages()
489+
assert "Parameter usage report:" in report
490+
assert "Updatable class: ParamsMap, Prefix: test" in report
491+
assert "Sampler parameters used: param1, param2" in report
492+
assert "Internal parameters used:" not in report
493+
494+
# Test case 3: Usage with only internal parameters
495+
params = ParamsMap({"internal1": 1.0, "internal2": 2.0})
496+
params.record_usage(
497+
cls=ParamsMap,
498+
prefix="test",
499+
sampler_params=[],
500+
internal_params=["internal1", "internal2"],
501+
)
502+
report = params.report_usages()
503+
assert "Parameter usage report:" in report
504+
assert "Updatable class: ParamsMap, Prefix: test" in report
505+
assert "Sampler parameters used:" not in report
506+
assert "Internal parameters used: internal1, internal2" in report
507+
508+
# Test case 4: Usage with both sampler and internal parameters
509+
params = ParamsMap({"s1": 1.0, "s2": 2.0, "i1": 3.0, "i2": 4.0})
510+
params.record_usage(
511+
cls=ParamsMap,
512+
prefix="prefix1",
513+
sampler_params=["s1", "s2"],
514+
internal_params=["i1", "i2"],
515+
)
516+
report = params.report_usages()
517+
assert "Parameter usage report:" in report
518+
assert "Updatable class: ParamsMap, Prefix: prefix1" in report
519+
assert "Sampler parameters used: s1, s2" in report
520+
assert "Internal parameters used: i1, i2" in report
521+
522+
# Test case 5: Multiple usages with mixed scenarios
523+
params = ParamsMap(
524+
{
525+
"a": 1.0,
526+
"b": 2.0,
527+
"c": 3.0,
528+
"d": 4.0,
529+
"e": 5.0,
530+
"f": 6.0,
531+
}
532+
)
533+
# First usage: both types
534+
params.record_usage(
535+
cls=ParamsMap,
536+
prefix="prefix1",
537+
sampler_params=["a", "b"],
538+
internal_params=["c"],
539+
)
540+
# Second usage: only sampler
541+
params.record_usage(
542+
cls=ParamsMap,
543+
prefix="prefix2",
544+
sampler_params=["d"],
545+
internal_params=[],
546+
)
547+
# Third usage: only internal
548+
params.record_usage(
549+
cls=ParamsMap,
550+
prefix=None,
551+
sampler_params=[],
552+
internal_params=["e", "f"],
553+
)
554+
555+
report = params.report_usages()
556+
lines = report.split("\n")
557+
558+
# Check header
559+
assert lines[0] == "Parameter usage report:"
560+
561+
# Check first usage
562+
assert "Updatable class: ParamsMap, Prefix: prefix1" in report
563+
assert "Sampler parameters used: a, b" in report
564+
assert "Internal parameters used: c" in report
565+
566+
# Check second usage
567+
assert "Updatable class: ParamsMap, Prefix: prefix2" in report
568+
assert "Sampler parameters used: d" in report
569+
570+
# Check third usage (with None prefix)
571+
assert "Updatable class: ParamsMap, Prefix: None" in report
572+
assert "Internal parameters used: e, f" in report
573+
574+
# Verify we have the expected number of usage records
575+
assert len(params.usages) == 3
576+
577+
482578
def test_params_map_union():
483579
p1 = ParamsMap({"a": 1.0})
484580
p2 = ParamsMap({"b": 2.0})

tests/test_updatable.py

Lines changed: 21 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -20,18 +20,18 @@
2020
class MinimalUpdatable(Updatable):
2121
"""A concrete time that implements Updatable."""
2222

23-
def __init__(self):
23+
def __init__(self, prefix: str | None = None):
2424
"""Initialize object with defaulted value."""
25-
super().__init__()
25+
super().__init__(prefix)
2626
self.a = parameters.register_new_updatable_parameter(default_value=1.0)
2727

2828

2929
class SimpleUpdatable(Updatable):
3030
"""A concrete type that implements Updatable."""
3131

32-
def __init__(self):
32+
def __init__(self, prefix: str | None = None):
3333
"""Initialize object with defaulted values."""
34-
super().__init__()
34+
super().__init__(prefix)
3535

3636
self.x = parameters.register_new_updatable_parameter(default_value=2.0)
3737
self.y = parameters.register_new_updatable_parameter(default_value=3.0)
@@ -54,6 +54,23 @@ def _get_derived_parameters(self) -> DerivedParameterCollection:
5454
return derived_parameters
5555

5656

57+
def test_updatable_reports():
58+
su = SimpleUpdatable("bob")
59+
mu = MinimalUpdatable("larry")
60+
su.mu = mu # pylint: disable=attribute-defined-outside-init
61+
62+
params = ParamsMap({"bob_x": 1.0, "bob_y": 2.0, "larry_a": 3.0})
63+
before_use = params.report_usages()
64+
65+
assert before_use == "No Updatables have been updated."
66+
su.update(params)
67+
after_use = params.report_usages()
68+
assert "Updatable class: SimpleUpdatable, Prefix: bob" in after_use
69+
assert "Sampler parameters used: x, y" in after_use
70+
assert "Updatable class: MinimalUpdatable, Prefix: larry" in after_use
71+
assert "Sampler parameters used: a" in after_use
72+
73+
5774
def test_get_params_names():
5875
obj = SimpleUpdatable()
5976
found_names = obj.get_params_names()

tutorial/_quarto.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ format:
6363

6464
reference-location: margin
6565
citation-location: margin
66-
subtitle: "version 1.14.0a0"
66+
subtitle: "version 1.14.0"
6767
authors:
6868
- Marc Paterno
6969
- Sandro Vitenti

0 commit comments

Comments
 (0)