diff options
author | Paweł Redman <pawel.redman@gmail.com> | 2019-04-28 17:29:18 +0200 |
---|---|---|
committer | Paweł Redman <pawel.redman@gmail.com> | 2019-04-28 19:07:20 +0200 |
commit | 52a8a95a8553f5b2a1224db474e6cf1d649533c8 (patch) | |
tree | 003e0c94839b6daebef19049c75ba0ed43082a7b /src |
Initial commit
Diffstat (limited to 'src')
-rw-r--r-- | src/file.py | 44 | ||||
-rw-r--r-- | src/main.py | 19 | ||||
-rw-r--r-- | src/phys.py | 99 | ||||
-rw-r--r-- | src/ui.py | 421 | ||||
-rw-r--r-- | src/ui_widgets.py | 226 |
5 files changed, 809 insertions, 0 deletions
diff --git a/src/file.py b/src/file.py new file mode 100644 index 0000000..4285207 --- /dev/null +++ b/src/file.py @@ -0,0 +1,44 @@ +import re, sys, traceback, json +import phys + +file_format_version = 1 + +def save_system(path, system): + ser = dict() + ser["version"] = file_format_version + + ser["ignore"] = system.ignore + ser["elements"] = [] + for pol in system.elements: + el = { + "type": pol.type, + "angle": pol.angle, + "delta": pol.delta, + "ref": pol.ref + } + + ser["elements"].append(el) + + with open(path, "w") as fd: + fd.write(json.dumps(ser)) + +def open_system(path): + system = phys.System() + + with open(path, "r") as fd: + ser = json.loads(fd.read()) + + if ser["version"] != file_format_version: + raise ValueError("Bad file version: expected %d, found %d" \ + % (file_format_version, ser["version"])) + + system.ignore = ser["ignore"] + + for el in ser["elements"]: + pol = phys.Polarizer(str(el["type"])) + pol.angle = float(el["angle"]) + pol.delta = float(el["delta"]) + pol.ref = el["ref"] + system.elements.append(pol) + + return system
\ No newline at end of file diff --git a/src/main.py b/src/main.py new file mode 100644 index 0000000..df90be5 --- /dev/null +++ b/src/main.py @@ -0,0 +1,19 @@ +import re, sys, traceback +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtCore import * + +import ui, phys, io + +system = phys.System() + +class Window(QMainWindow): + def __init__(self): + super().__init__() + self.statusBar() + self.setWindowTitle("Polarizzazione italiana") + ui.setup(self, system) + +app = QApplication(sys.argv) +win = Window() +sys.exit(app.exec_()) diff --git a/src/phys.py b/src/phys.py new file mode 100644 index 0000000..2109f28 --- /dev/null +++ b/src/phys.py @@ -0,0 +1,99 @@ +import re, sys, traceback +import numpy as np +import scipy.optimize +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtCore import * + +from ui import * + +def R(theta): + return np.array([[np.cos(theta), np.sin(theta)], + [-np.sin(theta), np.cos(theta)]]) + + +class Ellipse: + def __init__(self, state): + # FIXME: a less brute-force way of doing this + if state is None: + self.alpha = np.nan + self.theta = np.nan + self.e = np.nan + self.a = np.nan + self.b = np.nan + return + + def x(theta): + return np.real(np.exp(1j * theta) * state) + + def r(theta): + return np.linalg.norm(x(theta)) + + def angle(x): + a = np.arctan2(x[1], x[0]) + if a < 0: + a += 2 * np.pi + if a > np.pi: + a -= np.pi + return a + + opt = scipy.optimize.minimize_scalar(r, bounds=[0, np.pi], \ + method="bounded") + self.b = r(opt.x) + opt = scipy.optimize.minimize_scalar(lambda x: -r(x), \ + bounds=[0, np.pi], method="bounded") + self.a = r(opt.x) + + self.alpha = angle(x(opt.x)) + + self.e = self.b / self.a + self.theta = np.arctan(self.e) + if self.alpha > angle(x(opt.x + 0.001)): + self.theta *= -1 + +class Polarizer: + def __init__(self, type, delta=0): + self.type = type + self.angle = 0 + self.delta = delta + self.ref = False # FIXME: move this to UI or System + self.set_type(type) + + def set_type(self, type): + if type == "linear": + self.M = np.array([[1, 0], [0, 0]]) + elif type == "quarterwave": + self.M = np.exp(-1j / 4 * np.pi) * \ + np.array([[1, 0], [0, 1j]]) + else: + raise ValueError("bad Polarizer type: %s" % type) + self.type = type + + def mul(self, state): + # unpolarized light + if state is None: + if self.type == "linear": + return np.dot(R(-self.angle - self.delta), \ + np.array([[1], [0]])) + else: + return None + + M = np.matmul(R(-self.angle - self.delta), \ + np.matmul(self.M, R(self.angle + self.delta))) + return np.dot(M, state) + +class System: + def __init__(self): + self.elements = list() + self.ignore = list() + + def recalculate(system): + system.states = [None] * len(system.elements) + system.ellipses = list() + + state = None + for i, pol in enumerate(system.elements): + if i >= len(system.ignore) or not system.ignore[i]: + state = pol.mul(state) + system.states[i] = state + system.ellipses.append(Ellipse(state)) diff --git a/src/ui.py b/src/ui.py new file mode 100644 index 0000000..7803d3d --- /dev/null +++ b/src/ui.py @@ -0,0 +1,421 @@ +import numpy as np, scipy.optimize +import traceback +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtCore import * + +import phys, file + +# global GUI-related shit +class GUI: + table = None + table_rows = list() + monospace = QFont("monospace") + bigfont = QFont("sans-serif", pointSize=10, weight=1000) + +class Pens: + axes = QPen(Qt.gray) + + ellipse = QPen(Qt.black) + ellipse.setWidth(2) + + axis_linear = QPen(QColor(201, 141, 0)) + axis_linear.setWidth(2) + axis_linear.setStyle(Qt.DashDotLine) + + axis_fast = QPen(QColor(51, 87, 123)) + axis_fast.setWidth(2) + axis_fast.setStyle(Qt.DashDotLine) + + axis_slow = QPen(QColor(55, 123, 51)) + axis_slow.setWidth(2) + axis_slow.setStyle(Qt.DashDotLine) + + alpha = QPen(Qt.red) + theta = QPen(Qt.blue) + + radii = QPen(Qt.black) + radii.setStyle(Qt.DashDotLine) + +from ui_widgets import * + + + +class OptBox(QVBoxLayout): + def __init__(self, row, pol, rownum): + super().__init__() + + # Name + self.name = MeinGroßLabel("Element %d" % rownum) + self.addWidget(self.name) + + # Type / Enable box + self.hbox = QHBoxLayout() + self.addLayout(self.hbox) + + # Type combo + self.type = QComboBox() + self.type.addItem("Linear", "linear",) + self.type.addItem("Quarterwave plate", "quarterwave") + self.type.setCurrentIndex(0 if pol.type == "linear" else 1) + self.type.currentIndexChanged.connect(lambda : TableRow.type_change(row)) + self.hbox.addWidget(self.type) + + # Enable checkbox + self.enable = QCheckBox("Enable") + self.enable.setChecked(True) + self.enable.stateChanged.connect(update) + self.hbox.addWidget(self.enable) + + # Delta angle + self.delta = AngleSlider("Delta") + self.delta.setValue(pol.delta * 180 / np.pi) + self.delta.on_change = lambda: TableRow.angle_change(row) + self.addLayout(self.delta) + + # Angle reference + self.ref = QComboBox() + self.ref.addItem("Absolute", False) + for i, _ in enumerate(system.elements): + if i >= rownum - 1: + continue + self.ref.addItem("Relative to element #%d" % (i + 1), i) + + if row.ref is not False and i == row.ref: + self.ref.setCurrentIndex(row.ref + 1) + + self.ref.currentIndexChanged.connect(update) + self.addWidget(self.ref) + + # Angle + self.angle = AngleSlider("Angle") + self.angle.setValue(pol.angle * 180 / np.pi) + self.angle.on_change = lambda: TableRow.angle_change(row) + self.addLayout(self.angle) + self.addItem(ExpandingSpacer()) + + # Add + self.add_above = QPushButton("Add above") + self.add_above.clicked.connect(lambda: half_assed_element_creation(rownum - 1)) + self.add_below = QPushButton("Add below") + self.add_below.clicked.connect(lambda: half_assed_element_creation(rownum)) + self.delete = QPushButton("Delete") + self.delete.clicked.connect(lambda: half_assed_element_deletion(rownum - 1)) + + self.buttons = QHBoxLayout() + self.buttons.addWidget(self.add_above) + self.buttons.addWidget(self.add_below) + self.buttons.addWidget(self.delete) + self.addLayout(self.buttons) + + + +class TableRow: + def __init__(self, pol, grid, rownum): + self.pol = pol + self.grid = grid + self.rownum = rownum + self.angle = pol.angle + self.delta = pol.delta + self.ref = pol.ref + + self.optbox = OptBox(self, pol, rownum) + grid.addLayout(self.optbox, rownum, 1) + + self.ellipse = EllipseWidget(pol) + grid.addWidget(self.ellipse, rownum, 2) + + self.info = QVBoxLayout() + self.info_angle = MeinLabel() + self.info_angle.setAlignment(Qt.AlignLeft) + self.info.addWidget(self.info_angle) + self.info.addWidget(QLabel("Polarization state")) + self.info_jones = MeinLabel() + self.info_jones.setAlignment(Qt.AlignLeft) + self.info.addWidget(self.info_jones) + self.info.addItem(ExpandingSpacer()) + grid.addLayout(self.info, rownum, 3) + + + @staticmethod + def angle_change(row): + row.angle = row.optbox.angle.angle / 180 * np.pi + row.delta = row.optbox.delta.angle / 180 * np.pi + update() + + @staticmethod + def type_change(row): + row.pol.set_type(row.optbox.type.currentData()) + update() + + +def populate_table(): + GUI.table = QGridLayout() + GUI.table.setColumnStretch(1, 1) + GUI.table.setColumnStretch(2, 2) + GUI.table.setColumnStretch(3, 5) + + while GUI.opt_operand.count() > 0: # FIXME + GUI.opt_operand.removeItem(0) + + GUI.table_rows = list() + for i, pol in enumerate(system.elements): + GUI.table_rows.append(TableRow(pol, GUI.table, i + 1)) + GUI.opt_operand.addItem("Element #%d" % (i + 1), i) + +def update(): + system.ignore = [False] * len(system.elements) + for i, pol in enumerate(system.elements): + row = GUI.table_rows[i] + + pol.angle = row.angle + pol.delta = row.delta + pol.ref = row.optbox.ref.currentData() + if pol.ref is not False: + pol.angle += system.elements[pol.ref].angle + + if not row.optbox.enable.isChecked(): + system.ignore[i] = True + + system.recalculate() + + I = 1 + for i, pol in enumerate(system.elements): + row = GUI.table_rows[i] + + # update all the diagrams + row.ellipse.state = system.states[i] + row.ellipse.ellipse = system.ellipses[i] + row.ellipse.is_used = not system.ignore[i] + row.ellipse.repaint() + + text = "%s (%f°)\nat %f°" \ + % (pol.type, pol.delta * 180 / np.pi, + (pol.angle + pol.delta) * 180 / np.pi) + row.info_angle.setText(text) + + state = system.states[i] + ellipse = system.ellipses[i] + + if state is None: + I = 1 + row.info_jones.setText("Unpolarized") + else: + I = np.abs(state[0] * state[0].conjugate() \ + + state[1] * state[1].conjugate()) + + if state is None: + continue + + A, B = state + text = "I = %f\n" % I + text += "%f ∠ %+f°\n%f ∠ %+f°\n" % \ + (np.abs(A), 180 / np.pi * np.angle(A), \ + np.abs(B), 180 / np.pi * np.angle(B)) + text += "α = %f°\nφ = %+f°" % \ + (180 / np.pi * ellipse.alpha, 180 / np.pi * ellipse.theta) + text = text.replace("-", "- ").replace("+", "+ ") + row.info_jones.setText(text) + + GUI.widok.intensity = I + GUI.widok.repaint() + + #if GUI.auto_optimize.isChecked(): + # optimize() + + +#TODO later +#class AddElementWindow(QMainWindow): +# dialog = None +# +# def __init__(self): +# super().__init__() +# +# chuj = QLabel("adolf") +# self.setCentralWidget(chuj) +# self.show() +# +# +# def __del__(self): +# AddElementWindow.dialog = None +# +# def open(): +# if not AddElementWindow.dialog: +# AddElementWindow.dialog = AddElementWindow() +def half_assed_element_creation(index=None): + if index is None: + system.elements.append(phys.Polarizer("linear")) + else: + system.elements.insert(index, phys.Polarizer("linear")) + + populate_table() + GUI.table_frame = QFrame() + GUI.table_frame.setLayout(GUI.table) + GUI.scroll.setWidget(GUI.table_frame) + update() + +def half_assed_clear(): + system.elements = list() + populate_table() + GUI.table_frame = QFrame() + GUI.table_frame.setLayout(GUI.table) + GUI.scroll.setWidget(GUI.table_frame) + update() + +def half_assed_element_deletion(index): + del(system.elements[index]) + + populate_table() + GUI.table_frame = QFrame() + GUI.table_frame.setLayout(GUI.table) + GUI.scroll.setWidget(GUI.table_frame) + update() + + +last_save_path = None +file_filter = "Wery Omportant Zejta (*.woz)" + +def do_save(win, reuse_old): + global system, last_save_path + + if reuse_old and last_save_path: + path = last_save_path + else: + path, _ = QFileDialog.getSaveFileName(win, filter=file_filter) + if path == "": + return + + try: + file.save_system(path, system) + except: + traceback.print_exc() + + last_save_path = path + +def do_open(win): + global system + + path, _ = QFileDialog.getOpenFileName(win, filter=file_filter) + if path == "": + return + + try: + system = file.open_system(path) + except: + traceback.print_exc() + + populate_table() + GUI.table_frame = QFrame() + GUI.table_frame.setLayout(GUI.table) + GUI.scroll.setWidget(GUI.table_frame) + update() + +def setup_menubar(win): + menu = win.menuBar() + + # File + open = QAction("&Open a system", win) + open.setShortcut("Ctrl+O") + open.triggered.connect(lambda: do_open(win)) + save = QAction("&Save a system", win) + save.setShortcut("Ctrl+S") + save.triggered.connect(lambda: do_save(win, True)) + save_as = QAction("&Save a system as...", win) + save_as.triggered.connect(lambda: do_save(win, False)) + close = QAction("Exit", win) + close.setShortcut("Ctrl+Q") + close.triggered.connect(exit) + + file = menu.addMenu("&File") + file.addAction(open) + file.addAction(save) + file.addAction(save_as) + file.addAction(close) + + # System + add = QAction("&Add a new element", win) + add.setShortcut("Ctrl+N") + add.triggered.connect(half_assed_element_creation) + clear = QAction("&Remove all elements", win) + clear.triggered.connect(half_assed_clear) + + system = menu.addMenu("&System") + system.addAction(add) + system.addAction(clear) + + win.statusBar() + +# FIXME: refactor +def optimize(which): + if len(system.elements) == 0: + return + + op_idx = GUI.opt_operand.currentData() + op = system.elements[op_idx] + + def I(theta): + op.angle = theta + if op.ref is not False: + op.ref += system.elements[op.ref].angle + + state = None + for i, pol in enumerate(system.elements): + if i >= len(system.ignore) or not system.ignore[i]: + state = pol.mul(state) + if state is None: + return 1 + else: + return float(np.abs(state[0] * state[0].conjugate() \ + + state[1] * state[1].conjugate())) + + if which == "min": + f = I + else: + f = lambda theta: -I(theta) + + opt = scipy.optimize.minimize_scalar(f, bounds=[0, np.pi], method="bounded") + GUI.table_rows[op_idx].optbox.angle.edit.setText("%g" % round(opt.x * 180 / np.pi, 3)) + + +def setup(win, system_): + global system + system = system_ + + # Needless to say, I'm getting real tired of this whole Layout/Widget + # clusterfuck in Qt + root = QHBoxLayout() + root_fucking_random_container = QWidget() + root_fucking_random_container.setLayout(root) + win.setCentralWidget(root_fucking_random_container) + + rhs = QVBoxLayout() + + Widocques.image = QImage("/home/enneract/lab/test/c.jpg") + GUI.widok = Widocques() + rhs.addWidget(GUI.widok) + + optbox = QHBoxLayout() + rhs.addLayout(optbox) + + GUI.opt_operand = QComboBox() + optbox.addWidget(GUI.opt_operand) + button = QPushButton("Find a minimum") + button.pressed.connect(lambda: optimize("min")) + optbox.addWidget(button) + button = QPushButton("Find a maximum") + button.pressed.connect(lambda: optimize("max")) + optbox.addWidget(button) + + GUI.scroll = QScrollArea() + populate_table() + GUI.table_frame = QFrame() + GUI.table_frame.setLayout(GUI.table) + GUI.scroll.setWidget(GUI.table_frame) + + root.addWidget(GUI.scroll) + root.addLayout(rhs) + + setup_menubar(win) + + update() + win.show()
\ No newline at end of file diff --git a/src/ui_widgets.py b/src/ui_widgets.py new file mode 100644 index 0000000..48282c4 --- /dev/null +++ b/src/ui_widgets.py @@ -0,0 +1,226 @@ +import numpy as np +from PyQt5.QtWidgets import * +from PyQt5.QtGui import * +from PyQt5.QtCore import * + +from ui import GUI, Pens # the fuck + +class ExpandingSpacer(QSpacerItem): + def __init__(self): + super().__init__(0, 0, QSizePolicy.Minimum, \ + QSizePolicy.Expanding) + +class MeinLabel(QLabel): + def __init__(self, *args): + QLabel.__init__(self, *args) + self.setFont(GUI.monospace) + +class MeinGroßLabel(QLabel): + def __init__(self, *args): + QLabel.__init__(self, *args) + self.setFont(GUI.bigfont) + self.setAlignment(Qt.AlignHCenter) + +class EllipseWidget(QWidget): + def __init__(self, pol): + QWidget.__init__(self) + self.pol = pol + self.state = None + self.ellipse = None + self.is_used = True + + def minimumSizeHint(self): + return QSize(150, 150) + + def paintEvent(self, event): + P = QPainter(self) + + w, h = self.frameSize().width(), self.frameSize().height() + cx, cy = w / 2, h / 2 + r = min(w, h) / 2 + + # background + if self.is_used: + background = Qt.white + else: + background = QColor(0, 0, 0, 100) + P.fillRect(QRect(0, 0, w, h), background) + + # coordinate axes + P.setPen(Pens.axes) + P.drawLine(cx - 20 * r, cy, cx + 20 * r, cy) + P.drawLine(cx, cy - 20 * r, cx, cy + 20 * r) + + # polarizer axes + P.translate(cx, cy) + P.rotate(- (self.pol.angle + self.pol.delta) * 180 / np.pi) + if self.pol.type == "linear": + P.setPen(Pens.axis_linear) + P.drawLine(-20 * r, 0, 20 * r, 0) + else: + P.setPen(Pens.axis_fast) + P.drawLine(-20 * r, 0, 20 * r, 0) + P.drawText(0.92 * r, 0, "F") + P.setPen(Pens.axis_slow) + P.drawLine(0, -20 * r, 0, 20 * r) + P.drawText(0, -0.92 * r, "S") + + if self.state is None: + return + + P.resetTransform() + r *= 0.88 + P.translate(cx, cy) + P.scale(1, -1) + + # radii + P.setPen(Pens.radii) + csa = np.array([np.cos(self.ellipse.alpha), np.sin(self.ellipse.alpha)]) + xa = self.ellipse.a * r * csa + P.drawLine(-xa[0], -xa[1], xa[0], xa[1]) + csb = np.array([np.cos(self.ellipse.alpha + np.pi / 2), \ + np.sin(self.ellipse.alpha + np.pi / 2)]) + xb = self.ellipse.b * r * csb + P.drawLine(-xb[0], -xb[1], xb[0], xb[1]) + + def arc(x, y, r, t1, t2): + P.drawArc(x - r / 2, y - r / 2, r, r, \ + -180 * 16 / np.pi * t1, \ + -180 * 16 / np.pi * t2) + + # azimuth + P.setPen(Pens.alpha) + P.drawLine(0, 0, xa[0], xa[1]) + arc(0, 0, self.ellipse.a * r, 0, self.ellipse.alpha) + + # ellipticity + P.setPen(Pens.theta) + P.drawLine(-xa[0], -xa[1], xb[0], xb[1]) + arc(-xa[0], -xa[1], r / 2, self.ellipse.alpha, abs(self.ellipse.theta)) + + # the ellipse + P.setPen(Pens.ellipse) + path = QPainterPath() + for i, t in enumerate(np.linspace(0, 2 * np.pi, 100)): + x = r * np.real(np.exp(1j * t) * self.state) + f = path.moveTo if t == 0 else path.lineTo # FML + f(x[0], x[1]) # +Y should be upwards + + if abs(self.ellipse.theta) > 0.05 and i % 25 == 0: + N = r * np.real(np.exp(1j * (t + 0.001)) * self.state) - x + N /= np.linalg.norm(N) + if np.any(np.isnan(N)): + continue + + T = np.dot(np.array([[0, 1], [-1, 0]]), N) + + ax = x - N * 5 + T * 5 + P.drawLine(x[0], x[1], ax[0], ax[1]) + ax = x - N * 5 - T * 5 + P.drawLine(x[0], x[1], ax[0], ax[1]) + path.closeSubpath() + P.drawPath(path) + + if abs(self.ellipse.theta) <= 0.05: + x1 = xa - r / 20 * csb + x2 = xa + r / 20 * csb + P.drawLine(x1[0], x1[1], x2[0], x2[1]) + x1 = -xa - r / 20 * csb + x2 = -xa + r / 20 * csb + P.drawLine(x1[0], x1[1], x2[0], x2[1]) + + + +class AngleSlider(QHBoxLayout): + def __init__(self, label=None): + super().__init__() + self.angle = 0 + + if label: + self.addWidget(QLabel(label)) + + self.slider = QSlider(Qt.Horizontal) + self.slider.setMinimumWidth(100) + self.slider.setMinimum(0) + self.slider.setMaximum(180) + self.slider.setTickPosition(QSlider.TicksBelow) + self.slider.setTickInterval(45) + self.slider.valueChanged.connect(\ + lambda: AngleSlider.change(self, "bar")) + self.addWidget(self.slider) + + self.edit = QLineEdit() + self.edit.setText("0") + self.edit.setMinimumWidth(30) + self.edit.textChanged.connect(\ + lambda: AngleSlider.change(self, "edit")) + self.addWidget(self.edit) + self.addWidget(QLabel("°")) + + def setValue(self, angle): + self.angle = angle + self.slider.blockSignals(True) + self.slider.setValue(angle) + self.slider.blockSignals(False) + self.edit.blockSignals(True) + self.edit.setText("%g" % angle) + self.edit.blockSignals(False) + + @staticmethod + def change(self, source): + if source == "edit": + try: + angle = float(self.edit.text()) + except ValueError: + return + self.slider.blockSignals(True) + self.slider.setValue(angle) + self.slider.blockSignals(False) + elif source == "bar": + angle = self.slider.value() + self.edit.blockSignals(True) + self.edit.setText("%g" % angle) + self.edit.blockSignals(False) + self.angle = angle + self.on_change() + + + +class Widocques(QWidget): + image = None + + def __init__(self): + QWidget.__init__(self) + self.setSizePolicy(QSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)) + self.intensity = 1 + + def minimumSizeHint(self): + return QSize(100, 100) + + def paintEvent(self, event): + P = QPainter(self) + + w, h = self.frameSize().width(), self.frameSize().height() + + ar = Widocques.image.width() \ + / Widocques.image.height() + + if w / h > ar: + # pad left/right + w2 = h * ar + pad = (w - w2) / 2 + rect = QRect(pad, 0, w2, h) + else: + # pad top/bottom + h2 = w / ar + pad = (h - h2) / 2 + rect = QRect(0, pad, w, h2) + + w2 = rect.width() + h2 = rect.height() + rect2 = QRect(rect.left() + 0.1 * w2, rect.top() + 0.1 * h2, \ + w2 * 0.8, h2 * 0.8) + + P.fillRect(QRect(0, 0, w, h), Qt.black) + P.drawImage(rect2, Widocques.image) + P.fillRect(rect2, QColor(0, 0, 0, 255 * (1 - self.intensity))) |