def __init__(self, settings, database, project=None): super(PlotsMath, self).__init__() self.ui = Ui_PlotsMath() self.ui.setupUi(self) self.settings = settings self.project=project self.plot = QtCommons.nestWidget(self.ui.plot, QMathPlotWidget()) self.reference_dialog = ReferenceSpectraDialog(database) self.reference_dialog.fits_picked.connect(self.open_fits) self.toolbar = QToolBar('Instrument Response Toolbar') open_btn = QtCommons.addToolbarPopup(self.toolbar, text="Open...", icon_file=':/new_open_20') open_file_action = open_btn.menu().addAction('FITS file') open_btn.menu().addAction('Reference library', self.reference_dialog.show) self.blackbody_menu = blackbody.BlackBodyAction(self.blackbody, open_btn.menu()) if project: save_result = QtCommons.addToolbarPopup(self.toolbar, text='Save', icon_file=':/save_20') save_result.menu().addAction('As File', lambda: QtCommons.save_file('Save Operation Result...', FITS_EXTS, lambda f: self.save(f[0]), project.path)) save_result.menu().addAction('As Instrument Response', self.save_project_instrument_response) open_file_action.triggered.connect(lambda: QtCommons.open_file('Open FITS Spectrum',FITS_EXTS, lambda f: self.open_fits(f[0]), project.path)) else: open_file_action.triggered.connect(lambda: open_file_sticky('Open FITS Spectrum',FITS_EXTS, lambda f: self.open_fits(f[0]), self.settings, CALIBRATED_PROFILE, [RAW_PROFILE])) self.toolbar.addAction(QIcon(':/save_20'), 'Save', lambda: save_file_sticky('Save Operation Result...', 'FITS file (.fit)', lambda f: self.save(f[0]), self.settings, MATH_OPERATION, [CALIBRATED_PROFILE])) self.toolbar.addAction('Set operand', self.set_operand) self.toolbar.addSeparator() self.toolbar.addAction(self.ui.actionZoom) self.ui.actionZoom.triggered.connect(self.start_zoom) self.toolbar.addAction(self.ui.actionReset_Zoom) self.ui.actionReset_Zoom.triggered.connect(self.reset_zoom) self.toolbar.addSeparator() self.operands_model = QStandardItemModel() self.ui.operands_listview.setModel(self.operands_model) remove_btn = QtCommons.addToolbarPopup(self.toolbar, text='Remove...') remove_btn.menu().addAction(self.ui.actionSelectPointsToRemove) remove_btn.menu().addAction("Before point", lambda: spectrum_trim_dialog(self.spectrum, 'before', self.plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) remove_btn.menu().addAction("After point", lambda: spectrum_trim_dialog(self.spectrum, 'after', self.plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) self.ui.clear_operands.clicked.connect(self.operands_model.clear) self.ui.remove_operand.clicked.connect(lambda: self.operands_model.removeRows(self.ui.operands_listview.selectionModel().selectedRows()[0].row(), 1)) self.operands_model.rowsInserted.connect(lambda: self.ui.clear_operands.setEnabled(self.operands_model.rowCount() > 0) ) self.operands_model.rowsRemoved.connect(lambda: self.ui.clear_operands.setEnabled(self.operands_model.rowCount() > 0) ) self.ui.operands_listview.selectionModel().selectionChanged.connect(lambda s, u: self.ui.remove_operand.setEnabled(len(s))) self.ui.actionSelectPointsToRemove.triggered.connect(self.pick_rm_points) self.undo = Undo(None, self.draw) self.undo.add_actions(self.toolbar) self.ui.spline_factor.valueChanged.connect(self.factor_valueChanged) self.ui.spline_degrees.valueChanged.connect(lambda v: self.draw()) self.ui.spline_factor_auto.toggled.connect(lambda v: self.draw()) self.ui.spline_factor_auto.toggled.connect(lambda v: self.ui.spline_factor.setEnabled(not v)) self.ui.execute.clicked.connect(self.execute_operation) self.plot.figure.tight_layout()
def __init__(self): # Default size self._width = _WORLD_WIDTH self._height = _WORLD_HEIGHT self._depth = _WORLD_DEPTH # Our undo buffer self._undo = Undo() # Init data self._initialise_data() # Callback when our data changes self.notify_changed = None # Ambient occlusion type effect self._occlusion = True
class VoxelData(object): # Constants for referring to axis X_AXIS = 1 Y_AXIS = 2 Z_AXIS = 3 # World dimension properties @property def width(self): return self._width @property def height(self): return self._height @property def depth(self): return self._depth @property def changed(self): return self._changed @changed.setter def changed(self, value): if value and not self._changed: # Let whoever is watching us know about the change self._changed = value if self.notify_changed: self.Color() self._changed = value @property def occlusion(self): return self._occlusion @occlusion.setter def occlusion(self, value): self._occlusion = value def __init__(self): # Default size self._width = _WORLD_WIDTH self._height = _WORLD_HEIGHT self._depth = _WORLD_DEPTH # Our undo buffer self._undo = Undo() # Init data self._initialise_data() # Callback when our data changes self.notify_changed = None # Ambient occlusion type effect self._occlusion = True self._undoFillNew = [] self._undoFillOld = [] # Initialise our data def _initialise_data(self): # Our scene data self._data = self.blank_data() # Create empty selection self._selection = set() # Our cache of non-empty voxels (coordinate groups) self._cache = [] # Flag indicating if our data has changed self._changed = False # Reset undo buffer self._undo.clear() # Animation self._frame_count = 1 self._current_frame = 0 self._frames = [self._data] # Return an empty voxel space def blank_data(self): return [[[0 for _ in xrange(self.depth)] for _ in xrange(self.height)] for _ in xrange(self.width)] def is_valid_bounds(self, x, y, z): return x >= 0 and x < self.width and y >= 0 and y < self.height and z >= 0 and z < self.depth # Return the number of animation frames def get_frame_count(self): return self._frame_count # Change to the given frame def select_frame(self, frame_number): # Sanity if frame_number < 0 or frame_number >= self._frame_count: return # Make sure we really have a pointer to the current data self._frames[self._current_frame] = self._data # Change to new frame self._data = self._frames[frame_number] self._current_frame = frame_number self._undo.frame = self._current_frame self._cache_rebuild() self.changed = True self.clear_selection() def insert_frame(self, index, copy_current=True): if copy_current: data = self.get_data() else: data = self.blank_data() # If current frame is at the position of the new frame # We must move out of the way. if self._current_frame == index: self.select_frame(index - 1) self._frames.insert(index, data) self._undo.add_frame(index) self._frame_count += 1 self.select_frame(index) # Add a new frame by copying the current one def add_frame(self, copy_current=True): if copy_current: data = self.get_data() else: data = self.blank_data() self._frames.insert(self._current_frame + 1, data) self._undo.add_frame(self._current_frame + 1) self._frame_count += 1 self.select_frame(self._current_frame + 1) def copy_to_current(self, index): data = self._frames[index - 1] self.set_data(data) # Delete the current frame def delete_frame(self): # Sanity - we can't have no frames at all if self._frame_count <= 1: return # Remember the frame we want to delete killframe = self._current_frame # Select a different frame self.select_previous_frame() # Remove the old frame del self._frames[killframe] self._undo.delete_frame(killframe) self._frame_count -= 1 # If we only have one frame left, must be first frame if self._frame_count == 1: self._current_frame = 0 # If we wrapped around, fix the frame pointer if self._current_frame > killframe: self._current_frame -= 1 # Change to the next frame (with wrap) def select_next_frame(self): nextframe = self._current_frame + 1 if nextframe >= self._frame_count: nextframe = 0 self.select_frame(nextframe) # Change to the previous frame (with wrap) def select_previous_frame(self): prevframe = self._current_frame - 1 if prevframe < 0: prevframe = self._frame_count - 1 self.select_frame(prevframe) # Get current frame number def get_frame_number(self): return self._current_frame def is_free(self, data): for x, y, z, col in data: if self.get(x, y, z) != 0: return False return True # Set a voxel to the given state def set(self, x, y, z, state, undo=True, fill=0): # If this looks like a QT Color instance, convert it if hasattr(state, "getRgb"): c = state.getRgb() state = c[0] << 24 | c[1] << 16 | c[2] << 8 | 0xff # Check bounds if not self.is_valid_bounds(x, y, z): return False # Add to undo if undo: if fill > 0: self._undoFillOld.append((x, y, z, self._data[x][y][z])) self._undoFillNew.append((x, y, z, state)) if fill == 2: self.completeUndoFill() else: self._undo.add( UndoItem(Undo.SET_VOXEL, (x, y, z, self._data[x][y][z]), (x, y, z, state))) # Set the voxel self._data[x][y][z] = state if state != EMPTY: if (x, y, z) not in self._cache: self._cache.append((x, y, z)) else: if (x, y, z) in self._cache: self._cache.remove((x, y, z)) self.changed = True return True def completeUndoFill(self): self._undo.add( UndoItem(Undo.FILL, self._undoFillOld, self._undoFillNew)) self._undoFillOld = [] self._undoFillNew = [] def select(self, x, y, z): self._selection.add((x, y, z)) def deselect(self, x, y, z): if (x, y, z) in self._selection: self._selection.remove((x, y, z)) def is_selected(self, x, y, z): return (x, y, z) in self._selection def clear_selection(self): self._selection.clear() # Get the state of the given voxel def get(self, x, y, z): if not self.is_valid_bounds(x, y, z): return EMPTY return self._data[x][y][z] # Return a copy of the voxel data def get_data(self): return copy.deepcopy(self._data) # Set all of our data at once def set_data(self, data): self._data = copy.deepcopy(data) self._cache_rebuild() self.changed = True # Clear our voxel data def clear(self): self._initialise_data() # Return full vertex list def get_vertices(self): vertices = [] colors = [] color_ids = [] normals = [] uvs = [] for x, y, z in self._cache: v, c, n, cid, uv = self._get_voxel_vertices(x, y, z) vertices += v if (x, y, z) in self._selection: clen = len(c) / 3 c = [] for i in range(clen): c.append(255) c.append(0) c.append(255) colors += c normals += n color_ids += cid uvs += uv return (vertices, colors, normals, color_ids, uvs) # Called to notify us that our data has been saved. i.e. we can set # our "changed" status back to False. def saved(self): self.changed = False # Count the number of non-empty voxels from the list of coordinates def _count_voxels(self, coordinates): count = 0 for x, y, z in coordinates: if self.get(x, y, z) != EMPTY: count += 1 return count # Return the verticies for the given voxel. We center our vertices at the origin def _get_voxel_vertices(self, x, y, z): vertices = [] colors = [] normals = [] color_ids = [] uvs = [] # Remember voxel coordinates vx, vy, vz = x, y, z # Determine if we have filled voxels around us front = self.get(x, y, z - 1) == EMPTY left = self.get(x - 1, y, z) == EMPTY right = self.get(x + 1, y, z) == EMPTY top = self.get(x, y + 1, z) == EMPTY back = self.get(x, y, z + 1) == EMPTY bottom = self.get(x, y - 1, z) == EMPTY # Get our color c = self.get(x, y, z) r = (c & 0xff000000) >> 24 g = (c & 0xff0000) >> 16 b = (c & 0xff00) >> 8 # Calculate shades for our 4 occlusion levels shades = [] for c in range(5): shades.append((int(r * math.pow(OCCLUSION, c)), int(g * math.pow(OCCLUSION, c)), int(b * math.pow(OCCLUSION, c)))) # Encode our voxel space coordinates as colors, used for face selection # We use 7 bits per coordinate and the bottom 3 bits for face: # 0 - front # 1 - top # 2 - left # 3 - right # 4 - back # 5 - bottom voxel_id = (x & 0x7f) << 17 | (y & 0x7f) << 10 | (z & 0x7f) << 3 id_r = (voxel_id & 0xff0000) >> 16 id_g = (voxel_id & 0xff00) >> 8 id_b = (voxel_id & 0xff) # Adjust coordinates to the origin x, y, z = self.voxel_to_world(x, y, z) # Front face if front: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx, vy + 1, vz - 1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx - 1, vy, vz - 1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx + 1, vy, vz - 1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx, vy - 1, vz - 1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx - 1, vy - 1, vz - 1) != EMPTY: occ1 += 1 if self.get(vx - 1, vy + 1, vz - 1) != EMPTY: occ2 += 1 if self.get(vx + 1, vy - 1, vz - 1) != EMPTY: occ3 += 1 if self.get(vx + 1, vy + 1, vz - 1) != EMPTY: occ4 += 1 vertices += (x, y, z) colors += shades[occ1] vertices += (x, y + 1, z) colors += shades[occ2] vertices += (x + 1, y, z) colors += shades[occ3] vertices += (x + 1, y, z) colors += shades[occ3] vertices += (x, y + 1, z) colors += shades[occ2] vertices += (x + 1, y + 1, z) colors += shades[occ4] uvs += (0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1) normals += (0, 0, 1) * 6 color_ids += (id_r, id_g, id_b) * 6 # Top face if top: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx, vy + 1, vz + 1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx - 1, vy + 1, vz) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx + 1, vy + 1, vz) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx, vy + 1, vz - 1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx - 1, vy + 1, vz - 1) != EMPTY: occ1 += 1 if self.get(vx + 1, vy + 1, vz - 1) != EMPTY: occ3 += 1 if self.get(vx + 1, vy + 1, vz + 1) != EMPTY: occ4 += 1 if self.get(vx - 1, vy + 1, vz + 1) != EMPTY: occ2 += 1 vertices += (x, y + 1, z) colors += shades[occ1] vertices += (x, y + 1, z - 1) colors += shades[occ2] vertices += (x + 1, y + 1, z) colors += shades[occ3] vertices += (x + 1, y + 1, z) colors += shades[occ3] vertices += (x, y + 1, z - 1) colors += shades[occ2] vertices += (x + 1, y + 1, z - 1) colors += shades[occ4] uvs += (0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1) normals += (0, 1, 0) * 6 color_ids += (id_r, id_g, id_b | 1) * 6 # Right face if right: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx + 1, vy + 1, vz) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx + 1, vy, vz - 1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx + 1, vy, vz + 1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx + 1, vy - 1, vz) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx + 1, vy - 1, vz - 1) != EMPTY: occ1 += 1 if self.get(vx + 1, vy + 1, vz - 1) != EMPTY: occ2 += 1 if self.get(vx + 1, vy - 1, vz + 1) != EMPTY: occ3 += 1 if self.get(vx + 1, vy + 1, vz + 1) != EMPTY: occ4 += 1 vertices += (x + 1, y, z) colors += shades[occ1] vertices += (x + 1, y + 1, z) colors += shades[occ2] vertices += (x + 1, y, z - 1) colors += shades[occ3] vertices += (x + 1, y, z - 1) colors += shades[occ3] vertices += (x + 1, y + 1, z) colors += shades[occ2] vertices += (x + 1, y + 1, z - 1) colors += shades[occ4] uvs += (0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1) normals += (1, 0, 0) * 6 color_ids += (id_r, id_g, id_b | 3) * 6 # Left face if left: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx - 1, vy + 1, vz) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx - 1, vy, vz + 1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx - 1, vy, vz - 1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx - 1, vy - 1, vz) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx - 1, vy - 1, vz + 1) != EMPTY: occ1 += 1 if self.get(vx - 1, vy + 1, vz + 1) != EMPTY: occ2 += 1 if self.get(vx - 1, vy - 1, vz - 1) != EMPTY: occ3 += 1 if self.get(vx - 1, vy + 1, vz - 1) != EMPTY: occ4 += 1 vertices += (x, y, z - 1) colors += shades[occ1] vertices += (x, y + 1, z - 1) colors += shades[occ2] vertices += (x, y, z) colors += shades[occ3] vertices += (x, y, z) colors += shades[occ3] vertices += (x, y + 1, z - 1) colors += shades[occ2] vertices += (x, y + 1, z) colors += shades[occ4] uvs += (0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1) normals += (-1, 0, 0) * 6 color_ids += (id_r, id_g, id_b | 2) * 6 # Back face if back: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx, vy + 1, vz + 1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx + 1, vy, vz + 1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx - 1, vy, vz + 1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx, vy - 1, vz + 1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx + 1, vy - 1, vz + 1) != EMPTY: occ1 += 1 if self.get(vx + 1, vy + 1, vz + 1) != EMPTY: occ2 += 1 if self.get(vx - 1, vy - 1, vz + 1) != EMPTY: occ3 += 1 if self.get(vx - 1, vy + 1, vz + 1) != EMPTY: occ4 += 1 vertices += (x + 1, y, z - 1) colors += shades[occ1] vertices += (x + 1, y + 1, z - 1) colors += shades[occ2] vertices += (x, y, z - 1) colors += shades[occ3] vertices += (x, y, z - 1) colors += shades[occ3] vertices += (x + 1, y + 1, z - 1) colors += shades[occ2] vertices += (x, y + 1, z - 1) colors += shades[occ4] uvs += (0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1) normals += (0, 0, -1) * 6 color_ids += (id_r, id_g, id_b | 4) * 6 # Bottom face if bottom: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx, vy - 1, vz - 1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx - 1, vy - 1, vz) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx + 1, vy - 1, vz) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx, vy - 1, vz + 1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx - 1, vy - 1, vz + 1) != EMPTY: occ1 += 1 if self.get(vx - 1, vy - 1, vz - 1) != EMPTY: occ2 += 1 if self.get(vx + 1, vy - 1, vz + 1) != EMPTY: occ3 += 1 if self.get(vx + 1, vy - 1, vz - 1) != EMPTY: occ4 += 1 vertices += (x, y, z - 1) colors += shades[occ1] vertices += (x, y, z) colors += shades[occ2] vertices += (x + 1, y, z - 1) colors += shades[occ3] vertices += (x + 1, y, z - 1) colors += shades[occ3] vertices += (x, y, z) colors += shades[occ2] vertices += (x + 1, y, z) colors += shades[occ4] uvs += (0, 0, 0, 1, 1, 0, 1, 0, 0, 1, 1, 1) normals += (0, -1, 0) * 6 color_ids += (id_r, id_g, id_b | 5) * 6 return (vertices, colors, normals, color_ids, uvs) # Return vertices for a floor grid def get_grid_vertices(self): grid = [] # builds the Y_plane for z in xrange(self.depth + 1): gx, gy, gz = self.voxel_to_world(0, 0, z) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(self.width, 0, z) grid += (gx, gy, gz) for x in xrange(self.width + 1): gx, gy, gz = self.voxel_to_world(x, 0, 0) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(x, 0, self.depth) grid += (gx, gy, gz) # builds the Z_plane for x in xrange(self.width + 1): gx, gy, gz = self.voxel_to_world(x, 0, self.depth) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(x, self.height, self.depth) grid += (gx, gy, gz) for y in xrange(self.height + 1): gx, gy, gz = self.voxel_to_world(0, y, self.depth) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(self.width, y, self.depth) grid += (gx, gy, gz) # builds the X_plane for y in xrange(self.height + 1): gx, gy, gz = self.voxel_to_world(0, y, 0) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(0, y, self.depth) grid += (gx, gy, gz) for z in xrange(self.depth + 1): gx, gy, gz = self.voxel_to_world(0, 0, z) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(0, self.height, z) grid += (gx, gy, gz) return grid # Convert voxel space coordinates to world space def voxel_to_world(self, x, y, z): x = (x - self.width // 2) - 0.5 y = (y - self.height // 2) - 0.5 z = (z - self.depth // 2) - 0.5 z = -z return x, y, z # Convert world space coordinates to voxel space def world_to_voxel(self, x, y, z): x = (x + self.width // 2) + 0.5 y = (y + self.height // 2) + 0.5 z = (z - self.depth // 2) - 0.5 z = -z return x, y, z # Rebuild our cache def _cache_rebuild(self): self._cache = [] for x in range(self.width): for z in range(self.depth): for y in range(self.height): if self._data[x][y][z] != EMPTY: self._cache.append((x, y, z)) # Calculate the actual bounding box of the model in voxel space # Consider all animation frames def get_bounding_box(self): minx = 999 miny = 999 minz = 999 maxx = -999 maxy = -999 maxz = -999 for data in self._frames: for x in range(self.width): for z in range(self.depth): for y in range(self.height): if data[x][y][z] != EMPTY: if x < minx: minx = x if x > maxx: maxx = x if y < miny: miny = y if y > maxy: maxy = y if z < minz: minz = z if z > maxz: maxz = z width = (maxx - minx) + 1 height = (maxy - miny) + 1 depth = (maxz - minz) + 1 return minx, miny, minz, width, height, depth # Resize the voxel space. If no dimensions given, adjust to bounding box. # We offset all voxels on all axis by the given amount. # Resize all animation frames def resize(self, width=None, height=None, depth=None, shift=0): # Reset undo buffer self._undo.clear() # No dimensions, use bounding box mx, my, mz, cwidth, cheight, cdepth = self.get_bounding_box() if not width: width, height, depth = cwidth, cheight, cdepth for i, frame in enumerate(self._frames): # Create new data structure of the required size data = [[[0 for _ in xrange(depth)] for _ in xrange(height)] for _ in xrange(width)] # Adjust ranges movewidth = min(width, cwidth) moveheight = min(height, cheight) movedepth = min(depth, cdepth) # Calculate translation dx = (0 - mx) + shift dy = (0 - my) + shift dz = (0 - mz) + shift # Copy data over at new location for x in xrange(mx, mx + movewidth): for y in xrange(my, my + moveheight): for z in xrange(mz, mz + movedepth): data[x + dx][y + dy][z + dz] = frame[x][y][z] self._frames[i] = data self._data = self._frames[self._current_frame] # Set new dimensions self._width = width self._height = height self._depth = depth # Rebuild our cache self._cache_rebuild() self.changed = True # Rotate voxels in voxel space 90 degrees def rotate_about_axis(self, axis): # Reset undo buffer self._undo.clear() if axis == self.Y_AXIS: width = self.depth # note swap height = self.height depth = self.width elif axis == self.X_AXIS: width = self.width height = self.depth depth = self.height elif axis == self.Z_AXIS: width = self.height height = self.width depth = self.depth for i, frame in enumerate(self._frames): # Create new temporary data structure data = [[[0 for _ in xrange(depth)] for _ in xrange(height)] for _ in xrange(width)] # Copy data over at new location for tx in xrange(0, self.width): for ty in xrange(0, self.height): for tz in xrange(0, self.depth): if axis == self.Y_AXIS: dx = (-tz) - 1 dy = ty dz = tx elif axis == self.X_AXIS: dx = tx dy = (-tz) - 1 dz = ty elif axis == self.Z_AXIS: dx = ty dy = (-tx) - 1 dz = tz data[dx][dy][dz] = frame[tx][ty][tz] self._frames[i] = data self._width = width self._height = height self._depth = depth self._data = self._frames[self._current_frame] # Rebuild our cache self._cache_rebuild() self.changed = True # Mirror voxels in a axis def mirror_in_axis(self, axis): # Reset undo buffer self._undo.clear() for i, frame in enumerate(self._frames): # Create new temporary data structure data = [[[0 for _ in xrange(self.depth)] for _ in xrange(self.height)] for _ in xrange(self.width)] # Copy data over at new location for tx in xrange(0, self.width): for ty in xrange(0, self.height): for tz in xrange(0, self.depth): if axis == self.Y_AXIS: dx = tx dy = (-ty) - 1 dz = tz elif axis == self.X_AXIS: dx = (-tx) - 1 dy = ty dz = tz elif axis == self.Z_AXIS: dx = tx dy = ty dz = (-tz) - 1 data[dx][dy][dz] = frame[tx][ty][tz] self._frames[i] = data self._data = self._frames[self._current_frame] # Rebuild our cache self._cache_rebuild() self.changed = True # Translate the voxel data. def translate(self, x, y, z, undo=True): # Sanity if x == 0 and y == 0 and z == 0: return # Add to undo if undo: self._undo.add(UndoItem(Undo.TRANSLATE, (-x, -y, -z), (x, y, z))) # Create new temporary data structure data = [[[0 for _ in xrange(self.depth)] for _ in xrange(self.height)] for _ in xrange(self.width)] # Copy data over at new location for tx in xrange(0, self.width): for ty in xrange(0, self.height): for tz in xrange(0, self.depth): dx = (tx + x) % self.width dy = (ty + y) % self.height dz = (tz + z) % self.depth data[dx][dy][dz] = self._data[tx][ty][tz] self._data = data self._frames[self._current_frame] = self._data # Rebuild our cache self._cache_rebuild() self.changed = True # Undo previous operation def undo(self): self.clear_selection() op = self._undo.undo() # Voxel edit if op and op.operation == Undo.SET_VOXEL: data = op.olddata self.set(data[0], data[1], data[2], data[3], False) elif op and op.operation == Undo.FILL: d = op.olddata for data in d: self.set(data[0], data[1], data[2], data[3], False) # Translation elif op and op.operation == Undo.TRANSLATE: data = op.olddata self.translate(data[0], data[1], data[2], False) # Redo an undone operation def redo(self): op = self._undo.redo() # Voxel edit if op and op.operation == Undo.SET_VOXEL: data = op.newdata self.set(data[0], data[1], data[2], data[3], False) elif op and op.operation == Undo.FILL: d = op.newdata for data in d: self.set(data[0], data[1], data[2], data[3], False) # Translation elif op and op.operation == Undo.TRANSLATE: data = op.newdata self.translate(data[0], data[1], data[2], False) # Enable/Disable undo buffer def disable_undo(self): self._undo.enabled = False def enable_undo(self): self._undo.enabled = True
def __init__(self, fits_file, settings, database, project=None): super(FinishSpectrum, self).__init__() self.settings = settings self.ui = Ui_FinishSpectrum() self.ui.setupUi(self) self.profile_line = None self.project = project self.fits_spectrum = FitsSpectrum(fits_file) self.undo = Undo(self.fits_spectrum.spectrum, self.draw) try: fits_file.index_of('ORIGINAL_DATA') except KeyError: hdu = fits.ImageHDU(data = fits_file[0].data, header = fits_file[0].header, name='ORIGINAL_DATA') fits_file.append(hdu) self.fits_spectrum.spectrum.normalize_to_max() self.spectrum = self.fits_spectrum.spectrum self.spectrum_plot = QtCommons.nestWidget(self.ui.plot, QMathPlotWidget()) self.spectrum_plot.mouse_moved.connect(Instances.MainWindow.print_coordinates) self.split_view() self.toolbar = QToolBar('Finish Spectrum Toolbar') if project: instrument_response_action = QtCommons.addToolbarPopup(self.toolbar, "Instrument Response") instrument_response_action.menu().addAction('From FITS file...', lambda: open_file_sticky('Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[0]), settings, MATH_OPERATION, [RAW_PROFILE])) for instrument_response in project.get_instrument_responses(): print("Adding instrument response {}".format(instrument_response)) instrument_response_action.menu().addAction(os.path.basename(instrument_response[1]), lambda: self.instrument_response(instrument_response[1])) else: self.toolbar.addAction('Instrument Response', lambda: open_file_sticky('Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[0]), settings, MATH_OPERATION, [RAW_PROFILE])) self.toolbar.addAction("Zoom", lambda: self.spectrum_plot.select_zoom(self.profile_plot.axes)) self.toolbar.addAction("Reset Zoom", lambda: self.spectrum_plot.reset_zoom(self.spectrum.wavelengths, self.spectrum.fluxes.min(), self.spectrum.fluxes.max(), self.profile_plot.axes)) remove_action = QtCommons.addToolbarPopup(self.toolbar, "Remove") remove_action.menu().addAction("Before point", lambda: spectrum_trim_dialog(self.spectrum, 'before', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) remove_action.menu().addAction("After point", lambda: spectrum_trim_dialog(self.spectrum, 'after', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) self.undo.add_actions(self.toolbar) self.toolbar.addSeparator() self.reference_spectra_dialog = ReferenceSpectraDialog(database, self.fits_spectrum.spectrum) self.reference_spectra_dialog.setup_menu(self.toolbar, self.profile_plot.axes, settings) lines_menu = QtCommons.addToolbarPopup(self.toolbar, "Spectral Lines..") lines_menu.menu().addAction('Lines Database', lambda: self.lines_dialog.show()) lines_menu.menu().addAction('Custom line', self.add_custom_line) labels_action = QtCommons.addToolbarPopup(self.toolbar, "Labels..") self.object_properties = ObjectProperties(fits_file, project=project) labels_action.menu().addAction('Title', self.add_title) if self.object_properties: labels_action.menu().addAction('Information from FITS file', self.add_fits_information_label) labels_action.menu().addAction('Custom', self.add_label) self.object_properties_dialog = ObjectPropertiesDialog(settings, self.object_properties) self.toolbar.addAction("Object properties", self.object_properties_dialog.show) self.labels, self.lines = [], [] for label in self.fits_spectrum.labels(): self.add_label(text=label['text'], coords=label['coords'], type=label['type'], fontsize=label['fontsize']) self.toolbar.addSeparator() if project: self.toolbar.addAction(QIcon(':/image_20'), "Export Image...", lambda: QtCommons.save_file('Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), project.directory_path(Project.EXPORTED_IMAGES))) self.toolbar.addAction(QIcon(':/save_20'), 'Save', self.save_finished_in_project) else: self.toolbar.addAction(QIcon(':/image_20'), "Export Image...", lambda: save_file_sticky('Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), self.settings, EXPORT_IMAGES, [CALIBRATED_PROFILE])) self.toolbar.addAction(QIcon(':/save_20'), 'Save', lambda: save_file_sticky('Save plot...', 'FITS file (.fit)', lambda f: self.__save(f[0]), self.settings, CALIBRATED_PROFILE)) self.lines_dialog = LinesDialog(database, settings, self.spectrum_plot, self.profile_plot.axes) self.lines_dialog.lines.connect(self.add_lines) for line in self.fits_spectrum.lines_labels(): self.lines.append(ReferenceLine(line['text'], line['wavelength'], self.profile_plot.axes, lambda line: self.lines.remove(line), show_wavelength=line['display_wavelength'], fontsize=line['fontsize'], position=line['position']))
class FinishSpectrum(QWidget): def __init__(self, fits_file, settings, database, project=None): super(FinishSpectrum, self).__init__() self.settings = settings self.ui = Ui_FinishSpectrum() self.ui.setupUi(self) self.profile_line = None self.project = project self.fits_spectrum = FitsSpectrum(fits_file) self.undo = Undo(self.fits_spectrum.spectrum, self.draw) try: fits_file.index_of('ORIGINAL_DATA') except KeyError: hdu = fits.ImageHDU(data = fits_file[0].data, header = fits_file[0].header, name='ORIGINAL_DATA') fits_file.append(hdu) self.fits_spectrum.spectrum.normalize_to_max() self.spectrum = self.fits_spectrum.spectrum self.spectrum_plot = QtCommons.nestWidget(self.ui.plot, QMathPlotWidget()) self.spectrum_plot.mouse_moved.connect(Instances.MainWindow.print_coordinates) self.split_view() self.toolbar = QToolBar('Finish Spectrum Toolbar') if project: instrument_response_action = QtCommons.addToolbarPopup(self.toolbar, "Instrument Response") instrument_response_action.menu().addAction('From FITS file...', lambda: open_file_sticky('Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[0]), settings, MATH_OPERATION, [RAW_PROFILE])) for instrument_response in project.get_instrument_responses(): print("Adding instrument response {}".format(instrument_response)) instrument_response_action.menu().addAction(os.path.basename(instrument_response[1]), lambda: self.instrument_response(instrument_response[1])) else: self.toolbar.addAction('Instrument Response', lambda: open_file_sticky('Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[0]), settings, MATH_OPERATION, [RAW_PROFILE])) self.toolbar.addAction("Zoom", lambda: self.spectrum_plot.select_zoom(self.profile_plot.axes)) self.toolbar.addAction("Reset Zoom", lambda: self.spectrum_plot.reset_zoom(self.spectrum.wavelengths, self.spectrum.fluxes.min(), self.spectrum.fluxes.max(), self.profile_plot.axes)) remove_action = QtCommons.addToolbarPopup(self.toolbar, "Remove") remove_action.menu().addAction("Before point", lambda: spectrum_trim_dialog(self.spectrum, 'before', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) remove_action.menu().addAction("After point", lambda: spectrum_trim_dialog(self.spectrum, 'after', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) self.undo.add_actions(self.toolbar) self.toolbar.addSeparator() self.reference_spectra_dialog = ReferenceSpectraDialog(database, self.fits_spectrum.spectrum) self.reference_spectra_dialog.setup_menu(self.toolbar, self.profile_plot.axes, settings) lines_menu = QtCommons.addToolbarPopup(self.toolbar, "Spectral Lines..") lines_menu.menu().addAction('Lines Database', lambda: self.lines_dialog.show()) lines_menu.menu().addAction('Custom line', self.add_custom_line) labels_action = QtCommons.addToolbarPopup(self.toolbar, "Labels..") self.object_properties = ObjectProperties(fits_file, project=project) labels_action.menu().addAction('Title', self.add_title) if self.object_properties: labels_action.menu().addAction('Information from FITS file', self.add_fits_information_label) labels_action.menu().addAction('Custom', self.add_label) self.object_properties_dialog = ObjectPropertiesDialog(settings, self.object_properties) self.toolbar.addAction("Object properties", self.object_properties_dialog.show) self.labels, self.lines = [], [] for label in self.fits_spectrum.labels(): self.add_label(text=label['text'], coords=label['coords'], type=label['type'], fontsize=label['fontsize']) self.toolbar.addSeparator() if project: self.toolbar.addAction(QIcon(':/image_20'), "Export Image...", lambda: QtCommons.save_file('Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), project.directory_path(Project.EXPORTED_IMAGES))) self.toolbar.addAction(QIcon(':/save_20'), 'Save', self.save_finished_in_project) else: self.toolbar.addAction(QIcon(':/image_20'), "Export Image...", lambda: save_file_sticky('Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), self.settings, EXPORT_IMAGES, [CALIBRATED_PROFILE])) self.toolbar.addAction(QIcon(':/save_20'), 'Save', lambda: save_file_sticky('Save plot...', 'FITS file (.fit)', lambda f: self.__save(f[0]), self.settings, CALIBRATED_PROFILE)) self.lines_dialog = LinesDialog(database, settings, self.spectrum_plot, self.profile_plot.axes) self.lines_dialog.lines.connect(self.add_lines) for line in self.fits_spectrum.lines_labels(): self.lines.append(ReferenceLine(line['text'], line['wavelength'], self.profile_plot.axes, lambda line: self.lines.remove(line), show_wavelength=line['display_wavelength'], fontsize=line['fontsize'], position=line['position'])) def add_custom_line(self): wl = QInputDialog.getDouble(self, "Custom Line", "Enter line wavelength in Å", self.fits_spectrum.spectrum.wavelengths[0],self.fits_spectrum.spectrum.wavelengths[0],self.fits_spectrum.spectrum.wavelengths[-1],3) if not wl[1]: return self.add_lines([{'name': 'Custom Line', 'lambda': wl[0]}]) def add_lines(self, lines): for line in lines: self.lines.append(ReferenceLine(line['name'], line['lambda'], self.profile_plot.axes, lambda line: self.lines.remove(line))) def synthetize_img(wavelengths, fluxes): f_fluxes = lambda f: math.pow(f, 3/5) colors = [wavelength_to_rgb(w/10., f_fluxes(fluxes[i])) for i,w in enumerate(wavelengths)] im_height = 150 colors = np.array(colors*im_height).reshape(im_height,len(colors),4) return colors, im_height def split_view(self): figure = self.spectrum_plot.figure figure.clear() self.gs = gridspec.GridSpec(40,1) self.profile_plot = figure.add_subplot(self.gs[0:-6]) self.synthetize = figure.add_subplot(self.gs[-3:-1], sharex = self.profile_plot) self.synthetize.yaxis.set_visible(False) self.synthetize.xaxis.set_visible(False) self.draw() def draw(self): # self.profile_plot.clear() if self.profile_line: self.profile_line.remove() self.profile_line = self.profile_plot.plot(self.spectrum.wavelengths, self.spectrum.fluxes, color='blue')[0] self.synthetize.axes.set_axis_bgcolor('black') with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(FinishSpectrum.synthetize_img, self.spectrum.wavelengths, self.spectrum.fluxes) future.add_done_callback(lambda f: self.synthetize.imshow(f.result()[0], extent=[self.spectrum.wavelengths[0], self.spectrum.wavelengths[-1], 0, f.result()[1]]) ) self.profile_plot.axes.set_xlabel('wavelength (Å)') self.profile_plot.axes.set_ylabel('relative flux') self.profile_plot.axes.xaxis.set_major_locator(MaxNLocator(16)) # TODO: settings for customization? self.profile_plot.axes.xaxis.set_minor_locator(MaxNLocator(200)) self.spectrum_plot.figure.canvas.draw() self.gs.tight_layout(self.spectrum_plot.figure) def instrument_response(self, filename): print("Applying instrument response {}".format(filename)) instrument_response_file = fits.open(filename) instrument_response = FitsSpectrum(instrument_response_file) response = instrument_response.spectrum response.normalize_to_max() range = (max(response.wavelengths[0], self.spectrum.wavelengths[0] ), min(response.wavelengths[-1], self.spectrum.wavelengths[-1])) self.spectrum.cut(self.spectrum.wavelength_index(range[0]), self.spectrum.wavelength_index(range[1])) spline = InterpolatedUnivariateSpline(response.wavelengths, response.fluxes) response_data = [spline(x) for x in self.spectrum.wavelengths] self.spectrum.fluxes /= response_data self.spectrum.normalize_to_max() self.draw() def save_image(self, filename): Notification('Image {} saved in {}'.format(os.path.basename(filename), os.path.dirname(filename)), title='File Saved', type='success', timeout=5) self.spectrum_plot.figure.savefig(filename, bbox_inches='tight', dpi=300) def save_finished_in_project(self): self.project.add_file(Project.FINISHED_PROFILES, self.__save, self.object_properties) def __save(self, filename): self.fits_spectrum.save(filename, spectral_lines = self.lines, labels = self.labels) def add_title(self): title = self.object_properties.name if self.object_properties else 'Title - double click to edit' self.add_label(text=title, coords=(self.spectrum.wavelengths[len(self.spectrum.wavelengths)/2-100], 0.95), fontsize=25, type='lineedit') def add_fits_information_label(self): info_text = "Object Name: {}, type: {}, spectral class: {}\nCoordinates: {}\nDate: {}\nObserver: {}\nEquipment: {}\nPosition: {}".format( self.object_properties.name, self.object_properties.type, self.object_properties.sptype, self.object_properties.printable_coordinates(), self.object_properties.date.toString(), self.object_properties.observer, self.object_properties.equipment, self.object_properties.position ) self.add_label(info_text, type='textbox', coords=(self.spectrum.wavelengths[len(self.spectrum.wavelengths)/4*3], 0.80), fontsize=14) self.profile_plot.figure.canvas.draw() def add_label(self, text=None, type='textbox', coords = None, fontsize = 12, color='black'): if not coords: coords = (self.spectrum.wavelengths[len(self.spectrum.wavelengths)/2], 0.5) self.labels.append((type, MoveableLabel(text=text if text else 'Label - double click to edit', on_dblclick=lambda l: self.edit_label(l, type=type), x=coords[0], y=coords[1], fontsize=fontsize, color=color, axes=self.profile_plot.axes))) self.profile_plot.figure.canvas.draw() def edit_label(self, label, type='lineedit'): def remove_label(self, label, dialog): label.remove() self.labels.remove([l for l in self.labels if l[1] == label][0]) self.profile_plot.figure.canvas.draw() dialog.reject() dialog = QDialog() dialog.setWindowTitle("Edit Label") dialog.setLayout(QVBoxLayout()) font_size = QSpinBox() font_size.setValue(label.get_fontsize()) dialog.layout().addWidget(QLabel("Font Size")) dialog.layout().addWidget(font_size) text_edit = None if type == 'lineedit': text_edit = QLineEdit(label.get_text()) else: text_edit = QTextEdit() text_edit.setPlainText(label.get_text()) dialog.layout().addWidget(QLabel("Text")) dialog.layout().addWidget(text_edit) button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(dialog.accept) button_box.rejected.connect(dialog.reject) remove_button = QPushButton('Remove') remove_button.clicked.connect(lambda: remove_label(self, label, dialog)) dialog.layout().addWidget(remove_button) dialog.layout().addWidget(button_box) if QDialog.Accepted != dialog.exec(): return label.set_text(text_edit.text() if type=='lineedit' else text_edit.toPlainText()) label.set_fontsize(font_size.value()) label.axes.figure.canvas.draw()
class VoxelData(object): # Constants for referring to axis X_AXIS = 1 Y_AXIS = 2 Z_AXIS = 3 # World dimension properties @property def width(self): return self._width @property def height(self): return self._height @property def depth(self): return self._depth @property def changed(self): return self._changed @changed.setter def changed(self, value): if value and not self._changed: # Let whoever is watching us know about the change self._changed = value if self.notify_changed: self.notify_changed() self._changed = value @property def occlusion(self): return self._occlusion @occlusion.setter def occlusion(self, value): self._occlusion = value def __init__(self): # Default size self._width = _WORLD_WIDTH self._height = _WORLD_HEIGHT self._depth = _WORLD_DEPTH # Our undo buffer self._undo = Undo() # Init data self._initialise_data() # Callback when our data changes self.notify_changed = None # Ambient occlusion type effect self._occlusion = True # Initialise our data def _initialise_data(self): # Our scene data self._data = self.blank_data() # Our cache of non-empty voxels (coordinate groups) self._cache = [] # Flag indicating if our data has changed self._changed = False # Reset undo buffer self._undo.clear() # Animation self._frame_count = 1 self._current_frame = 0 self._frames = [self._data] # Return an empty voxel space def blank_data(self): return [[[0 for _ in xrange(self.depth)] for _ in xrange(self.height)] for _ in xrange(self.width)] def is_valid_bounds(self, x, y, z): return ( x >= 0 and x < self.width and y >= 0 and y < self.height and z >= 0 and z < self.depth ) # Return the number of animation frames def get_frame_count(self): return self._frame_count # Change to the given frame def select_frame(self, frame_number): # Sanity if frame_number < 0 or frame_number >= self._frame_count: return # Make sure we really have a pointer to the current data self._frames[self._current_frame] = self._data # Change to new frame self._data = self._frames[frame_number] self._current_frame = frame_number self._undo.frame = self._current_frame self._cache_rebuild() self.changed = True # Add a new frame by copying the current one def add_frame(self, copy_current = True): if copy_current: data = self.get_data() else: data = self.blank_data() self._frames.insert(self._current_frame+1, data) self._undo.add_frame(self._current_frame+1) self._frame_count += 1 self.select_frame(self._current_frame+1) # Delete the current frame def delete_frame(self): # Sanity - we can't have no frames at all if self._frame_count <= 1: return # Remember the frame we want to delete killframe = self._current_frame # Select a different frame self.select_previous_frame() # Remove the old frame del self._frames[killframe] self._undo.delete_frame(killframe) self._frame_count -= 1 # If we only have one frame left, must be first frame if self._frame_count == 1: self._current_frame = 0 # If we wrapped around, fix the frame pointer if self._current_frame > killframe: self._current_frame -= 1 # Change to the next frame (with wrap) def select_next_frame(self): nextframe = self._current_frame+1 if nextframe >= self._frame_count: nextframe = 0 self.select_frame(nextframe) # Change to the previous frame (with wrap) def select_previous_frame(self): prevframe = self._current_frame-1 if prevframe < 0: prevframe = self._frame_count-1 self.select_frame(prevframe) # Get current frame number def get_frame_number(self): return self._current_frame # Set a voxel to the given state def set(self, x, y, z, state, undo = True): # If this looks like a QT Color instance, convert it if hasattr(state, "getRgb"): c = state.getRgb() state = c[0]<<24 | c[1]<<16 | c[2]<<8 | 0xff # Check bounds if ( not self.is_valid_bounds(x, y, z ) ): return False # Set the voxel if ( self.is_valid_bounds(x, y, z ) ): # Add to undo if undo: self._undo.add(UndoItem(Undo.SET_VOXEL, (x, y, z, self._data[x][y][z]), (x, y, z, state))) self._data[x][y][z] = state if state != EMPTY: if (x,y,z) not in self._cache: self._cache.append((x,y,z)) else: if (x,y,z) in self._cache: self._cache.remove((x,y,z)) self.changed = True return True # Get the state of the given voxel def get(self, x, y, z): if ( not self.is_valid_bounds(x, y, z ) ): return EMPTY return self._data[x][y][z] # Return a copy of the voxel data def get_data(self): return copy.deepcopy(self._data) # Set all of our data at once def set_data(self, data): self._data = copy.deepcopy(data) self._cache_rebuild() self.changed = True # Clear our voxel data def clear(self): self._initialise_data() # Return full vertex list def get_vertices(self): vertices = [] colours = [] colour_ids = [] normals = [] uvs = [] for x,y,z in self._cache: v, c, n, cid, uv = self._get_voxel_vertices(x, y, z) vertices += v colours += c normals += n colour_ids += cid uvs += uv return (vertices, colours, normals, colour_ids, uvs) # Called to notify us that our data has been saved. i.e. we can set # our "changed" status back to False. def saved(self): self.changed = False # Count the number of non-empty voxels from the list of coordinates def _count_voxels(self, coordinates): count = 0 for x,y,z in coordinates: if self.get(x, y, z) != EMPTY: count += 1 return count # Return the verticies for the given voxel. We center our vertices at the origin def _get_voxel_vertices(self, x, y, z): vertices = [] colours = [] normals = [] colour_ids = [] uvs = [] # Remember voxel coordinates vx, vy, vz = x,y,z # Determine if we have filled voxels around us front = self.get(x, y, z-1) == EMPTY left = self.get(x-1, y, z) == EMPTY right = self.get(x+1, y, z) == EMPTY top = self.get(x, y+1, z) == EMPTY back = self.get(x, y, z+1) == EMPTY bottom = self.get(x, y-1, z) == EMPTY # Get our colour c = self.get(x, y, z) r = (c & 0xff000000)>>24 g = (c & 0xff0000)>>16 b = (c & 0xff00)>>8 # Calculate shades for our 4 occlusion levels shades = [] for c in range(5): shades.append(( int(r*math.pow(OCCLUSION,c)), int(g*math.pow(OCCLUSION,c)), int(b*math.pow(OCCLUSION,c)))) # Encode our voxel space coordinates as colours, used for face selection # We use 7 bits per coordinate and the bottom 3 bits for face: # 0 - front # 1 - top # 2 - left # 3 - right # 4 - back # 5 - bottom voxel_id = (x & 0x7f)<<17 | (y & 0x7f)<<10 | (z & 0x7f)<<3 id_r = (voxel_id & 0xff0000)>>16 id_g = (voxel_id & 0xff00)>>8 id_b = (voxel_id & 0xff) # Adjust coordinates to the origin x, y, z = self.voxel_to_world(x, y, z) # Front face if front: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx,vy+1,vz-1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx-1,vy,vz-1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx+1,vy,vz-1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx,vy-1,vz-1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx-1,vy-1,vz-1) != EMPTY: occ1 += 1 if self.get(vx-1,vy+1,vz-1) != EMPTY: occ2 += 1 if self.get(vx+1,vy-1,vz-1) != EMPTY: occ3 += 1 if self.get(vx+1,vy+1,vz-1) != EMPTY: occ4 += 1 vertices += (x, y, z) colours += shades[occ1] vertices += (x, y+1, z) colours += shades[occ2] vertices += (x+1, y, z) colours += shades[occ3] vertices += (x+1, y, z) colours += shades[occ3] vertices += (x, y+1, z) colours += shades[occ2] vertices += (x+1, y+1, z) colours += shades[occ4] uvs += (0,0,0,1,1,0,1,0,0,1,1,1) normals += (0, 0, 1) * 6 colour_ids += (id_r, id_g, id_b) * 6 # Top face if top: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx,vy+1,vz+1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx-1,vy+1,vz) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx+1,vy+1,vz) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx,vy+1,vz-1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx-1,vy+1,vz-1) != EMPTY: occ1 += 1 if self.get(vx+1,vy+1,vz-1) != EMPTY: occ3 += 1 if self.get(vx+1,vy+1,vz+1) != EMPTY: occ4 += 1 if self.get(vx-1,vy+1,vz+1) != EMPTY: occ2 += 1 vertices += (x, y+1, z) colours += shades[occ1] vertices += (x, y+1, z-1) colours += shades[occ2] vertices += (x+1, y+1, z) colours += shades[occ3] vertices += (x+1, y+1, z) colours += shades[occ3] vertices += (x, y+1, z-1) colours += shades[occ2] vertices += (x+1, y+1, z-1) colours += shades[occ4] uvs += (0,0,0,1,1,0,1,0,0,1,1,1) normals += (0, 1, 0) * 6 colour_ids += (id_r, id_g, id_b | 1) * 6 # Right face if right: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx+1,vy+1,vz) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx+1,vy,vz-1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx+1,vy,vz+1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx+1,vy-1,vz) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx+1,vy-1,vz-1) != EMPTY: occ1 += 1 if self.get(vx+1,vy+1,vz-1) != EMPTY: occ2 += 1 if self.get(vx+1,vy-1,vz+1) != EMPTY: occ3 += 1 if self.get(vx+1,vy+1,vz+1) != EMPTY: occ4 += 1 vertices += (x+1, y, z) colours += shades[occ1] vertices += (x+1, y+1, z) colours += shades[occ2] vertices += (x+1, y, z-1) colours += shades[occ3] vertices += (x+1, y, z-1) colours += shades[occ3] vertices += (x+1, y+1, z) colours += shades[occ2] vertices += (x+1, y+1, z-1) colours += shades[occ4] uvs += (0,0,0,1,1,0,1,0,0,1,1,1) normals += (1, 0, 0) * 6 colour_ids += (id_r, id_g, id_b | 3) * 6 # Left face if left: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx-1,vy+1,vz) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx-1,vy,vz+1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx-1,vy,vz-1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx-1,vy-1,vz) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx-1,vy-1,vz+1) != EMPTY: occ1 += 1 if self.get(vx-1,vy+1,vz+1) != EMPTY: occ2 += 1 if self.get(vx-1,vy-1,vz-1) != EMPTY: occ3 += 1 if self.get(vx-1,vy+1,vz-1) != EMPTY: occ4 += 1 vertices += (x, y, z-1) colours += shades[occ1] vertices += (x, y+1, z-1) colours += shades[occ2] vertices += (x, y, z) colours += shades[occ3] vertices += (x, y, z) colours += shades[occ3] vertices += (x, y+1, z-1) colours += shades[occ2] vertices += (x, y+1, z) colours += shades[occ4] uvs += (0,0,0,1,1,0,1,0,0,1,1,1) normals += (-1, 0, 0) * 6 colour_ids += (id_r, id_g, id_b | 2) * 6 # Back face if back: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx,vy+1,vz+1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx+1,vy,vz+1) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx-1,vy,vz+1) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx,vy-1,vz+1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx+1,vy-1,vz+1) != EMPTY: occ1 += 1 if self.get(vx+1,vy+1,vz+1) != EMPTY: occ2 += 1 if self.get(vx-1,vy-1,vz+1) != EMPTY: occ3 += 1 if self.get(vx-1,vy+1,vz+1) != EMPTY: occ4 += 1 vertices += (x+1, y, z-1) colours += shades[occ1] vertices += (x+1, y+1, z-1) colours += shades[occ2] vertices += (x, y, z-1) colours += shades[occ3] vertices += (x, y, z-1) colours += shades[occ3] vertices += (x+1, y+1, z-1) colours += shades[occ2] vertices += (x, y+1, z-1) colours += shades[occ4] uvs += (0,0,0,1,1,0,1,0,0,1,1,1) normals += (0, 0, -1) * 6 colour_ids += (id_r, id_g, id_b | 4) * 6 # Bottom face if bottom: occ1 = 0 occ2 = 0 occ3 = 0 occ4 = 0 if self._occlusion: if self.get(vx,vy-1,vz-1) != EMPTY: occ2 += 1 occ4 += 1 if self.get(vx-1,vy-1,vz) != EMPTY: occ1 += 1 occ2 += 1 if self.get(vx+1,vy-1,vz) != EMPTY: occ3 += 1 occ4 += 1 if self.get(vx,vy-1,vz+1) != EMPTY: occ1 += 1 occ3 += 1 if self.get(vx-1,vy-1,vz+1) != EMPTY: occ1 += 1 if self.get(vx-1,vy-1,vz-1) != EMPTY: occ2 += 1 if self.get(vx+1,vy-1,vz+1) != EMPTY: occ3 += 1 if self.get(vx+1,vy-1,vz-1) != EMPTY: occ4 += 1 vertices += (x, y, z-1) colours += shades[occ1] vertices += (x, y, z) colours += shades[occ2] vertices += (x+1, y, z-1) colours += shades[occ3] vertices += (x+1, y, z-1) colours += shades[occ3] vertices += (x, y, z) colours += shades[occ2] vertices += (x+1, y, z) colours += shades[occ4] uvs += (0,0,0,1,1,0,1,0,0,1,1,1) normals += (0, -1, 0) * 6 colour_ids += (id_r, id_g, id_b | 5) * 6 return (vertices, colours, normals, colour_ids, uvs) # Return vertices for a floor grid def get_grid_vertices(self): grid = [] #builds the Y_plane for z in xrange(self.depth+1): gx, gy, gz = self.voxel_to_world(0, 0, z) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(self.width, 0, z) grid += (gx, gy, gz) for x in xrange(self.width+1): gx, gy, gz = self.voxel_to_world(x, 0, 0) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(x, 0, self.depth) grid += (gx, gy, gz) #builds the Z_plane for x in xrange(self.width+1): gx, gy, gz = self.voxel_to_world(x, 0, self.depth) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(x, self.height, self.depth) grid += (gx, gy, gz) for y in xrange(self.height+1): gx, gy, gz = self.voxel_to_world(0, y, self.depth) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(self.width, y, self.depth) grid += (gx, gy, gz) #builds the X_plane for y in xrange(self.height+1): gx, gy, gz = self.voxel_to_world(0, y, 0) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(0, y, self.depth) grid += (gx, gy, gz) for z in xrange(self.depth+1): gx, gy, gz = self.voxel_to_world(0, 0, z) grid += (gx, gy, gz) gx, gy, gz = self.voxel_to_world(0, self.height, z) grid += (gx, gy, gz) return grid # Convert voxel space coordinates to world space def voxel_to_world(self, x, y, z): x = (x - self.width//2)-0.5 y = (y - self.height//2)-0.5 z = (z - self.depth//2)-0.5 z = -z return x, y, z # Convert world space coordinates to voxel space def world_to_voxel(self, x, y, z): x = (x + self.width//2)+0.5 y = (y + self.height//2)+0.5 z = (z - self.depth//2)-0.5 z = -z return x, y, z # Rebuild our cache def _cache_rebuild(self): self._cache = [] for x in range(self.width): for z in range(self.depth): for y in range(self.height): if self._data[x][y][z] != EMPTY: self._cache.append((x, y, z)) # Calculate the actual bounding box of the model in voxel space # Consider all animation frames def get_bounding_box(self): minx = 999 miny = 999 minz = 999 maxx = -999 maxy = -999 maxz = -999 for data in self._frames: for x in range(self.width): for z in range(self.depth): for y in range(self.height): if data[x][y][z] != EMPTY: if x < minx: minx = x if x > maxx: maxx = x if y < miny: miny = y if y > maxy: maxy = y if z < minz: minz = z if z > maxz: maxz = z width = (maxx-minx)+1 height = (maxy-miny)+1 depth = (maxz-minz)+1 return minx, miny, minz, width, height, depth # Resize the voxel space. If no dimensions given, adjust to bounding box. # We offset all voxels on all axis by the given amount. # Resize all animation frames def resize(self, width = None, height = None, depth = None, shift = 0): # Reset undo buffer self._undo.clear() # No dimensions, use bounding box mx, my, mz, cwidth, cheight, cdepth = self.get_bounding_box() if not width: width, height, depth = cwidth, cheight, cdepth for i, frame in enumerate(self._frames): # Create new data structure of the required size data = [[[0 for _ in xrange(depth)] for _ in xrange(height)] for _ in xrange(width)] # Adjust ranges movewidth = min(width, cwidth) moveheight = min(height, cheight) movedepth = min(depth, cdepth) # Calculate translation dx = (0-mx)+shift dy = (0-my)+shift dz = (0-mz)+shift # Copy data over at new location for x in xrange(mx, mx+movewidth): for y in xrange(my, my+moveheight): for z in xrange(mz, mz+movedepth): data[x+dx][y+dy][z+dz] = frame[x][y][z] self._frames[i] = data self._data = self._frames[self._current_frame] # Set new dimensions self._width = width self._height = height self._depth = depth # Rebuild our cache self._cache_rebuild() self.changed = True # Rotate voxels in voxel space 90 degrees def rotate_about_axis(self, axis): # Reset undo buffer self._undo.clear() if axis == self.Y_AXIS: width = self.depth # note swap height = self.height depth = self.width elif axis == self.X_AXIS: width = self.width height = self.depth depth = self.height elif axis == self.Z_AXIS: width = self.height height = self.width depth = self.depth for i, frame in enumerate(self._frames): # Create new temporary data structure data = [[[0 for _ in xrange(depth)] for _ in xrange(height)] for _ in xrange(width)] # Copy data over at new location for tx in xrange(0, self.width): for ty in xrange(0, self.height): for tz in xrange(0, self.depth): if axis == self.Y_AXIS: dx = (-tz)-1 dy = ty dz = tx elif axis == self.X_AXIS: dx = tx dy = (-tz)-1 dz = ty elif axis == self.Z_AXIS: dx = ty dy = (-tx)-1 dz = tz data[dx][dy][dz] = frame[tx][ty][tz] self._frames[i] = data self._width = width self._height = height self._depth = depth self._data = self._frames[self._current_frame] # Rebuild our cache self._cache_rebuild() self.changed = True # Translate the voxel data. def translate(self, x, y, z, undo = True): # Sanity if x == 0 and y == 0 and z == 0: return # Add to undo if undo: self._undo.add(UndoItem(Undo.TRANSLATE, (-x, -y, -z), (x, y, z))) # Create new temporary data structure data = [[[0 for _ in xrange(self.depth)] for _ in xrange(self.height)] for _ in xrange(self.width)] # Copy data over at new location for tx in xrange(0, self.width): for ty in xrange(0, self.height): for tz in xrange(0, self.depth): dx = (tx+x) % self.width dy = (ty+y) % self.height dz = (tz+z) % self.depth data[dx][dy][dz] = self._data[tx][ty][tz] self._data = data self._frames[self._current_frame] = self._data # Rebuild our cache self._cache_rebuild() self.changed = True # Undo previous operation def undo(self): op = self._undo.undo() # Voxel edit if op and op.operation == Undo.SET_VOXEL: data = op.olddata self.set(data[0], data[1], data[2], data[3], False) # Translation elif op and op.operation == Undo.TRANSLATE: data = op.olddata self.translate(data[0], data[1], data[2], False) # Redo an undone operation def redo(self): op = self._undo.redo() # Voxel edit if op and op.operation == Undo.SET_VOXEL: data = op.newdata self.set(data[0], data[1], data[2], data[3], False) # Translation elif op and op.operation == Undo.TRANSLATE: data = op.newdata self.translate(data[0], data[1], data[2], False) # Enable/Disable undo buffer def disable_undo(self): self._undo.enabled = False def enable_undo(self): self._undo.enabled = True
class PlotsMath(QWidget): F_X = Qt.UserRole + 1 FITS_SPECTRUM = Qt.UserRole + 2 def __init__(self, settings, database, project=None): super(PlotsMath, self).__init__() self.ui = Ui_PlotsMath() self.ui.setupUi(self) self.settings = settings self.project=project self.plot = QtCommons.nestWidget(self.ui.plot, QMathPlotWidget()) self.reference_dialog = ReferenceSpectraDialog(database) self.reference_dialog.fits_picked.connect(self.open_fits) self.toolbar = QToolBar('Instrument Response Toolbar') open_btn = QtCommons.addToolbarPopup(self.toolbar, text="Open...", icon_file=':/new_open_20') open_file_action = open_btn.menu().addAction('FITS file') open_btn.menu().addAction('Reference library', self.reference_dialog.show) self.blackbody_menu = blackbody.BlackBodyAction(self.blackbody, open_btn.menu()) if project: save_result = QtCommons.addToolbarPopup(self.toolbar, text='Save', icon_file=':/save_20') save_result.menu().addAction('As File', lambda: QtCommons.save_file('Save Operation Result...', FITS_EXTS, lambda f: self.save(f[0]), project.path)) save_result.menu().addAction('As Instrument Response', self.save_project_instrument_response) open_file_action.triggered.connect(lambda: QtCommons.open_file('Open FITS Spectrum',FITS_EXTS, lambda f: self.open_fits(f[0]), project.path)) else: open_file_action.triggered.connect(lambda: open_file_sticky('Open FITS Spectrum',FITS_EXTS, lambda f: self.open_fits(f[0]), self.settings, CALIBRATED_PROFILE, [RAW_PROFILE])) self.toolbar.addAction(QIcon(':/save_20'), 'Save', lambda: save_file_sticky('Save Operation Result...', 'FITS file (.fit)', lambda f: self.save(f[0]), self.settings, MATH_OPERATION, [CALIBRATED_PROFILE])) self.toolbar.addAction('Set operand', self.set_operand) self.toolbar.addSeparator() self.toolbar.addAction(self.ui.actionZoom) self.ui.actionZoom.triggered.connect(self.start_zoom) self.toolbar.addAction(self.ui.actionReset_Zoom) self.ui.actionReset_Zoom.triggered.connect(self.reset_zoom) self.toolbar.addSeparator() self.operands_model = QStandardItemModel() self.ui.operands_listview.setModel(self.operands_model) remove_btn = QtCommons.addToolbarPopup(self.toolbar, text='Remove...') remove_btn.menu().addAction(self.ui.actionSelectPointsToRemove) remove_btn.menu().addAction("Before point", lambda: spectrum_trim_dialog(self.spectrum, 'before', self.plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) remove_btn.menu().addAction("After point", lambda: spectrum_trim_dialog(self.spectrum, 'after', self.plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) self.ui.clear_operands.clicked.connect(self.operands_model.clear) self.ui.remove_operand.clicked.connect(lambda: self.operands_model.removeRows(self.ui.operands_listview.selectionModel().selectedRows()[0].row(), 1)) self.operands_model.rowsInserted.connect(lambda: self.ui.clear_operands.setEnabled(self.operands_model.rowCount() > 0) ) self.operands_model.rowsRemoved.connect(lambda: self.ui.clear_operands.setEnabled(self.operands_model.rowCount() > 0) ) self.ui.operands_listview.selectionModel().selectionChanged.connect(lambda s, u: self.ui.remove_operand.setEnabled(len(s))) self.ui.actionSelectPointsToRemove.triggered.connect(self.pick_rm_points) self.undo = Undo(None, self.draw) self.undo.add_actions(self.toolbar) self.ui.spline_factor.valueChanged.connect(self.factor_valueChanged) self.ui.spline_degrees.valueChanged.connect(lambda v: self.draw()) self.ui.spline_factor_auto.toggled.connect(lambda v: self.draw()) self.ui.spline_factor_auto.toggled.connect(lambda v: self.ui.spline_factor.setEnabled(not v)) self.ui.execute.clicked.connect(self.execute_operation) self.plot.figure.tight_layout() def blackbody(self, blackbody): self.spectrum = blackbody.spectrum() self.spectrum_name = "Blackbody radiation for {0}".format(blackbody.kelvin) self.undo.set_spectrum(self.spectrum) self.spectrum.normalize_to_max() self.draw() def open_fits(self, filename): fits_file = fits.open(filename) fits_spectrum = FitsSpectrum(fits_file) self.spectrum_name = fits_spectrum.name() self.spectrum = fits_spectrum.spectrum self.undo.set_spectrum(self.spectrum) self.spectrum.normalize_to_max() if self.spectrum.dispersion() <0.4: print("dispersion too high ({}), reducing spectrum resolution".format(self.spectrum.dispersion())) self.spectrum.resample(self.spectrum.dispersion() / 0.4) self.draw() @pyqtSlot(float) def factor_valueChanged(self, f): self.draw() def pick_rm_points(self): self.plot.rm_element('zoom') self.plot.add_span_selector('pick_rm_points', lambda min,max: self.rm_points(min,max+1),direction='horizontal') def start_zoom(self): self.plot.rm_element('pick_rm_points') self.plot.select_zoom() def draw(self): self.ui.spline_degrees_value.setText("{}".format(self.ui.spline_degrees.value())) spline_factor = self.ui.spline_factor.value() if not self.ui.spline_factor_auto.isChecked() else None spline = UnivariateSpline(self.spectrum.wavelengths, self.spectrum.fluxes, k=self.ui.spline_degrees.value(), s=spline_factor) self.f_x = lambda x: spline(x) self.plot.plot(None, self.spectrum.wavelengths, self.spectrum.fluxes, '--', self.spectrum.wavelengths, spline(self.spectrum.wavelengths), '-') self.plot.figure.tight_layout() self.plot.figure.canvas.draw() def rm_points(self, wmin, wmax): self.undo.save_undo() x_min = self.spectrum.wavelength_index(max(self.spectrum.wavelengths[0], wmin)) x_max = self.spectrum.wavelength_index(min(self.spectrum.wavelengths[-1], wmax)) m=(self.spectrum.fluxes[x_max]-self.spectrum.fluxes[x_min])/(x_max-x_min) q = self.spectrum.fluxes[x_min] f = lambda x: x * m + q self.spectrum.fluxes[x_min:x_max] = np.fromfunction(f, self.spectrum.fluxes[x_min:x_max].shape) self.draw() def trim(self, direction): point = QInputDialog.getInt(None, 'Trim curve', 'Enter wavelength for trimming', self.spectrum.wavelengths[0] if direction == 'before' else self.spectrum.wavelengths[-1], self.spectrum.wavelengths[0], self.spectrum.wavelengths[-1]) if not point[1]: return self.undo.save_undo() if direction == 'before': self.spectrum.cut(start=self.spectrum.wavelength_index(point[0])) else: self.spectrum.cut(end=self.spectrum.wavelength_index(point[0])) self.reset_zoom() self.draw() def set_operand(self): item = QStandardItem(self.spectrum_name) item.setData(self.f_x, PlotsMath.F_X) item.setData(self.spectrum, PlotsMath.FITS_SPECTRUM) self.operands_model.appendRow(item) def execute_operation(self): max_wavelengths = lambda operands: np.arange(max([o[0].wavelengths[0] for o in operands]), min([o[0].wavelengths[-1] for o in operands])) datasets = lambda operands, wavelengths: [PlotsMath.__data(o[1], wavelengths) for o in operands] operands = [(self.operands_model.item(a).data(PlotsMath.FITS_SPECTRUM), self.operands_model.item(a).data(PlotsMath.F_X)) for a in np.arange(self.operands_model.rowCount())] def divide(operands): if len(operands) > 2: print("Division supports only 2 operands, truncating") wavelengths = max_wavelengths(operands[0:2]) datas = datasets(operands[0:2], wavelengths) return (wavelengths, datas[0]/datas[1]) def mean(operands): wavelengths = max_wavelengths(operands) mean_data = np.zeros(wavelengths.shape) for data in datasets(operands, wavelengths): mean_data += data return (wavelengths, mean_data/len(wavelengths)) operations = { 0: divide, 1: mean } try: wavelengths, data = operations[self.ui.operation_type.currentIndex()](operands) self.spectrum = Spectrum(data, wavelengths) self.spectrum.normalize_to_max() self.undo.set_spectrum(self.spectrum) self.ui.spline_degrees.setValue(5) self.ui.spline_factor.setValue(0) self.ui.spline_factor_auto.setChecked(False) self.draw() except IndexError: QMessageBox.warning(None, "Error", "Datasets are not compatible. Maybe you need to calibrate better, or use a different reference file") def save_project_instrument_response(self): name = QInputDialog.getText(self, 'Enter Name', 'Enter new instrument response name for saving') if name[1]: self.project.add_file(Project.INSTRUMENT_RESPONSES, lambda f: self.save(f), bare_name=name[0]) def save(self, filename): hdu = fits.PrimaryHDU( PlotsMath.__data(self.f_x, self.spectrum.wavelengths)) #hdu = fits.PrimaryHDU( self.spectrum.fluxes) fits_file = fits.HDUList([hdu]) hdu.header['CRPIX1'] = 1 hdu.header['CRVAL1'] = self.spectrum.wavelengths[0] hdu.header['CDELT1'] = self.spectrum.dispersion() hdu.writeto(filename, clobber=True) def reset_zoom(self): self.plot.reset_zoom(self.spectrum.wavelengths, self.spectrum.fluxes.min(), self.spectrum.fluxes.max()) def __data(f_x, xrange): return np.fromfunction(lambda x: f_x(x+xrange[0]), xrange.shape)
def __init__(self, fits_file, settings, database, project=None): super(FinishSpectrum, self).__init__() self.settings = settings self.ui = Ui_FinishSpectrum() self.ui.setupUi(self) self.profile_line = None self.project = project self.fits_spectrum = FitsSpectrum(fits_file) self.undo = Undo(self.fits_spectrum.spectrum, self.draw) try: fits_file.index_of('ORIGINAL_DATA') except KeyError: hdu = fits.ImageHDU(data=fits_file[0].data, header=fits_file[0].header, name='ORIGINAL_DATA') fits_file.append(hdu) self.fits_spectrum.spectrum.normalize_to_max() self.spectrum = self.fits_spectrum.spectrum self.spectrum_plot = QtCommons.nestWidget(self.ui.plot, QMathPlotWidget()) self.spectrum_plot.mouse_moved.connect( Instances.MainWindow.print_coordinates) self.split_view() self.toolbar = QToolBar('Finish Spectrum Toolbar') if project: instrument_response_action = QtCommons.addToolbarPopup( self.toolbar, "Instrument Response") instrument_response_action.menu().addAction( 'From FITS file...', lambda: open_file_sticky( 'Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[ 0]), settings, MATH_OPERATION, [RAW_PROFILE])) for instrument_response in project.get_instrument_responses(): print("Adding instrument response {}".format( instrument_response)) instrument_response_action.menu().addAction( os.path.basename(instrument_response[1]), lambda: self.instrument_response(instrument_response[1])) else: self.toolbar.addAction( 'Instrument Response', lambda: open_file_sticky( 'Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[ 0]), settings, MATH_OPERATION, [RAW_PROFILE])) self.toolbar.addAction( "Zoom", lambda: self.spectrum_plot.select_zoom(self.profile_plot.axes)) self.toolbar.addAction( "Reset Zoom", lambda: self.spectrum_plot.reset_zoom( self.spectrum.wavelengths, self.spectrum.fluxes.min(), self.spectrum.fluxes.max(), self.profile_plot.axes)) remove_action = QtCommons.addToolbarPopup(self.toolbar, "Remove") remove_action.menu().addAction( "Before point", lambda: spectrum_trim_dialog(self.spectrum, 'before', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) remove_action.menu().addAction( "After point", lambda: spectrum_trim_dialog(self.spectrum, 'after', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) self.undo.add_actions(self.toolbar) self.toolbar.addSeparator() self.reference_spectra_dialog = ReferenceSpectraDialog( database, self.fits_spectrum.spectrum) self.reference_spectra_dialog.setup_menu(self.toolbar, self.profile_plot.axes, settings) lines_menu = QtCommons.addToolbarPopup(self.toolbar, "Spectral Lines..") lines_menu.menu().addAction('Lines Database', lambda: self.lines_dialog.show()) lines_menu.menu().addAction('Custom line', self.add_custom_line) labels_action = QtCommons.addToolbarPopup(self.toolbar, "Labels..") self.object_properties = ObjectProperties(fits_file, project=project) labels_action.menu().addAction('Title', self.add_title) if self.object_properties: labels_action.menu().addAction('Information from FITS file', self.add_fits_information_label) labels_action.menu().addAction('Custom', self.add_label) self.object_properties_dialog = ObjectPropertiesDialog( settings, self.object_properties) self.toolbar.addAction("Object properties", self.object_properties_dialog.show) self.labels, self.lines = [], [] for label in self.fits_spectrum.labels(): self.add_label(text=label['text'], coords=label['coords'], type=label['type'], fontsize=label['fontsize']) self.toolbar.addSeparator() if project: self.toolbar.addAction( QIcon(':/image_20'), "Export Image...", lambda: QtCommons.save_file( 'Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), project.directory_path(Project.EXPORTED_IMAGES))) self.toolbar.addAction(QIcon(':/save_20'), 'Save', self.save_finished_in_project) else: self.toolbar.addAction( QIcon(':/image_20'), "Export Image...", lambda: save_file_sticky( 'Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), self.settings, EXPORT_IMAGES, [CALIBRATED_PROFILE])) self.toolbar.addAction( QIcon(':/save_20'), 'Save', lambda: save_file_sticky( 'Save plot...', 'FITS file (.fit)', lambda f: self.__save( f[0]), self.settings, CALIBRATED_PROFILE)) self.lines_dialog = LinesDialog(database, settings, self.spectrum_plot, self.profile_plot.axes) self.lines_dialog.lines.connect(self.add_lines) for line in self.fits_spectrum.lines_labels(): self.lines.append( ReferenceLine(line['text'], line['wavelength'], self.profile_plot.axes, lambda line: self.lines.remove(line), show_wavelength=line['display_wavelength'], fontsize=line['fontsize'], position=line['position']))
class FinishSpectrum(QWidget): def __init__(self, fits_file, settings, database, project=None): super(FinishSpectrum, self).__init__() self.settings = settings self.ui = Ui_FinishSpectrum() self.ui.setupUi(self) self.profile_line = None self.project = project self.fits_spectrum = FitsSpectrum(fits_file) self.undo = Undo(self.fits_spectrum.spectrum, self.draw) try: fits_file.index_of('ORIGINAL_DATA') except KeyError: hdu = fits.ImageHDU(data=fits_file[0].data, header=fits_file[0].header, name='ORIGINAL_DATA') fits_file.append(hdu) self.fits_spectrum.spectrum.normalize_to_max() self.spectrum = self.fits_spectrum.spectrum self.spectrum_plot = QtCommons.nestWidget(self.ui.plot, QMathPlotWidget()) self.spectrum_plot.mouse_moved.connect( Instances.MainWindow.print_coordinates) self.split_view() self.toolbar = QToolBar('Finish Spectrum Toolbar') if project: instrument_response_action = QtCommons.addToolbarPopup( self.toolbar, "Instrument Response") instrument_response_action.menu().addAction( 'From FITS file...', lambda: open_file_sticky( 'Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[ 0]), settings, MATH_OPERATION, [RAW_PROFILE])) for instrument_response in project.get_instrument_responses(): print("Adding instrument response {}".format( instrument_response)) instrument_response_action.menu().addAction( os.path.basename(instrument_response[1]), lambda: self.instrument_response(instrument_response[1])) else: self.toolbar.addAction( 'Instrument Response', lambda: open_file_sticky( 'Open Instrument Response Profile', FITS_EXTS, lambda f: self.instrument_response(f[ 0]), settings, MATH_OPERATION, [RAW_PROFILE])) self.toolbar.addAction( "Zoom", lambda: self.spectrum_plot.select_zoom(self.profile_plot.axes)) self.toolbar.addAction( "Reset Zoom", lambda: self.spectrum_plot.reset_zoom( self.spectrum.wavelengths, self.spectrum.fluxes.min(), self.spectrum.fluxes.max(), self.profile_plot.axes)) remove_action = QtCommons.addToolbarPopup(self.toolbar, "Remove") remove_action.menu().addAction( "Before point", lambda: spectrum_trim_dialog(self.spectrum, 'before', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) remove_action.menu().addAction( "After point", lambda: spectrum_trim_dialog(self.spectrum, 'after', self.profile_plot.axes, lambda: self.draw(), self, before_removal=self.undo.save_undo)) self.undo.add_actions(self.toolbar) self.toolbar.addSeparator() self.reference_spectra_dialog = ReferenceSpectraDialog( database, self.fits_spectrum.spectrum) self.reference_spectra_dialog.setup_menu(self.toolbar, self.profile_plot.axes, settings) lines_menu = QtCommons.addToolbarPopup(self.toolbar, "Spectral Lines..") lines_menu.menu().addAction('Lines Database', lambda: self.lines_dialog.show()) lines_menu.menu().addAction('Custom line', self.add_custom_line) labels_action = QtCommons.addToolbarPopup(self.toolbar, "Labels..") self.object_properties = ObjectProperties(fits_file, project=project) labels_action.menu().addAction('Title', self.add_title) if self.object_properties: labels_action.menu().addAction('Information from FITS file', self.add_fits_information_label) labels_action.menu().addAction('Custom', self.add_label) self.object_properties_dialog = ObjectPropertiesDialog( settings, self.object_properties) self.toolbar.addAction("Object properties", self.object_properties_dialog.show) self.labels, self.lines = [], [] for label in self.fits_spectrum.labels(): self.add_label(text=label['text'], coords=label['coords'], type=label['type'], fontsize=label['fontsize']) self.toolbar.addSeparator() if project: self.toolbar.addAction( QIcon(':/image_20'), "Export Image...", lambda: QtCommons.save_file( 'Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), project.directory_path(Project.EXPORTED_IMAGES))) self.toolbar.addAction(QIcon(':/save_20'), 'Save', self.save_finished_in_project) else: self.toolbar.addAction( QIcon(':/image_20'), "Export Image...", lambda: save_file_sticky( 'Export plot to image', 'PNG (*.png);;PDF (*.pdf);;PostScript (*.ps);;SVG (*.svg)', lambda f: self.save_image(f[0]), self.settings, EXPORT_IMAGES, [CALIBRATED_PROFILE])) self.toolbar.addAction( QIcon(':/save_20'), 'Save', lambda: save_file_sticky( 'Save plot...', 'FITS file (.fit)', lambda f: self.__save( f[0]), self.settings, CALIBRATED_PROFILE)) self.lines_dialog = LinesDialog(database, settings, self.spectrum_plot, self.profile_plot.axes) self.lines_dialog.lines.connect(self.add_lines) for line in self.fits_spectrum.lines_labels(): self.lines.append( ReferenceLine(line['text'], line['wavelength'], self.profile_plot.axes, lambda line: self.lines.remove(line), show_wavelength=line['display_wavelength'], fontsize=line['fontsize'], position=line['position'])) def add_custom_line(self): wl = QInputDialog.getDouble( self, "Custom Line", "Enter line wavelength in Å", self.fits_spectrum.spectrum.wavelengths[0], self.fits_spectrum.spectrum.wavelengths[0], self.fits_spectrum.spectrum.wavelengths[-1], 3) if not wl[1]: return self.add_lines([{'name': 'Custom Line', 'lambda': wl[0]}]) def add_lines(self, lines): for line in lines: self.lines.append( ReferenceLine(line['name'], line['lambda'], self.profile_plot.axes, lambda line: self.lines.remove(line))) def synthetize_img(wavelengths, fluxes): f_fluxes = lambda f: math.pow(f, 3 / 5) colors = [ wavelength_to_rgb(w / 10., f_fluxes(fluxes[i])) for i, w in enumerate(wavelengths) ] im_height = 150 colors = np.array(colors * im_height).reshape(im_height, len(colors), 4) return colors, im_height def split_view(self): figure = self.spectrum_plot.figure figure.clear() self.gs = gridspec.GridSpec(40, 1) self.profile_plot = figure.add_subplot(self.gs[0:-6]) self.synthetize = figure.add_subplot(self.gs[-3:-1], sharex=self.profile_plot) self.synthetize.yaxis.set_visible(False) self.synthetize.xaxis.set_visible(False) self.draw() def draw(self): # self.profile_plot.clear() if self.profile_line: self.profile_line.remove() self.profile_line = self.profile_plot.plot(self.spectrum.wavelengths, self.spectrum.fluxes, color='blue')[0] self.synthetize.axes.set_facecolor('black') with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(FinishSpectrum.synthetize_img, self.spectrum.wavelengths, self.spectrum.fluxes) future.add_done_callback(lambda f: self.synthetize.imshow( f.result()[0], extent=[ self.spectrum.wavelengths[0], self.spectrum.wavelengths[ -1], 0, f.result()[1] ])) self.profile_plot.axes.set_xlabel('wavelength (Å)') self.profile_plot.axes.set_ylabel('relative flux') self.profile_plot.axes.xaxis.set_major_locator( MaxNLocator(16)) # TODO: settings for customization? self.profile_plot.axes.xaxis.set_minor_locator(MaxNLocator(200)) self.spectrum_plot.figure.canvas.draw() self.gs.tight_layout(self.spectrum_plot.figure) def instrument_response(self, filename): print("Applying instrument response {}".format(filename)) instrument_response_file = fits.open(filename) instrument_response = FitsSpectrum(instrument_response_file) response = instrument_response.spectrum response.normalize_to_max() range = (max(response.wavelengths[0], self.spectrum.wavelengths[0]), min(response.wavelengths[-1], self.spectrum.wavelengths[-1])) self.spectrum.cut(self.spectrum.wavelength_index(range[0]), self.spectrum.wavelength_index(range[1])) spline = InterpolatedUnivariateSpline(response.wavelengths, response.fluxes) response_data = [spline(x) for x in self.spectrum.wavelengths] self.spectrum.fluxes /= response_data self.spectrum.normalize_to_max() self.draw() def save_image(self, filename): Notification('Image {} saved in {}'.format(os.path.basename(filename), os.path.dirname(filename)), title='File Saved', type='success', timeout=5) self.spectrum_plot.figure.savefig(filename, bbox_inches='tight', dpi=300) def save_finished_in_project(self): self.project.add_file(Project.FINISHED_PROFILES, self.__save, self.object_properties) def __save(self, filename): self.fits_spectrum.save(filename, spectral_lines=self.lines, labels=self.labels) def add_title(self): title = self.object_properties.name if self.object_properties else 'Title - double click to edit' self.add_label( text=title, coords=( self.spectrum.wavelengths[len(self.spectrum.wavelengths) / 2 - 100], 0.95), fontsize=25, type='lineedit') def add_fits_information_label(self): info_text = "Object Name: {}, type: {}, spectral class: {}\nCoordinates: {}\nDate: {}\nObserver: {}\nEquipment: {}\nPosition: {}".format( self.object_properties.name, self.object_properties.type, self.object_properties.sptype, self.object_properties.printable_coordinates(), self.object_properties.date.toString(), self.object_properties.observer, self.object_properties.equipment, self.object_properties.position) self.add_label( info_text, type='textbox', coords=(self.spectrum.wavelengths[len(self.spectrum.wavelengths) / 4 * 3], 0.80), fontsize=14) self.profile_plot.figure.canvas.draw() def add_label(self, text=None, type='textbox', coords=None, fontsize=12, color='black'): if not coords: coords = ( self.spectrum.wavelengths[len(self.spectrum.wavelengths) / 2], 0.5) self.labels.append( (type, MoveableLabel( text=text if text else 'Label - double click to edit', on_dblclick=lambda l: self.edit_label(l, type=type), x=coords[0], y=coords[1], fontsize=fontsize, color=color, axes=self.profile_plot.axes))) self.profile_plot.figure.canvas.draw() def edit_label(self, label, type='lineedit'): def remove_label(self, label, dialog): label.remove() self.labels.remove([l for l in self.labels if l[1] == label][0]) self.profile_plot.figure.canvas.draw() dialog.reject() dialog = QDialog() dialog.setWindowTitle("Edit Label") dialog.setLayout(QVBoxLayout()) font_size = QSpinBox() font_size.setValue(label.get_fontsize()) dialog.layout().addWidget(QLabel("Font Size")) dialog.layout().addWidget(font_size) text_edit = None if type == 'lineedit': text_edit = QLineEdit(label.get_text()) else: text_edit = QTextEdit() text_edit.setPlainText(label.get_text()) dialog.layout().addWidget(QLabel("Text")) dialog.layout().addWidget(text_edit) button_box = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel) button_box.accepted.connect(dialog.accept) button_box.rejected.connect(dialog.reject) remove_button = QPushButton('Remove') remove_button.clicked.connect( lambda: remove_label(self, label, dialog)) dialog.layout().addWidget(remove_button) dialog.layout().addWidget(button_box) if QDialog.Accepted != dialog.exec(): return label.set_text(text_edit.text() if type == 'lineedit' else text_edit.toPlainText()) label.set_fontsize(font_size.value()) label.axes.figure.canvas.draw()