optocad_backscattermeter 0.0.0__tar.gz
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.
- optocad_backscattermeter-0.0.0/PKG-INFO +41 -0
- optocad_backscattermeter-0.0.0/README.md +21 -0
- optocad_backscattermeter-0.0.0/justfile +55 -0
- optocad_backscattermeter-0.0.0/mise.toml +6 -0
- optocad_backscattermeter-0.0.0/pyproject.toml +124 -0
- optocad_backscattermeter-0.0.0/src/optocad_backscattermeter/__init__.py +1 -0
- optocad_backscattermeter-0.0.0/src/optocad_backscattermeter/cli.py +143 -0
- optocad_backscattermeter-0.0.0/src/optocad_backscattermeter/configuration/1 +70 -0
- optocad_backscattermeter-0.0.0/src/optocad_backscattermeter/configuration.py +76 -0
- optocad_backscattermeter-0.0.0/src/optocad_backscattermeter/fortran/display.f95 +30 -0
- optocad_backscattermeter-0.0.0/src/optocad_backscattermeter/parser.py +257 -0
- optocad_backscattermeter-0.0.0/src/optocad_backscattermeter/utils.py +167 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: optocad_backscattermeter
|
|
3
|
+
Version: 0.0.0
|
|
4
|
+
Summary: Tool to operate with the optocad model of the backscattermeter at LAPP
|
|
5
|
+
Project-URL: Documentation, https://git.linarphy.net/linarphy/optocad_backscattermeter/wiki
|
|
6
|
+
Project-URL: Homepage, https://git.linarphy.net/linarphy/optocad_backscattermeter
|
|
7
|
+
Project-URL: Source, https://git.linarphy.net/linarphy/optocad_backscattermeter
|
|
8
|
+
Project-URL: Issues, https://git.linarphy.net/linarphy/optocad_backscattermeter/issues
|
|
9
|
+
Author-email: linarphy <linarphy@linarphy.net>
|
|
10
|
+
License-Expression: GPL-3.0-or-later
|
|
11
|
+
Keywords: backscattered light,virgo
|
|
12
|
+
Classifier: Development Status :: 1 - Planning
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering
|
|
17
|
+
Requires-Python: >=3.14.7
|
|
18
|
+
Requires-Dist: astropy>=8.0.2
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# Optocad backscattermeter
|
|
22
|
+
|
|
23
|
+
This package is used to manage and work with optocad models of
|
|
24
|
+
[the backscattermeter at LAPP](https://doi.org/10.48550/arXiv.2202.07370).
|
|
25
|
+
|
|
26
|
+
## Getting started
|
|
27
|
+
|
|
28
|
+
### Requirements
|
|
29
|
+
|
|
30
|
+
- Python >= 3.14.7
|
|
31
|
+
- rich >= 13.9.4 (console output)
|
|
32
|
+
- optocad (you need to be in the LVK collaboration) with `occr` in your
|
|
33
|
+
`$PATH`.
|
|
34
|
+
|
|
35
|
+
### Installation
|
|
36
|
+
|
|
37
|
+
To install this package in the current python environment, use
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
python -m pip install optocad_backscattermeter
|
|
41
|
+
```
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# Optocad backscattermeter
|
|
2
|
+
|
|
3
|
+
This package is used to manage and work with optocad models of
|
|
4
|
+
[the backscattermeter at LAPP](https://doi.org/10.48550/arXiv.2202.07370).
|
|
5
|
+
|
|
6
|
+
## Getting started
|
|
7
|
+
|
|
8
|
+
### Requirements
|
|
9
|
+
|
|
10
|
+
- Python >= 3.14.7
|
|
11
|
+
- rich >= 13.9.4 (console output)
|
|
12
|
+
- optocad (you need to be in the LVK collaboration) with `occr` in your
|
|
13
|
+
`$PATH`.
|
|
14
|
+
|
|
15
|
+
### Installation
|
|
16
|
+
|
|
17
|
+
To install this package in the current python environment, use
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
python -m pip install optocad_backscattermeter
|
|
21
|
+
```
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# run tests
|
|
2
|
+
test:
|
|
3
|
+
hatch run test:coverage run -m unittest tests/test*.py
|
|
4
|
+
hatch run test:coverage report
|
|
5
|
+
rm .coverage
|
|
6
|
+
|
|
7
|
+
# for development: check typing error and fix them if possible
|
|
8
|
+
fix: test
|
|
9
|
+
hatch run dev:ruff format
|
|
10
|
+
hatch run dev:ruff check --fix
|
|
11
|
+
hatch run dev:ty check
|
|
12
|
+
markdownlint-cli2 --fix "**/*.md"
|
|
13
|
+
|
|
14
|
+
# for development: check typing error without fixing anything
|
|
15
|
+
check: test
|
|
16
|
+
hatch run dev:ruff check
|
|
17
|
+
hatch run dev:ty check
|
|
18
|
+
markdownlint-cli2 "**/*.md"
|
|
19
|
+
|
|
20
|
+
# generate stubs
|
|
21
|
+
stubs:
|
|
22
|
+
rm -rf typings/
|
|
23
|
+
hatch run stubs:stubgen -p optocad_backscattermeter --output=typings
|
|
24
|
+
|
|
25
|
+
# update the package
|
|
26
|
+
update: && uninstall install test
|
|
27
|
+
git pull
|
|
28
|
+
hatch env prune
|
|
29
|
+
|
|
30
|
+
# install the package in the current environment
|
|
31
|
+
install: test
|
|
32
|
+
python -m pip install . -e
|
|
33
|
+
|
|
34
|
+
# uninstall the package from the current environment
|
|
35
|
+
uninstall:
|
|
36
|
+
python -m pip uninstall optocad_backscattermeter
|
|
37
|
+
|
|
38
|
+
# for development: build release
|
|
39
|
+
build: check
|
|
40
|
+
hatch build
|
|
41
|
+
|
|
42
|
+
# for development: delete files created by build
|
|
43
|
+
clean-build: build
|
|
44
|
+
rm -rf dist
|
|
45
|
+
|
|
46
|
+
# for development: publish release to pypi and my own package registry
|
|
47
|
+
publish: build && clean-build
|
|
48
|
+
hatch publish
|
|
49
|
+
hatch publish -r https://git.linarphy.net/api/packages/linarphy/pypi/ || true
|
|
50
|
+
|
|
51
|
+
# for development: main command to run after a code change
|
|
52
|
+
release: check && publish
|
|
53
|
+
jj bookmark advance
|
|
54
|
+
jj git push --all
|
|
55
|
+
./hooks/post-commit
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "optocad_backscattermeter"
|
|
3
|
+
version = "0.0.0"
|
|
4
|
+
description = "Tool to operate with the optocad model of the backscattermeter at LAPP"
|
|
5
|
+
authors = [{ name = "linarphy", email = "linarphy@linarphy.net" }]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
requires-python = ">=3.14.7"
|
|
8
|
+
license = " GPL-3.0-or-later"
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Programming Language :: Python :: 3",
|
|
11
|
+
"Intended Audience :: Science/Research",
|
|
12
|
+
"Operating System :: OS Independent",
|
|
13
|
+
"Development Status :: 1 - Planning",
|
|
14
|
+
"Topic :: Scientific/Engineering",
|
|
15
|
+
]
|
|
16
|
+
dependencies = ["astropy>=8.0.2"]
|
|
17
|
+
keywords = ["virgo", "backscattered light"]
|
|
18
|
+
|
|
19
|
+
[project.urls]
|
|
20
|
+
Documentation = "https://git.linarphy.net/linarphy/optocad_backscattermeter/wiki"
|
|
21
|
+
Homepage = "https://git.linarphy.net/linarphy/optocad_backscattermeter"
|
|
22
|
+
Source = "https://git.linarphy.net/linarphy/optocad_backscattermeter"
|
|
23
|
+
Issues = "https://git.linarphy.net/linarphy/optocad_backscattermeter/issues"
|
|
24
|
+
|
|
25
|
+
[project.scripts]
|
|
26
|
+
optocad_backscattermeter = "optocad_backscattermeter.cli:main"
|
|
27
|
+
|
|
28
|
+
[build-system]
|
|
29
|
+
requires = ["hatchling"]
|
|
30
|
+
build-backend = "hatchling.build"
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["src/optocad_backscattermeter", "typings/optocad_backscattermeter"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.targets.sdist]
|
|
36
|
+
exclude = [
|
|
37
|
+
"/.git",
|
|
38
|
+
"/.jj",
|
|
39
|
+
"/.forgejo",
|
|
40
|
+
"/.mypy_cache",
|
|
41
|
+
"/ruff_cache",
|
|
42
|
+
"/hooks",
|
|
43
|
+
"/tests",
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
[tool.hatch.envs.default]
|
|
47
|
+
installer = "uv"
|
|
48
|
+
|
|
49
|
+
[tool.hatch.envs.dev]
|
|
50
|
+
installer = "uv"
|
|
51
|
+
dependency-groups = ["dev"]
|
|
52
|
+
|
|
53
|
+
[tool.hatch.envs.stubs]
|
|
54
|
+
installer = "uv"
|
|
55
|
+
dependency-groups = ["stubs"]
|
|
56
|
+
|
|
57
|
+
[tool.hatch.envs.test]
|
|
58
|
+
installer = "uv"
|
|
59
|
+
dependency-groups = ["test"]
|
|
60
|
+
|
|
61
|
+
[tool.ruff]
|
|
62
|
+
line-length = 72
|
|
63
|
+
|
|
64
|
+
[tool.ruff.lint]
|
|
65
|
+
select = [
|
|
66
|
+
"E",
|
|
67
|
+
"W",
|
|
68
|
+
"F",
|
|
69
|
+
"UP",
|
|
70
|
+
"B",
|
|
71
|
+
"SIM",
|
|
72
|
+
"I",
|
|
73
|
+
"ERA",
|
|
74
|
+
"ANN",
|
|
75
|
+
"S",
|
|
76
|
+
"BLE",
|
|
77
|
+
"FBT",
|
|
78
|
+
"A",
|
|
79
|
+
"COM",
|
|
80
|
+
"C4",
|
|
81
|
+
"EXE",
|
|
82
|
+
"FIX",
|
|
83
|
+
"FA",
|
|
84
|
+
"INT",
|
|
85
|
+
"ISC",
|
|
86
|
+
"PIE",
|
|
87
|
+
"T20",
|
|
88
|
+
"PYI",
|
|
89
|
+
"Q",
|
|
90
|
+
"RSE",
|
|
91
|
+
"RET",
|
|
92
|
+
"SLF",
|
|
93
|
+
"SLOT",
|
|
94
|
+
"TD",
|
|
95
|
+
"TID",
|
|
96
|
+
"TC",
|
|
97
|
+
"ARG",
|
|
98
|
+
"PTH",
|
|
99
|
+
"C90",
|
|
100
|
+
"NPY",
|
|
101
|
+
"N",
|
|
102
|
+
"PERF",
|
|
103
|
+
"D",
|
|
104
|
+
"PL",
|
|
105
|
+
"UP",
|
|
106
|
+
"FURB",
|
|
107
|
+
"RUF",
|
|
108
|
+
"TRY",
|
|
109
|
+
]
|
|
110
|
+
ignore = ["COM812", "ANN401", "D202", "D203", "D212"]
|
|
111
|
+
|
|
112
|
+
[tool.ruff.lint.extend-per-file-ignores]
|
|
113
|
+
"tests/**/*.py" = ["PLR2004", "S311", "E501"]
|
|
114
|
+
|
|
115
|
+
[tool.ruff.format]
|
|
116
|
+
quote-style = "double"
|
|
117
|
+
indent-style = "space"
|
|
118
|
+
docstring-code-format = true
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
[dependency-groups]
|
|
122
|
+
dev = ["ty>=0.0.83", "ruff>=0.16.8"]
|
|
123
|
+
stubs = ["mypy<=1.20.0"]
|
|
124
|
+
test = ["coverage>=7.14", "rich"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Entry-point package."""
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"""Cli entry-point."""
|
|
2
|
+
|
|
3
|
+
from argparse import ArgumentParser, Namespace, _SubParsersAction
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from sys import argv
|
|
6
|
+
|
|
7
|
+
from astropy.units import Quantity
|
|
8
|
+
|
|
9
|
+
from optocad_backscattermeter.utils import scan
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def build_parser() -> ArgumentParser:
|
|
13
|
+
"""Build argument parser."""
|
|
14
|
+
parser = ArgumentParser(
|
|
15
|
+
prog="optocad_backscattermeter",
|
|
16
|
+
description="Run one or multiple OptoCad simulation.",
|
|
17
|
+
)
|
|
18
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
19
|
+
build_scan(subparsers)
|
|
20
|
+
|
|
21
|
+
return parser
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def build_scan(
|
|
25
|
+
subparsers: _SubParsersAction[ArgumentParser],
|
|
26
|
+
) -> ArgumentParser:
|
|
27
|
+
"""Build scan parser."""
|
|
28
|
+
scan = subparsers.add_parser(name="scan")
|
|
29
|
+
scan.add_argument(
|
|
30
|
+
"-n",
|
|
31
|
+
"--configuration-name",
|
|
32
|
+
type=str,
|
|
33
|
+
required=True,
|
|
34
|
+
help="name of the configuration",
|
|
35
|
+
)
|
|
36
|
+
scan.add_argument(
|
|
37
|
+
"-m",
|
|
38
|
+
"--motorized-mirror-angle",
|
|
39
|
+
type=str,
|
|
40
|
+
required=True,
|
|
41
|
+
nargs="+",
|
|
42
|
+
help=(
|
|
43
|
+
"angle offset of the motorized mirror from 45 deg. Can be "
|
|
44
|
+
"written with unit, default to deg. Can be a list of "
|
|
45
|
+
"value for a scan"
|
|
46
|
+
),
|
|
47
|
+
)
|
|
48
|
+
scan.add_argument(
|
|
49
|
+
"-s",
|
|
50
|
+
"--sample-angle",
|
|
51
|
+
type=str,
|
|
52
|
+
required=True,
|
|
53
|
+
nargs="+",
|
|
54
|
+
help=(
|
|
55
|
+
"angle offset of the sample compared to the orthogonal."
|
|
56
|
+
"Can be written with unit, default to deg. Can be a list "
|
|
57
|
+
"of value for a scan"
|
|
58
|
+
),
|
|
59
|
+
)
|
|
60
|
+
scan.add_argument(
|
|
61
|
+
"-p",
|
|
62
|
+
"--sample-position",
|
|
63
|
+
type=str,
|
|
64
|
+
required=True,
|
|
65
|
+
nargs="+",
|
|
66
|
+
help=(
|
|
67
|
+
"position of the sample on the suspended bench. This is "
|
|
68
|
+
"the distance between the start of the bench and the "
|
|
69
|
+
"sample. Can be written with unit, default to cm. Can be a "
|
|
70
|
+
"list of value for a scan"
|
|
71
|
+
),
|
|
72
|
+
)
|
|
73
|
+
scan.add_argument(
|
|
74
|
+
"-o",
|
|
75
|
+
"--output",
|
|
76
|
+
type=Path,
|
|
77
|
+
required=False,
|
|
78
|
+
help="File to write the result to. If none, stdout",
|
|
79
|
+
)
|
|
80
|
+
scan.add_argument(
|
|
81
|
+
"-w",
|
|
82
|
+
"--overwrite",
|
|
83
|
+
action="store_true",
|
|
84
|
+
help=(
|
|
85
|
+
"Overwrite the file if it exists. Useless if output is not "
|
|
86
|
+
"specified"
|
|
87
|
+
),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
return scan
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def to_quantity(values: list[str], default_unit: str) -> Quantity:
|
|
94
|
+
"""Parse quantity, allows float: add default unit in this case."""
|
|
95
|
+
result = []
|
|
96
|
+
for value in values:
|
|
97
|
+
try:
|
|
98
|
+
result.append(Quantity(float(value), unit=default_unit))
|
|
99
|
+
except ValueError:
|
|
100
|
+
result.append(Quantity(value))
|
|
101
|
+
|
|
102
|
+
return Quantity(result)
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def run_scan(namespace: Namespace) -> int:
|
|
106
|
+
"""Run scan simulation."""
|
|
107
|
+
|
|
108
|
+
result = scan(
|
|
109
|
+
configuration_name=namespace.configuration_name,
|
|
110
|
+
motorized_mirror_angle=to_quantity(
|
|
111
|
+
namespace.motorized_mirror_angle, "deg"
|
|
112
|
+
),
|
|
113
|
+
sample_angle=to_quantity(namespace.sample_angle, "deg"),
|
|
114
|
+
sample_position=to_quantity(namespace.sample_position, "cm"),
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if namespace.output is None:
|
|
118
|
+
print(result) # noqa: T201
|
|
119
|
+
else:
|
|
120
|
+
if namespace.output.exists() and not namespace.overwrite:
|
|
121
|
+
message = (
|
|
122
|
+
f"output file {namespace.output} already exists, use "
|
|
123
|
+
"--overwrite to replace it"
|
|
124
|
+
)
|
|
125
|
+
raise ValueError(message)
|
|
126
|
+
|
|
127
|
+
result.write(
|
|
128
|
+
namespace.output,
|
|
129
|
+
format="ascii.ecsv",
|
|
130
|
+
overwrite=namespace.overwrite,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return 0
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def main() -> int:
|
|
137
|
+
"""Enter the main CLI entry-point."""
|
|
138
|
+
|
|
139
|
+
namespace = build_parser().parse_args(argv[1:])
|
|
140
|
+
|
|
141
|
+
if namespace.command == "scan":
|
|
142
|
+
return run_scan(namespace)
|
|
143
|
+
return 1
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# BEAM
|
|
2
|
+
b x=36, y=0, w=350e-6, ag=-180, z=0
|
|
3
|
+
|
|
4
|
+
# BS1
|
|
5
|
+
d ac=srt, x=17, y=0, rd=1., ag=135 # BS1 @ 1, 1, 1.2
|
|
6
|
+
+ t, dm=0.2
|
|
7
|
+
|
|
8
|
+
# MM
|
|
9
|
+
d ac=r, x=17, y=-6, rd=1., ag={absolute_motorized_mirror_angle} # MM @ 1, 0, 1.2
|
|
10
|
+
+ d, dm=0.2
|
|
11
|
+
|
|
12
|
+
# L1
|
|
13
|
+
d ac=t, x=-0.5, y=-6, rd=1., ag=0, f=50 # L1 @ 0, 1.5, 1.2
|
|
14
|
+
+ t, dm=0.2
|
|
15
|
+
|
|
16
|
+
# M4
|
|
17
|
+
d ac=r, x=-3.5, y=-6, rd=1., ag=-217 # M4 @ 0, 1.5, 1.2
|
|
18
|
+
+ d, dm=0.2
|
|
19
|
+
|
|
20
|
+
# M5
|
|
21
|
+
d ac=r, x=3.2, y=-28.5, rd=1., ag=-82.0 # M5 @ 0, -1.5, 1.2
|
|
22
|
+
+ d, dm=0.2
|
|
23
|
+
|
|
24
|
+
# L2
|
|
25
|
+
#d ac=t, x=3, y=-13.5, rd=1., ag=90, f=-10 # L3 @ 1.5, 0, 1.2
|
|
26
|
+
# + t, dm=0.2
|
|
27
|
+
|
|
28
|
+
# L3
|
|
29
|
+
d ac=t, x=2.961387143, y=16.5, rd=2., ag=90, f=40 # L3 @ 2.5, 0, 1.2
|
|
30
|
+
+ t, dm=0.2
|
|
31
|
+
|
|
32
|
+
# start of the bench
|
|
33
|
+
d ac=t, x=2.961387143, y=22.5, rd=2., ag=90, m=0 # start of the bench @ 2.5, 0, 1.2
|
|
34
|
+
+ t, dm=0.2
|
|
35
|
+
|
|
36
|
+
# M6
|
|
37
|
+
d ac=r, x=17, y=4.062426105, rd=1., ag=45 # M6 @ 1.5, 0, 1.2
|
|
38
|
+
+ d, dm=0.2
|
|
39
|
+
|
|
40
|
+
# M7
|
|
41
|
+
d ac=r, x=12.5, y=0, rd=1., ag=-135 # M7 @ -2.5, 0, 1.2
|
|
42
|
+
+ d, dm=0.2
|
|
43
|
+
|
|
44
|
+
# BS2
|
|
45
|
+
d ac={{srt}}2, x=12.5, y=4, rd=1., ag=45 # BS2 @ -3, 1, 1.2
|
|
46
|
+
+ t, dm=0.2
|
|
47
|
+
|
|
48
|
+
# BS3
|
|
49
|
+
d ac={{srt}}2, x=-7.5, y=4, rd=1., ag=-45 # BS3 @ 0, 1.5, 1.2
|
|
50
|
+
+ t, dm=0.2
|
|
51
|
+
|
|
52
|
+
# M8
|
|
53
|
+
d ac=r, x=12.5, y=14, rd=1., ag=45 # M8 @ 1.5, 0, 1.2
|
|
54
|
+
+ d, dm=0.2
|
|
55
|
+
|
|
56
|
+
# Camera
|
|
57
|
+
d ac=d, x=-14.5, y=4.124852211, rd=1., ag=0 # C1 @ -1.5, 0, 1.2
|
|
58
|
+
+ d, dm=0.2
|
|
59
|
+
|
|
60
|
+
# PD1
|
|
61
|
+
d ac=d, x=-20, y=14, rd=1., ag=0 # PD1 @ -2, 0, 1.2
|
|
62
|
+
+ d, dm=0.2
|
|
63
|
+
|
|
64
|
+
# PD2
|
|
65
|
+
d ac=d, x=-7.312721683, y=-8, rd=1., ag=90 # PD2 @ -1, -1.5, 1.2
|
|
66
|
+
+ d, dm=0.2
|
|
67
|
+
|
|
68
|
+
# sample
|
|
69
|
+
d ac=r, x=2.961387143, y={absolute_sample_position}, rd=1., ag={absolute_sample_angle} # sample @ -1.5, 1, 1.2
|
|
70
|
+
+ d, dm=0.2
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"""Configuration class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from importlib.resources import files
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import TYPE_CHECKING, ClassVar
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from astropy.units import Quantity
|
|
10
|
+
|
|
11
|
+
START_OF_BENCH_Y_CM = 22.5
|
|
12
|
+
MOTORIZED_MIRROR_ANGLE_INIT_DEG = -45
|
|
13
|
+
SAMPLE_ANGLE_INIT_DEG = 90
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass
|
|
17
|
+
class Configuration:
|
|
18
|
+
"""Backscattermeter setup configuration."""
|
|
19
|
+
|
|
20
|
+
configuration_name: str
|
|
21
|
+
motorized_mirror_angle: Quantity
|
|
22
|
+
sample_angle: Quantity
|
|
23
|
+
sample_position: Quantity
|
|
24
|
+
configuration_version: ClassVar[str] = "1.0"
|
|
25
|
+
|
|
26
|
+
def __post_init__(self) -> None:
|
|
27
|
+
"""Validate and normalize units."""
|
|
28
|
+
self.sample_angle = self.sample_angle.to("deg")
|
|
29
|
+
self.motorized_mirror_angle = self.motorized_mirror_angle.to(
|
|
30
|
+
"deg"
|
|
31
|
+
)
|
|
32
|
+
self.sample_position = self.sample_position.to("cm")
|
|
33
|
+
|
|
34
|
+
@property
|
|
35
|
+
def configuration_file(self) -> Path:
|
|
36
|
+
"""Path to the current configuration file."""
|
|
37
|
+
|
|
38
|
+
return Path(
|
|
39
|
+
str(
|
|
40
|
+
files("optocad_backscattermeter")
|
|
41
|
+
/ "configuration"
|
|
42
|
+
/ self.configuration_name
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
def read_configuration_file(self) -> str:
|
|
47
|
+
"""Read the configuration file."""
|
|
48
|
+
|
|
49
|
+
return self.configuration_file.read_text()
|
|
50
|
+
|
|
51
|
+
def format(self, configuration: str) -> str:
|
|
52
|
+
"""Format the configuration."""
|
|
53
|
+
|
|
54
|
+
return configuration.format(
|
|
55
|
+
absolute_motorized_mirror_angle=(
|
|
56
|
+
MOTORIZED_MIRROR_ANGLE_INIT_DEG
|
|
57
|
+
+ self.motorized_mirror_angle.to_value("deg")
|
|
58
|
+
),
|
|
59
|
+
absolute_sample_position=START_OF_BENCH_Y_CM
|
|
60
|
+
+ self.sample_position.to_value("cm"),
|
|
61
|
+
absolute_sample_angle=SAMPLE_ANGLE_INIT_DEG
|
|
62
|
+
+ self.sample_angle.to_value("deg"),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
def generate(self) -> str:
|
|
66
|
+
"""Generate the .ocd file of this configuration."""
|
|
67
|
+
|
|
68
|
+
return self.format(self.read_configuration_file())
|
|
69
|
+
|
|
70
|
+
def serialize(self) -> dict[str, str]:
|
|
71
|
+
"""Serialize this object to a dict."""
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
"configuration version": self.configuration_version,
|
|
75
|
+
"configuration name": self.configuration_name,
|
|
76
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
Program SAMPLE
|
|
2
|
+
use optocad
|
|
3
|
+
use rsplot
|
|
4
|
+
|
|
5
|
+
call oc_init(psize=(/450., 800./),unit='cm')
|
|
6
|
+
|
|
7
|
+
call oc_frame(-30., -40., 50., 100., scale=0.5)
|
|
8
|
+
|
|
9
|
+
call oc_set(rix = (/1.45/), &
|
|
10
|
+
lambda = 1.054e-6, &
|
|
11
|
+
print = 'rs act dx dy x1 x2 ang med lb')
|
|
12
|
+
|
|
13
|
+
call oc_input('', nib)
|
|
14
|
+
|
|
15
|
+
icb0=1
|
|
16
|
+
icb1=2
|
|
17
|
+
icb2=ps_color((/1., .6, .6/))
|
|
18
|
+
icb3=ps_color((/1., .8, .8/))
|
|
19
|
+
|
|
20
|
+
do ib=1,nib
|
|
21
|
+
call oc_trace(ib)
|
|
22
|
+
call oc_beam(3.0, fill=icb3)
|
|
23
|
+
call oc_beam(2.0, fill=icb2)
|
|
24
|
+
call oc_beam(1.0, fill=icb1)
|
|
25
|
+
call oc_beam(0.0, icb0, 5, .1, rns=2., rnc=2)
|
|
26
|
+
end do
|
|
27
|
+
call oc_surf
|
|
28
|
+
|
|
29
|
+
call oc_exit
|
|
30
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
"""Parser class."""
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from enum import StrEnum
|
|
5
|
+
|
|
6
|
+
from astropy.units import Unit
|
|
7
|
+
|
|
8
|
+
FIELDS = [
|
|
9
|
+
"rs",
|
|
10
|
+
"s1",
|
|
11
|
+
"s2",
|
|
12
|
+
"cp",
|
|
13
|
+
"md",
|
|
14
|
+
"a",
|
|
15
|
+
"asoi",
|
|
16
|
+
"rd",
|
|
17
|
+
"ang",
|
|
18
|
+
"Ang",
|
|
19
|
+
"an1",
|
|
20
|
+
"an2",
|
|
21
|
+
"w0t",
|
|
22
|
+
"w0s",
|
|
23
|
+
"w1t",
|
|
24
|
+
"w1s",
|
|
25
|
+
"w2t",
|
|
26
|
+
"w2s",
|
|
27
|
+
"z0t",
|
|
28
|
+
"z0s",
|
|
29
|
+
"z1t",
|
|
30
|
+
"z1s",
|
|
31
|
+
"z2t",
|
|
32
|
+
"z2s",
|
|
33
|
+
"R1t",
|
|
34
|
+
"R1s",
|
|
35
|
+
"R2t",
|
|
36
|
+
"R2s",
|
|
37
|
+
"C1t",
|
|
38
|
+
"C1s",
|
|
39
|
+
"C2t",
|
|
40
|
+
"C2s",
|
|
41
|
+
"ipp",
|
|
42
|
+
"tpp",
|
|
43
|
+
"pw",
|
|
44
|
+
"pd",
|
|
45
|
+
"pa",
|
|
46
|
+
"tlt",
|
|
47
|
+
"tls",
|
|
48
|
+
"ph",
|
|
49
|
+
"gp1t",
|
|
50
|
+
"gp1s",
|
|
51
|
+
"gp2t",
|
|
52
|
+
"gp2s",
|
|
53
|
+
"Gp1",
|
|
54
|
+
"Gp2",
|
|
55
|
+
"Gp1t",
|
|
56
|
+
"Gp1s",
|
|
57
|
+
"Gp2t",
|
|
58
|
+
"Gp2s",
|
|
59
|
+
"x1",
|
|
60
|
+
"y1",
|
|
61
|
+
"x2",
|
|
62
|
+
"y2",
|
|
63
|
+
"dx",
|
|
64
|
+
"dy",
|
|
65
|
+
"sas",
|
|
66
|
+
"Sas",
|
|
67
|
+
"label",
|
|
68
|
+
] # from optocad documentation
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
@dataclass
|
|
72
|
+
class Column:
|
|
73
|
+
"""A column of a table."""
|
|
74
|
+
|
|
75
|
+
name: str
|
|
76
|
+
unit: Unit | None
|
|
77
|
+
values: list[str | float | int | bool] = field(default_factory=list)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
@dataclass
|
|
81
|
+
class Table:
|
|
82
|
+
"""A table containing multiple columns."""
|
|
83
|
+
|
|
84
|
+
columns: list[Column]
|
|
85
|
+
|
|
86
|
+
def get(self, name: str) -> Column:
|
|
87
|
+
"""Get a column from its name."""
|
|
88
|
+
|
|
89
|
+
for column in self.columns:
|
|
90
|
+
if column.name == name:
|
|
91
|
+
return column
|
|
92
|
+
|
|
93
|
+
message = f"Column {name} not found."
|
|
94
|
+
raise KeyError(message)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
@dataclass
|
|
98
|
+
class Beam:
|
|
99
|
+
"""Multiple tables."""
|
|
100
|
+
|
|
101
|
+
tables: list[Table] = field(default_factory=list)
|
|
102
|
+
|
|
103
|
+
def add_table(self, columns: list[Column]) -> None:
|
|
104
|
+
"""Add a table to the beam."""
|
|
105
|
+
|
|
106
|
+
self.tables.append(Table(columns=columns))
|
|
107
|
+
|
|
108
|
+
def last(self) -> Table:
|
|
109
|
+
"""Return the last table added."""
|
|
110
|
+
|
|
111
|
+
return self.tables[-1]
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
@dataclass
|
|
115
|
+
class ParserResult:
|
|
116
|
+
"""Main parser result."""
|
|
117
|
+
|
|
118
|
+
beams: list[Beam] = field(default_factory=list)
|
|
119
|
+
|
|
120
|
+
def add_beam(self) -> None:
|
|
121
|
+
"""Add a beam to the result."""
|
|
122
|
+
|
|
123
|
+
self.beams.append(Beam())
|
|
124
|
+
|
|
125
|
+
def last(self) -> Beam:
|
|
126
|
+
"""Return the last beam added."""
|
|
127
|
+
|
|
128
|
+
return self.beams[-1]
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class ParserState(StrEnum):
|
|
132
|
+
"""Parser possible state."""
|
|
133
|
+
|
|
134
|
+
BEFORE = "not in a beam, still waiting for it"
|
|
135
|
+
BEAM_START = "first line of a beam"
|
|
136
|
+
IN_TABLE = "inside a table"
|
|
137
|
+
BETWEEN_TABLE = "table closed maybe still in a beam"
|
|
138
|
+
AFTER = "not in a beam anymore"
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
@dataclass
|
|
142
|
+
class Parser:
|
|
143
|
+
"""Parse OptoCad output."""
|
|
144
|
+
|
|
145
|
+
state: ParserState = ParserState.BEFORE
|
|
146
|
+
result: ParserResult = field(default_factory=ParserResult)
|
|
147
|
+
|
|
148
|
+
def parse(self, content: str) -> ParserResult:
|
|
149
|
+
"""Parse the given content."""
|
|
150
|
+
|
|
151
|
+
self.state = ParserState.BEFORE
|
|
152
|
+
self.result = ParserResult()
|
|
153
|
+
|
|
154
|
+
for line in content.splitlines():
|
|
155
|
+
self.parse_line(line)
|
|
156
|
+
|
|
157
|
+
return self.result
|
|
158
|
+
|
|
159
|
+
def parse_line( # noqa: C901
|
|
160
|
+
self,
|
|
161
|
+
line: str,
|
|
162
|
+
) -> None:
|
|
163
|
+
"""Parse a single line in function of the current state."""
|
|
164
|
+
|
|
165
|
+
if self.state is ParserState.BEFORE:
|
|
166
|
+
result = self.parse_beam(line)
|
|
167
|
+
if result:
|
|
168
|
+
self.state = ParserState.BEAM_START
|
|
169
|
+
elif self.state is ParserState.BEAM_START:
|
|
170
|
+
result = self.parse_header(line)
|
|
171
|
+
if not result:
|
|
172
|
+
message = f"Cannot parse line: {line!r}"
|
|
173
|
+
raise ValueError(message)
|
|
174
|
+
self.state = ParserState.IN_TABLE
|
|
175
|
+
elif self.state is ParserState.IN_TABLE:
|
|
176
|
+
result = self.parse_row(line)
|
|
177
|
+
if not result:
|
|
178
|
+
self.state = ParserState.BETWEEN_TABLE
|
|
179
|
+
elif self.state is ParserState.BETWEEN_TABLE:
|
|
180
|
+
result = self.parse_header(line)
|
|
181
|
+
if result:
|
|
182
|
+
self.state = ParserState.IN_TABLE
|
|
183
|
+
else:
|
|
184
|
+
result = self.parse_beam(line)
|
|
185
|
+
if result:
|
|
186
|
+
self.state = ParserState.BEAM_START
|
|
187
|
+
else:
|
|
188
|
+
self.state = ParserState.AFTER
|
|
189
|
+
elif self.state is ParserState.AFTER:
|
|
190
|
+
pass
|
|
191
|
+
|
|
192
|
+
def parse_beam(self, line: str) -> bool:
|
|
193
|
+
"""Read a line announcing a new beam."""
|
|
194
|
+
|
|
195
|
+
result = line.startswith(
|
|
196
|
+
f"Beam # {len(self.result.beams) + 1}:"
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
if result:
|
|
200
|
+
self.result.add_beam()
|
|
201
|
+
|
|
202
|
+
return result
|
|
203
|
+
|
|
204
|
+
def parse_header(self, line: str) -> bool:
|
|
205
|
+
"""Parse header line."""
|
|
206
|
+
|
|
207
|
+
columns_name = line.split()
|
|
208
|
+
|
|
209
|
+
if not columns_name:
|
|
210
|
+
return False
|
|
211
|
+
|
|
212
|
+
columns = []
|
|
213
|
+
|
|
214
|
+
for column in columns_name:
|
|
215
|
+
name, bracket, rest = column.partition("[")
|
|
216
|
+
unit = rest[:-1] if bracket and rest.endswith("]") else None
|
|
217
|
+
|
|
218
|
+
if bracket and unit is None:
|
|
219
|
+
message = f"Cannot parse {column} name."
|
|
220
|
+
raise ValueError(message)
|
|
221
|
+
|
|
222
|
+
if name not in FIELDS:
|
|
223
|
+
return False
|
|
224
|
+
|
|
225
|
+
if unit is not None:
|
|
226
|
+
unit = Unit(unit)
|
|
227
|
+
|
|
228
|
+
columns.append(Column(name=name, unit=unit))
|
|
229
|
+
|
|
230
|
+
self.result.last().add_table(columns=columns)
|
|
231
|
+
|
|
232
|
+
return True
|
|
233
|
+
|
|
234
|
+
def parse_row(self, line: str) -> bool:
|
|
235
|
+
"""Parse table row."""
|
|
236
|
+
|
|
237
|
+
values = line.split(
|
|
238
|
+
maxsplit=len(self.result.last().last().columns) - 1
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if not values:
|
|
242
|
+
return False
|
|
243
|
+
|
|
244
|
+
for column, value in zip(
|
|
245
|
+
self.result.last().last().columns, values, strict=True
|
|
246
|
+
):
|
|
247
|
+
key = column.name
|
|
248
|
+
if key in ["rs", "s1", "s2", "cp"]:
|
|
249
|
+
column.values.append(int(value))
|
|
250
|
+
elif key == "md":
|
|
251
|
+
column.values.append(bool(int(value)))
|
|
252
|
+
elif key in ["label", "a", "asoi"]:
|
|
253
|
+
column.values.append(value)
|
|
254
|
+
else:
|
|
255
|
+
column.values.append(float(value))
|
|
256
|
+
|
|
257
|
+
return True
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
"""Utils function."""
|
|
2
|
+
|
|
3
|
+
from importlib.resources import files
|
|
4
|
+
from itertools import product
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from shutil import which
|
|
7
|
+
from subprocess import run
|
|
8
|
+
from tempfile import NamedTemporaryFile
|
|
9
|
+
|
|
10
|
+
from astropy.table import QTable, vstack
|
|
11
|
+
from astropy.units import Quantity
|
|
12
|
+
from numpy import atleast_1d, concatenate
|
|
13
|
+
|
|
14
|
+
from optocad_backscattermeter.configuration import Configuration
|
|
15
|
+
from optocad_backscattermeter.parser import Parser
|
|
16
|
+
|
|
17
|
+
EXECUTABLE_NAME = "occr"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def find_optocad() -> Path:
|
|
21
|
+
"""Find the path to OptoCad from the $PATH."""
|
|
22
|
+
|
|
23
|
+
executable = which(EXECUTABLE_NAME)
|
|
24
|
+
|
|
25
|
+
if executable is None:
|
|
26
|
+
message = (
|
|
27
|
+
f"{EXECUTABLE_NAME!r} not found in $PATH. Did you install "
|
|
28
|
+
"OptoCad ?"
|
|
29
|
+
)
|
|
30
|
+
raise FileNotFoundError(message)
|
|
31
|
+
|
|
32
|
+
return Path(executable)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run_optocad(
|
|
36
|
+
ocd_file: Path,
|
|
37
|
+
fortran_display: Path,
|
|
38
|
+
executable: Path | None = None,
|
|
39
|
+
) -> str:
|
|
40
|
+
"""
|
|
41
|
+
Run OptoCad on the given .ocd file.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
ocd_file: Path
|
|
46
|
+
Path to the ocd file.
|
|
47
|
+
fortran_display: Path
|
|
48
|
+
Path to the necessary Fortran script used to load the ocd file.
|
|
49
|
+
executable: Path | None
|
|
50
|
+
Path to the executable. If None, find the path from the $PATH.
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
if executable is None:
|
|
55
|
+
executable = find_optocad()
|
|
56
|
+
|
|
57
|
+
completed = run( # noqa: S603
|
|
58
|
+
[str(executable), str(fortran_display), str(ocd_file)],
|
|
59
|
+
capture_output=True,
|
|
60
|
+
text=True,
|
|
61
|
+
check=True,
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
return completed.stdout
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def to_table(configuration: Configuration, content: str) -> QTable:
|
|
68
|
+
"""Parse OptoCad output and convert it into an astropy table."""
|
|
69
|
+
|
|
70
|
+
result = Parser().parse(content)
|
|
71
|
+
|
|
72
|
+
beams = []
|
|
73
|
+
for beam_number, beam in enumerate(result.beams):
|
|
74
|
+
header = {column.name for column in beam.tables[0].columns}
|
|
75
|
+
|
|
76
|
+
columns = {key: [] for key in sorted(header)}
|
|
77
|
+
columns["table number"] = []
|
|
78
|
+
# merge tables
|
|
79
|
+
for number, table in enumerate(beam.tables):
|
|
80
|
+
if header != {column.name for column in table.columns}:
|
|
81
|
+
message = "Different header on the same beam."
|
|
82
|
+
raise ValueError(message)
|
|
83
|
+
|
|
84
|
+
for column in table.columns:
|
|
85
|
+
if column.unit is not None:
|
|
86
|
+
columns[column.name].append(
|
|
87
|
+
Quantity(column.values, column.unit)
|
|
88
|
+
)
|
|
89
|
+
else:
|
|
90
|
+
columns[column.name].append(column.values)
|
|
91
|
+
|
|
92
|
+
columns["table number"].append(
|
|
93
|
+
[number] * len(table.columns[0].values)
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
for key, column in columns.items():
|
|
97
|
+
columns[key] = concatenate(column) # type: ignore[ty:invalid-assignment]
|
|
98
|
+
|
|
99
|
+
table = QTable(data=columns)
|
|
100
|
+
table["beam number"] = beam_number
|
|
101
|
+
table["motorized mirror angle"] = (
|
|
102
|
+
configuration.motorized_mirror_angle
|
|
103
|
+
)
|
|
104
|
+
table["sample angle"] = configuration.sample_angle
|
|
105
|
+
table["sample position"] = configuration.sample_position
|
|
106
|
+
|
|
107
|
+
beams.append(table)
|
|
108
|
+
|
|
109
|
+
table = vstack(beams)
|
|
110
|
+
table.meta = configuration.serialize()
|
|
111
|
+
|
|
112
|
+
return table
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def scan(
|
|
116
|
+
configuration_name: str,
|
|
117
|
+
motorized_mirror_angle: Quantity,
|
|
118
|
+
sample_angle: Quantity,
|
|
119
|
+
sample_position: Quantity,
|
|
120
|
+
) -> QTable:
|
|
121
|
+
"""
|
|
122
|
+
Run one simulation per combination of the scanned values.
|
|
123
|
+
|
|
124
|
+
Each parameter is a scalar or a vector of quantities; several
|
|
125
|
+
vectors are combined as a cartesian product. Scalars are
|
|
126
|
+
treated as single-value vectors.
|
|
127
|
+
"""
|
|
128
|
+
|
|
129
|
+
parameters = {
|
|
130
|
+
"motorized_mirror_angle": atleast_1d(motorized_mirror_angle),
|
|
131
|
+
"sample_angle": atleast_1d(sample_angle),
|
|
132
|
+
"sample_position": atleast_1d(sample_position),
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
tables = []
|
|
136
|
+
for combination in product(*parameters.values()):
|
|
137
|
+
arguments = dict(zip(parameters, combination, strict=True))
|
|
138
|
+
arguments["configuration_name"] = configuration_name
|
|
139
|
+
|
|
140
|
+
table = main(configuration=Configuration(**arguments))
|
|
141
|
+
tables.append(table)
|
|
142
|
+
|
|
143
|
+
return vstack(tables)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def main(configuration: Configuration) -> QTable:
|
|
147
|
+
"""Run the configuration and give back the result."""
|
|
148
|
+
|
|
149
|
+
with NamedTemporaryFile("w+") as file:
|
|
150
|
+
file.write(configuration.generate())
|
|
151
|
+
file.flush()
|
|
152
|
+
|
|
153
|
+
fortran_display = Path(
|
|
154
|
+
str(
|
|
155
|
+
files("optocad_backscattermeter")
|
|
156
|
+
/ "fortran"
|
|
157
|
+
/ "display.f95"
|
|
158
|
+
)
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
return to_table(
|
|
162
|
+
configuration,
|
|
163
|
+
run_optocad(
|
|
164
|
+
ocd_file=Path(file.name),
|
|
165
|
+
fortran_display=fortran_display,
|
|
166
|
+
),
|
|
167
|
+
)
|