bfflearn 0.1.0.dev1__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.
- bff/__init__.py +30 -0
- bff/bayes/__init__.py +11 -0
- bff/bayes/gaussian_process.py +285 -0
- bff/bayes/kernels.py +71 -0
- bff/bayes/learning.py +484 -0
- bff/bayes/likelihoods.py +142 -0
- bff/bayes/posterior.py +62 -0
- bff/bayes/priors.py +179 -0
- bff/bayes/results.py +327 -0
- bff/bayes/utils.py +333 -0
- bff/cli.py +254 -0
- bff/data.py +93 -0
- bff/domain/__init__.py +12 -0
- bff/domain/bias.py +149 -0
- bff/domain/specs.py +347 -0
- bff/domain/trainset.py +201 -0
- bff/io/__init__.py +12 -0
- bff/io/colvars.py +186 -0
- bff/io/commands.py +20 -0
- bff/io/cp2k.py +330 -0
- bff/io/logs.py +213 -0
- bff/io/mdp.py +98 -0
- bff/io/plumed.py +184 -0
- bff/io/progress.py +74 -0
- bff/io/schedulers.py +118 -0
- bff/io/utils.py +208 -0
- bff/mcmc/__init__.py +3 -0
- bff/mcmc/convergence.py +168 -0
- bff/mcmc/proposal.py +254 -0
- bff/mcmc/sampler.py +481 -0
- bff/plotting.py +362 -0
- bff/qoi/__init__.py +9 -0
- bff/qoi/analysis.py +209 -0
- bff/qoi/data.py +169 -0
- bff/qoi/hbonds.py +255 -0
- bff/qoi/rdf.py +108 -0
- bff/qoi/routines.py +272 -0
- bff/tools.py +168 -0
- bff/topology.py +581 -0
- bff/workflows/__init__.py +0 -0
- bff/workflows/configs.py +1147 -0
- bff/workflows/learn.py +77 -0
- bff/workflows/md.py +326 -0
- bff/workflows/prepare.py +788 -0
- bff/workflows/qoi.py +260 -0
- bff/workflows/runsims.py +620 -0
- bff/workflows/simulate.py +28 -0
- bff/workflows/train.py +69 -0
- bff/workflows/validate.py +29 -0
- bfflearn-0.1.0.dev1.dist-info/METADATA +913 -0
- bfflearn-0.1.0.dev1.dist-info/RECORD +55 -0
- bfflearn-0.1.0.dev1.dist-info/WHEEL +5 -0
- bfflearn-0.1.0.dev1.dist-info/entry_points.txt +2 -0
- bfflearn-0.1.0.dev1.dist-info/licenses/LICENSE +674 -0
- bfflearn-0.1.0.dev1.dist-info/top_level.txt +1 -0
bff/qoi/routines.py
ADDED
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import importlib
|
|
3
|
+
import importlib.util
|
|
4
|
+
import sys
|
|
5
|
+
from dataclasses import dataclass, field
|
|
6
|
+
from functools import lru_cache
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from typing import Any, Callable, Mapping, Sequence
|
|
9
|
+
|
|
10
|
+
from .data import QoI
|
|
11
|
+
from .hbonds import compute_all_hbonds
|
|
12
|
+
from .rdf import compute_all_rdfs
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _split_routine_spec(spec: str) -> tuple[str, str]:
|
|
16
|
+
if ":" not in spec:
|
|
17
|
+
raise ValueError(
|
|
18
|
+
"Imported analysis routines must be specified as "
|
|
19
|
+
"'module:function' or 'path/to/file.py:function'."
|
|
20
|
+
)
|
|
21
|
+
return spec.split(":", maxsplit=1)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _builtin_callable(name: str) -> Callable[..., Any]:
|
|
25
|
+
try:
|
|
26
|
+
return BUILTIN_ROUTINES[name]
|
|
27
|
+
except KeyError as exc:
|
|
28
|
+
known = ", ".join(sorted(BUILTIN_ROUTINES))
|
|
29
|
+
raise ValueError(
|
|
30
|
+
f"Unknown builtin analysis routine {name!r}. "
|
|
31
|
+
f"Known routines are: {known}."
|
|
32
|
+
) from exc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _user_module_name(module_path: Path) -> str:
|
|
36
|
+
digest = hashlib.sha1(str(module_path).encode("utf-8")).hexdigest()[:12]
|
|
37
|
+
return f"bff_user_routine_{module_path.stem}_{digest}"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _load_module_from_path(module_path: Path) -> Any:
|
|
41
|
+
if not module_path.exists():
|
|
42
|
+
raise ValueError(f"Analysis routine file not found: {module_path}")
|
|
43
|
+
|
|
44
|
+
module_name = _user_module_name(module_path)
|
|
45
|
+
existing = sys.modules.get(module_name)
|
|
46
|
+
if existing is not None:
|
|
47
|
+
return existing
|
|
48
|
+
|
|
49
|
+
module_spec = importlib.util.spec_from_file_location(module_name, module_path)
|
|
50
|
+
if module_spec is None or module_spec.loader is None:
|
|
51
|
+
raise ValueError(f"Could not load analysis routine module {module_path}.")
|
|
52
|
+
|
|
53
|
+
module = importlib.util.module_from_spec(module_spec)
|
|
54
|
+
sys.modules[module_name] = module
|
|
55
|
+
module_spec.loader.exec_module(module)
|
|
56
|
+
return module
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _import_module(module_name: str) -> Any:
|
|
60
|
+
if module_name.endswith(".py") or "/" in module_name:
|
|
61
|
+
return _load_module_from_path(Path(module_name).resolve())
|
|
62
|
+
return importlib.import_module(module_name)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
@lru_cache(maxsize=None)
|
|
66
|
+
def _load_routine(spec: str) -> Callable[..., Any]:
|
|
67
|
+
"""Resolve a builtin or import-string routine specification."""
|
|
68
|
+
if ":" not in spec:
|
|
69
|
+
return _builtin_callable(spec)
|
|
70
|
+
|
|
71
|
+
module_name, attr_name = _split_routine_spec(spec)
|
|
72
|
+
module = _import_module(module_name)
|
|
73
|
+
try:
|
|
74
|
+
fn = getattr(module, attr_name)
|
|
75
|
+
except AttributeError as exc:
|
|
76
|
+
raise ValueError(
|
|
77
|
+
f"Callable {attr_name!r} was not found in module {module_name!r}."
|
|
78
|
+
) from exc
|
|
79
|
+
if not callable(fn):
|
|
80
|
+
raise ValueError(f"Resolved object {spec!r} is not callable.")
|
|
81
|
+
return fn
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
BUILTIN_ROUTINES: dict[str, Callable[..., QoI]] = {
|
|
85
|
+
"rdf": compute_all_rdfs,
|
|
86
|
+
"hb": compute_all_hbonds,
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@dataclass(frozen=True, slots=True)
|
|
91
|
+
class AnalysisRoutineConfig:
|
|
92
|
+
"""Configuration for a single-trajectory analysis routine.
|
|
93
|
+
|
|
94
|
+
Attributes
|
|
95
|
+
----------
|
|
96
|
+
routine
|
|
97
|
+
Builtin selector or importable routine specification.
|
|
98
|
+
options
|
|
99
|
+
Keyword arguments forwarded to the analysis routine.
|
|
100
|
+
"""
|
|
101
|
+
routine: str
|
|
102
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@dataclass(frozen=True, slots=True)
|
|
106
|
+
class AnalysisRuntimeConfig:
|
|
107
|
+
"""Runtime settings for the QoI analysis workflow."""
|
|
108
|
+
|
|
109
|
+
in_memory: bool = False
|
|
110
|
+
gc_collect: bool = False
|
|
111
|
+
maxtasksperchild: int = 100
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _normalize_qoi_mapping(result: Mapping[str, Any]) -> dict[str, QoI]:
|
|
115
|
+
"""Validate and normalize a mapping of QoI outputs."""
|
|
116
|
+
normalized: dict[str, QoI] = {}
|
|
117
|
+
for name, value in result.items():
|
|
118
|
+
if not isinstance(value, QoI):
|
|
119
|
+
raise TypeError(
|
|
120
|
+
"Mapping-valued analysis routines must return QoI objects."
|
|
121
|
+
)
|
|
122
|
+
if value.name != name:
|
|
123
|
+
raise ValueError(
|
|
124
|
+
f"QoI mapping key {name!r} does not match QoI.name "
|
|
125
|
+
f"{value.name!r}."
|
|
126
|
+
)
|
|
127
|
+
normalized[name] = value
|
|
128
|
+
return normalized
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
@dataclass(frozen=True, slots=True)
|
|
132
|
+
class RuntimeRoutine:
|
|
133
|
+
fn: Callable[..., Any]
|
|
134
|
+
options: dict[str, Any] = field(default_factory=dict)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def _normalize_routine_config(
|
|
138
|
+
config: Mapping[str, Any],
|
|
139
|
+
*,
|
|
140
|
+
base_dir: Path | None = None,
|
|
141
|
+
) -> AnalysisRoutineConfig:
|
|
142
|
+
"""Validate one routine entry from the QoI configuration."""
|
|
143
|
+
if not isinstance(config, Mapping):
|
|
144
|
+
raise ValueError(
|
|
145
|
+
"Each analysis routine specification must be a mapping with a "
|
|
146
|
+
"'routine' field."
|
|
147
|
+
)
|
|
148
|
+
if "routine" not in config:
|
|
149
|
+
raise ValueError("Each analysis routine must define 'routine'.")
|
|
150
|
+
routine = config["routine"]
|
|
151
|
+
if not isinstance(routine, str):
|
|
152
|
+
raise ValueError("Analysis routine 'routine' must be a string.")
|
|
153
|
+
if "kwargs" in config:
|
|
154
|
+
raise ValueError(
|
|
155
|
+
"Routine options must be provided as flat keys. "
|
|
156
|
+
"Nested 'kwargs' are no longer supported."
|
|
157
|
+
)
|
|
158
|
+
extra_options = {
|
|
159
|
+
key: value
|
|
160
|
+
for key, value in config.items()
|
|
161
|
+
if key != "routine"
|
|
162
|
+
}
|
|
163
|
+
options = dict(extra_options)
|
|
164
|
+
|
|
165
|
+
if ":" in routine:
|
|
166
|
+
module_name, attr_name = _split_routine_spec(routine)
|
|
167
|
+
if module_name.endswith(".py") or "/" in module_name:
|
|
168
|
+
if base_dir is None:
|
|
169
|
+
module_path = Path(module_name).resolve()
|
|
170
|
+
else:
|
|
171
|
+
module_path = (base_dir / module_name).resolve()
|
|
172
|
+
routine = f"{module_path}:{attr_name}"
|
|
173
|
+
|
|
174
|
+
return AnalysisRoutineConfig(
|
|
175
|
+
routine=routine,
|
|
176
|
+
options=options,
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def normalize_routine_list(
|
|
181
|
+
routines: Sequence[Mapping[str, Any]],
|
|
182
|
+
*,
|
|
183
|
+
base_dir: Path | None = None,
|
|
184
|
+
) -> tuple[AnalysisRoutineConfig, ...]:
|
|
185
|
+
if not isinstance(routines, Sequence) or isinstance(routines, (str, bytes)):
|
|
186
|
+
raise ValueError("Analysis routines must be provided as a non-empty list.")
|
|
187
|
+
if not routines:
|
|
188
|
+
raise ValueError("Analysis routines must be provided as a non-empty list.")
|
|
189
|
+
return tuple(
|
|
190
|
+
_normalize_routine_config(routine, base_dir=base_dir)
|
|
191
|
+
for routine in routines
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def normalize_analysis_runtime_config(
|
|
196
|
+
config: Mapping[str, Any] | None,
|
|
197
|
+
) -> AnalysisRuntimeConfig:
|
|
198
|
+
"""Validate and normalize the runtime-only QoI ``run`` section."""
|
|
199
|
+
if config is None:
|
|
200
|
+
config = {}
|
|
201
|
+
if not isinstance(config, Mapping):
|
|
202
|
+
raise ValueError("'run' must be a mapping.")
|
|
203
|
+
if "routines" in config or "systems" in config:
|
|
204
|
+
raise ValueError(
|
|
205
|
+
"'run' only accepts runtime settings. "
|
|
206
|
+
"Define routines under each entry in 'refset.systems'."
|
|
207
|
+
)
|
|
208
|
+
|
|
209
|
+
maxtasksperchild = int(config.get("maxtasksperchild", 100))
|
|
210
|
+
if maxtasksperchild <= 0:
|
|
211
|
+
raise ValueError("'run.maxtasksperchild' must be a positive integer.")
|
|
212
|
+
|
|
213
|
+
return AnalysisRuntimeConfig(
|
|
214
|
+
in_memory=bool(config.get("in_memory", False)),
|
|
215
|
+
gc_collect=bool(config.get("gc_collect", False)),
|
|
216
|
+
maxtasksperchild=maxtasksperchild,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
def run_analysis_routines(
|
|
221
|
+
routines: Sequence[RuntimeRoutine],
|
|
222
|
+
*,
|
|
223
|
+
universe: Any,
|
|
224
|
+
start: int,
|
|
225
|
+
stop: int | None,
|
|
226
|
+
step: int,
|
|
227
|
+
) -> dict[str, QoI]:
|
|
228
|
+
"""Run all configured routines for one trajectory and merge QoI outputs."""
|
|
229
|
+
result: dict[str, QoI] = {}
|
|
230
|
+
for routine in routines:
|
|
231
|
+
qoi_result = routine.fn(
|
|
232
|
+
universe=universe,
|
|
233
|
+
start=start,
|
|
234
|
+
stop=stop,
|
|
235
|
+
step=step,
|
|
236
|
+
**routine.options,
|
|
237
|
+
)
|
|
238
|
+
if isinstance(qoi_result, QoI):
|
|
239
|
+
qoi_mapping = {qoi_result.name: qoi_result}
|
|
240
|
+
elif isinstance(qoi_result, Mapping):
|
|
241
|
+
qoi_mapping = _normalize_qoi_mapping(qoi_result)
|
|
242
|
+
else:
|
|
243
|
+
raise TypeError(
|
|
244
|
+
"Analysis routine must return a QoI object or a mapping of "
|
|
245
|
+
"QoI objects."
|
|
246
|
+
)
|
|
247
|
+
duplicates = set(result) & set(qoi_mapping)
|
|
248
|
+
if duplicates:
|
|
249
|
+
names = ", ".join(sorted(duplicates))
|
|
250
|
+
raise ValueError(
|
|
251
|
+
f"Analysis routines produced duplicate QoI output(s): {names}."
|
|
252
|
+
)
|
|
253
|
+
result.update(qoi_mapping)
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
|
|
257
|
+
def build_analysis_routines(
|
|
258
|
+
systems: Sequence[Sequence[AnalysisRoutineConfig]],
|
|
259
|
+
) -> list[tuple[RuntimeRoutine, ...]]:
|
|
260
|
+
"""Build runtime analysis routines for all trajectory positions."""
|
|
261
|
+
routines_by_system: list[tuple[RuntimeRoutine, ...]] = []
|
|
262
|
+
for system in systems:
|
|
263
|
+
runtime_routines: list[RuntimeRoutine] = []
|
|
264
|
+
for routine in system:
|
|
265
|
+
runtime_routines.append(
|
|
266
|
+
RuntimeRoutine(
|
|
267
|
+
fn=_load_routine(routine.routine),
|
|
268
|
+
options=dict(routine.options),
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
routines_by_system.append(tuple(runtime_routines))
|
|
272
|
+
return routines_by_system
|
bff/tools.py
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from typing import Callable
|
|
3
|
+
|
|
4
|
+
import MDAnalysis as mda
|
|
5
|
+
import numpy as np
|
|
6
|
+
from MDAnalysis.guesser.tables import masses as MDA_MASSES
|
|
7
|
+
from scipy.constants import atomic_mass
|
|
8
|
+
from scipy.spatial.transform import Rotation as R
|
|
9
|
+
|
|
10
|
+
MASSES = np.array(list(MDA_MASSES.values()))
|
|
11
|
+
ELEMENTS = list(MDA_MASSES.keys())
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def _normalized_dimensions(dimensions: np.ndarray | None) -> np.ndarray | None:
|
|
15
|
+
"""Return a usable unit-cell array or ``None`` when box data is missing."""
|
|
16
|
+
if dimensions is None:
|
|
17
|
+
return None
|
|
18
|
+
|
|
19
|
+
arr = np.asarray(dimensions, dtype=float).reshape(-1)
|
|
20
|
+
if arr.size < 3:
|
|
21
|
+
return None
|
|
22
|
+
if not np.all(np.isfinite(arr[:3])):
|
|
23
|
+
return None
|
|
24
|
+
if np.any(arr[:3] <= 0):
|
|
25
|
+
return None
|
|
26
|
+
return arr
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def modify_dcd_frc_header(fn: str) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Modify the header of a DCD file to include the 'CORD' flag.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
fn : str
|
|
36
|
+
Path to the DCD file to be modified.
|
|
37
|
+
|
|
38
|
+
Notes
|
|
39
|
+
-----
|
|
40
|
+
This function opens the file in binary mode and writes the 'CORD' flag
|
|
41
|
+
at the appropriate position in the header.
|
|
42
|
+
"""
|
|
43
|
+
flag = 'CORD'.encode()
|
|
44
|
+
with open(fn, 'rb+') as f:
|
|
45
|
+
f.seek(4)
|
|
46
|
+
f.write(flag)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def compute_distances(
|
|
50
|
+
universe: mda.Universe, ag1: mda.AtomGroup, ag2: mda.AtomGroup,
|
|
51
|
+
start: int = None, stop: int = None, step: int = None,
|
|
52
|
+
pbc: bool = True
|
|
53
|
+
) -> np.ndarray:
|
|
54
|
+
"""Compute distances between two AtomGroups over a trajectory."""
|
|
55
|
+
|
|
56
|
+
start = start or 0
|
|
57
|
+
stop = stop or len(universe.trajectory)
|
|
58
|
+
step = step or 1
|
|
59
|
+
displacements = [
|
|
60
|
+
ag1.positions[:, np.newaxis] - ag2.positions
|
|
61
|
+
for ts in universe.trajectory[start:stop:step]
|
|
62
|
+
]
|
|
63
|
+
if not displacements:
|
|
64
|
+
shape = (0, len(ag1), len(ag2))
|
|
65
|
+
return np.empty(shape, dtype=float)
|
|
66
|
+
|
|
67
|
+
displacements = np.asarray(displacements, dtype=float)
|
|
68
|
+
if pbc:
|
|
69
|
+
box = np.asarray(get_unitcell(universe)[:3], dtype=float)
|
|
70
|
+
displacements -= np.round(displacements / box) * box
|
|
71
|
+
return np.linalg.norm(displacements, axis=-1)
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def get_unitcell(
|
|
75
|
+
universe: mda.Universe,
|
|
76
|
+
ts: mda.coordinates.base.Timestep | None = None,
|
|
77
|
+
) -> np.ndarray:
|
|
78
|
+
"""Return the current or fallback unit cell for a universe.
|
|
79
|
+
|
|
80
|
+
Parameters
|
|
81
|
+
----------
|
|
82
|
+
universe
|
|
83
|
+
MDAnalysis universe that provides the default box information.
|
|
84
|
+
ts
|
|
85
|
+
Optional timestep whose box should be preferred.
|
|
86
|
+
|
|
87
|
+
Returns
|
|
88
|
+
-------
|
|
89
|
+
numpy.ndarray
|
|
90
|
+
Unit-cell vector of length 6.
|
|
91
|
+
|
|
92
|
+
Raises
|
|
93
|
+
------
|
|
94
|
+
ValueError
|
|
95
|
+
If neither the timestep nor the universe provides box dimensions.
|
|
96
|
+
"""
|
|
97
|
+
ts_dimensions = None if ts is None else _normalized_dimensions(ts.dimensions)
|
|
98
|
+
if ts_dimensions is not None:
|
|
99
|
+
return ts_dimensions
|
|
100
|
+
|
|
101
|
+
dimensions = _normalized_dimensions(universe.dimensions)
|
|
102
|
+
if dimensions is not None:
|
|
103
|
+
return dimensions
|
|
104
|
+
|
|
105
|
+
default = _normalized_dimensions(getattr(universe, "_bff_default_dimensions", None))
|
|
106
|
+
if default is None:
|
|
107
|
+
raise ValueError(
|
|
108
|
+
"Trajectory frames do not define box dimensions and no fallback "
|
|
109
|
+
"unit cell is available."
|
|
110
|
+
)
|
|
111
|
+
if ts is not None and ts.dimensions is None:
|
|
112
|
+
ts.dimensions = default
|
|
113
|
+
return default
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def random_placement(coords: np.ndarray, box: np.ndarray) -> np.ndarray:
|
|
117
|
+
"""Randomly place a molecule within a box."""
|
|
118
|
+
displacement = np.random.rand(3) * box
|
|
119
|
+
rotation = R.random().as_matrix()
|
|
120
|
+
coords = coords @ rotation.T
|
|
121
|
+
coords += displacement
|
|
122
|
+
return coords
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def guess_box(n_mol: int) -> np.ndarray:
|
|
126
|
+
"""Approximates a cubic box size based on density of neat water."""
|
|
127
|
+
mass = float(n_mol) * 18.015 * atomic_mass # kg
|
|
128
|
+
density = 1000 # kg/m^3
|
|
129
|
+
length = np.cbrt(mass / density) * 1e10 # Angstroms
|
|
130
|
+
return np.array([length] * 3 + [90, 90, 90])
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def sigmoid(x: np.ndarray, x0: float = 3, scale: float = 5) -> np.ndarray:
|
|
134
|
+
"""Smoothly transitions from 0 to 1 around x=3."""
|
|
135
|
+
x = np.asarray(x)
|
|
136
|
+
arg = (x - x0) * scale
|
|
137
|
+
return 1 / (1 + np.exp(- arg))
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def rdf_sigmoid_mean(n_bins: int, r_range: tuple, ref_rdf: np.ndarray) -> np.ndarray:
|
|
141
|
+
"""Create a sigmoid function for concatenated RDFs."""
|
|
142
|
+
r0, r1 = r_range
|
|
143
|
+
dr_half = (r1 - r0) / (2 * n_bins)
|
|
144
|
+
r = np.linspace(r0, r1, n_bins, endpoint=False) + dr_half
|
|
145
|
+
n_rdf = ref_rdf.size // n_bins
|
|
146
|
+
return np.tile(sigmoid(r), n_rdf)
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def extract_defaults(fn: Callable) -> dict[str, object]:
|
|
150
|
+
"""
|
|
151
|
+
Extract default values from the function signature.
|
|
152
|
+
|
|
153
|
+
Parameters
|
|
154
|
+
----------
|
|
155
|
+
fn : callable
|
|
156
|
+
The function from which to extract default values.
|
|
157
|
+
|
|
158
|
+
Returns
|
|
159
|
+
-------
|
|
160
|
+
dict
|
|
161
|
+
A dictionary with parameter names as keys and their default values.
|
|
162
|
+
"""
|
|
163
|
+
sig = inspect.signature(fn)
|
|
164
|
+
return {
|
|
165
|
+
k: v.default
|
|
166
|
+
for k, v in sig.parameters.items()
|
|
167
|
+
if v.default is not inspect.Parameter.empty
|
|
168
|
+
}
|