optocad_backscattermeter 0.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+ )
@@ -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,11 @@
1
+ optocad_backscattermeter/__init__.py,sha256=VUIQVWewj0apwFDp4PuWCAMlFOEYxpMmBmE8xak-pgU,27
2
+ optocad_backscattermeter/cli.py,sha256=HIVLC8a7JBm1XOwdNDCuCLt3WB61EYFMUDrJk8V5ulk,3799
3
+ optocad_backscattermeter/configuration.py,sha256=gKf_YCwC06bKu8_-vMBtpQ1c3PFrxysZgcmBb1U8HQw,2229
4
+ optocad_backscattermeter/parser.py,sha256=Cr7CI0q7mVzJ2JNPgiSn3kNi7oZukQ9heb5uI_VZ_c8,5732
5
+ optocad_backscattermeter/utils.py,sha256=Dyjp1hm3i-fW5yZuCDxDHqNRr-xaLtIZFHkB98votqk,4673
6
+ optocad_backscattermeter/configuration/1,sha256=4CPF0XpF4ws6KOY6omqfSVkK1EhiNNEtdMdEGQ8NFQI,1505
7
+ optocad_backscattermeter/fortran/display.f95,sha256=jEC1usoEBodmRbxoDAnt8Htq7EcbNZ3RQJL_fRf4sto,599
8
+ optocad_backscattermeter-0.0.0.dist-info/METADATA,sha256=AkDcHlAzqAd75B8wCVvirGmvdqHzCkf-eiCFL9XGkoM,1421
9
+ optocad_backscattermeter-0.0.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
10
+ optocad_backscattermeter-0.0.0.dist-info/entry_points.txt,sha256=R9EP0sIxR1pUaK54B-y450qeTaIC27b_OU_xxqiVq0Q,79
11
+ optocad_backscattermeter-0.0.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ optocad_backscattermeter = optocad_backscattermeter.cli:main