Skip to content
Merged
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
4 changes: 2 additions & 2 deletions autofit/config/general.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ profiling:
repeats: 1 # The number of repeat function calls used to measure run-times when profiling.
test:
check_likelihood_function: true # if True, when a search is resumed the likelihood of a previous sample is recalculated to ensure it is consistent with the previous run.
exception_override: false
exception_override: false # If True, disable model assertions during likelihood evaluation.
lh_timeout_seconds: # If a float is input, the log_likelihood_function call is timed out after this many seconds, to diagnose infinite loops. Default is None, meaning no timeout.
log_likelihood_ceiling: # If a float is input, log likelihoods whose magnitude exceeds it are treated as numerical garbage and replaced by the resample figure of merit. Blank (null) or inf disables the guard, which is the default: a log likelihood scales with the noise-map units, so a fixed magnitude can reject a legitimate fit on a badly-scaled dataset. Opt in where the units are known (e.g. autolens_profiling sets 1.0e+20).
parallel_profile: false
parallel_profile: false # If True, save worker profiling statistics; also enable profiling.parallel_profile.
18 changes: 9 additions & 9 deletions autofit/config/logging.yaml
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
version: 1
disable_existing_loggers: false
version: 1 # Python logging dictionary schema version (1).
disable_existing_loggers: false # If True, disable existing non-root loggers when configuring logging.

handlers:
console:
class: logging.StreamHandler
level: INFO
stream: ext://sys.stdout
formatter: formatter
class: logging.StreamHandler # Python handler class used to emit log records.
level: INFO # Minimum logging severity emitted by this handler or logger.
stream: ext://sys.stdout # Output stream receiving console log records.
formatter: formatter # Name of the formatter applied by this handler.

root:
level: INFO
handlers: [ console ]
level: INFO # Minimum logging severity emitted by this handler or logger.
handlers: [ console ] # Names of handlers receiving root logger records.

formatters:
formatter:
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s'
format: '%(asctime)s - %(name)s - %(levelname)s - %(message)s' # Python logging format string applied to each log record.

total_files_open : false # Whether to output info on the total number of open files in the system, used for debugging parallelism issues.
36 changes: 18 additions & 18 deletions autofit/config/notation.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,29 @@

label:
label:
centre: x
normalization: norm
parameter0: a
parameter1: b
parameter2: c
rate: \lambda
sigma: \sigma
centre: x # Plot label used for the centre parameter.
normalization: norm # Plot label used for the normalization parameter.
parameter0: a # Plot label used for the parameter0 parameter.
parameter1: b # Plot label used for the parameter1 parameter.
parameter2: c # Plot label used for the parameter2 parameter.
rate: \lambda # Plot label used for the rate parameter.
sigma: \sigma # Plot label used for the sigma parameter.
superscript:
Exponential: e
Gaussian: g
ModelComponent0: M0
ModelComponent1: M1
Exponential: e # Class suffix for an unnamed Exponential Model; named components use component paths.
Gaussian: g # Class suffix for an unnamed Gaussian Model; named components use component paths.
ModelComponent0: M0 # Class suffix for an unnamed ModelComponent0 Model; named components use component paths.
ModelComponent1: M1 # Class suffix for an unnamed ModelComponent1 Model; named components use component paths.

# label_format: The format certain parameters are output as in output files like the `model.results` file.

# For example, if `centre={:.2d}`, the format of the centre parameter in results files will use this Python format.

label_format:
format:
centre: '{:.2f}'
normalization: '{:.2f}'
parameter0: '{:.2f}'
parameter1: '{:.2f}'
parameter2: '{:.2f}'
rate: '{:.2f}'
sigma: '{:.2f}'
centre: '{:.2f}' # Python format string used for centre in result summaries.
normalization: '{:.2f}' # Python format string used for normalization in result summaries.
parameter0: '{:.2f}' # Python format string used for parameter0 in result summaries.
parameter1: '{:.2f}' # Python format string used for parameter1 in result summaries.
parameter2: '{:.2f}' # Python format string used for parameter2 in result summaries.
rate: '{:.2f}' # Python format string used for rate in result summaries.
sigma: '{:.2f}' # Python format string used for sigma in result summaries.
10 changes: 5 additions & 5 deletions autofit/config/output.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ default: true # If true then files which are not explicitly listed here are outp
# the `samples_summary.json` file. However, without a `samples.csv` file these types of tasks cannot be performed
# after the fit is complete, for example via the database.

samples: true
samples: true # If True, retain the sampled parameter values, likelihoods and weights in samples.csv.

# The `samples.csv` file contains every accepted sampled value of every free parameter with its log likelihood and
# weight. For certain searches, the majority of samples have a very low weight and have no numerical impact on the
Expand All @@ -31,7 +31,7 @@ samples: true

# Set value to empty (e.g. delete 1.0e-10 below) to disable this feature.

samples_weight_threshold: 1.0e-10
samples_weight_threshold: 1.0e-10 # Exclude samples with weights below this threshold from samples.csv; null disables filtering.

### Search Internal ###

Expand All @@ -53,7 +53,7 @@ samples_weight_threshold: 1.0e-10
# every walker. This information is required to do certain analyes and make certain plots, therefore deleting the
# folder means this information is list.

search_internal: false
search_internal: false # If False, delete the search internal checkpoint directory after the fit completes.

