spectre-core 0.0.8__py3-none-any.whl → 0.0.10__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.
- spectre_core/__init__.py +0 -3
- spectre_core/_file_io/__init__.py +15 -0
- spectre_core/_file_io/file_handlers.py +128 -0
- spectre_core/capture_configs/__init__.py +29 -0
- spectre_core/capture_configs/_capture_config.py +85 -0
- spectre_core/capture_configs/_capture_templates.py +222 -0
- spectre_core/capture_configs/_parameters.py +110 -0
- spectre_core/capture_configs/_pconstraints.py +82 -0
- spectre_core/capture_configs/_ptemplates.py +450 -0
- spectre_core/capture_configs/_pvalidators.py +173 -0
- spectre_core/chunks/__init__.py +17 -201
- spectre_core/chunks/{base.py → _base.py} +15 -60
- spectre_core/chunks/_chunks.py +200 -0
- spectre_core/chunks/{factory.py → _factory.py} +6 -7
- spectre_core/chunks/library/{callisto/chunk.py → _callisto.py} +4 -7
- spectre_core/chunks/library/{fixed/chunk.py → _fixed_center_frequency.py} +7 -64
- spectre_core/chunks/library/_swept_center_frequency.py +103 -0
- spectre_core/config/__init__.py +20 -0
- spectre_core/config/_paths.py +77 -0
- spectre_core/config/_time_formats.py +15 -0
- spectre_core/exceptions.py +4 -5
- spectre_core/logging/__init__.py +11 -0
- spectre_core/logging/_configure.py +35 -0
- spectre_core/logging/_decorators.py +19 -0
- spectre_core/{logging.py → logging/_log_handlers.py} +13 -58
- spectre_core/plotting/__init__.py +7 -1
- spectre_core/plotting/{base.py → _base.py} +40 -20
- spectre_core/plotting/_format.py +18 -0
- spectre_core/plotting/{panel_stack.py → _panel_stack.py} +50 -48
- spectre_core/plotting/_panels.py +234 -0
- spectre_core/post_processing/__init__.py +14 -0
- spectre_core/post_processing/_base.py +119 -0
- spectre_core/post_processing/_factory.py +23 -0
- spectre_core/post_processing/_post_processor.py +40 -0
- spectre_core/post_processing/library/_fixed_center_frequency.py +115 -0
- spectre_core/post_processing/library/_swept_center_frequency.py +382 -0
- spectre_core/receivers/__init__.py +12 -2
- spectre_core/receivers/_base.py +352 -0
- spectre_core/receivers/{factory.py → _factory.py} +2 -2
- spectre_core/receivers/_spec_names.py +20 -0
- spectre_core/receivers/gr/__init__.py +3 -0
- spectre_core/receivers/gr/_base.py +33 -0
- spectre_core/receivers/gr/_rsp1a.py +158 -0
- spectre_core/receivers/gr/_test.py +123 -0
- spectre_core/receivers/library/_rsp1a.py +61 -0
- spectre_core/receivers/library/_test.py +221 -0
- spectre_core/spectrograms/__init__.py +18 -0
- spectre_core/spectrograms/{analytical.py → _analytical.py} +29 -27
- spectre_core/spectrograms/{array_operations.py → _array_operations.py} +47 -1
- spectre_core/spectrograms/{spectrogram.py → _spectrogram.py} +62 -35
- spectre_core/spectrograms/{transform.py → _transform.py} +76 -89
- spectre_core/{receivers/library → wgetting}/__init__.py +4 -2
- spectre_core/wgetting/_callisto.py +155 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/METADATA +1 -1
- spectre_core-0.0.10.dist-info/RECORD +63 -0
- spectre_core/cfg.py +0 -116
- spectre_core/chunks/library/__init__.py +0 -8
- spectre_core/chunks/library/sweep/__init__.py +0 -0
- spectre_core/chunks/library/sweep/chunk.py +0 -400
- spectre_core/dynamic_imports.py +0 -22
- spectre_core/file_handlers/base.py +0 -68
- spectre_core/file_handlers/configs.py +0 -271
- spectre_core/file_handlers/json.py +0 -40
- spectre_core/file_handlers/text.py +0 -21
- spectre_core/plotting/factory.py +0 -26
- spectre_core/plotting/format.py +0 -19
- spectre_core/plotting/library/__init__.py +0 -7
- spectre_core/plotting/library/frequency_cuts/panel.py +0 -74
- spectre_core/plotting/library/integral_over_frequency/panel.py +0 -34
- spectre_core/plotting/library/spectrogram/panel.py +0 -92
- spectre_core/plotting/library/time_cuts/panel.py +0 -77
- spectre_core/plotting/panel_register.py +0 -13
- spectre_core/receivers/base.py +0 -415
- spectre_core/receivers/library/rsp1a/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/__init__.py +0 -0
- spectre_core/receivers/library/rsp1a/gr/fixed.py +0 -104
- spectre_core/receivers/library/rsp1a/gr/sweep.py +0 -129
- spectre_core/receivers/library/rsp1a/receiver.py +0 -68
- spectre_core/receivers/library/rspduo/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/__init__.py +0 -0
- spectre_core/receivers/library/rspduo/gr/tuner_1_fixed.py +0 -114
- spectre_core/receivers/library/rspduo/gr/tuner_1_sweep.py +0 -131
- spectre_core/receivers/library/rspduo/gr/tuner_2_fixed.py +0 -120
- spectre_core/receivers/library/rspduo/gr/tuner_2_sweep.py +0 -119
- spectre_core/receivers/library/rspduo/receiver.py +0 -97
- spectre_core/receivers/library/test/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/__init__.py +0 -0
- spectre_core/receivers/library/test/gr/cosine_signal_1.py +0 -83
- spectre_core/receivers/library/test/gr/tagged_staircase.py +0 -93
- spectre_core/receivers/library/test/receiver.py +0 -178
- spectre_core/receivers/validators.py +0 -193
- spectre_core/watchdog/__init__.py +0 -6
- spectre_core/watchdog/base.py +0 -105
- spectre_core/watchdog/factory.py +0 -22
- spectre_core/watchdog/library/__init__.py +0 -10
- spectre_core/watchdog/library/fixed/__init__.py +0 -0
- spectre_core/watchdog/library/fixed/event_handler.py +0 -41
- spectre_core/watchdog/library/sweep/event_handler.py +0 -55
- spectre_core/watchdog/post_processor.py +0 -50
- spectre_core/web_fetch/callisto.py +0 -101
- spectre_core-0.0.8.dist-info/RECORD +0 -74
- /spectre_core/chunks/{chunk_register.py → _register.py} +0 -0
- /spectre_core/{watchdog/event_handler_register.py → post_processing/_register.py} +0 -0
- /spectre_core/receivers/{receiver_register.py → _register.py} +0 -0
- /spectre_core/{chunks/library/callisto/__init__.py → receivers/gr/_rspduo.py} +0 -0
- /spectre_core/{chunks/library/fixed/__init__.py → receivers/library/_rspduo.py} +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/LICENSE +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/WHEEL +0 -0
- {spectre_core-0.0.8.dist-info → spectre_core-0.0.10.dist-info}/top_level.txt +0 -0
spectre_core/__init__.py
CHANGED
@@ -0,0 +1,15 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
"""
|
6
|
+
Internal file handling.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from .file_handlers import (
|
10
|
+
BaseFileHandler, JsonHandler, TextHandler,
|
11
|
+
)
|
12
|
+
|
13
|
+
__all__ = [
|
14
|
+
"BaseFileHandler", "JsonHandler", "TextHandler"
|
15
|
+
]
|
@@ -0,0 +1,128 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
import os
|
6
|
+
import json
|
7
|
+
from abc import ABC, abstractmethod
|
8
|
+
from typing import Any, Optional
|
9
|
+
|
10
|
+
|
11
|
+
class BaseFileHandler(ABC):
|
12
|
+
def __init__(self,
|
13
|
+
parent_path: str,
|
14
|
+
base_file_name: str,
|
15
|
+
extension: Optional[str] = None):
|
16
|
+
self._parent_path = parent_path
|
17
|
+
self._base_file_name = base_file_name
|
18
|
+
self._extension = extension
|
19
|
+
|
20
|
+
|
21
|
+
@abstractmethod
|
22
|
+
def read(self) -> Any:
|
23
|
+
pass
|
24
|
+
|
25
|
+
|
26
|
+
@property
|
27
|
+
def parent_path(self) -> str:
|
28
|
+
return self._parent_path
|
29
|
+
|
30
|
+
|
31
|
+
@property
|
32
|
+
def base_file_name(self) -> str:
|
33
|
+
return self._base_file_name
|
34
|
+
|
35
|
+
|
36
|
+
@property
|
37
|
+
def extension(self) -> Optional[str]:
|
38
|
+
return self._extension
|
39
|
+
|
40
|
+
|
41
|
+
@property
|
42
|
+
def file_name(self) -> str:
|
43
|
+
return self._base_file_name if (self._extension is None) else f"{self._base_file_name}.{self._extension}"
|
44
|
+
|
45
|
+
|
46
|
+
@property
|
47
|
+
def file_path(self) -> str:
|
48
|
+
return os.path.join(self._parent_path, self.file_name)
|
49
|
+
|
50
|
+
|
51
|
+
@property
|
52
|
+
def exists(self) -> bool:
|
53
|
+
return os.path.exists(self.file_path)
|
54
|
+
|
55
|
+
|
56
|
+
def make_parent_path(self) -> None:
|
57
|
+
os.makedirs(self.parent_path, exist_ok=True)
|
58
|
+
|
59
|
+
|
60
|
+
def delete(self,
|
61
|
+
ignore_if_missing: bool = False) -> None:
|
62
|
+
if not self.exists and not ignore_if_missing:
|
63
|
+
raise FileNotFoundError(f"{self.file_name} does not exist, and so cannot be deleted")
|
64
|
+
else:
|
65
|
+
os.remove(self.file_path)
|
66
|
+
|
67
|
+
|
68
|
+
def cat(self) -> None:
|
69
|
+
print(self.read())
|
70
|
+
|
71
|
+
|
72
|
+
class JsonHandler(BaseFileHandler):
|
73
|
+
def __init__(self,
|
74
|
+
parent_path: str,
|
75
|
+
base_file_name: str,
|
76
|
+
extension: str = "json",
|
77
|
+
**kwargs):
|
78
|
+
|
79
|
+
self._dict = None # cache
|
80
|
+
super().__init__(parent_path,
|
81
|
+
base_file_name,
|
82
|
+
extension,
|
83
|
+
**kwargs)
|
84
|
+
|
85
|
+
|
86
|
+
def read(self) -> dict[str, Any]:
|
87
|
+
with open(self.file_path, 'r') as f:
|
88
|
+
return json.load(f)
|
89
|
+
|
90
|
+
|
91
|
+
def save(self,
|
92
|
+
d: dict,
|
93
|
+
force: bool = False) -> None:
|
94
|
+
self.make_parent_path()
|
95
|
+
|
96
|
+
if self.exists:
|
97
|
+
if force:
|
98
|
+
pass
|
99
|
+
else:
|
100
|
+
raise RuntimeError((f"{self.file_name} already exists, write has been abandoned. "
|
101
|
+
f"You can override this functionality with `force`"))
|
102
|
+
|
103
|
+
with open(self.file_path, 'w') as file:
|
104
|
+
json.dump(d, file, indent=4)
|
105
|
+
|
106
|
+
|
107
|
+
@property
|
108
|
+
def dict(self) -> dict[str, Any]:
|
109
|
+
if self._dict is None:
|
110
|
+
self._dict = self.read()
|
111
|
+
return self._dict
|
112
|
+
|
113
|
+
|
114
|
+
class TextHandler(BaseFileHandler):
|
115
|
+
def __init__(self,
|
116
|
+
parent_path: str,
|
117
|
+
base_file_name: str,
|
118
|
+
extension: str = "txt",
|
119
|
+
**kwargs):
|
120
|
+
super().__init__(parent_path,
|
121
|
+
base_file_name,
|
122
|
+
extension,
|
123
|
+
**kwargs)
|
124
|
+
|
125
|
+
|
126
|
+
def read(self) -> dict:
|
127
|
+
with open(self.file_path, 'r') as f:
|
128
|
+
return f.read()
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
"""
|
6
|
+
Capture configuration files.
|
7
|
+
"""
|
8
|
+
|
9
|
+
from ._pvalidators import PValidators
|
10
|
+
from ._capture_config import CaptureConfig
|
11
|
+
from ._parameters import (
|
12
|
+
Parameter, Parameters, parse_string_parameters, make_parameters
|
13
|
+
)
|
14
|
+
from ._capture_templates import (
|
15
|
+
CaptureTemplate, CaptureModes, get_base_capture_template, make_base_capture_template
|
16
|
+
)
|
17
|
+
from ._pconstraints import (
|
18
|
+
PConstraint, PConstraints, Bound, OneOf
|
19
|
+
)
|
20
|
+
from ._ptemplates import (
|
21
|
+
PTemplate, PNames, get_base_ptemplate,
|
22
|
+
)
|
23
|
+
|
24
|
+
__all__ = [
|
25
|
+
"Parameter", "Parameters", "parse_string_parameters", "make_parameters",
|
26
|
+
"PConstraint", "PConstraints", "Bound", "OneOf", "PNames", "PTemplate",
|
27
|
+
"get_base_ptemplate", "PValidators", "CaptureConfig", "CaptureTemplate",
|
28
|
+
"CaptureModes", "get_base_capture_template", "make_base_capture_template"
|
29
|
+
]
|
@@ -0,0 +1,85 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
|
7
|
+
from spectre_core._file_io import JsonHandler
|
8
|
+
from spectre_core.config import get_configs_dir_path
|
9
|
+
from spectre_core.exceptions import InvalidTagError
|
10
|
+
from ._parameters import (
|
11
|
+
Parameter,
|
12
|
+
Parameters,
|
13
|
+
make_parameters
|
14
|
+
)
|
15
|
+
|
16
|
+
@dataclass
|
17
|
+
class _CaptureConfigKeys:
|
18
|
+
RECEIVER_NAME = "receiver_name"
|
19
|
+
RECEIVER_MODE = "receiver_mode"
|
20
|
+
PARAMETERS = "parameters"
|
21
|
+
|
22
|
+
|
23
|
+
class CaptureConfig(JsonHandler):
|
24
|
+
def __init__(self,
|
25
|
+
tag: str):
|
26
|
+
self._validate_tag(tag)
|
27
|
+
self._tag = tag
|
28
|
+
super().__init__(get_configs_dir_path(),
|
29
|
+
f"capture_{tag}")
|
30
|
+
|
31
|
+
@property
|
32
|
+
def tag(self) -> str:
|
33
|
+
"""Unique identifier for the capture config."""
|
34
|
+
return self._tag
|
35
|
+
|
36
|
+
|
37
|
+
def _validate_tag(self,
|
38
|
+
tag: str) -> None:
|
39
|
+
if "_" in tag:
|
40
|
+
raise InvalidTagError(f"Tags cannot contain an underscore. Received {tag}")
|
41
|
+
if "callisto" in tag:
|
42
|
+
raise InvalidTagError(f'"callisto" cannot be a substring in a native tag. Received "{tag}"')
|
43
|
+
|
44
|
+
|
45
|
+
@property
|
46
|
+
def receiver_name(self) -> str:
|
47
|
+
"""The name of the receiver which created the capture config."""
|
48
|
+
return self.dict[_CaptureConfigKeys.RECEIVER_NAME]
|
49
|
+
|
50
|
+
|
51
|
+
@property
|
52
|
+
def receiver_mode(self) -> str:
|
53
|
+
"""The mode of the receiver which created the capture config."""
|
54
|
+
return self.dict[_CaptureConfigKeys.RECEIVER_MODE]
|
55
|
+
|
56
|
+
|
57
|
+
@property
|
58
|
+
def parameters(self) -> Parameters:
|
59
|
+
"""The parameters stored inside the capture config."""
|
60
|
+
return make_parameters( self.dict[_CaptureConfigKeys.PARAMETERS] )
|
61
|
+
|
62
|
+
|
63
|
+
def get_parameter(self,
|
64
|
+
name: str) -> Parameter:
|
65
|
+
return self.parameters.get_parameter(name)
|
66
|
+
|
67
|
+
|
68
|
+
def get_parameter_value(self,
|
69
|
+
name: str) -> Parameter:
|
70
|
+
return self.parameters.get_parameter_value(name)
|
71
|
+
|
72
|
+
|
73
|
+
def save_parameters(self,
|
74
|
+
receiver_name: str,
|
75
|
+
receiver_mode: str,
|
76
|
+
parameters: Parameters,
|
77
|
+
force: bool = False):
|
78
|
+
"""Write the input parameters to file."""
|
79
|
+
d = {
|
80
|
+
_CaptureConfigKeys.RECEIVER_MODE: receiver_mode,
|
81
|
+
_CaptureConfigKeys.RECEIVER_NAME: receiver_name,
|
82
|
+
_CaptureConfigKeys.PARAMETERS : parameters.to_dict()
|
83
|
+
}
|
84
|
+
self.save(d,
|
85
|
+
force=force)
|
@@ -0,0 +1,222 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from copy import deepcopy
|
6
|
+
from typing import Any
|
7
|
+
from dataclasses import dataclass
|
8
|
+
|
9
|
+
from ._parameters import Parameter, Parameters
|
10
|
+
from ._pconstraints import PConstraint
|
11
|
+
from ._ptemplates import (
|
12
|
+
PTemplate,
|
13
|
+
PNames,
|
14
|
+
get_base_ptemplate
|
15
|
+
)
|
16
|
+
|
17
|
+
class CaptureTemplate:
|
18
|
+
"""A managed collection of PTemplates"""
|
19
|
+
def __init__(self):
|
20
|
+
self._ptemplates: dict[str, PTemplate] = {}
|
21
|
+
|
22
|
+
|
23
|
+
@property
|
24
|
+
def name_list(self) -> list[str]:
|
25
|
+
"""List the names of all stored PTemplates."""
|
26
|
+
return list(self._ptemplates.keys())
|
27
|
+
|
28
|
+
|
29
|
+
def add_ptemplate(self,
|
30
|
+
ptemplate: PTemplate) -> None:
|
31
|
+
"""Add a ptemplate to this capture template."""
|
32
|
+
self._ptemplates[ptemplate.name] = ptemplate
|
33
|
+
|
34
|
+
|
35
|
+
def get_ptemplate(self,
|
36
|
+
parameter_name: str) -> PTemplate:
|
37
|
+
"""Get the ptemplate corresponding with the parameter name."""
|
38
|
+
if parameter_name not in self._ptemplates:
|
39
|
+
raise ValueError(f"Parameter with name '{parameter_name}' is not found in the template. "
|
40
|
+
f"Expected one of {self.name_list}")
|
41
|
+
return self._ptemplates[parameter_name]
|
42
|
+
|
43
|
+
|
44
|
+
def __apply_parameter_template(self,
|
45
|
+
parameter: Parameter):
|
46
|
+
"""Apply the corresponding parameter template to the input parameter"""
|
47
|
+
ptemplate = self.get_ptemplate(parameter.name)
|
48
|
+
parameter.value = ptemplate.apply_template(parameter.value)
|
49
|
+
|
50
|
+
|
51
|
+
def __apply_parameter_templates(self,
|
52
|
+
parameters: Parameters) -> None:
|
53
|
+
"""Apply the corresponding parameter template to all explictly specified parameters"""
|
54
|
+
for parameter in parameters:
|
55
|
+
self.__apply_parameter_template(parameter)
|
56
|
+
|
57
|
+
|
58
|
+
def __fill_missing_with_defaults(self,
|
59
|
+
parameters: Parameters) -> None:
|
60
|
+
"""For any missing parameters (as per the capture template), use the corresponding default value."""
|
61
|
+
for ptemplate in self:
|
62
|
+
if ptemplate.name not in parameters.name_list:
|
63
|
+
parameter = ptemplate.make_parameter()
|
64
|
+
parameters.add_parameter(parameter.name,
|
65
|
+
parameter.value)
|
66
|
+
|
67
|
+
|
68
|
+
def apply_template(self,
|
69
|
+
parameters: Parameters) -> Parameters:
|
70
|
+
"""Validate parameters, fill missing with defaults, and return anew."""
|
71
|
+
self.__apply_parameter_templates(parameters)
|
72
|
+
self.__fill_missing_with_defaults(parameters)
|
73
|
+
return parameters
|
74
|
+
|
75
|
+
|
76
|
+
def __iter__(self):
|
77
|
+
"""Iterate over stored ptemplates"""
|
78
|
+
yield from self._ptemplates.values()
|
79
|
+
|
80
|
+
|
81
|
+
def set_default(self,
|
82
|
+
parameter_name: str,
|
83
|
+
default: Any) -> None:
|
84
|
+
"""Set the default of an existing ptemplate."""
|
85
|
+
self.get_ptemplate(parameter_name).default = default
|
86
|
+
|
87
|
+
|
88
|
+
def set_defaults(self,
|
89
|
+
*ptuples: tuple[str, Any]) -> None:
|
90
|
+
"""Update defaults for multiple ptemplates."""
|
91
|
+
for (parameter_name, default) in ptuples:
|
92
|
+
self.set_default(parameter_name, default)
|
93
|
+
|
94
|
+
|
95
|
+
def enforce_default(self,
|
96
|
+
parameter_name: str) -> None:
|
97
|
+
"""Enforce the default of an existing ptemplate"""
|
98
|
+
self.get_ptemplate(parameter_name).enforce_default = True
|
99
|
+
|
100
|
+
|
101
|
+
def enforce_defaults(self,
|
102
|
+
*parameter_names: str) -> None:
|
103
|
+
"""Enforce defaults for multiple parameter names."""
|
104
|
+
for name in parameter_names:
|
105
|
+
self.enforce_default(name)
|
106
|
+
|
107
|
+
|
108
|
+
def add_pconstraint(self,
|
109
|
+
parameter_name: str,
|
110
|
+
pconstraints: list[PConstraint]) -> None:
|
111
|
+
"""Add a pconstraint to an existing ptemplate"""
|
112
|
+
for pconstraint in pconstraints:
|
113
|
+
self.get_ptemplate(parameter_name).add_pconstraint(pconstraint)
|
114
|
+
|
115
|
+
|
116
|
+
def to_dict(self) -> dict:
|
117
|
+
return {ptemplate.name: ptemplate.to_dict() for ptemplate in self}
|
118
|
+
|
119
|
+
|
120
|
+
def make_base_capture_template(*parameter_names: str):
|
121
|
+
"""Make a capture template, composed entirely of base ptemplates."""
|
122
|
+
capture_template = CaptureTemplate()
|
123
|
+
for name in parameter_names:
|
124
|
+
capture_template.add_ptemplate( get_base_ptemplate(name) )
|
125
|
+
return capture_template
|
126
|
+
|
127
|
+
|
128
|
+
@dataclass(frozen=True)
|
129
|
+
class CaptureModes:
|
130
|
+
"""Pre-defined capture types"""
|
131
|
+
FIXED_CENTER_FREQUENCY: str = "fixed-center-frequency"
|
132
|
+
SWEPT_CENTER_FREQUENCY: str = "swept-center-frequency"
|
133
|
+
|
134
|
+
|
135
|
+
def _make_fixed_frequency_capture_template(
|
136
|
+
) -> CaptureTemplate:
|
137
|
+
"""The absolute minimum required parameters for any fixed frequency capture template."""
|
138
|
+
capture_template = make_base_capture_template(
|
139
|
+
PNames.BATCH_SIZE,
|
140
|
+
PNames.CENTER_FREQUENCY,
|
141
|
+
PNames.CHUNK_KEY,
|
142
|
+
PNames.EVENT_HANDLER_KEY,
|
143
|
+
PNames.FREQUENCY_RESOLUTION,
|
144
|
+
PNames.INSTRUMENT,
|
145
|
+
PNames.OBS_ALT,
|
146
|
+
PNames.OBS_LAT,
|
147
|
+
PNames.OBS_LON,
|
148
|
+
PNames.OBJECT,
|
149
|
+
PNames.ORIGIN,
|
150
|
+
PNames.SAMPLE_RATE,
|
151
|
+
PNames.TELESCOPE,
|
152
|
+
PNames.TIME_RANGE,
|
153
|
+
PNames.TIME_RESOLUTION,
|
154
|
+
PNames.WATCH_EXTENSION,
|
155
|
+
PNames.WINDOW_HOP,
|
156
|
+
PNames.WINDOW_SIZE,
|
157
|
+
PNames.WINDOW_TYPE,
|
158
|
+
)
|
159
|
+
capture_template.set_defaults(
|
160
|
+
(PNames.EVENT_HANDLER_KEY, CaptureModes.FIXED_CENTER_FREQUENCY),
|
161
|
+
(PNames.CHUNK_KEY, CaptureModes.FIXED_CENTER_FREQUENCY),
|
162
|
+
(PNames.WATCH_EXTENSION, "bin")
|
163
|
+
)
|
164
|
+
capture_template.enforce_defaults(
|
165
|
+
PNames.EVENT_HANDLER_KEY,
|
166
|
+
PNames.CHUNK_KEY,
|
167
|
+
PNames.WATCH_EXTENSION
|
168
|
+
)
|
169
|
+
return capture_template
|
170
|
+
|
171
|
+
def _make_swept_frequency_capture_template(
|
172
|
+
) -> CaptureTemplate:
|
173
|
+
"""The absolute minimum required parameters for any swept frequency capture template."""
|
174
|
+
capture_template = make_base_capture_template(
|
175
|
+
PNames.BATCH_SIZE,
|
176
|
+
PNames.CHUNK_KEY,
|
177
|
+
PNames.EVENT_HANDLER_KEY,
|
178
|
+
PNames.FREQUENCY_RESOLUTION,
|
179
|
+
PNames.FREQUENCY_STEP,
|
180
|
+
PNames.INSTRUMENT,
|
181
|
+
PNames.MAX_FREQUENCY,
|
182
|
+
PNames.MIN_FREQUENCY,
|
183
|
+
PNames.OBS_ALT,
|
184
|
+
PNames.OBS_LAT,
|
185
|
+
PNames.OBS_LON,
|
186
|
+
PNames.OBJECT,
|
187
|
+
PNames.ORIGIN,
|
188
|
+
PNames.SAMPLE_RATE,
|
189
|
+
PNames.SAMPLES_PER_STEP,
|
190
|
+
PNames.TELESCOPE,
|
191
|
+
PNames.TIME_RANGE,
|
192
|
+
PNames.TIME_RESOLUTION,
|
193
|
+
PNames.WATCH_EXTENSION,
|
194
|
+
PNames.WINDOW_HOP,
|
195
|
+
PNames.WINDOW_SIZE,
|
196
|
+
PNames.WINDOW_TYPE)
|
197
|
+
capture_template.set_defaults(
|
198
|
+
(PNames.EVENT_HANDLER_KEY, CaptureModes.SWEPT_CENTER_FREQUENCY),
|
199
|
+
(PNames.CHUNK_KEY, CaptureModes.SWEPT_CENTER_FREQUENCY),
|
200
|
+
(PNames.WATCH_EXTENSION, "bin")
|
201
|
+
)
|
202
|
+
capture_template.enforce_defaults(
|
203
|
+
PNames.EVENT_HANDLER_KEY,
|
204
|
+
PNames.CHUNK_KEY,
|
205
|
+
PNames.WATCH_EXTENSION
|
206
|
+
)
|
207
|
+
return capture_template
|
208
|
+
|
209
|
+
|
210
|
+
_base_capture_templates = {
|
211
|
+
CaptureModes.FIXED_CENTER_FREQUENCY: _make_fixed_frequency_capture_template(),
|
212
|
+
CaptureModes.SWEPT_CENTER_FREQUENCY: _make_swept_frequency_capture_template()
|
213
|
+
}
|
214
|
+
|
215
|
+
def get_base_capture_template(
|
216
|
+
capture_mode: str
|
217
|
+
) -> CaptureTemplate:
|
218
|
+
"""Create a fresh deep copy of a pre-defined capture template"""
|
219
|
+
if capture_mode not in _base_capture_templates:
|
220
|
+
raise KeyError(f"No capture template found for the capture mode '{capture_mode}'. "
|
221
|
+
f"Expected one of {list(_base_capture_templates.keys())}")
|
222
|
+
return deepcopy( _base_capture_templates[capture_mode] )
|
@@ -0,0 +1,110 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from typing import Any, Optional, TypeVar
|
6
|
+
|
7
|
+
T = TypeVar('T')
|
8
|
+
|
9
|
+
class Parameter:
|
10
|
+
"""A named value."""
|
11
|
+
def __init__(self,
|
12
|
+
name: str,
|
13
|
+
value: Optional[T] = None):
|
14
|
+
self._name = name
|
15
|
+
self._value: Optional[T] = value
|
16
|
+
|
17
|
+
|
18
|
+
@property
|
19
|
+
def name(self) -> str:
|
20
|
+
"""The parameter name."""
|
21
|
+
return self._name
|
22
|
+
|
23
|
+
|
24
|
+
@property
|
25
|
+
def value(self) -> Optional[T]:
|
26
|
+
"""The parameter value."""
|
27
|
+
return self._value
|
28
|
+
|
29
|
+
|
30
|
+
@value.setter
|
31
|
+
def value(self, v: Optional[T]) -> None:
|
32
|
+
"""Update the parameter value."""
|
33
|
+
self._value = v
|
34
|
+
|
35
|
+
|
36
|
+
class Parameters:
|
37
|
+
"""A collection of parameters."""
|
38
|
+
def __init__(self):
|
39
|
+
self._parameters: dict[str, Parameter] = {}
|
40
|
+
|
41
|
+
|
42
|
+
@property
|
43
|
+
def name_list(self) -> list[str]:
|
44
|
+
"""List the names of stored parameters."""
|
45
|
+
return list(self._parameters.keys())
|
46
|
+
|
47
|
+
|
48
|
+
def add_parameter(self,
|
49
|
+
name: str,
|
50
|
+
value: Optional[T] = None,
|
51
|
+
force: bool = False) -> None:
|
52
|
+
"""Add a new parameter."""
|
53
|
+
if name in self._parameters and not force:
|
54
|
+
raise ValueError(f"Cannot add a parameter with name '{name}', "
|
55
|
+
f"since a parameter already exists with that name. "
|
56
|
+
f"You can overrride this functionality with 'force', "
|
57
|
+
f"to overwrite the existing parameter.")
|
58
|
+
self._parameters[name] = Parameter(name, value)
|
59
|
+
|
60
|
+
|
61
|
+
def get_parameter(self,
|
62
|
+
name: str) -> Parameter:
|
63
|
+
"""Get the parameter with the corresponding name."""
|
64
|
+
if name not in self._parameters:
|
65
|
+
raise KeyError(f"Parameter with name '{name}' does not exist. "
|
66
|
+
f"Expected one of {self.name_list}")
|
67
|
+
return self._parameters[name]
|
68
|
+
|
69
|
+
|
70
|
+
def get_parameter_value(self,
|
71
|
+
name: str) -> Optional[T]:
|
72
|
+
"""Get the value of the parameter with the corresponding name."""
|
73
|
+
return self.get_parameter(name).value
|
74
|
+
|
75
|
+
|
76
|
+
def __iter__(self):
|
77
|
+
"""Iterate over stored parameters"""
|
78
|
+
yield from self._parameters.values()
|
79
|
+
|
80
|
+
|
81
|
+
def to_dict(self) -> dict:
|
82
|
+
"""Convert the class instance to an equivalent dictionary representation"""
|
83
|
+
return {p.name: p.value for p in self}
|
84
|
+
|
85
|
+
def _parse_string_parameter(string_parameter: str) -> list[str]:
|
86
|
+
"""Parse string of the form 'a=b; into a list of the form [a, b]"""
|
87
|
+
if not string_parameter or '=' not in string_parameter:
|
88
|
+
raise ValueError(f"Invalid format: '{string_parameter}'. Expected 'KEY=VALUE'.")
|
89
|
+
if string_parameter.startswith('=') or string_parameter.endswith('='):
|
90
|
+
raise ValueError(f"Invalid format: '{string_parameter}'. Expected 'KEY=VALUE'.")
|
91
|
+
# remove leading and trailing whitespace.
|
92
|
+
string_parameter = string_parameter.strip()
|
93
|
+
return string_parameter.split('=', 1)
|
94
|
+
|
95
|
+
|
96
|
+
def parse_string_parameters(string_parameters: list[str]) -> dict[str, str]:
|
97
|
+
"""Parses a list of strings of the form 'a=b'; into a dictionary mapping each 'a' to each 'b'"""
|
98
|
+
d = {}
|
99
|
+
for string_parameter in string_parameters:
|
100
|
+
k, v = _parse_string_parameter(string_parameter)
|
101
|
+
d[k] = v
|
102
|
+
return d
|
103
|
+
|
104
|
+
|
105
|
+
def make_parameters(d: dict[str, Any]):
|
106
|
+
"""Make an instance of parameters based on the input dictionary"""
|
107
|
+
parameters = Parameters()
|
108
|
+
for k, v in d.items():
|
109
|
+
parameters.add_parameter(k, v)
|
110
|
+
return parameters
|
@@ -0,0 +1,82 @@
|
|
1
|
+
# SPDX-FileCopyrightText: © 2024 Jimmy Fitzpatrick <jcfitzpatrick12@gmail.com>
|
2
|
+
# This file is part of SPECTRE
|
3
|
+
# SPDX-License-Identifier: GPL-3.0-or-later
|
4
|
+
|
5
|
+
from dataclasses import dataclass
|
6
|
+
from abc import ABC, abstractmethod
|
7
|
+
from typing import TypeVar, Optional, Any
|
8
|
+
from numbers import Number
|
9
|
+
|
10
|
+
T = TypeVar('T')
|
11
|
+
|
12
|
+
class PConstraint(ABC):
|
13
|
+
"""Abstract base class for parameter constraints."""
|
14
|
+
|
15
|
+
@abstractmethod
|
16
|
+
def constrain(self, value: T) -> None:
|
17
|
+
"""Apply a constraint to the input parameter."""
|
18
|
+
pass
|
19
|
+
|
20
|
+
def __format__(self, format_spec: str = "") -> str:
|
21
|
+
attrs = ", ".join(f"{key}={value!r}" for key, value in vars(self).items())
|
22
|
+
return f"{self.__class__.__name__}({attrs})"
|
23
|
+
|
24
|
+
class Bound(PConstraint):
|
25
|
+
"""Constrain a parameter value to a specific range."""
|
26
|
+
|
27
|
+
def __init__(self,
|
28
|
+
lower_bound: Optional[Number] = None,
|
29
|
+
upper_bound: Optional[Number] = None,
|
30
|
+
strict_lower: bool = False,
|
31
|
+
strict_upper: bool = False):
|
32
|
+
self._lower_bound = lower_bound
|
33
|
+
self._upper_bound = upper_bound
|
34
|
+
self._strict_lower = strict_lower
|
35
|
+
self._strict_upper = strict_upper
|
36
|
+
|
37
|
+
def constrain(self, value: Number) -> None:
|
38
|
+
if self._lower_bound is not None:
|
39
|
+
if self._strict_lower and value <= self._lower_bound:
|
40
|
+
raise ValueError(f"Value must be strictly greater than {self._lower_bound}. "
|
41
|
+
f"Got {value}.")
|
42
|
+
if not self._strict_lower and value < self._lower_bound:
|
43
|
+
raise ValueError(f"Value must be greater than or equal to {self._lower_bound}. "
|
44
|
+
f"Got {value}.")
|
45
|
+
|
46
|
+
if self._upper_bound is not None:
|
47
|
+
if self._strict_upper and value >= self._upper_bound:
|
48
|
+
raise ValueError(f"Value must be strictly less than {self._upper_bound}. "
|
49
|
+
f"Got {value}.")
|
50
|
+
if not self._strict_upper and value > self._upper_bound:
|
51
|
+
raise ValueError(f"Value must be less than or equal to {self._upper_bound}. "
|
52
|
+
f"Got {value}.")
|
53
|
+
|
54
|
+
|
55
|
+
class OneOf(PConstraint):
|
56
|
+
"""Constrain a parameter to one of a set of defined options."""
|
57
|
+
|
58
|
+
def __init__(self, options: list[Any] = None):
|
59
|
+
self._options = options
|
60
|
+
|
61
|
+
|
62
|
+
def constrain(self, value: Any) -> None:
|
63
|
+
if value not in self._options:
|
64
|
+
raise ValueError(f"Value must be one of {self._options}. Got {value}.")
|
65
|
+
|
66
|
+
|
67
|
+
class _PowerOfTwo(PConstraint):
|
68
|
+
"""Constrain a parameter value to be a power of two."""
|
69
|
+
|
70
|
+
def constrain(self, value: Number) -> None:
|
71
|
+
if value <= 0 or (value & (value - 1)) != 0:
|
72
|
+
raise ValueError(f"Value must be a power of two. Got {value}.")
|
73
|
+
|
74
|
+
|
75
|
+
@dataclass(frozen=True)
|
76
|
+
class PConstraints:
|
77
|
+
"""Ready-made pconstraint instances for frequent use-cases."""
|
78
|
+
power_of_two = _PowerOfTwo()
|
79
|
+
enforce_positive = Bound(lower_bound=0, strict_lower=True)
|
80
|
+
enforce_negative = Bound(upper_bound=0, strict_upper=True)
|
81
|
+
enforce_non_negative = Bound(lower_bound=0, strict_lower=False)
|
82
|
+
enforce_non_positive = Bound(upper_bound=0, strict_upper=False)
|