pymagnetos 0.1.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.
- pymagnetos/__init__.py +15 -0
- pymagnetos/cli.py +40 -0
- pymagnetos/core/__init__.py +19 -0
- pymagnetos/core/_config.py +340 -0
- pymagnetos/core/_data.py +132 -0
- pymagnetos/core/_processor.py +905 -0
- pymagnetos/core/config_models.py +57 -0
- pymagnetos/core/gui/__init__.py +6 -0
- pymagnetos/core/gui/_base_mainwindow.py +819 -0
- pymagnetos/core/gui/widgets/__init__.py +19 -0
- pymagnetos/core/gui/widgets/_batch_processing.py +319 -0
- pymagnetos/core/gui/widgets/_configuration.py +167 -0
- pymagnetos/core/gui/widgets/_files.py +129 -0
- pymagnetos/core/gui/widgets/_graphs.py +93 -0
- pymagnetos/core/gui/widgets/_param_content.py +20 -0
- pymagnetos/core/gui/widgets/_popup_progressbar.py +29 -0
- pymagnetos/core/gui/widgets/_text_logger.py +32 -0
- pymagnetos/core/signal_processing.py +1004 -0
- pymagnetos/core/utils.py +85 -0
- pymagnetos/log.py +126 -0
- pymagnetos/py.typed +0 -0
- pymagnetos/pytdo/__init__.py +6 -0
- pymagnetos/pytdo/_config.py +24 -0
- pymagnetos/pytdo/_config_models.py +59 -0
- pymagnetos/pytdo/_tdoprocessor.py +1052 -0
- pymagnetos/pytdo/assets/config_default.toml +84 -0
- pymagnetos/pytdo/gui/__init__.py +26 -0
- pymagnetos/pytdo/gui/_worker.py +106 -0
- pymagnetos/pytdo/gui/main.py +617 -0
- pymagnetos/pytdo/gui/widgets/__init__.py +8 -0
- pymagnetos/pytdo/gui/widgets/_buttons.py +66 -0
- pymagnetos/pytdo/gui/widgets/_configuration.py +78 -0
- pymagnetos/pytdo/gui/widgets/_graphs.py +280 -0
- pymagnetos/pytdo/gui/widgets/_param_content.py +137 -0
- pymagnetos/pyuson/__init__.py +7 -0
- pymagnetos/pyuson/_config.py +26 -0
- pymagnetos/pyuson/_config_models.py +71 -0
- pymagnetos/pyuson/_echoprocessor.py +1901 -0
- pymagnetos/pyuson/assets/config_default.toml +92 -0
- pymagnetos/pyuson/gui/__init__.py +26 -0
- pymagnetos/pyuson/gui/_worker.py +135 -0
- pymagnetos/pyuson/gui/main.py +767 -0
- pymagnetos/pyuson/gui/widgets/__init__.py +7 -0
- pymagnetos/pyuson/gui/widgets/_buttons.py +95 -0
- pymagnetos/pyuson/gui/widgets/_configuration.py +85 -0
- pymagnetos/pyuson/gui/widgets/_graphs.py +248 -0
- pymagnetos/pyuson/gui/widgets/_param_content.py +193 -0
- pymagnetos-0.1.0.dist-info/METADATA +23 -0
- pymagnetos-0.1.0.dist-info/RECORD +51 -0
- pymagnetos-0.1.0.dist-info/WHEEL +4 -0
- pymagnetos-0.1.0.dist-info/entry_points.txt +7 -0
pymagnetos/core/utils.py
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
"""Utility functions."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Mapping
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def merge_dict_nested(
|
|
8
|
+
dic_user: Mapping[str, Any], dic_default: dict[str, Any]
|
|
9
|
+
) -> dict[str, Any]:
|
|
10
|
+
"""
|
|
11
|
+
Merge two dictionaries.
|
|
12
|
+
|
|
13
|
+
Keep elements from `dic_user` if it exists, with a fallback on elements in
|
|
14
|
+
`dic_default`. All nested dictionaries are also treated like this.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
dic_user : dict-like
|
|
19
|
+
Dict being merged, its values have priority over `dic_default`.
|
|
20
|
+
dic_default : dict-like
|
|
21
|
+
Dict being merged, its values are used if they are not in `dic_user`.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
dic : dict
|
|
26
|
+
Merged dict.
|
|
27
|
+
"""
|
|
28
|
+
dic_copy = dic_default.copy()
|
|
29
|
+
for key, value in dic_user.items():
|
|
30
|
+
if key in dic_default:
|
|
31
|
+
if isinstance(value, Mapping):
|
|
32
|
+
dic_copy[key] = merge_dict_nested(value, dic_default[key])
|
|
33
|
+
else:
|
|
34
|
+
if key in dic_user:
|
|
35
|
+
dic_copy[key] = dic_user[key]
|
|
36
|
+
|
|
37
|
+
else:
|
|
38
|
+
dic_copy[key] = dic_user[key]
|
|
39
|
+
|
|
40
|
+
return dic_copy
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def cast_type_dict(
|
|
44
|
+
dic: dict[str, Any], in_type: type, out_type: type
|
|
45
|
+
) -> dict[str, Any]:
|
|
46
|
+
"""
|
|
47
|
+
Convert `in_type` value to `out_type` values in a dictionary.
|
|
48
|
+
|
|
49
|
+
Nested dictionaries are supported, so the casted types can't be `dict`.
|
|
50
|
+
|
|
51
|
+
Parameters
|
|
52
|
+
----------
|
|
53
|
+
dic : dict
|
|
54
|
+
Dict in which values are casted.
|
|
55
|
+
in_type, out_type : type
|
|
56
|
+
`in_type` types are casted to `out_type`. Can't be `dict`.
|
|
57
|
+
"""
|
|
58
|
+
dic_copy = dic.copy()
|
|
59
|
+
for key, value in dic.items():
|
|
60
|
+
if isinstance(value, dict):
|
|
61
|
+
dic_copy[key] = cast_type_dict(value, in_type, out_type)
|
|
62
|
+
else:
|
|
63
|
+
if isinstance(dic_copy[key], in_type):
|
|
64
|
+
dic_copy[key] = out_type(dic[key])
|
|
65
|
+
|
|
66
|
+
return dic_copy
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def strip_none_dict(dic: dict) -> dict:
|
|
70
|
+
"""
|
|
71
|
+
Strip elements that are None from `dic`.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
dic : dict
|
|
76
|
+
Dict in which None values are removed.
|
|
77
|
+
"""
|
|
78
|
+
dic_copy = dic.copy()
|
|
79
|
+
for key, value in dic.items():
|
|
80
|
+
if isinstance(value, dict):
|
|
81
|
+
dic_copy[key] = strip_none_dict(value)
|
|
82
|
+
elif value is None:
|
|
83
|
+
dic_copy.pop(key)
|
|
84
|
+
|
|
85
|
+
return dic_copy
|
pymagnetos/log.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
"""Logging utilities."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
import warnings
|
|
6
|
+
from logging.handlers import RotatingFileHandler
|
|
7
|
+
from pathlib import Path
|
|
8
|
+
from types import TracebackType
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from rich.logging import RichHandler
|
|
12
|
+
|
|
13
|
+
__all__ = ["configure_logger"]
|
|
14
|
+
|
|
15
|
+
# Setup logging directory
|
|
16
|
+
APP_DIR = Path.home() / ".pymagnetos"
|
|
17
|
+
APP_DIR.mkdir(parents=True, exist_ok=True)
|
|
18
|
+
|
|
19
|
+
APP_NAME = "pymagnetos"
|
|
20
|
+
LOGGING_FORMAT = "{asctime}.{msecs:3g} [{levelname}] : {message}"
|
|
21
|
+
CONSOLE_HANDLER = RichHandler(log_time_format="%H:%m:%S.%f")
|
|
22
|
+
FILE_FORMATTER = logging.Formatter(
|
|
23
|
+
LOGGING_FORMAT, style="{", datefmt="%Y-%m-%d %H:%M:%S"
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def configure_logger(
|
|
28
|
+
logger: logging.Logger, filename: str, log_level: str = "INFO"
|
|
29
|
+
) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Configure logger handlers.
|
|
32
|
+
|
|
33
|
+
Parameters
|
|
34
|
+
----------
|
|
35
|
+
filename : str
|
|
36
|
+
Name of the file in which log will be written to. The file will be created under
|
|
37
|
+
$HOME/.pymagnetos/.
|
|
38
|
+
log_level : str, optional
|
|
39
|
+
Logging level to choose from ("DEBUG", "INFO", "WARNING", "ERROR"). Default is
|
|
40
|
+
"INFO".
|
|
41
|
+
"""
|
|
42
|
+
# Define the file handler
|
|
43
|
+
log_file = APP_DIR / filename
|
|
44
|
+
file_handler = RotatingFileHandler(
|
|
45
|
+
log_file, mode="a", encoding="utf-8", maxBytes=int(1e6), backupCount=1
|
|
46
|
+
)
|
|
47
|
+
file_handler.setLevel(log_level)
|
|
48
|
+
file_handler.setFormatter(FILE_FORMATTER)
|
|
49
|
+
|
|
50
|
+
# Add handlers and set log levels
|
|
51
|
+
remove_file_handlers(logger, log_file, mode="same")
|
|
52
|
+
CONSOLE_HANDLER.setLevel(log_level)
|
|
53
|
+
logger.addHandler(CONSOLE_HANDLER)
|
|
54
|
+
logger.addHandler(file_handler)
|
|
55
|
+
logger.setLevel(log_level)
|
|
56
|
+
|
|
57
|
+
# Get the core logger and add the same handlers
|
|
58
|
+
core_logger = logging.getLogger(APP_NAME + ".core")
|
|
59
|
+
remove_file_handlers(core_logger, str(log_file), mode="all")
|
|
60
|
+
core_logger.addHandler(CONSOLE_HANDLER)
|
|
61
|
+
core_logger.addHandler(file_handler)
|
|
62
|
+
core_logger.setLevel(log_level)
|
|
63
|
+
|
|
64
|
+
# Set the logger to log exceptions
|
|
65
|
+
log_uncaugth_exceptions(logger)
|
|
66
|
+
|
|
67
|
+
logger.info(f"Logging to {log_file}")
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def remove_file_handlers(
|
|
71
|
+
logger: logging.Logger,
|
|
72
|
+
file: str | Path,
|
|
73
|
+
mode: Literal["same", "all"] = "same",
|
|
74
|
+
warn: bool = True,
|
|
75
|
+
) -> None:
|
|
76
|
+
"""
|
|
77
|
+
Remove file handlers from a logger.
|
|
78
|
+
|
|
79
|
+
In mode "same", only handlers that log to the same file will be removed, in mode
|
|
80
|
+
"all", all file handlers are removed.
|
|
81
|
+
|
|
82
|
+
Parameters
|
|
83
|
+
----------
|
|
84
|
+
logger : logging.Logger
|
|
85
|
+
Logger instance.
|
|
86
|
+
file : str or Path
|
|
87
|
+
Path to the log file.
|
|
88
|
+
mode : {"same", "all"}, optional
|
|
89
|
+
"same" : remove the handler only if it logs to the same file (default).
|
|
90
|
+
"all": remove all file handlers.
|
|
91
|
+
warn : bool, optional
|
|
92
|
+
Issue a warning when removing a handler set to a different file. Default is
|
|
93
|
+
True.
|
|
94
|
+
"""
|
|
95
|
+
file = str(file)
|
|
96
|
+
for idx in range(len(logger.handlers)):
|
|
97
|
+
handler = logger.handlers[idx]
|
|
98
|
+
if isinstance(handler, logging.FileHandler):
|
|
99
|
+
if handler.baseFilename == file:
|
|
100
|
+
logger.removeHandler(handler)
|
|
101
|
+
else:
|
|
102
|
+
if mode == "all":
|
|
103
|
+
if warn:
|
|
104
|
+
warnings.warn(
|
|
105
|
+
f"The logger was writing to {handler.baseFilename}, "
|
|
106
|
+
"it will stop"
|
|
107
|
+
)
|
|
108
|
+
logger.removeHandler(handler)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def log_uncaugth_exceptions(logger: logging.Logger) -> None:
|
|
112
|
+
"""Make uncaugth exceptions to be logged by the logger."""
|
|
113
|
+
|
|
114
|
+
def handle_exception(
|
|
115
|
+
exc_type: type[BaseException],
|
|
116
|
+
exc_value: BaseException,
|
|
117
|
+
exc_traceback: TracebackType | None,
|
|
118
|
+
) -> None:
|
|
119
|
+
if not issubclass(exc_type, KeyboardInterrupt):
|
|
120
|
+
logger.critical(
|
|
121
|
+
"Uncaught exception", exc_info=(exc_type, exc_value, exc_traceback)
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
sys.__excepthook__(exc_type, exc_value, exc_traceback)
|
|
125
|
+
|
|
126
|
+
sys.excepthook = handle_exception
|
pymagnetos/py.typed
ADDED
|
File without changes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
from pathlib import Path
|
|
2
|
+
from typing import Any
|
|
3
|
+
|
|
4
|
+
from ..core import BaseConfig
|
|
5
|
+
from ._config_models import TDOConfiguration
|
|
6
|
+
|
|
7
|
+
CONFIG_DEFAULT = Path(__file__).parent / "assets" / "config_default.toml"
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class TDOConfig(BaseConfig):
|
|
11
|
+
def __init__(
|
|
12
|
+
self,
|
|
13
|
+
user_file: str | Path | None = None,
|
|
14
|
+
default_file: str | Path | None = None,
|
|
15
|
+
**overrides: Any,
|
|
16
|
+
) -> None:
|
|
17
|
+
if not default_file:
|
|
18
|
+
default_file = CONFIG_DEFAULT
|
|
19
|
+
|
|
20
|
+
super().__init__(TDOConfiguration, user_file, default_file, **overrides)
|
|
21
|
+
|
|
22
|
+
@classmethod
|
|
23
|
+
def loads(cls, json_data: str | bytes, **kwargs) -> "TDOConfig":
|
|
24
|
+
return super()._loads(TDOConfiguration, json_data, **kwargs)
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""Pydantic models for the TDOProcessor Config."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Sequence
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
from pydantic import BaseModel, ConfigDict
|
|
8
|
+
|
|
9
|
+
from ..core.config_models import File, Metadata, Nexus, Parameters
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class Settings(BaseModel):
|
|
13
|
+
"""Settings for signal extraction, analysis and plotting."""
|
|
14
|
+
|
|
15
|
+
# Extraction
|
|
16
|
+
max_time: float
|
|
17
|
+
|
|
18
|
+
spectro_nperseg: int
|
|
19
|
+
spectro_win_size: int
|
|
20
|
+
spectro_noverlap: int
|
|
21
|
+
|
|
22
|
+
barycenters_fwindow: float
|
|
23
|
+
barycenters_fast: bool
|
|
24
|
+
|
|
25
|
+
time_offset: float
|
|
26
|
+
|
|
27
|
+
# Analysis
|
|
28
|
+
poly_window: Sequence[float]
|
|
29
|
+
poly_deg: int
|
|
30
|
+
npoints_interp_inverse: int
|
|
31
|
+
fft_window: Sequence[float]
|
|
32
|
+
fft_pad_mult: int
|
|
33
|
+
max_bfreq: float
|
|
34
|
+
|
|
35
|
+
# Display
|
|
36
|
+
offset: float
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class TDOConfiguration(BaseModel):
|
|
40
|
+
"""A model specialized for TDO experiments."""
|
|
41
|
+
|
|
42
|
+
expid: str
|
|
43
|
+
data_directory: Path
|
|
44
|
+
|
|
45
|
+
filenames: dict[str, str | Path] = dict()
|
|
46
|
+
|
|
47
|
+
files: dict[str, File]
|
|
48
|
+
|
|
49
|
+
measurements: dict[str, int]
|
|
50
|
+
|
|
51
|
+
parameters: Parameters
|
|
52
|
+
settings: Settings
|
|
53
|
+
|
|
54
|
+
metadata: Metadata
|
|
55
|
+
|
|
56
|
+
nx: dict[str, dict[str, Any]]
|
|
57
|
+
nexus: Nexus
|
|
58
|
+
|
|
59
|
+
model_config = ConfigDict(validate_assignment=True)
|