### Start Point ###

Expand All @@ -62,7 +62,7 @@ search_internal: false
# the visualizer wrapped in the Analysis class, and therefore should show things like the quality of the fit
# to the data and the residuals at the start point.

start_point: true
start_point: true # If True, output visualization of the initializer start-point fit.

### Latent Variables ###

Expand Down Expand Up @@ -108,4 +108,4 @@ model_graph: false # `model.graph`: graphical-model factor graph summary. Only m
# Opt-in until the lens acceptance renders pass (model-figures epic phase 3). Unlike other keys this
# one does NOT fall back to `default` when absent -- an absent key means off.

model_figure: false
model_figure: false # If True, output model.png beside model.info.
90 changes: 90 additions & 0 deletions scripts/check_config_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Report config leaves lacking a comment on their line or immediately above.

Run with one or more config directories; priors/ is excluded. Without arguments,
check this checkout's packaged autofit/config directory. Only YAML is loaded:
the scientific packages and their configuration stack are never imported.
"""

import argparse
from pathlib import Path

import yaml


def comment_lines(text):
"""Find nonempty comments, masking YAML scalar tokens (including quoted #)."""
masked = list(text)
for token in yaml.scan(text):
if isinstance(token, yaml.tokens.ScalarToken):
for index in range(token.start_mark.index, token.end_mark.index):
if masked[index] != "\n":
masked[index] = " "
return {
index
for index, line in enumerate("".join(masked).splitlines())
if "#" in line and line.split("#", 1)[1].strip()
}


def uncommented_leaves(path):
text = path.read_text()
comments = comment_lines(text)
lines = text.splitlines()
preceding_comments = {
line for line in comments if lines[line].lstrip().startswith("#")
}
findings = []

def walk(node, keys=()):
if isinstance(node, yaml.MappingNode):
for key, value in node.value:
path_keys = keys + (key.value,)
if isinstance(value, yaml.MappingNode):
walk(value, path_keys)
else:
line = key.start_mark.line
if line not in comments and line - 1 not in preceding_comments:
findings.append((path, line + 1, ".".join(path_keys)))

for document in yaml.compose_all(text):
walk(document)
return findings


def check_roots(roots):
findings = []
for root in roots:
root = Path(root)
if not root.is_dir():
raise ValueError(f"Config directory does not exist: {root}")
files = sorted(
path
for path in root.rglob("*")
if path.is_file()
and path.suffix in {".yaml", ".yml"}
and "priors" not in path.relative_to(root).parts
)
if not files:
raise ValueError(f"No non-prior YAML files found: {root}")
for path in files:
findings.extend(uncommented_leaves(path))
return findings


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("roots", nargs="*", type=Path)
args = parser.parse_args(argv)
roots = args.roots or [Path(__file__).resolve().parents[1] / "autofit/config"]
try:
findings = check_roots(roots)
except (OSError, ValueError, yaml.YAMLError) as error:
parser.error(str(error))
for path, line, key in findings:
print(f"{path}:{line}: {key}")
print(f"Uncommented config leaves: {len(findings)}")
return bool(findings)


if __name__ == "__main__":
raise SystemExit(main())
60 changes: 60 additions & 0 deletions test_autofit/test_config_comments.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""The comment audit distinguishes YAML values from explanatory comments."""

import importlib.util
from pathlib import Path

import pytest


SCRIPT = Path(__file__).resolve().parents[1] / "scripts/check_config_comments.py"
spec = importlib.util.spec_from_file_location("check_config_comments", SCRIPT)
audit = importlib.util.module_from_spec(spec)
spec.loader.exec_module(audit)


def test_yaml_comments_and_terminal_values(tmp_path):
path = tmp_path / "settings.yaml"
path.write_text(
'quoted: "# literal"\n'
"single: '# literal' # Explanation.\n"
"empty_comment: true # \n"
"# Nullable setting.\n"
"nullable:\n"
"# Sequence setting.\n"
"sequence: [one, two]\n"
"block: |\n"
" # This is scalar content, not a comment.\n"
" fake_key: true\n"
"# Separated prose does not meet the adjacent-line rule.\n"
"\n"
"nested:\n"
" missing: false\n"
)
assert [(line, key) for _, line, key in audit.check_roots([tmp_path])] == [
(1, "quoted"),
(3, "empty_comment"),
(8, "block"),
(14, "nested.missing"),
]


def test_priors_excluded_and_yml_included(tmp_path):
priors = tmp_path / "priors"
priors.mkdir()
(priors / "model.yaml").write_text("uncommented: true\n")
(tmp_path / "settings.yml").write_text("flag: true # Enable feature.\n")
assert audit.check_roots([tmp_path]) == []


def test_missing_and_empty_roots_fail(tmp_path):
with pytest.raises(ValueError, match="does not exist"):
audit.check_roots([tmp_path / "missing"])
with pytest.raises(ValueError, match="No non-prior YAML"):
audit.check_roots([tmp_path])


def test_cli_exit_status_and_packaged_defaults(tmp_path, capsys):
assert audit.main([]) == 0
(tmp_path / "settings.yaml").write_text("flag: true\n")
assert audit.main([str(tmp_path)]) == 1
assert "settings.yaml:1: flag" in capsys.readouterr().out
Loading