Jarvis-HEP-Sampler 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.
- jarvis_hep_sampler/__init__.py +141 -0
- jarvis_hep_sampler/_check_restart.py +29 -0
- jarvis_hep_sampler/_version.py +1 -0
- jarvis_hep_sampler/contexts.py +69 -0
- jarvis_hep_sampler/errors.py +81 -0
- jarvis_hep_sampler/loader.py +237 -0
- jarvis_hep_sampler/log.py +97 -0
- jarvis_hep_sampler/metadata.py +210 -0
- jarvis_hep_sampler/options.py +243 -0
- jarvis_hep_sampler/output.py +139 -0
- jarvis_hep_sampler/py.typed +0 -0
- jarvis_hep_sampler/registry.py +516 -0
- jarvis_hep_sampler/session.py +347 -0
- jarvis_hep_sampler/streams.py +64 -0
- jarvis_hep_sampler/template.py +62 -0
- jarvis_hep_sampler/templates/independent.py.tmpl +47 -0
- jarvis_hep_sampler/testing.py +365 -0
- jarvis_hep_sampler-1.0.0.dist-info/METADATA +178 -0
- jarvis_hep_sampler-1.0.0.dist-info/RECORD +22 -0
- jarvis_hep_sampler-1.0.0.dist-info/WHEEL +5 -0
- jarvis_hep_sampler-1.0.0.dist-info/licenses/LICENSE +21 -0
- jarvis_hep_sampler-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
"""Jarvis-HEP-Sampler: the plug-in SDK for user-defined Jarvis-HEP samplers.
|
|
2
|
+
|
|
3
|
+
A plug-in is one ``.py`` file with a ``PLUGIN`` dict, an optional
|
|
4
|
+
``OPTIONS_SCHEMA`` and a few functions (``prepare`` / ``draw`` / hooks).
|
|
5
|
+
Users create it with ``Jarvis sampler init``, load it once with
|
|
6
|
+
``Jarvis sampler load``, and then write ``Sampling.Method: <Name>`` like any
|
|
7
|
+
built-in method.
|
|
8
|
+
|
|
9
|
+
This package owns the plug-in format: metadata rules, loading, the user
|
|
10
|
+
registry, random streams, output checks, the logging facade and the offline
|
|
11
|
+
test kit. Jarvis-HEP owns the runtime. Names are imported lazily so reading
|
|
12
|
+
the registry stays light.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from importlib import import_module
|
|
18
|
+
from typing import TYPE_CHECKING, Any
|
|
19
|
+
|
|
20
|
+
from jarvis_hep_sampler._version import __version__
|
|
21
|
+
|
|
22
|
+
_LAZY_ATTRS: dict[str, tuple[str, str]] = {
|
|
23
|
+
# metadata
|
|
24
|
+
"API_VERSION": ("jarvis_hep_sampler.metadata", "API_VERSION"),
|
|
25
|
+
"PluginMetadata": ("jarvis_hep_sampler.metadata", "PluginMetadata"),
|
|
26
|
+
"check_method_name": ("jarvis_hep_sampler.metadata", "check_method_name"),
|
|
27
|
+
# contexts and logging
|
|
28
|
+
"BlockContext": ("jarvis_hep_sampler.contexts", "BlockContext"),
|
|
29
|
+
"DrawContext": ("jarvis_hep_sampler.contexts", "DrawContext"),
|
|
30
|
+
"SamplerStats": ("jarvis_hep_sampler.contexts", "SamplerStats"),
|
|
31
|
+
"SetupContext": ("jarvis_hep_sampler.contexts", "SetupContext"),
|
|
32
|
+
"LogBackend": ("jarvis_hep_sampler.log", "LogBackend"),
|
|
33
|
+
"SamplerLog": ("jarvis_hep_sampler.log", "SamplerLog"),
|
|
34
|
+
# errors
|
|
35
|
+
"PluginConfigError": ("jarvis_hep_sampler.errors", "PluginConfigError"),
|
|
36
|
+
"PluginError": ("jarvis_hep_sampler.errors", "PluginError"),
|
|
37
|
+
"PluginLoadError": ("jarvis_hep_sampler.errors", "PluginLoadError"),
|
|
38
|
+
"PluginOutputError": ("jarvis_hep_sampler.errors", "PluginOutputError"),
|
|
39
|
+
"PluginResumeMismatch": ("jarvis_hep_sampler.errors", "PluginResumeMismatch"),
|
|
40
|
+
"RegistryError": ("jarvis_hep_sampler.errors", "RegistryError"),
|
|
41
|
+
# streams
|
|
42
|
+
"STREAM_PROTOCOL": ("jarvis_hep_sampler.streams", "STREAM_PROTOCOL"),
|
|
43
|
+
# options
|
|
44
|
+
"OptionIssue": ("jarvis_hep_sampler.options", "OptionIssue"),
|
|
45
|
+
"ReservedSettings": ("jarvis_hep_sampler.options", "ReservedSettings"),
|
|
46
|
+
"parse_reserved": ("jarvis_hep_sampler.options", "parse_reserved"),
|
|
47
|
+
"schema_issues": ("jarvis_hep_sampler.options", "schema_issues"),
|
|
48
|
+
"split_bounds": ("jarvis_hep_sampler.options", "split_bounds"),
|
|
49
|
+
# loading and running
|
|
50
|
+
"LoadedPlugin": ("jarvis_hep_sampler.loader", "LoadedPlugin"),
|
|
51
|
+
"load_plugin_file": ("jarvis_hep_sampler.loader", "load_plugin_file"),
|
|
52
|
+
"load_plugin_source": ("jarvis_hep_sampler.loader", "load_plugin_source"),
|
|
53
|
+
"IndependentSession": ("jarvis_hep_sampler.session", "IndependentSession"),
|
|
54
|
+
# registry and template
|
|
55
|
+
"LoadResult": ("jarvis_hep_sampler.registry", "LoadResult"),
|
|
56
|
+
"RegistryEntry": ("jarvis_hep_sampler.registry", "RegistryEntry"),
|
|
57
|
+
"SamplerRegistry": ("jarvis_hep_sampler.registry", "SamplerRegistry"),
|
|
58
|
+
"samplers_home": ("jarvis_hep_sampler.registry", "samplers_home"),
|
|
59
|
+
"render_template": ("jarvis_hep_sampler.template", "render_template"),
|
|
60
|
+
"write_template": ("jarvis_hep_sampler.template", "write_template"),
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
# Kept literal so linters and IDEs see the exports; a test pins it to _LAZY_ATTRS.
|
|
64
|
+
__all__ = [
|
|
65
|
+
"API_VERSION",
|
|
66
|
+
"BlockContext",
|
|
67
|
+
"DrawContext",
|
|
68
|
+
"IndependentSession",
|
|
69
|
+
"LoadResult",
|
|
70
|
+
"LoadedPlugin",
|
|
71
|
+
"LogBackend",
|
|
72
|
+
"OptionIssue",
|
|
73
|
+
"PluginConfigError",
|
|
74
|
+
"PluginError",
|
|
75
|
+
"PluginLoadError",
|
|
76
|
+
"PluginMetadata",
|
|
77
|
+
"PluginOutputError",
|
|
78
|
+
"PluginResumeMismatch",
|
|
79
|
+
"RegistryEntry",
|
|
80
|
+
"RegistryError",
|
|
81
|
+
"ReservedSettings",
|
|
82
|
+
"STREAM_PROTOCOL",
|
|
83
|
+
"SamplerLog",
|
|
84
|
+
"SamplerRegistry",
|
|
85
|
+
"SamplerStats",
|
|
86
|
+
"SetupContext",
|
|
87
|
+
"__version__",
|
|
88
|
+
"check_method_name",
|
|
89
|
+
"load_plugin_file",
|
|
90
|
+
"load_plugin_source",
|
|
91
|
+
"parse_reserved",
|
|
92
|
+
"render_template",
|
|
93
|
+
"samplers_home",
|
|
94
|
+
"schema_issues",
|
|
95
|
+
"split_bounds",
|
|
96
|
+
"write_template",
|
|
97
|
+
]
|
|
98
|
+
|
|
99
|
+
if TYPE_CHECKING:
|
|
100
|
+
from jarvis_hep_sampler.contexts import BlockContext, DrawContext, SamplerStats, SetupContext
|
|
101
|
+
from jarvis_hep_sampler.errors import (
|
|
102
|
+
PluginConfigError,
|
|
103
|
+
PluginError,
|
|
104
|
+
PluginLoadError,
|
|
105
|
+
PluginOutputError,
|
|
106
|
+
PluginResumeMismatch,
|
|
107
|
+
RegistryError,
|
|
108
|
+
)
|
|
109
|
+
from jarvis_hep_sampler.loader import LoadedPlugin, load_plugin_file, load_plugin_source
|
|
110
|
+
from jarvis_hep_sampler.log import LogBackend, SamplerLog
|
|
111
|
+
from jarvis_hep_sampler.metadata import API_VERSION, PluginMetadata, check_method_name
|
|
112
|
+
from jarvis_hep_sampler.options import (
|
|
113
|
+
OptionIssue,
|
|
114
|
+
ReservedSettings,
|
|
115
|
+
parse_reserved,
|
|
116
|
+
schema_issues,
|
|
117
|
+
split_bounds,
|
|
118
|
+
)
|
|
119
|
+
from jarvis_hep_sampler.registry import (
|
|
120
|
+
LoadResult,
|
|
121
|
+
RegistryEntry,
|
|
122
|
+
SamplerRegistry,
|
|
123
|
+
samplers_home,
|
|
124
|
+
)
|
|
125
|
+
from jarvis_hep_sampler.session import IndependentSession
|
|
126
|
+
from jarvis_hep_sampler.streams import STREAM_PROTOCOL
|
|
127
|
+
from jarvis_hep_sampler.template import render_template, write_template
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def __getattr__(name: str) -> Any:
|
|
131
|
+
target = _LAZY_ATTRS.get(name)
|
|
132
|
+
if target is None:
|
|
133
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
134
|
+
module_name, attr = target
|
|
135
|
+
value = getattr(import_module(module_name), attr)
|
|
136
|
+
globals()[name] = value
|
|
137
|
+
return value
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def __dir__() -> list[str]:
|
|
141
|
+
return sorted(set(__all__) | set(globals()))
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Private subprocess worker used by testing.check_plugin; no public CLI."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import base64
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import numpy as np
|
|
11
|
+
|
|
12
|
+
from jarvis_hep_sampler.loader import load_plugin_source
|
|
13
|
+
from jarvis_hep_sampler.session import IndependentSession
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def main() -> None:
|
|
17
|
+
data = json.loads(Path(sys.argv[1]).read_text(encoding="utf-8"))
|
|
18
|
+
plugin = load_plugin_source(base64.b64decode(data["source"]), origin=data["origin"])
|
|
19
|
+
session = IndependentSession(
|
|
20
|
+
plugin, seed=data["seed"], point_number=data["point_number"],
|
|
21
|
+
variable_names=data["variable_names"], options=data["options"],
|
|
22
|
+
)
|
|
23
|
+
session.prepare()
|
|
24
|
+
tail = np.array([u for _, u in session.points(data["start"])])
|
|
25
|
+
np.save(sys.argv[2], tail, allow_pickle=False)
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
if __name__ == "__main__":
|
|
29
|
+
main()
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = "1.0.0"
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"""Read-only contexts passed to plug-in functions (technical design §4.3)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from jarvis_hep_sampler.log import SamplerLog
|
|
12
|
+
from jarvis_hep_sampler.streams import prepare_stream
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@dataclass(frozen=True)
|
|
16
|
+
class SetupContext:
|
|
17
|
+
"""Argument of ``prepare(setup)`` and of the settings hook."""
|
|
18
|
+
|
|
19
|
+
seed: int
|
|
20
|
+
point_number: int
|
|
21
|
+
dimension: int
|
|
22
|
+
variable_names: tuple[str, ...]
|
|
23
|
+
options: Mapping[str, Any]
|
|
24
|
+
log: SamplerLog
|
|
25
|
+
|
|
26
|
+
def rng(self) -> np.random.Generator:
|
|
27
|
+
"""A fresh generator on the stream reserved for ``prepare``."""
|
|
28
|
+
return prepare_stream(self.seed)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class DrawContext:
|
|
33
|
+
"""First argument of ``draw(context, rng, plan)``."""
|
|
34
|
+
|
|
35
|
+
candidate_index: int
|
|
36
|
+
point_number: int
|
|
37
|
+
dimension: int
|
|
38
|
+
variable_names: tuple[str, ...]
|
|
39
|
+
options: Mapping[str, Any]
|
|
40
|
+
log: SamplerLog
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@dataclass(frozen=True)
|
|
44
|
+
class BlockContext:
|
|
45
|
+
"""First argument of ``draw_block(context, rng, plan)``.
|
|
46
|
+
|
|
47
|
+
The block covers candidate indices
|
|
48
|
+
``[block_index * block_size, (block_index + 1) * block_size)``.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
block_index: int
|
|
52
|
+
block_size: int
|
|
53
|
+
point_number: int
|
|
54
|
+
dimension: int
|
|
55
|
+
variable_names: tuple[str, ...]
|
|
56
|
+
options: Mapping[str, Any]
|
|
57
|
+
log: SamplerLog
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@dataclass(frozen=True)
|
|
61
|
+
class SamplerStats:
|
|
62
|
+
"""Read-only counters passed to the summary, stop-reason and status hooks."""
|
|
63
|
+
|
|
64
|
+
candidates: int
|
|
65
|
+
accepted: int
|
|
66
|
+
point_number: int
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
__all__ = ["BlockContext", "DrawContext", "SamplerStats", "SetupContext"]
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
"""Exceptions raised for sampler plug-ins (technical design §4.7).
|
|
2
|
+
|
|
3
|
+
Every error carries the context Jarvis-HEP prints in the sampler failure
|
|
4
|
+
record: the method, the plug-in source, the task-card path and, for output
|
|
5
|
+
errors, the candidate index. The plug-in's own exception, when there is one,
|
|
6
|
+
is chained as ``__cause__`` so its traceback reaches the log file.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from typing import Any
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class PluginError(Exception):
|
|
15
|
+
"""Base class for all plug-in errors."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
message: str,
|
|
20
|
+
*,
|
|
21
|
+
method: str | None = None,
|
|
22
|
+
source: str | None = None,
|
|
23
|
+
yaml_path: str | None = None,
|
|
24
|
+
candidate_index: int | None = None,
|
|
25
|
+
) -> None:
|
|
26
|
+
super().__init__(message)
|
|
27
|
+
self.message = message
|
|
28
|
+
self.method = method
|
|
29
|
+
self.source = source
|
|
30
|
+
self.yaml_path = yaml_path
|
|
31
|
+
self.candidate_index = candidate_index
|
|
32
|
+
|
|
33
|
+
def context_rows(self) -> list[tuple[str, Any]]:
|
|
34
|
+
"""Return the known context as ``(field, value)`` rows."""
|
|
35
|
+
rows: list[tuple[str, Any]] = []
|
|
36
|
+
if self.method:
|
|
37
|
+
rows.append(("method", self.method))
|
|
38
|
+
if self.candidate_index is not None:
|
|
39
|
+
rows.append(("candidate", self.candidate_index))
|
|
40
|
+
if self.yaml_path:
|
|
41
|
+
rows.append(("at", self.yaml_path))
|
|
42
|
+
if self.source:
|
|
43
|
+
rows.append(("source", self.source))
|
|
44
|
+
return rows
|
|
45
|
+
|
|
46
|
+
def __str__(self) -> str:
|
|
47
|
+
rows = self.context_rows()
|
|
48
|
+
if not rows:
|
|
49
|
+
return self.message
|
|
50
|
+
details = ", ".join(f"{key} {value}" for key, value in rows)
|
|
51
|
+
return f"{self.message} ({details})"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
class PluginLoadError(PluginError):
|
|
55
|
+
"""The plug-in file cannot be imported or does not follow the file convention."""
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class PluginConfigError(PluginError):
|
|
59
|
+
"""The options, ``validate_options`` or ``prepare`` rejected the configuration."""
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class PluginOutputError(PluginError):
|
|
63
|
+
"""``draw`` / ``draw_block`` raised, or returned a point that breaks the output contract."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
class PluginResumeMismatch(PluginError):
|
|
67
|
+
"""A resumed scan does not match the identity recorded by the original run."""
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
class RegistryError(PluginError):
|
|
71
|
+
"""The user sampler registry is unreadable or one of its entries is broken."""
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
__all__ = [
|
|
75
|
+
"PluginConfigError",
|
|
76
|
+
"PluginError",
|
|
77
|
+
"PluginLoadError",
|
|
78
|
+
"PluginOutputError",
|
|
79
|
+
"PluginResumeMismatch",
|
|
80
|
+
"RegistryError",
|
|
81
|
+
]
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
"""Load a plug-in file (technical design §4.1).
|
|
2
|
+
|
|
3
|
+
The file is read once, hashed, compiled and executed from the same bytes, so
|
|
4
|
+
the recorded SHA-256 always matches the code that runs. ``sys.path`` is not
|
|
5
|
+
touched; the module is registered under a private name derived from its path
|
|
6
|
+
and content.
|
|
7
|
+
|
|
8
|
+
Importing a plug-in runs user code. Only ``Jarvis sampler load`` / ``check``
|
|
9
|
+
and the run preflight do it; ``Jarvis validate`` / ``man`` read the registry.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
import hashlib
|
|
15
|
+
import inspect
|
|
16
|
+
import sys
|
|
17
|
+
import types
|
|
18
|
+
from collections.abc import Callable, Iterable, Mapping
|
|
19
|
+
from dataclasses import dataclass, field
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from jarvis_hep_sampler.errors import PluginError, PluginLoadError
|
|
24
|
+
from jarvis_hep_sampler.metadata import (
|
|
25
|
+
GENERATIONAL,
|
|
26
|
+
INDEPENDENT,
|
|
27
|
+
SUPPORTED_PROFILES,
|
|
28
|
+
PluginMetadata,
|
|
29
|
+
parse_metadata,
|
|
30
|
+
)
|
|
31
|
+
from jarvis_hep_sampler.options import check_options_schema
|
|
32
|
+
|
|
33
|
+
# name -> number of positional arguments the function must accept.
|
|
34
|
+
PROFILE_FUNCTIONS: Mapping[str, Mapping[str, int]] = {
|
|
35
|
+
INDEPENDENT: {"prepare": 1, "draw": 3, "draw_block": 3},
|
|
36
|
+
GENERATIONAL: {"init_state": 1, "ask": 3, "tell": 3},
|
|
37
|
+
}
|
|
38
|
+
OPTIONAL_HOOKS: Mapping[str, int] = {
|
|
39
|
+
"validate_options": 1,
|
|
40
|
+
"settings_rows": 2,
|
|
41
|
+
"summary_rows": 2,
|
|
42
|
+
"stop_reason": 2,
|
|
43
|
+
"status": 2,
|
|
44
|
+
}
|
|
45
|
+
_ARGUMENTS = {
|
|
46
|
+
"prepare": "(setup)",
|
|
47
|
+
"draw": "(context, rng, plan)",
|
|
48
|
+
"draw_block": "(context, rng, plan)",
|
|
49
|
+
"init_state": "(setup)",
|
|
50
|
+
"ask": "(state, context, rng)",
|
|
51
|
+
"tell": "(state, context, results)",
|
|
52
|
+
"validate_options": "(options)",
|
|
53
|
+
"settings_rows": "(setup, plan)",
|
|
54
|
+
"summary_rows": "(plan, stats)",
|
|
55
|
+
"stop_reason": "(plan, stats)",
|
|
56
|
+
"status": "(plan, stats)",
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def sha256_bytes(payload: bytes) -> str:
|
|
61
|
+
return hashlib.sha256(payload).hexdigest()
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass(frozen=True)
|
|
65
|
+
class LoadedPlugin:
|
|
66
|
+
"""An imported, structurally valid plug-in."""
|
|
67
|
+
|
|
68
|
+
metadata: PluginMetadata
|
|
69
|
+
options_schema: Mapping[str, Any] | None
|
|
70
|
+
functions: Mapping[str, Callable[..., Any]]
|
|
71
|
+
source_path: Path
|
|
72
|
+
source_sha256: str
|
|
73
|
+
source_bytes: bytes = field(repr=False)
|
|
74
|
+
module: types.ModuleType = field(repr=False)
|
|
75
|
+
|
|
76
|
+
@property
|
|
77
|
+
def method(self) -> str:
|
|
78
|
+
return self.metadata.method
|
|
79
|
+
|
|
80
|
+
@property
|
|
81
|
+
def hooks(self) -> tuple[str, ...]:
|
|
82
|
+
"""Names of the functions the plug-in defines, sorted."""
|
|
83
|
+
return tuple(sorted(self.functions))
|
|
84
|
+
|
|
85
|
+
def function(self, name: str) -> Callable[..., Any] | None:
|
|
86
|
+
return self.functions.get(name)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def load_plugin_file(path: str | Path, *, reserved_names: Iterable[str] = ()) -> LoadedPlugin:
|
|
90
|
+
"""Import the plug-in at *path* and check it against the file convention."""
|
|
91
|
+
source = Path(path).expanduser()
|
|
92
|
+
if not source.is_file():
|
|
93
|
+
raise PluginLoadError(f"plug-in file not found: {source}", source=str(source))
|
|
94
|
+
resolved = source.resolve()
|
|
95
|
+
try:
|
|
96
|
+
payload = resolved.read_bytes()
|
|
97
|
+
except OSError as exc:
|
|
98
|
+
raise PluginLoadError(f"cannot read {resolved}: {exc}", source=str(resolved)) from exc
|
|
99
|
+
return load_plugin_source(payload, origin=resolved, reserved_names=reserved_names)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def load_plugin_source(
|
|
103
|
+
payload: bytes, *, origin: str | Path, reserved_names: Iterable[str] = ()
|
|
104
|
+
) -> LoadedPlugin:
|
|
105
|
+
"""Import a plug-in from its source bytes; *origin* names it in tracebacks."""
|
|
106
|
+
origin_path = Path(origin)
|
|
107
|
+
origin_text = str(origin_path)
|
|
108
|
+
digest = sha256_bytes(payload)
|
|
109
|
+
module_name = (
|
|
110
|
+
f"_jarvis_sampler_plugin_{sha256_bytes(origin_text.encode('utf-8'))[:12]}_{digest[:12]}"
|
|
111
|
+
)
|
|
112
|
+
try:
|
|
113
|
+
code = compile(payload, origin_text, "exec", dont_inherit=True)
|
|
114
|
+
except SyntaxError as exc:
|
|
115
|
+
raise PluginLoadError(
|
|
116
|
+
f"cannot compile the plug-in: {exc.msg} (line {exc.lineno})", source=origin_text
|
|
117
|
+
) from exc
|
|
118
|
+
|
|
119
|
+
module = types.ModuleType(module_name)
|
|
120
|
+
module.__file__ = origin_text
|
|
121
|
+
previous = sys.modules.get(module_name)
|
|
122
|
+
sys.modules[module_name] = module
|
|
123
|
+
try:
|
|
124
|
+
exec(code, module.__dict__)
|
|
125
|
+
plugin = _inspect_module(
|
|
126
|
+
module,
|
|
127
|
+
origin=origin_path,
|
|
128
|
+
digest=digest,
|
|
129
|
+
payload=payload,
|
|
130
|
+
reserved_names=reserved_names,
|
|
131
|
+
)
|
|
132
|
+
except PluginError:
|
|
133
|
+
_restore_module(module_name, previous)
|
|
134
|
+
raise
|
|
135
|
+
except Exception as exc:
|
|
136
|
+
_restore_module(module_name, previous)
|
|
137
|
+
raise PluginLoadError(
|
|
138
|
+
f"importing the plug-in failed: {type(exc).__name__}: {exc}", source=origin_text
|
|
139
|
+
) from exc
|
|
140
|
+
except BaseException:
|
|
141
|
+
_restore_module(module_name, previous)
|
|
142
|
+
raise
|
|
143
|
+
return plugin
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def _restore_module(name: str, previous: types.ModuleType | None) -> None:
|
|
147
|
+
if previous is None:
|
|
148
|
+
sys.modules.pop(name, None)
|
|
149
|
+
else:
|
|
150
|
+
sys.modules[name] = previous
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _inspect_module(
|
|
154
|
+
module: types.ModuleType,
|
|
155
|
+
*,
|
|
156
|
+
origin: Path,
|
|
157
|
+
digest: str,
|
|
158
|
+
payload: bytes,
|
|
159
|
+
reserved_names: Iterable[str],
|
|
160
|
+
) -> LoadedPlugin:
|
|
161
|
+
namespace = module.__dict__
|
|
162
|
+
source = str(origin)
|
|
163
|
+
if "PLUGIN" not in namespace:
|
|
164
|
+
raise PluginLoadError("the plug-in defines no PLUGIN dict", source=source)
|
|
165
|
+
try:
|
|
166
|
+
metadata = parse_metadata(namespace["PLUGIN"], reserved_names=reserved_names)
|
|
167
|
+
except PluginLoadError as exc:
|
|
168
|
+
exc.source = exc.source or source
|
|
169
|
+
raise
|
|
170
|
+
method = metadata.method
|
|
171
|
+
|
|
172
|
+
def fail(message: str) -> PluginLoadError:
|
|
173
|
+
return PluginLoadError(message, method=method, source=source)
|
|
174
|
+
|
|
175
|
+
if metadata.profile not in SUPPORTED_PROFILES:
|
|
176
|
+
raise fail(
|
|
177
|
+
f"profile {metadata.profile!r} is reserved for a later Jarvis-HEP-Sampler; "
|
|
178
|
+
f"this version runs: {', '.join(SUPPORTED_PROFILES)}"
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
schema = namespace.get("OPTIONS_SCHEMA")
|
|
182
|
+
if schema is not None:
|
|
183
|
+
try:
|
|
184
|
+
schema = check_options_schema(schema)
|
|
185
|
+
except PluginLoadError as exc:
|
|
186
|
+
raise fail(exc.message) from exc
|
|
187
|
+
|
|
188
|
+
wanted = {**PROFILE_FUNCTIONS[metadata.profile], **OPTIONAL_HOOKS}
|
|
189
|
+
functions: dict[str, Callable[..., Any]] = {}
|
|
190
|
+
for name, arity in wanted.items():
|
|
191
|
+
if name not in namespace:
|
|
192
|
+
continue
|
|
193
|
+
candidate = namespace[name]
|
|
194
|
+
if not callable(candidate):
|
|
195
|
+
raise fail(f"{name} must be a function {_ARGUMENTS[name]}")
|
|
196
|
+
_check_arity(name, candidate, arity, fail)
|
|
197
|
+
functions[name] = candidate
|
|
198
|
+
|
|
199
|
+
if metadata.profile == INDEPENDENT:
|
|
200
|
+
if metadata.block_size is not None:
|
|
201
|
+
if "draw_block" not in functions:
|
|
202
|
+
raise fail("PLUGIN['block_size'] is set, so the plug-in must define draw_block()")
|
|
203
|
+
elif "draw" not in functions:
|
|
204
|
+
if "draw_block" in functions:
|
|
205
|
+
raise fail("draw_block() needs PLUGIN['block_size']; or define draw() instead")
|
|
206
|
+
raise fail("the independent profile needs a draw(context, rng, plan) function")
|
|
207
|
+
|
|
208
|
+
return LoadedPlugin(
|
|
209
|
+
metadata=metadata,
|
|
210
|
+
options_schema=schema,
|
|
211
|
+
functions=functions,
|
|
212
|
+
source_path=origin,
|
|
213
|
+
source_sha256=digest,
|
|
214
|
+
source_bytes=payload,
|
|
215
|
+
module=module,
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
def _check_arity(name: str, function: Callable[..., Any], arity: int, fail) -> None:
|
|
220
|
+
try:
|
|
221
|
+
signature = inspect.signature(function)
|
|
222
|
+
except (TypeError, ValueError):
|
|
223
|
+
return
|
|
224
|
+
try:
|
|
225
|
+
signature.bind(*([None] * arity))
|
|
226
|
+
except TypeError as exc:
|
|
227
|
+
raise fail(f"{name} must accept {_ARGUMENTS[name]}: {exc}") from exc
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
__all__ = [
|
|
231
|
+
"LoadedPlugin",
|
|
232
|
+
"OPTIONAL_HOOKS",
|
|
233
|
+
"PROFILE_FUNCTIONS",
|
|
234
|
+
"load_plugin_file",
|
|
235
|
+
"load_plugin_source",
|
|
236
|
+
"sha256_bytes",
|
|
237
|
+
]
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"""Logging facade handed to plug-ins (technical design §4.5).
|
|
2
|
+
|
|
3
|
+
Plug-ins never touch a logger directly. They call the ``log`` object they get
|
|
4
|
+
in every context. Inside Jarvis-HEP the backend writes to
|
|
5
|
+
``Jarvis-HEP.Sampler.<Method>`` in the Jarvis layout; offline (``check``,
|
|
6
|
+
:mod:`jarvis_hep_sampler.testing`) it writes to the standard ``logging``
|
|
7
|
+
logger ``jarvis_hep_sampler.<Method>``.
|
|
8
|
+
|
|
9
|
+
The runtime alone writes the lifecycle records (start, settings, ready,
|
|
10
|
+
checkpoint, failure, result, summary). A plug-in writes only step tables (S5),
|
|
11
|
+
adjustments (S7) and saved-file lines (S11), plus free-form messages.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
from __future__ import annotations
|
|
15
|
+
|
|
16
|
+
import logging
|
|
17
|
+
from collections.abc import Iterable, Sequence
|
|
18
|
+
from typing import Any, Protocol
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class LogBackend(Protocol):
|
|
22
|
+
"""Where :class:`SamplerLog` sends its records."""
|
|
23
|
+
|
|
24
|
+
def emit(self, level: int, message: str) -> None: ...
|
|
25
|
+
|
|
26
|
+
def emit_table(self, level: int, title: str, rows: Sequence[tuple[str, Any]]) -> None: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_table(title: str, rows: Iterable[tuple[str, Any]]) -> str:
|
|
30
|
+
"""Render a two-column ``Field | Value`` table like Jarvis-HEP's sampler log."""
|
|
31
|
+
normalized = [(str(key), str(value)) for key, value in rows]
|
|
32
|
+
if not normalized:
|
|
33
|
+
return str(title)
|
|
34
|
+
key_width = max(len("Field"), *(len(key) for key, _ in normalized))
|
|
35
|
+
value_width = max(len("Value"), *(len(value) for _, value in normalized))
|
|
36
|
+
lines = [
|
|
37
|
+
str(title),
|
|
38
|
+
f"{'Field':<{key_width}} | {'Value':<{value_width}}",
|
|
39
|
+
f"{'-' * key_width} | {'-' * value_width}",
|
|
40
|
+
]
|
|
41
|
+
lines.extend(f"{key:<{key_width}} | {value}" for key, value in normalized)
|
|
42
|
+
return "\n".join(lines)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class StdlibLogBackend:
|
|
46
|
+
"""Offline backend: the standard ``logging`` logger ``jarvis_hep_sampler.<method>``."""
|
|
47
|
+
|
|
48
|
+
def __init__(self, method: str) -> None:
|
|
49
|
+
self.logger = logging.getLogger(f"jarvis_hep_sampler.{method}")
|
|
50
|
+
|
|
51
|
+
def emit(self, level: int, message: str) -> None:
|
|
52
|
+
self.logger.log(level, message)
|
|
53
|
+
|
|
54
|
+
def emit_table(self, level: int, title: str, rows: Sequence[tuple[str, Any]]) -> None:
|
|
55
|
+
self.logger.log(level, format_table(title, rows))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
class SamplerLog:
|
|
59
|
+
"""The ``log`` object a plug-in receives in ``SetupContext`` / ``DrawContext``."""
|
|
60
|
+
|
|
61
|
+
def __init__(self, method: str, backend: LogBackend | None = None) -> None:
|
|
62
|
+
self._method = str(method)
|
|
63
|
+
self._backend: LogBackend = backend if backend is not None else StdlibLogBackend(method)
|
|
64
|
+
|
|
65
|
+
@property
|
|
66
|
+
def method(self) -> str:
|
|
67
|
+
return self._method
|
|
68
|
+
|
|
69
|
+
def step(self, title: str, rows: Iterable[tuple[str, Any]] = ()) -> None:
|
|
70
|
+
"""S5: one table per generation or stage, e.g. ``log.step("Chains ->", rows)``."""
|
|
71
|
+
self._backend.emit_table(
|
|
72
|
+
logging.INFO, str(title), [(str(key), value) for key, value in rows]
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
def adjust(self, name: str, old: Any, new: Any, why: str) -> None:
|
|
76
|
+
"""S7: a setting that was changed, ignored or clamped."""
|
|
77
|
+
self._backend.emit(logging.WARNING, f"{name}: {old} -> {new} ({why})")
|
|
78
|
+
|
|
79
|
+
def saved(self, what: str, path: Any) -> None:
|
|
80
|
+
"""S11: a result file the plug-in wrote."""
|
|
81
|
+
self._backend.emit(logging.WARNING, f"{what} saved to {path}")
|
|
82
|
+
|
|
83
|
+
def debug(self, message: str, *args: Any) -> None:
|
|
84
|
+
self._backend.emit(logging.DEBUG, _format(message, args))
|
|
85
|
+
|
|
86
|
+
def info(self, message: str, *args: Any) -> None:
|
|
87
|
+
self._backend.emit(logging.INFO, _format(message, args))
|
|
88
|
+
|
|
89
|
+
def warning(self, message: str, *args: Any) -> None:
|
|
90
|
+
self._backend.emit(logging.WARNING, _format(message, args))
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _format(message: str, args: tuple[Any, ...]) -> str:
|
|
94
|
+
return str(message) % args if args else str(message)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
__all__ = ["LogBackend", "SamplerLog", "StdlibLogBackend", "format_table"]
|