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 +74 -0
- phoq/__main__.py +260 -0
- phoq/analysis.py +482 -0
- phoq/core.py +598 -0
- phoq/detectors.py +218 -0
- phoq/engine.py +296 -0
- phoq/generators.py +294 -0
- phoq/instruments.py +682 -0
- phoq/loading.py +67 -0
- phoq/modulators.py +217 -0
- phoq/parties.py +657 -0
- phoq/passive.py +213 -0
- phoq/runner.py +100 -0
- phoq/server.py +533 -0
- phoq/sources.py +199 -0
- phoq/units.py +108 -0
- phoq/web/README.md +68 -0
- phoq/web/index.html +188 -0
- phoq/web/js/api.js +62 -0
- phoq/web/js/console.js +155 -0
- phoq/web/js/fiber.js +229 -0
- phoq/web/js/interaction.js +252 -0
- phoq/web/js/laptop.js +208 -0
- phoq/web/js/layout.js +180 -0
- phoq/web/js/main.js +393 -0
- phoq/web/js/parts.js +623 -0
- phoq/web/js/scene.js +224 -0
- phoq/web/js/scope.js +352 -0
- phoq/web/js/textures.js +145 -0
- phoq/web/js/ui.js +305 -0
- phoq/web/js/util.js +87 -0
- phoq/web/style.css +318 -0
- phoq-0.1.0.dist-info/METADATA +502 -0
- phoq-0.1.0.dist-info/RECORD +37 -0
- phoq-0.1.0.dist-info/WHEEL +4 -0
- phoq-0.1.0.dist-info/entry_points.txt +3 -0
- phoq-0.1.0.dist-info/licenses/LICENSE +661 -0
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()
|