phoq 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.
phoq/__init__.py ADDED
@@ -0,0 +1,74 @@
1
+ """phoq — a headless fiber-optics and quantum-communication lab.
2
+
3
+ Build an experiment from components, wire it, run it::
4
+
5
+ from phoq import Setup, Laser, Fiber, Photodiode, Oscilloscope
6
+
7
+ setup = Setup.from_chain(Laser(power_dbm=-7), Fiber(length=2), Photodiode(), Oscilloscope())
8
+ print(setup.run().summary())
9
+
10
+ Every computation happens here in Python. ``phoq serve my_setup.py`` starts a FastAPI server
11
+ and the 3D virtual lab (``phoq/web``) as an optional, live visualizer of the same objects.
12
+ """
13
+
14
+ from importlib.metadata import PackageNotFoundError, version as _version
15
+
16
+ from .core import Component, Param, Port, Setup, Transfer
17
+ from .detectors import Photodiode, PowerMeter, PowerReading, SinglePhotonDetector
18
+ from .engine import Network, report, synthesize, trace
19
+ from .generators import DCPowerSupply, FunctionGenerator
20
+ from .instruments import CounterResult, Oscilloscope, PhotonCounter, ScopeResult, Trace
21
+ from .modulators import IntensityModulator, PhaseModulator
22
+ from .passive import (
23
+ Attenuator,
24
+ BeamSplitter,
25
+ Coupler,
26
+ Fiber,
27
+ FiberSpool,
28
+ Isolator,
29
+ PhaseShifter,
30
+ )
31
+ from .runner import Results, Runner, Sweep
32
+ from .sources import Laser
33
+
34
+ try:
35
+ __version__ = _version("phoq")
36
+ except PackageNotFoundError: # running from a checkout that is not installed
37
+ __version__ = "0.0.0"
38
+
39
+ __all__ = [
40
+ "Attenuator",
41
+ "BeamSplitter",
42
+ "Component",
43
+ "CounterResult",
44
+ "Coupler",
45
+ "DCPowerSupply",
46
+ "Fiber",
47
+ "FiberSpool",
48
+ "FunctionGenerator",
49
+ "IntensityModulator",
50
+ "Isolator",
51
+ "Laser",
52
+ "Network",
53
+ "Oscilloscope",
54
+ "Param",
55
+ "PhaseModulator",
56
+ "PhaseShifter",
57
+ "PhotonCounter",
58
+ "Photodiode",
59
+ "Port",
60
+ "PowerMeter",
61
+ "PowerReading",
62
+ "Results",
63
+ "Runner",
64
+ "ScopeResult",
65
+ "Setup",
66
+ "SinglePhotonDetector",
67
+ "Sweep",
68
+ "Trace",
69
+ "Transfer",
70
+ "report",
71
+ "synthesize",
72
+ "trace",
73
+ "__version__",
74
+ ]
phoq/__main__.py ADDED
@@ -0,0 +1,260 @@
1
+ """Command line: ``phoq {run,serve,export,catalog} ...``."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import argparse
6
+ import sys
7
+ import webbrowser
8
+ from pathlib import Path
9
+
10
+ from .core import Setup
11
+
12
+
13
+ def _load(path: str | None) -> Setup:
14
+ return Setup.load(path) if path else Setup("untitled")
15
+
16
+
17
+ def _tasks(paths):
18
+ from .analysis import load_tasks
19
+
20
+ return [t for p in paths for t in load_tasks(p)]
21
+
22
+
23
+ def _parties_and_setup(args):
24
+ """Parties from --protocol / --party, and the setup from the file or the protocol's build()."""
25
+ from .parties import load_parties, load_protocol
26
+
27
+ parties, protocol = [], None
28
+ if getattr(args, "protocol", None):
29
+ protocol = load_protocol(args.protocol)
30
+ parties += list(protocol.PARTIES)
31
+ for path in getattr(args, "party", None) or []:
32
+ parties += load_parties(path)
33
+ if args.file:
34
+ setup = _load(args.file)
35
+ elif protocol is not None:
36
+ setup = protocol.build()
37
+ else:
38
+ setup = Setup("untitled")
39
+ return parties, setup
40
+
41
+
42
+ def cmd_run(args) -> None:
43
+ parties, setup = _parties_and_setup(args)
44
+ print(setup.describe(), "\n")
45
+ tasks = _tasks(args.analysis)
46
+ if parties:
47
+ from .parties import Experiment
48
+
49
+ print(
50
+ f"parties: {', '.join(p.name for p in parties)}"
51
+ + (f" · tasks: {', '.join(t.name for t in tasks)}" if tasks else "")
52
+ + f" — {args.blocks} blocks\n"
53
+ )
54
+ Experiment(setup, parties, tasks, seed=args.seed).run(args.blocks)
55
+ return
56
+ if tasks:
57
+ from .analysis import Session
58
+
59
+ print(f"analysis: {', '.join(t.name for t in tasks)} — {args.blocks} batches\n")
60
+ Session(setup, tasks, seed=args.seed).run(args.blocks)
61
+ return
62
+ results = setup.run(*args.instrument, seed=args.seed)
63
+ print(results.summary())
64
+ if args.plot:
65
+ import matplotlib
66
+
67
+ matplotlib.use("Agg")
68
+ import matplotlib.pyplot as plt
69
+
70
+ scopes = [r for r in results.values() if hasattr(r, "plot")]
71
+ if not scopes:
72
+ print("no oscilloscope in this setup; nothing to plot", file=sys.stderr)
73
+ return
74
+ fig, axes = plt.subplots(
75
+ len(scopes), 1, figsize=(8, 3.4 * len(scopes)), squeeze=False
76
+ )
77
+ for ax, r in zip(axes[:, 0], scopes):
78
+ r.plot(ax)
79
+ fig.tight_layout()
80
+ fig.savefig(args.plot, dpi=140)
81
+ print(f"\nsaved {args.plot}")
82
+
83
+
84
+ def cmd_serve(args) -> None:
85
+ from .server import serve
86
+
87
+ parties, setup = _parties_and_setup(args)
88
+ if args.no_save:
89
+ save = None
90
+ elif args.save:
91
+ save = Path(args.save)
92
+ elif args.file and args.file.endswith(".json"):
93
+ save = Path(args.file)
94
+ else:
95
+ save = (
96
+ Path(args.file).with_suffix(".json")
97
+ if args.file
98
+ else Path("lab_setup.json")
99
+ )
100
+ url = f"http://{args.host}:{args.port}"
101
+ print(f"phoq · {setup!r}")
102
+ print(f" virtual lab {url}")
103
+ print(f" API docs {url}/docs")
104
+ print(f" autosave {save or 'off'}")
105
+ if args.open:
106
+ webbrowser.open(url)
107
+ tasks = _tasks(args.analysis)
108
+ if tasks:
109
+ print(f" analysis {', '.join(t.name for t in tasks)}")
110
+ if parties:
111
+ print(
112
+ f" parties {', '.join(p.name for p in parties)} (classical channel on localhost TCP)"
113
+ )
114
+ serve(
115
+ setup,
116
+ host=args.host,
117
+ port=args.port,
118
+ save_path=save,
119
+ web_dir=Path(args.web) if args.web else None,
120
+ seed=args.seed,
121
+ tasks=tasks,
122
+ parties=parties or None,
123
+ )
124
+
125
+
126
+ def cmd_export(args) -> None:
127
+ setup = _load(args.file)
128
+ text = setup.to_python() if args.format == "python" else None
129
+ if args.format == "json":
130
+ import json
131
+
132
+ text = json.dumps(setup.to_dict(), indent=2, ensure_ascii=False) + "\n"
133
+ if args.output:
134
+ Path(args.output).write_text(text)
135
+ print(f"wrote {args.output}")
136
+ else:
137
+ print(text)
138
+
139
+
140
+ def cmd_catalog(args) -> None:
141
+ from .server import catalog
142
+
143
+ for t in catalog()["types"]:
144
+ print(f"{t['type']:<22} {t['label']} — {t['description']}")
145
+ print(
146
+ f"{'':<22} ports: {', '.join(p['name'] + ('*' if p['kind'] == 'electrical' else '') for p in t['ports'])}"
147
+ )
148
+ print(f"{'':<22} params: {', '.join(p['name'] for p in t['params'])}\n")
149
+
150
+
151
+ def main(argv: list[str] | None = None) -> None:
152
+ ap = argparse.ArgumentParser(
153
+ prog="phoq",
154
+ description="phoq — headless photonics & quantum-communication lab with an optional 3D visualizer.",
155
+ )
156
+ sub = ap.add_subparsers(dest="cmd", required=True)
157
+
158
+ p = sub.add_parser("run", help="run a setup headlessly and print the measurements")
159
+ p.add_argument(
160
+ "file",
161
+ nargs="?",
162
+ help="setup: a .py defining `setup` or `build()`, a .json, or a package directory such as examples/mpqkd; optional with --protocol",
163
+ )
164
+ p.add_argument(
165
+ "-i",
166
+ "--instrument",
167
+ action="append",
168
+ default=[],
169
+ help="only acquire these instruments",
170
+ )
171
+ p.add_argument("--seed", type=int, default=None)
172
+ p.add_argument(
173
+ "--plot", metavar="PNG", help="save the oscilloscope traces as an image"
174
+ )
175
+ p.add_argument(
176
+ "--analysis",
177
+ action="append",
178
+ default=[],
179
+ metavar="SCRIPT.py",
180
+ help="run the @task functions of this script on the counter batches",
181
+ )
182
+ p.add_argument(
183
+ "--protocol",
184
+ metavar="NAME",
185
+ help="protocol package with PARTIES and build(): a path, a folder under ./examples (mpqkd), or a module",
186
+ )
187
+ p.add_argument(
188
+ "--party",
189
+ action="append",
190
+ default=[],
191
+ metavar="FILE.py",
192
+ help="a file defining a Party subclass (repeatable)",
193
+ )
194
+ p.add_argument(
195
+ "--blocks",
196
+ "--batches",
197
+ dest="blocks",
198
+ type=int,
199
+ default=10,
200
+ help="blocks of pulses to run",
201
+ )
202
+ p.set_defaults(fn=cmd_run)
203
+
204
+ p = sub.add_parser("serve", help="serve a setup to the 3D virtual lab")
205
+ p.add_argument(
206
+ "file",
207
+ nargs="?",
208
+ help="setup: a .py, a .json or a package directory; omit for an empty table",
209
+ )
210
+ p.add_argument("--host", default="127.0.0.1")
211
+ p.add_argument("--port", type=int, default=8000)
212
+ p.add_argument(
213
+ "--save",
214
+ metavar="JSON",
215
+ help="autosave edits here (default: next to the setup file)",
216
+ )
217
+ p.add_argument("--no-save", action="store_true", help="don't write edits to disk")
218
+ p.add_argument(
219
+ "--web",
220
+ metavar="DIR",
221
+ help="virtual lab folder (default: the one shipped in the package)",
222
+ )
223
+ p.add_argument("--seed", type=int, default=None)
224
+ p.add_argument(
225
+ "--analysis",
226
+ action="append",
227
+ default=[],
228
+ metavar="SCRIPT.py",
229
+ help="run the @task functions of this script alongside, each with a console",
230
+ )
231
+ p.add_argument(
232
+ "--protocol",
233
+ metavar="NAME",
234
+ help="protocol package with PARTIES and build(): a path, a folder under ./examples (mpqkd), or a module",
235
+ )
236
+ p.add_argument(
237
+ "--party",
238
+ action="append",
239
+ default=[],
240
+ metavar="FILE.py",
241
+ help="a file defining a Party subclass (repeatable)",
242
+ )
243
+ p.add_argument("--open", action="store_true", help="open the browser")
244
+ p.set_defaults(fn=cmd_serve)
245
+
246
+ p = sub.add_parser("export", help="convert a setup to JSON or Python")
247
+ p.add_argument("file")
248
+ p.add_argument("--format", choices=("json", "python"), default="python")
249
+ p.add_argument("-o", "--output")
250
+ p.set_defaults(fn=cmd_export)
251
+
252
+ p = sub.add_parser("catalog", help="list component types, ports and parameters")
253
+ p.set_defaults(fn=cmd_catalog)
254
+
255
+ args = ap.parse_args(argv)
256
+ args.fn(args)
257
+
258
+
259
+ if __name__ == "__main__":
260
+ main()