text:
"""
Run QM calculations at multiple levels consecutively, using queuing system

Automate Multi-Level calculations

This should help with running a set of QM calculations at several levels (e.g.,
increasing basis set size), while intelligently using the queuing system such
as Sun Grid Engine.
It can receive the signal from the queuing system that the job will be killed
soon and consequently submit a continuation of the job, using the
intermediate files to speed up subsequent calculations.
"""

########################################################################
#                                                                      #
# This script/module was written by Thomas Heavey in 2019.             #
#        theavey@bu.edu     thomasjheavey@gmail.com                    #
#                                                                      #
# Copyright 2018-2019 Thomas J. Heavey IV                              #
#                                                                      #
# Licensed under the Apache License, Version 2.0 (the "License");      #
# you may not use this file except in compliance with the License.     #
# You may obtain a copy of the License at                              #
#                                                                      #
#    http://www.apache.org/licenses/LICENSE-2.0                        #
#                                                                      #
# Unless required by applicable law or agreed to in writing, software  #
# distributed under the License is distributed on an "AS IS" BASIS,    #
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or      #
# implied.                                                             #
# See the License for the specific language governing permissions and  #
# limitations under the License.                                       #
#                                                                      #
########################################################################

import json
import logging
import MDAnalysis as mda
import numpy as np
import os
import pandas as pd
import paratemp
from paratemp.geometries import XYZ
import pathlib
import random
import re
import shutil
import signal
import subprocess
import sys
import threading
import thtools
import time
from typing import List
from gautools import tools
import functools
import filecmp

if not sys.version_info >= (3, 6):
    raise ValueError('Python >= 3.6 is required')

log = logging.getLogger(__name__)
log.setLevel(logging.DEBUG)

def _setup_log(level=logging.WARNING):
    global handler
    handler = logging.StreamHandler()
    handler.setLevel(level=level)
    formatter = logging.Formatter('%(asctime)s - %(name)s - '
                                  '%(levelname)s - %(message)s')
    handler.setFormatter(formatter)
    log.addHandler(handler)

obabel_module_lines = ('\n'
                       'module load wxwidgets/3.0.2\n'
                       'module load openbabel/2.4.1\n')

signal_catch_lines = ('PY_PID=$!\n'
                      'trap "kill -n 12 $PY_PID" usr2\n'
                      'wait\n\n')

def log_exception(f):
    @functools.wraps(f)
    def log_exc(*args, **kwargs):
        try:
            return f(*args, **kwargs)
        except Exception:
            log.exception(f'An exception was raised in {f.__name__}!')
            raise

    return log_exc

class Calc(object):
    """
    An object for consecutive Gaussian calculation using a queuing system

    Environment variables that need to be defined:
        * HOSTNAME
        * JOB_ID
        * NSLOTS
        * SGE_STDOUT_PATH
    An exception will be raised if any of these are not defined when doing
    :func:`Calc.run_calc()`.
    """

    def __init__(self, status=None, base_name=None, ind=None,
                 geometry=None, top=None, traj=None,
                 criteria=None, react_dist=None, mgi_dicts=None):
        """

        :param str status: The path to the status file to be read for a
            calculation restart. If this is not a restarted job, this should
            be None (the default).
        :param str base_name:
        :param int ind:
        :type geometry: pathlib.Path or str
        :param geometry: File from which to get the starting coordinates. This
            argument will take priority if top and traj are also given.
            Currently, this must be an XYZ file, but it shouldn't be too hard
            to implement using openbabel to convert to an XYZ.
        :type top: pathlib.Path or str
        :param top:
        :type traj: pathlib.Path or str
        :param traj:
        :param dict criteria: The criteria for selecting frames from the
            trajectory.
            This is a dict with distance names (or other columns that will
            be in `Universe.data`) as the keys and the values being a
            List-like of min and max values.
            For example, `{'c1_c2': (1.5, 4.0), 'c1_c3': (2.2, 5.1)}` will
            select frames where 'c1_c2' is between 1.5 and 4.0 and 'c1_c3'
            is between 2.2 and 5.1.
        :type react_dist: str or float
        :param react_dist: Distance to set between the two reacting atoms (
            with indices 20 and 39).
            If this argument as given evaluates to False, no movement/changes
            to the geometry will be made.
        :param List[dict] mgi_dicts:
        :return:
        """
        if status is not None:
            # TODO this could be done (better?) with a classmethod
            self.rerun = True
            self._status = StatusDict(status)
            self._json_name = status
            try:
                self.args = self.status['args']
            except KeyError:
                self.log.error('Attempting to do a calculation restart, '
                               f'but it seems that the status file {status} '
                               'was empty or at least did not have "args" in '
                               'it.')
                raise
            a = self.args
            base_name = a['base_name']
            ind = a['ind']
            try:
                geometry = a['geometry']
            except KeyError:
                geometry = None
            top = a['top']
            traj = a['traj']
            criteria = a['criteria']
            react_dist = a['react_dist']
            try:
                mgi_dicts = a['mgi_dicts']
            except KeyError:
                mgi_dicts = a['ugt_dicts']
            self._base_name = self.status['base_name']
        else:
            self.rerun = False
            self.args = {
                'base_name': base_name,
                'ind': ind, 'geometry': geometry,
                'top': top, 'traj': traj,
                'criteria': criteria,
                'react_dist': react_dist,
                'mgi_dicts': mgi_dicts}
            self.oniom = True if 'oniom' in mgi_dicts[0].keys() else False
            self.check_args()
            self._base_name = '{}-ind{}'.format(base_name, ind)
            self._json_name = '{}.json'.format(self._base_name)
            self._status = StatusDict(self._json_name)
        self.geometry = geometry
        self.top = top
        self.traj = traj
        self.criteria = criteria
        self.react_dist = react_dist
        self.mgi_dicts = mgi_dicts
        self.log = logging.getLogger(self.__class__.__name__)
        self.log.setLevel(logging.DEBUG)
        f_handler = logging.FileHandler('{}.log'.format(self._base_name))
        f_handler.setLevel(logging.DEBUG)
        f_formatter = logging.Formatter('%(asctime)s - %(name)s - '
                                        '%(levelname)s - %(message)s')
        f_handler.setFormatter(f_formatter)
        self.log.addHandler(f_handler)
        self.log.addHandler(handler)
        self.log.debug('Initializing the log')
        self.mem, self.node = None, None
        self.scratch_path: pathlib.Path = None
        self.last_scratch_path: pathlib.Path = None
        self.n_slots, self.last_node = None, None
        self.cwd_path: pathlib.Path = None
        self.output_scratch_path: pathlib.Path = None
        self.chk_ln_path: pathlib.Path = None
        self.h_rt: str = None
        self.stdout_file: str = None
        self.resub_cl: List[str] = None
        self.job_id: str = None
        self.next_job_id: str = None
        self.resubmitted: bool = False

    def check_args(self):
        args = self.args.copy()
        geom = args.pop('geometry')
        top, traj = args.pop('top'), args.pop('traj')
        crit = args.pop('criteria')
        if (geom is None and
                (top is None or traj is None or crit is None) and
                (not self.oniom)):
            raise ValueError('either geometry or top, traj, and criteria must '
                             'be given')
        for key in args:
            if args[key] is None:
                raise ValueError(f'Argument "{key}" cannot be None')

    @property
    def status(self):
        return self._status

    @property
    def current_lvl(self):
        try:
            return self.status['current_lvl']
        except KeyError:
            raise AttributeError('Could not find current level')

    @current_lvl.setter
    def current_lvl(self, value):
        if not (isinstance(value, int) or value is None):
            raise TypeError('current_level must be an int or None.'
                            f'Given type {type(value)}')
        self.status['current_lvl'] = value

    @property
    def between_levels(self):
        # Might need to look for rwf file otherwise should start the
        # calculation again
        self.log.debug('Checking to see if calculation left off between '
                       'calculation levels')
        between = self.status['between_levels']
        if between is None:
            self.log.warning('No key in status for determining if between '
                             'levels currently')
            lvl = self.current_lvl
            if self.status[f'g_in_{lvl}'] == self.status['g_in_curr']:
                out_path = pathlib.Path(
                    self.status['g_in_curr']).with_suffix('.out')
                if out_path.exists():
                    self._advance_level()
                    between = True
                else:
                    between = False
            else:
                between = True
            self.status['between_levels'] = between
        return between

    @between_levels.setter
    def between_levels(self, value):
        if not isinstance(value, bool):
            raise TypeError(f'between_levels must be a bool, given type '
                            f'{type(value)}')
        self.status['between_levels'] = value

    @property
    def cleaned_up(self):
        cleaned = self.status['cleaned_up']
        if cleaned is None:
            self.log.warning('Could not find "cleaned_up" in status. Assuming '
                             'dirty')
            cleaned = False
            self.status['cleaned_up'] = cleaned
        return cleaned

    @cleaned_up.setter
    def cleaned_up(self, value):
        if not isinstance(value, bool):
            raise TypeError(f'cleaned_up must be a bool, given type '
                            f'{type(value)}')
        self.status['cleaned_up'] = value

    def _startup_tasks(self):
        """
        Some startup tasks to set variables for later use

        This requires the environment variables HOSTNAME and NSLOTS be set.
        :return: None
        """
        self.log.debug('Running some introductory tasks and setting variables')
        try:
            node = os.environ['HOSTNAME'].split('.')[0]
        except KeyError:
            self.log.exception('Could not find HOSTNAME!')
            raise
        self.node = node
        scratch_path = pathlib.Path('/net/{}/scratch/theavey'.format(node))
        scratch_path.mkdir(exist_ok=True)
        self.scratch_path = scratch_path
        try:
            self.job_id = os.environ['JOB_ID']
        except KeyError:
            self.log.exception('Could not find JOB_ID!')
            raise
        self.mem = thtools.job_tools.get_node_mem()
        try:
            n_slots = int(os.environ['NSLOTS'])
        except KeyError:
            self.log.exception('Could not find NSLOTS!')
            raise
        self.n_slots = n_slots
        self.h_rt = self._get_h_rt()
        self.log.info(f'Running on {node} using {n_slots} cores and up to '
                      f'{self.mem} GB mem for {self.h_rt} seconds')
        try:
            self.stdout_file = os.environ['SGE_STDOUT_PATH']
            self.log.debug(f'Using stdout path: {self.stdout_file}')
        except KeyError:
            self.log.exception('Could not find SGE_STDOUT_PATH!')
            raise
        if self.rerun:
            self.last_node = self.status['current_node']
            self.status['last_node'] = self.last_node
            node_list = self.status['node_list']
            self.last_scratch_path = pathlib.Path(self.status[
                                                      'current_scratch_dir'])
            self.status['last_scratch_dir'] = str(self.last_scratch_path)
            self._get_chk_ln_path()
            self._get_output_scratch_path()
        else:
            self.status['args'] = self.args
            self.status['base_name'] = self._base_name
            node_list = []
        self.status['node_list'] = node_list + [node]
        self.status['current_node'] = node
        self.status['current_scratch_dir'] = str(scratch_path)
        self.status['job_id'] = self.job_id
        self.cwd_path = pathlib.Path('.').resolve()
        self.status['cwd'] = str(self.cwd_path)
        self.log.info('Submitted from {} and will be running in {}'.format(
            self.cwd_path, self.scratch_path))
        self._make_resub_sh_and_cl()

    def _get_output_scratch_path(self):
        self.log.debug('Getting path to scratch output')
        output_scratch_path_ = self.status['output_scratch_path']
        if output_scratch_path_ is not None:
            self.output_scratch_path = pathlib.Path(
                output_scratch_path_)
        else:
            self.output_scratch_path = self.last_scratch_path.joinpath(
                self.status['g_in_curr']).with_suffix('.out')

    def _get_chk_ln_path(self):
        self.log.debug('Getting path to linked chk file')
        chk_ln_path_ = self.status['chk_ln_path']
        if chk_ln_path_ is not None:
            self.chk_ln_path = pathlib.Path(chk_ln_path_)
        else:
            self.chk_ln_path = pathlib.Path(
                f'{self._base_name}-running.chk').resolve()

    @log_exception
    def run_calc(self):
        """
        The primary function to start (or restart) running a calculation

        :return: None
        """
        self.log.debug('Welcome. Just starting to run this calculation')
        self._startup_tasks()
        if self.rerun:
            self.log.info('loaded previous status file: {}'.format(
                self._json_name))
            self.resume_calc()
        else:
            self.log.warning('No previous status file found. '
                             'Starting new calculation?')
            self.new_calc()

    def _make_rand_xyz(self):
        self.log.debug('Making XYZ file to start calculation')
        import tables
        u = paratemp.Universe(self.top, self.traj, verbosity=0)
        while True:
            try:
                u.read_data()
                break
            except tables.HDF5ExtError:
                self.log.warning(
                    'HDF5ExtError raised. Likely because trying to read the '
                    'store at the same time as another process. Waiting 5 '
                    'seconds and trying again.')
                time.sleep(5)
                continue
        frames = u.select_frames(self.criteria, 'QM_frames')
        select = random.choice(frames)
        self.status['source_frame_num'] = int(select)
        system: mda.AtomGroup = u.select_atoms('all')
        xyz_name = self._base_name + '.xyz'
        with mda.Writer(xyz_name, system.n_atoms) as w:
            u.trajectory[select]
            for frag in u.atoms.fragments:
                mda.lib.mdamath.make_whole(frag)
                # This should at least make the molecules whole if not
                # necessarily in the correct unit cell together.
            w.write(system)
        self.log.info(f'Wrote xyz file from frame {select} to {xyz_name}')
        return pathlib.Path(xyz_name).resolve()

    def _move_reactant_atoms(self, xyz_path):
        self.log.debug('Moving reactant atoms (20 and 39) to '
                       f'{self.react_dist}')
        xyz_name = str(xyz_path)
        bak_name = xyz_name + '.bak'
        paratemp.copy_no_overwrite(xyz_name, bak_name)
        self.status['original_xyz'] = bak_name
        self.log.info(f'Copied original geometry to {bak_name}')
        xyz = XYZ(xyz_name)
        diff = xyz.coords[19] - xyz.coords[38]
        direction = diff / np.linalg.norm(diff)
        xyz.coords[19] = xyz.coords[38] + self.react_dist * direction
        xyz.write(xyz_name)
        self.log.info(f'Wrote updated xyz file to {xyz_name}')

    def new_calc(self):
        self.log.debug('Setting up a new calculation')
        self.current_lvl = 0
        if self.geometry is not None:
            self.log.debug(f'Using provided geometry from {self.geometry}')
            xyz_path = pathlib.Path(self.geometry).resolve()
        elif self.oniom:
            self.log.debug('Using geometry from ONIOM inputs')
            xyz_path = 'oniom_input'
        else:
            xyz_path = self._make_rand_xyz()
        if self.react_dist:
            if self.oniom:
                self.log.warning('ONIOM calculation with react_dist not '
                                 'implemented. Ignoring react_dist.')
            else:
                self._move_reactant_atoms(xyz_path)
        self.status['starting_xyz'] = str(xyz_path)
        if not self.oniom and not xyz_path.exists():
            raise FileNotFoundError('Could not find start geometry that was '
                                    f'supposed to be at {xyz_path}')
        com_name = self._make_g_in(xyz_path)
        self._setup_and_run(com_name)

    def _setup_and_run(self, com_name):
        self.log.debug('Starting setup to run Gaussian')
        bn = self._base_name
        chk_ln_path = pathlib.Path(f'{bn}-running.chk').resolve()
        self.chk_ln_path = chk_ln_path
        self.status['chk_ln_path'] = str(chk_ln_path)
        chk_ln_path.symlink_to(self.scratch_path.joinpath(f'{bn}.chk'))
        self.log.info(f'Linked checkpoint file as {chk_ln_path}')
        if not self.resubmitted:
            self.resub_calc()
        self.status['manual_input'] = None
        self.status['manual_restart'] = None
        self.status['g_in_curr'] = com_name
        self.cleaned_up = False
        self.between_levels = False
        self.status['calc_cutoff'] = None
        self.status['gaussian_failed'] = None
        killed = self._run_gaussian(com_name)
        self.status['calc_cutoff'] = killed
        if killed:
            self.log.info('Exited from function running Gaussian because '
                          'SIGUSR2')
        else:
            try:
                self._check_normal_completion(self.output_scratch_path)
                self.log.info(f'Seemed to correctly finish level '
                              f'{self.current_lvl} calculation. Moving on to '
                              f'next level')
                self._advance_level()
            finally:
                self._copy_and_cleanup()
            self._next_calc()

    def _advance_level(self):
        self.log.debug(f'Advancing from {self.current_lvl}')
        self.between_levels = True
        self.current_lvl += 1

    def _make_g_in(self, xyz_path):
        self.log.debug(f'Making new Gaussian input from {xyz_path}')
        bn = self._base_name
        lvl = self.current_lvl
        com_name = f'{bn}-lvl{lvl}.com'
        try:
            mgi_dict = self.mgi_dicts[lvl]
        except IndexError:
            self.log.warning('Seems that there are no more calculation '
                             'levels to complete')
            raise self.NoMoreLevels
        tools.make_gaussian_input(
            out_file=com_name,
            xyz=str(xyz_path),
            job_name=bn,
            checkpoint=f'{bn}.chk',
            rwf=f'{bn}.rwf',
            nproc=self.n_slots, mem=self.mem,
            **mgi_dict
        )
        self.log.info('Wrote Gaussian input for '
                      f'level {lvl} job to {com_name}')
        self.status[f'g_in_{lvl}'] = com_name
        return com_name

    def _run_gaussian(self, com_name):
        self.log.debug('Doing final setup to run Gaussian')
        out_name = com_name.replace('com', 'out')
        com_path: pathlib.Path = self.cwd_path.joinpath(com_name)
        if not com_path.exists():
            raise FileNotFoundError('Gaussian input {} not found in '
                                    '{}'.format(com_name, self.cwd_path))
        out_path: pathlib.Path = self.scratch_path.joinpath(out_name)
        self.output_scratch_path = out_path
        self.status['output_scratch_path'] = str(out_path)
        old_sigusr1 = signal.signal(signal.SIGUSR1, self._signal_catch_done)
        old_sigusr2 = signal.signal(signal.SIGUSR2, self._signal_catch_time)
        cl = ['g16', ]
        killed = False
        with com_path.open('r') as f_in, out_path.open('w') as f_out:
            self.log.info('Starting Gaussian with input {} and writing '
                          'output to {}'.format(com_path, out_path))
            self.status['running'] = True
            proc = subprocess.Popen(cl, stdin=f_in, stdout=f_out,
                                    cwd=str(self.scratch_path))
            self.log.info('Started Gaussian; waiting for it to finish or '
                          'timeout')
            try:
                thread = threading.Thread(target=self._check_proc, args=(proc,))
                thread.start()
                signal.pause()
            except self.TimesUp:
                killed = True
                proc.terminate()  # Should be within `with` clause?
                self.log.info('Gaussian process terminated because of SIGUSR2')
            except self.GaussianDone:
                self.log.info('Gaussian process completed')
            finally:
                signal.signal(signal.SIGUSR1, old_sigusr1)
                signal.signal(signal.SIGUSR2, old_sigusr2)
        self.status['running'] = False
        return killed

    def _signal_catch_time(self, signum, frame):
        self.log.warning(f'Caught {signal.Signals(signum).name} signal! '
                         'Trying to quit Gaussian')
        raise self.TimesUp

    def _signal_catch_done(self, signum, frame):
        self.log.warning(f'Caught {signal.Signals(signum).name} signal! '
                         f'Likely, this was because Gaussian process exited')
        raise self.GaussianDone

    def _check_proc(self, proc):
        self.log.debug('Started process to check on Gaussian completion')
        while proc.poll() is None:
            time.sleep(15)
        self.log.warning('Gaussian process no longer running. Sending SIGUSR1')
        os.kill(os.getpid(), signal.SIGUSR1)

    def _copy_and_cleanup(self):
        self.log.debug('Attempting to copy back files and unlink chk file')
        com_name: str = self.status['g_in_curr']
        cc = self.status['calc_cutoff']
        killed: bool = True if cc is None else cc
        if killed:
            scratch_path = pathlib.Path(self.status['last_scratch_dir'])
        else:
            scratch_path = self.scratch_path
        if not killed:
            out_path = pathlib.Path(com_name.replace('com', 'out'))
            if self.status['gaussian_failed'] is True:
                out_path = self._make_unique_output_path(
                    f'{out_path.stem}-failed')
        else:
            out_path = self._make_unique_output_path(com_name[:-4])
        try:
            paratemp.copy_no_overwrite(str(self.output_scratch_path),
                                       str(out_path))
            self.log.debug(f'Copied back output file to {out_path}')
        except FileExistsError:
            if filecmp.cmp(str(self.output_scratch_path), str(out_path),
                           shallow=False):
                self.log.debug("Don't need to copy back output as it's already "
                               f"at {out_path}")
            else:
                self.log.error('Output files differ; unable to copy back.\n'
                               f'New: {self.output_scratch_path}\n'
                               f'Existing: {out_path}')
                raise
        if self.chk_ln_path.exists():
            self.chk_ln_path.unlink()
            self.log.debug(f'Unlinked checkpoint run file: {self.chk_ln_path}')
        chk_name = f'{self._base_name}.chk'
        scr_chk_path = scratch_path.joinpath(chk_name)
        if scr_chk_path.exists():
            shutil.copy(str(scr_chk_path), chk_name)
            self.log.debug(f'Copied back checkpoint file to {chk_name}')
        else:
            self.log.debug(f'chk file not found at {scr_chk_path} so not '
                           f'copied back')
        self.cleaned_up = True

    def _make_unique_output_path(self, com_base_name):
        outs = [str(p) for p in self.cwd_path.glob(com_base_name + '-*.out')]
        if not outs:
            new_out = f'{com_base_name}-1.out'
        else:
            def get_ind(s):
                match = re.search(r'(\d+)\.out', s)
                return int(match.group(1))
            outs.sort(key=get_ind)
            ind = get_ind(outs[-1])
            new_out = f'{com_base_name}-{ind+1}.out'
        return pathlib.Path(new_out).resolve()

    def _check_normal_completion(self, filepath):
        self.log.debug('Attempting to check for completion status of Gaussian')
        output = subprocess.check_output(['tail', '-n', '1', str(filepath)],
                                         universal_newlines=True)
        if 'normal termination' not in output.lower():
            self.log.error(f'Abnormal termination of Gaussian job in output: '
                           f'{filepath}')
            self._qdel_next_job()
            self.status['gaussian_failed'] = True
            raise self.GaussianError('Gaussian did not finish normally. '
                                     f'See output: {filepath}')
        self.status['gaussian_failed'] = False
        self.log.info(f'Normal termination of Gaussian job! Output at '
                      f'{filepath}')

    def resub_calc(self):
        self.log.info(f'resubmitting job with the following commandline:\n'
                      f'{self.resub_cl}')
        proc = subprocess.run(self.resub_cl,
                              stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT,
                              universal_newlines=True)
        self.log.info(f'The following was returned from qsub:\n{proc.stdout}')
        if proc.returncode:
            self.log.exception('Resubmission of calculation failed with '
                               f'returncode {proc.returncode}')
        match = re.search(r'(\d+)\s\("(\w.*)"\)', proc.stdout)
        if match:
            self.next_job_id = match.group(1)
            self.resubmitted = True
        else:
            self.log.warning('Could not find submitted job id from qsub '
                             'command. Will not be able to cancel it if this '
                             'Calc is completed')

    def _make_resub_sh_and_cl(self):
        """
        Make command line for a calculation for resuming in another job

        Requires SGE_STDOUT_PATH and JOB_ID for running `qstat`
        :return: None
        """
        self.log.debug('Setting up for calculation resubmission')
        arg_d = dict(pe=f'omp {self.n_slots}', M='theavey@bu.edu', m='eas',
                     l=f'h_rt={self.h_rt}', N=self._base_name, j='y',
                     o=self.stdout_file, notify='', hold_jid=self.job_id)
        resub_dir_path = self.cwd_path.joinpath('resub_scripts')
        resub_dir_path.mkdir(exist_ok=True)
        sub_sh_path = resub_dir_path.joinpath(f'resub-{self._base_name}.sh')
        curr_file = pathlib.Path(__file__).resolve()
        with sub_sh_path.open('w') as sub_sh:
            sub_sh.write('#!/bin/bash -l\n\n')
            for key in arg_d:
                sub_sh.write(f'#$ -{key} {arg_d[key]}\n')
            sub_sh.write(obabel_module_lines)
            sub_sh.write(f'\n{curr_file} --restart {self._json_name} &\n\n')
            sub_sh.write(signal_catch_lines)
        self.log.info(f'Wrote resubmission script to {sub_sh_path}')
        self.resub_cl = ['qsub', str(sub_sh_path)]

    def _get_h_rt(self):
        """
        Find the amount of time requested for the currently running job

        Requires JOB_ID and uses `qstat`.
        :return:
        """
        self.log.debug('Attempting to find requested job run time')
        cl = ['qstat', '-j', self.job_id]
        output: str = subprocess.check_output(cl, universal_newlines=True)
        for line in output.splitlines():
            m = re.search(r'h_rt=(\d+)', line)
            if m:
                self.log.debug(f'Found required info: {m.group(0)}')
                return m.group(1)
        self.log.error('Could not find requested run time! Assuming 24 hours')
        return '24:00:00'

    def resume_calc(self):
        self.log.debug('Attempting to resume calculation')
        if not self.cleaned_up:
            self._copy_and_cleanup()
        manual_input = self.status['manual_input']
        manual_restart = self.status['manual_restart']
        if manual_input is not None:
            com_name = self._update_g_in_memory_cpu_request(manual_input)
            self._setup_and_run(com_name)
        elif self.between_levels:
            self._copy_in_restart('chk')
            self._next_calc()
        else:
            if manual_restart is not None:
                com_name = self._update_g_in_memory_cpu_request(manual_restart)
            else:
                com_name = self._update_g_in_for_restart()
            self._copy_in_restart()
            self._setup_and_run(com_name)

    def _copy_in_restart(self, file='both'):
        f_type_options = ['chk', 'rwf']
        if file == 'both':
            f_types = f_type_options
        else:
            if file not in f_type_options:
                self.log.warning(f'unrecognized file type {file}; trying it '
                                 f'anyway...')
            f_types = [file]
        self.log.debug(f'Copying {f_types} files to scratch for restart')
        bn = self._base_name
        for f_type in f_types:
            old_path = self.last_scratch_path.joinpath(f'{bn}.{f_type}')
            if not old_path.exists():
                mes = f'Could not find old {f_type} file at {old_path}'
                self.log.error(mes)
                raise FileNotFoundError(mes)
            new_path = self.scratch_path.joinpath(old_path.name)
            if new_path.exists() and (new_path.stat().st_mtime >
                                      old_path.stat().st_mtime):
                self.log.debug(f'current {f_type} file newer than old '
                               f'({old_path}). Not replacing.')
            else:
                try:
                    shutil.copy(str(old_path), str(self.scratch_path))
                except shutil.SameFileError:
                    self.log.info('Working on the same node; no need to '
                                  f'copy {f_type} file')
        self.log.info(f'If necessary, copied {f_types} files from last '
                      f'scratch directory: {self.last_scratch_path}\nto '
                      f'node scratch dir: {self.scratch_path}')

    def _update_g_in_memory_cpu_request(self, com_name=None):
        self.log.debug('Updating Gaussian memory and cpu request for this node')
        com_name = self.status['g_in_curr'] if com_name is None else com_name
        lines = open(com_name, 'r').readlines()
        paratemp.copy_no_overwrite(com_name, com_name+'.bak')
        with open(com_name, 'w') as f_out:
            for line in lines:
                if '%mem=' in line:
                    line = f'%mem={self.mem}GB\n'
                elif '%cpu=' in line:
                    line = f'%cpu=0-{int(self.n_slots)-1}\n'
                f_out.write(line)
        os.remove(pathlib.Path(com_name+'.bak'))
        self.log.info(f'Updated Gaussian input to use all the memory '
                      f'on this node')
        return com_name

    def _update_g_in_for_restart(self):
        self.log.debug('Updating Gaussian input for restart')
        com_name = self._update_g_in_memory_cpu_request()
        lines = open(com_name, 'r').readlines()
        paratemp.copy_no_overwrite(com_name, com_name+'.bak')
        with open(com_name, 'w') as f_out:
            for line in lines:
                if line.startswith('#'):
                    line = '# Restart\n'
                f_out.write(line)
        os.remove(pathlib.Path(com_name+'.bak'))
        self.log.info(f'Updated Gaussian input to do a calculation restart')
        return com_name

    def _next_calc(self):
        self.log.debug('Moving on to next level calculation')
        out_path = pathlib.Path(self.status['g_in_curr']).with_suffix('.out')
        try:
            xyz_path_str = self._create_opt_xyz(out_path)
        except:
            self.log.error(f'Failed to create xyz file for {out_path}')
            self._qdel_next_job()
            raise
        try:
            com_name = self._make_g_in(xyz_path_str)
        except self.NoMoreLevels:
            self.log.info('No more calculation levels to complete! Completed '
                          f'all {self.current_lvl} levels')
            self._qdel_next_job()
            return None
        except FileExistsError:
            self.log.warning(f'Gaussian input file for level '
                             f'{self.current_lvl} already exists! This file '
                             f'will be used to start a new calculation')
            com_name = f'{self._base_name}-lvl{self.current_lvl}.com'
        self._setup_and_run(com_name)
        # This will get nested, but likely no more than twice (unless the
        # optimizations are very quick). This shouldn't be an issue,
        # and should never get near the recursion limit unless something goes
        # very wrong.

    def _qdel_next_job(self):
        self.log.debug('Deleting the re-submitted job from the queue')
        if self.next_job_id is not None:
            cl = ['qdel', self.next_job_id]
            output = subprocess.check_output(cl, stderr=subprocess.STDOUT)
            self.log.info('Cancelled job resubmission. qdel said: '
                          f'{output}')
        else:
            self.log.warning('Do not know job id of resubmission so '
                             'unable to delete it.')

    def _create_opt_xyz(self, out_path: pathlib.Path):
        self.log.debug('Converting output to xyz file for next level')
        xyz_path_str = str(out_path.with_suffix('.xyz'))
        success = self._run_obabel(out_path, xyz_path_str)
        if success:
            return xyz_path_str
        fchk_path = self._create_fchk()
        success = self._run_obabel(fchk_path, xyz_path_str)
        if success:
            return xyz_path_str
        raise self.NoOptXYZError

    def _run_obabel(self, out_path: pathlib.Path, xyz_path_str: str):
        self.log.debug('Running openbabel to convert geometry')
        cl = ['obabel', str(out_path), '-O',
              xyz_path_str]
        proc = subprocess.run(cl, stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT,
                              universal_newlines=True)
        if not (proc.returncode or
                '1 molecule converted' not in proc.stdout.lower()):
            self.log.info(f'Converted optimized structure to xyz file: '
                          f'{xyz_path_str}')
            return True
        mes = (f'obabel failed to convert {out_path} to an xyz file. '
               f'It said: {proc.stdout}')
        self.log.warning(mes)
        return False

    def _create_fchk(self):
        self.log.debug('Converting chk to formatted checkpoint')
        chk_name = f'{self._base_name}.chk'
        fchk_name = f'{self._base_name}.fchk'
        cl = ['formchk', chk_name, fchk_name]
        proc = subprocess.run(cl, stdout=subprocess.PIPE,
                              stderr=subprocess.STDOUT,
                              universal_newlines=True)
        self.log.debug(f'Ran Gaussian formchk and it said: {proc.stdout}')
        proc.check_returncode()
        return pathlib.Path(fchk_name).resolve()

    class SignalMessage(Exception):
        pass

    class TimesUp(SignalMessage):
        pass

    class GaussianDone(SignalMessage):
        pass

    class NoMoreLevels(Exception):
        pass

    class GaussianError(Exception):
        pass

    class NoOptXYZError(Exception):
        pass

class StatusDict(dict):
    """
    A dict subclass that writes the dict to disk every time a value gets set

    Note, any other action on the dict will not currently trigger a write to
    disk.

    The dict will be written in JSON to the path given at instantiation. If
    there is already a file at that path, it will be read and used as
    the initial definition of the dict. Otherwise, it will be instantiated as
    an empty dict.

    Keys in dictionary:

    * args: a Dict of the arguments given when starting the calculation.

    * current_node: Name of the node on which the job is currently running.
    This is set during :func:`Calc._startup_tasks`. It should be formatted as
    'scc-xxx' (e.g., 'scc-na1').

    * last_node: This is set during :func:`Calc._startup_tasks` if it's a
    restarted calculation. It will be set from the last 'current_node'.

    * node_list: This is a list of past and current nodes on which this
    calculation has run.

    * current_scratch_dir: str of the absolute path to the scratch directory
    on the current node.

    * base_name: the base name for this calculation including the index of
    this calculation.

    * cwd: str of the absolute path from which the current calculation was
    submitted.

    * last_scratch_dir: str of the absolute path to the scratch directory
    from which the last job was run, if this is not a new calculation.

    * source_frame_num: The index of the frame in the trajectory that was
    used to create the initial configuration.

    * original_xyz: str of the name of file with the coordinates as they were
    taken from the trajectory, before moving the reacting atoms to the
    correct distance. This will not be set if no distance correction is made.

    * starting_xyz: str of the name of the file with the coordinates for
    starting the calculation, before any optimization.

    * g_in_0: str of the name of the file with the initial input to Gaussian.
    * g_in_curr: str of the name of the currently running or most recent
        Gaussian input

    * current_lvl: int of current level of calculation running (max is len(
        ugt_dicts))

    * calc_cutoff: bool of whether the job finished or if it was cutoff
        because of running out of time.

    * cleaned_up: bool of whether linked files and old outputs have been
        cleaned up and copied back to the starting directory

    * chk_ln_path: str of path to where checkpoint file is linked in
        submission directory

    * output_scratch_path: str of path to where output is in the scratch
        directory

    * job_id: str of the job number from the sun grid system

    * between_levels: bool of if between levels (not in the middle of a
        calculation). Useful for figuring out where to restart.

    * gaussian_failed: bool of if Gaussian terminated abnormally. Will be None
        while Gaussian is running and before it gets checked.

    * manual_input: str of path to an input file to use to continue the
        calculation. Will not use the chk and rwf files.

    * manual_restart: str of path to an input file to use to restart the
        calculation. This will copy in the chk and rwf files that should be
        referenced in the header of the input.

    """
    def __init__(self, path):
        self.path = pathlib.Path(path).resolve()
        if pathlib.Path(path).is_file():
            d = json.load(open(path, 'r'))
            super(StatusDict, self).__init__(d)
        else:
            super(StatusDict, self).__init__()
        self.log = logging.getLogger(self.__class__.__name__)
        self.temp_path = self.path.with_suffix('.json.new')

    def __setitem__(self, key, value):
        try:
            super(StatusDict, self).__setitem__(key, value)
            json.dump(self, open(self.temp_path, 'w'), indent=4)
            os.rename(str(self.temp_path), str(self.path))
        except Exception:
            self.log.exception('Exception raised when trying to write status '
                               'file!')
            raise

    _defaults = dict(  # args=dict(),  # want this to be an Error
                     current_node=None,
                     last_node=None,
                     node_list=list(),
                     current_scratch_dir=None,
                     base_name=None,
                     cwd=None,
                     last_scratch_dir=None,
                     source_frame_num=None,
                     original_xyz=None,
                     starting_xyz=None,
                     # current_lvl=None,  # want this to be an Error
                     calc_cutoff=None, cleaned_up=None,
                     chk_ln_path=None, output_scratch_path=None,
                     job_id=None,
                     between_levels=None, gaussian_failed=None,
                     manual_input=None, manual_restart=None,
                     g_in_curr=None,
                     **{f'g_in_{i}': None for i in range(20)})

    def __getitem__(self, item):
        try:
            return super(StatusDict, self).__getitem__(item)
        except KeyError as ke:
            self.log.warning(f'Tried to access non-existent key "{item}" from '
                             'StatusDict')
            try:
                return self._defaults[item]
            except KeyError:
                raise ke

def _check_environ():
    log.debug('Checking and setting environment variables')
    if os.environ.get('SGE_ROOT', None) is None:
        raise ValueError('SGE_ROOT is not defined')
    if os.environ.get('SGE_CELL', None) is None:
        log.debug('Setting SGE_CELL to default')
        os.environ['SGE_CELL'] = 'default'
    if os.environ.get('DRMAA_LIBRARY_PATH', None) is None:
        lib_path = pathlib.Path(
            f"{os.environ['SGE_ROOT']}/lib/linux-x64/libdrmaa.so").resolve()
        log.debug(f'Setting DRMAA_LIBRARY_PATH to {lib_path}')
        os.environ['DRMAA_LIBRARY_PATH'] = str(lib_path)

def _process_paths(paths) -> List[pathlib.Path]:
    log.debug('Processing input paths list')
    statuses = list()
    if not paths:
        log.debug('paths was empty; using current directory')
        paths = [pathlib.Path.cwd()]
    for path in paths:
        path = pathlib.Path(path)
        if path.is_file():
            statuses.append(path)
            continue
        if path.is_dir():
            jsons = path.glob('*.json')
            for j in jsons:
                statuses.append(j)
    log.debug(f'Found {len(statuses)} json files to process')
    return statuses

def get_job_statuses(paths: List[str], df: pd.DataFrame = None):
    log.info('Getting job statuses')
    _check_environ()
    import drmaa
    log.debug('Imported DRMAA package')
    statuses = _process_paths(paths)
    if df is None:
        df = pd.DataFrame(
            {'system': ['str'], 'index': [0],
             'running_sr': [False], 'running_qr': [False],
             'current_lvl': [0], 'gaussian_failed': [False]},
        )
        df.drop(labels=[0], axis=0, inplace=True)
    signal.alarm(10)  # try to open session for 10 seconds (freezes with some
    # unidentified problems with drmaa
    with drmaa.Session() as session:
        signal.alarm(0)  # clear timer if successfully opened session
        log.debug('Opened DRMAA session and finding job statuses')
        for f_status in statuses:
            log.debug(f'Trying file {f_status}')
            name = f_status.stem
            m = re.search(r'(.*)-ind(\d+)', name)
            if m is None:
                continue
            system, index = m.groups()
            d_status = json.load(f_status.open('r'))
            running = bool(d_status['running'])
            job_id = d_status['job_id']
            try:
                q_running = session.jobStatus(job_id)
            except drmaa.InvalidJobException:
                q_running = None
            lvl = int(d_status['current_lvl'])
            failed = d_status['gaussian_failed']
            df.loc[name] = system, int(index), running, q_running, lvl, failed
    return df.sort_values(['system', 'index'])

if __name__ == '__main__':
    import argparse
    parser = argparse.ArgumentParser()
    parser.add_argument('-n', '--base_name', type=str,
                        help='base name for this calculation, likely not '
                             'including any index')
    parser.add_argument('-i', '--index', type=int,
                        help='index of this calculation')
    # TODO use mutually exclusive arguments here
    parser.add_argument('-x', '--xyz', type=str, default=None,
                        help='manual input geometry (will take precedence '
                             'over topology/trajectory frame selection)')
    parser.add_argument('-c', '--top', type=str, default=None,
                        help='topology/structure file (e.g., .gro, .xyz)')
    parser.add_argument('-f', '--trajectory', type=str, default=None,
                        help='trajectory file (e.g., .xtc, .trr, .dcd)')

    def parse_crit(kvv):
        k, vv = kvv.split('=')
        vs = tuple((float(v) for v in vv.split(',')))
        return k, vs

    parser.add_argument('-s', '--criteria', action='append',
                        type=parse_crit, metavar='key=min,max',
                        help='criteria for selection of possible frames from '
                             'the trajectory. To provide more than one '
                             'criterion, use this argument multiple times')
    parser.add_argument('-d', '--react_dist', type=float, default=False,
                        help='Distance to set between atoms 20 and 39, '
                             'in angstroms. If this evaluates to False, '
                             'no changes to the geometry will be made')
    parser.add_argument('-g', '--mgi_dicts', type=str,
                        help='path to json file that parses to a list of '
                             'dicts of arguments for make_gaussian_input in '
                             'order to create inputs to Gaussian')
    parser.add_argument('--restart', default=None,
                        help='Path to status file for resuming an already '
                             'started calculation')
    parser.add_argument('-j', '--job_status', nargs='*', default=None,
                        help='Folders or paths to status files to report the '
                             'status of. If nothing is given, status files in '
                             'current directly will be used. This flag cannot '
                             'be used with any other arguments other than -v.')
    parser.add_argument('-v', '--verbose', action='store_true',
                        help='If used, the stdout log will be set to DEBUG')
    p_args = parser.parse_args()
    if p_args.verbose:
        _setup_log(logging.DEBUG)
    else:
        _setup_log()
    if p_args.job_status is not None:
        status_df = get_job_statuses(p_args.job_status)
        print(status_df)
        sys.exit(0)
    elif p_args.restart is not None:
        calc = Calc(status=p_args.restart)
    else:
        _mgi_dicts = json.load(open(p_args.mgi_dicts, 'r'))
        _criteria = None if p_args.criteria is None else dict(p_args.criteria)
        calc = Calc(base_name=p_args.base_name,
                    ind=p_args.index,
                    geometry=p_args.xyz,
                    top=p_args.top,
                    traj=p_args.trajectory,
                    criteria=_criteria,
                    react_dist=p_args.react_dist,
                    mgi_dicts=_mgi_dicts
                    )
    calc.run_calc()


During a heart attack, clots or narrowed arteries block blood flow, harming or killing cells within the tissue. But the damage doesn't end after the crushing pain subsides. Instead, the heart's walls thin out, the organ becomes enlarged, and scar tissue forms. If nothing is done, the patient can eventually experience heart failure. But scientists now report they have developed gels that, in animal tests, can be injected into the heart to shore up weakened areas and prevent heart failure.
Heart attacks strike 750,000 people each year in the U.S., according to the American Heart Association. And more than 5 million U.S. residents are living with heart failure, with symptoms that progress from fatigue and shortness of breath to eventual death. "Heart failure is a huge problem, and few therapies are available for these patients," says Jason A. Burdick, Ph.D., leader of the study.
Treatments include lifestyle changes, medication, implants or heart transplants. Burdick, who is at the University of Pennsylvania (Penn), explains that these options often don't work well or, in the case of transplants, are hard to come by. So scientists are pursuing other treatment methods. For instance, researchers at other institutions have done animal studies in which they injected cells into the damaged section of the heart to try to repair damage. To prevent the cells from leaking out, those researchers embedded them in biodegradable "hydrogels"—water-swollen networks of polymer chains with a consistency similar to Jell-OTM. But the scientists noticed something odd when they ran control experiments in which they injected the hydrogel without added cells: Some of the animals' hearts still showed improvement compared with untreated animals.
Based on those findings, a handful of labs are now experimenting with hydrogel treatments, including two materials that are in clinical trials. Neither is from Burdick's lab, but as he notes, "It's important we all keep moving forward to figure out how this therapy could be used, because it's different than any current treatment." In addition, different types of hydrogels could suit different patients' needs.
Some experimental heart attack treatments require surgery to open up the chest, but the two hydrogel materials already in clinical trials are injected into the damaged tissue through a long catheter inserted through the skin—eliminating the need for open-chest surgery.
In fact, Burdick's gel is unique among hydrogels in providing mechanical support to stabilize the damaged area. In sheep studies, this gel limits formation of scar tissue, thinning of the heart's walls and enlargement of the heart. By preserving the organ's size, the gels also reduce leakage of blood through the mitral valve. Together, these benefits maintain the heart's blood-pumping ability and could stave off heart failure.
The team's materials are based on hyaluronic acid (HA), a type of sugar molecule that occurs naturally in the body. The researchers modified the HA molecules by attaching adamantane and cyclodextrin groups to allow the gels to flow through catheters, and they added thiol and methacrylate groups to enable post-injection cross-linking to stiffen the hydrogel. Once the researchers finalize the hydrogel formulation and delivery method, they hope to partner with a catheter firm to bring a product to market. Burdick's team and other research groups are also designing hydrogels that contain drugs or cells that can repair heart tissue.
More information: Engineered injectable supramolecular hydrogels for myocardial applications, 252nd National Meeting & Exposition of the American Chemical Society (ACS), 2016.
Heart disease is a major clinical problem and post myocardial infarction (MI), left ventricular (LV) remodeling ensues and leads to geometric changes that result in dilation and thinning of the myocardial wall. This increases stress in the infarct and healthy tissue and can ultimately result in heart failure. Injectable biomaterials are being investigated to address this clinical problem, including to alter stresses in the infarct region when injected and to deliver biologics, such as stem cells and biomolecules. My laboratory is interested in a class of hydrogels based on the molecule hyaluronic acid (HA) and we have synthesized variations of HA macromers that form hydrogels with a range of mechanical properties and degradation (from a few weeks to stable over many months). This tunability in properties allows us to investigate how material properties (e.g., mechanics and degradation) influence the ability of injectable HA hydrogels to alter stress profiles and LV remodeling and to deliver therapeutic molecules (e.g., TIMP-3, to alter matrix remodeling within infarcts). Most recently, we have designed a class of shear-thinning and self-assembling hydrogels that can be used for the delivery of mechanical signals, as well as cells and therapeutics. Ultimately, these iterations on material design are teaching us what important signals are needed in these hydrogels towards the next generation of translatable therapeutics for cardiac repair.


Metadata is hidden in our everyday lives almost everywhere. In fact, we are just about dependent on it. But How?
Imagine jumping in your car, plugging in your smartphone and cueing your favorite music playlist.
What automatically appears? – Album, Artist, Song Duration, Playlist. And more. This is metadata about your musical universe – telling you something about the music you own. This allows you to do things you could not do.
Your favorite GPS application is another example. The GPS coordinates are one of the many pieces of Metadata. If you use the Waze traffic app, metadata is correlated with traffic.
No matter what you do today or tomorrow, one thing is metadata. We are all addicted and we did not even know it.
What if we had a tool that could automatically scan for all of the metadata contained within our array of Enterprise and Cloud applications so that we can create a giant repository of all our metadata. Would this be nirvana? No, let me tell you why.
Metadata is meaningless as a data point. Take for example two GPS coordinates- one as a source and second as a destination. Going back to our map analogy, we need more than just two data points to compute our route. We need metadata containing all the information about all of the roads in between, and if we take it a step further, we want to know about construction delays and accidents to ascertain the fast route between point A and point B.
The point is it's all about metadata relationships. When we can relate metadata, we unleash the power of leveraging metadata to solve problems.
In the case of the mapping example we solve the problem of getting from point A to B the fastest with one important side benefit – we don't get lost.
Metadata in the enterprise isn't much different. But there's a challenge. Scanning for metadata typically involves schema.table.column which only provides one data point. Take it a step further and we can create a relationship between data source and destination, but it's still missing an important piece. You're probably wondering about now, what's missing? In any enterprise there are data transformations (typically implemented in either ETL or ELT applications) between the source and destination. Data lineage is incomplete and limited in it's use without the data transformations. Both business and IT users want to know what data transformations took place between source and destination to clearly understand the data flow from point A (source) to point B (transformation) to point C (destination). If we had all three then we'd truly unleash the power of metadata to solve common questions that IT and the business are asking. For example, IT benefits from improved dataops efficiency. Business benefits from better regulatory compliance. These are just two examples amongst a host of others.
Let's use a simple analogy. Think of metadata as a marble. Let's say you have 10,000 marbles spread all over your living room floor. How does this relate to the enterprise organization? You have an array of disparate systems housing all of your metadata marbles and you yearn to organize them to yield control over your metadata in a data governance context. As we've described earlier what's required is to unlock the power of the metadata relationships.
The challenge is to organize the relationships between the metadata marbles so that we can visualize how the data flows from source to destination with the transformations in between. Perhaps such an organization will reveal other interesting facts about your systems. How about finding systems that no one or very few users actually use? How about finding multiple systems that use the same data sources and apply very similar transformations for different sets of users? Maybe you could rationalize some of these systems saving valuable resources and gaining operational efficiencies?
Sounds easy in theory but what's required is a new approach – a metadata lineage extractor to do the hard work for us.
First let's describe the status quo. A connector or bridge is the traditional approach reading from exported metadata, system tables or a defined API to move selected metadata from one source to a metadata repository. Think of this as the Gen 1 approach to metadata. This traditional connector or bridge is limited by what the API allows you to do. For example, the API may not allow you to access all the metadata you want, for example, the transformations taking place in the ETL/ELT source code. The question is, is there a better way?
What if we could do more than just connect or bridge metadata from available system tables, metadata interchange formats or other metadata repositories? What if we could extract the data relationships and transformations directly from the source code, while enriching that information with metadata acquired from system catalog tables, business glossaries, master data management systems, reference data tables, operational metadata, or other metadata repositories along with user supplied metadata? If we could do this it would be a game changer.
This would be a fundamental shift in how metadata is acquired, viewed, and consumed. It would look like this. First, we'd start by analyzing the source code, followed by enriching that information with the metadata available from the standard metadata sources. Then we'd extract all of that into a rich meta-model that describes not only the data sets, but the flow of the data between those sets. Furthermore, we load it into the target information catalog in the format that it requires. Presto! We just solved one of the most perplexing issues to bring meaning to your metadata by connecting the dots from point A (source) to point B (transformation) to point C (destination).
So, the next time you hear people describe bridges or connectors, two common methods to extract metadata, think Lineage Extractor that extracts the data flow and transformations directly from the source code.
By utilizing Lineage Extractors to solve the metadata challenge facing every enterprise, you'll also get a side benefit – Visual Impact Analysis. You'll now be able to quickly comprehend visually the impact of any change to a data element or application on downstream data and applications. In addition, you'll know which tables, ETL jobs or BI reports are affected by a single change to a data element or a transformation rule. Imagine the impact on devops efficiencies this would create! Imagine the quality improvements as you would instantly know which downstream objects or applications need to be regression tested, as a result of any change anywhere in the enterprise data landscape.
As we started at the beginning of this blog – Metadata is meaningless as a data point. Now you understand how, using a Lineage Extractor approach over the traditional bridge or connector can be a game changer in how you capitalize on the power of your metadata.


text:
Inexpensive recombinant protein production induced by urea
A common chemical present in the urine can be utilized to initiate large-scale production of proteins needed by biotech companies, like hormones and antibodies.
Many proteins share important roles in the formation and function of synapses, study finds
Not just three, but even five proteins share important roles in the formation and function of synapses and can substitute for each other. This discovery was made by a team of the research focus "Mental Health & Neuroscience" of the Karl Landsteiner University of Health Sciences Krems (KL Krems) and the CavX PhD program of the Medical University of Innsbruck.
A new outlook on sow breeding using dietary nucleotides
The primary fetal organ systems' growth and development are strongly influenced by the nutrition status of the mother. Late gestation is when fetal growth is rapidly accelerated, and the nutritional needs of sows rise dramatically as a result of the fetus' rapid growth and the placenta's quick expansion.
Molecular mechanisms behind mitochondrial dysfunction in chronic heart failure
Chronic heart failure induces dysfunction in the cell's powerhouses, which is caused in part by an excess of an essential intermediate compound in energy production. Compensating for this by supplementing the diet could be a potential technique for treating heart failure.
New genome-wide association study of dyslexia
For the first time, researchers have identified a large number of genes that are reliably linked to dyslexia.
Novel method for comparing single-cell trajectories with tree alignment
The genes that are activated in a single cell can now be determined thanks to advances in high-throughput biological studies. However, interpreting the resulting complex datasets might be difficult.
From Osaka University 20 Oct 2022
Harsh parenting leads to depression later in life
Strict parenting can influence how the body reads the children's DNA. These modifications can become "hard-wired" into the DNA of kids who consider their parents to be harsh, enhancing their biological risk for depression in adolescence and later life.
Innovative method opens myriad new avenues for understanding the biology behind COVID-19
A highly innovative method using the latest technology has generated a comprehensive list of SARS-CoV-2 viral and human proteins that interact with each other, with one such interaction showing the virus directly influencing proteins that regulate the human immune system.
Drug discovery method facilitates promising treatment approaches for metabolic diseases
The body's chemical processes known as "metabolism" produce the raw materials needed for development and general health.
Building designer plant cuticles with important protective functions
Scientists are working to bioengineer a common defense mechanism that most plants develop naturally to protect against drought, insects and other environmental stresses.
Insights into the evolution of banana domestication
People on the island of New Guinea are said to have domesticated bananas 7,000 years ago. However, the history of banana domestication is convoluted, and their categorization is widely discussed, as the boundaries between species and subspecies are frequently ambiguous.
Making synthetic biology applications feasible using RNA origami
Synthetic biology aims to acquire comprehensive control over biological functions to produce designer organisms for a range of commercial, diagnostic, and therapeutic uses.
Cell illumination and targeted cell editing using RNA-based editing technology
An RNA-based editing tool has been developed by Duke University researchers, which targets specific cells rather than genes. Any cell type can be precisely targeted, and any protein of interest can be added only to that cell type.
New insight into human genome evolution
Researchers discovered that in one out of every 4,000 births, some of the genetic code from the mitochondria—the "batteries" that power human cells—inserts itself into the DNA, giving a startling new insight into how humans evolve.
Research team outlines three-step technique that lets human body defend against Hep B
A team from the University of Geneva (UNIGE) has unraveled the precise operation of this defensive mechanism after finding a crucial protein complex that is active when our body is infected by the virus, paving the path for novel treatment targets.
Novel molecules alleviate pain, avoid sedating effect
According to new research headed by UC San Francisco scientists, a newly found group of molecules relieved pain in mice whilst preventing the sedating effect that limits the usage of opiates.
Using in vivo self-assembled siRNA as part of combination therapy for ulcerative colitis
There is still a large unmet medical need for the treatment of ulcerative colitis, despite recent improvements in the understanding of the pathogenic mechanisms of ulcerative colitis.
"Junk" DNA could provide access to innovative treatments for neurological conditions
The breakthrough study from the University of Sheffield's Neuroscience Institute and Healthy Lifespan Institute offers critical new insights into the so-called junk DNA, or DNA that was previously believed to be unimportant to the coding of the genome, and how it affects neurological disorders like Motor Neuron Disease (MND) and Alzheimer's.
New insights into the regulatory mechanisms of cell fate decisions
The Babraham Institute's Reik lab's study has improved the understanding of the function of DNA methylation during the early stages of development.
Radical theory examines how charged macromolecules self-assemble
Researchers from the University of Massachusetts Amherst recently reported in the Proceedings of the National Academy of Sciences that uniformly charged macromolecules, or molecules like proteins or DNA, which contain a large number of atoms all with the same electrical charge, can self-assemble into very large structures.
Antibodies eBook

redpajama_set_name: RedPajamaCommonCrawl


text:
Bodog review
Bodog was created in 1994 – one of the oldest betting sites. Today it is one of the most recognizable names in the industry and has become a leader in online gaming. Since 2018, Bodog is also present in India targeting sports betting, live casinos, and poker. Bodog India is operated by Archer Media and has received its license from Curacao eGaming.
Bodog for Indian Bookies offers a wide range of choices for betting, such as sports betting, live casino games, poker, online clubs, etc. Plus, the navigation feature of the site is very simple and takes very little time to find what you are looking for placing a bet. There are no complex menus, and even new bettors can find the bet within a few clicks. Besides, the platform offers a good amount of welcome bonuses and promotions with many other surprises to attract players.
Overall, Bodog has a good reputation in the betting market, and the website is simple to understand and incredibly easy to explore.
Is Bodog safe in India
Bodog is a legal site and holds a license from a European authority. The website is operated under Secure Sockets Layer (SSL) technology, which guarantees the confidentiality of the players' personal and financial information. This way, you can be sure that no unauthorized party can access your data.
However, they cover all the basics in terms of security and safety that apply to their sports betting and casino products. Bettors can register and deposit/withdraw in Indian currency very easily and quickly.
Bodog cricket betting
Bodog is one of the oldest and best-betting sites for Indians. But the main magnetism site for Indian players is to allow them to place a bet on cricket and Bodog has provided the opportunity to place a bet on cricket matches for all Indian players.
They have a huge variety of markets, including bets on the major international and domestic matches, upcoming Indian Premier League, tournaments held in India, and other countries, as well as odds for the T20 World Cup and numerous other events/competitions.
Bodog covers games in all three formats, so no matter your preferred game format, you will certainly find something to keep you entertained at Bodog.
Overall, the best part is the cricket betting section after Bodog Sportsbook, and it's a huge part for Indian players when.
If you join Bodog for the first time, you are entitled to the various bonuses and promotions offered by the different sections, such as sports betting, casino, poker, or other.
Once registered on Bodog, you will benefit from various bonuses and promotions. Players will receive a sign-up bonus at different levels every day.
For Casino: Previously, it was if a player would register before December 31 and makes the first deposit; they would get a welcome bonus of 120%. In short, if you make a first deposit of Rs. Five hundred or more, you will receive a first deposit bonus of 120% up to Rs. 25,000 during the promotion period.
For Poker: Now, players can earn reward points while playing poker and earn up to Rs. 7,000. And to take advantage of this bonus, you must make the first deposit of Rs. Five hundred or more to get a first deposit bonus from Rs. 7,000.
For Live Casino: Players can get 80% or up to Rs. 8000 on the first deposit of Rs. 1000. However, this offer is only available to new players who register during the promotion period.
Four Match Deposit Bonuses
The platform also offers a bonus of up to Rs 200,000, such as:
On 1st deposit, players will receive a reward of up to Rs 50,000
On the 2nd deposit, players will receive an award of Rs 25,000.
On the 3rd deposit, players will receive a reward of up to Rs 50,000
On the 4th deposit, players will receive a reward of up to Rs 75,000.
The sponsorship reward is reasonable in correlation with other web betting platforms. Players will receive Rs 1,000 for each successful referral, and your friend will receive up to Rs. 500. Bonuses will be credited to your account within seven days of your friend's compliance with the terms and conditions. But don't forget that the bonus amount will expire within 14 days of your companion's registration.
Weekly Rebate
This weekly rebate for players is a much more energetic alternative contrasting with the surprising reward. Players can get discounts when they play at Live Casino, Sportsbook, Virtual Sports, Poker, Casino, or Slots, and the maximum the person can win is Rs. 7,500 or 0.4% for each week.
Surprise Bonus
The daily surprise reward gives players the chance to win up to 40% of their account balance. Although it sounds attractive, it is not as players must have to play games worth Rs 100,000 within 24 hours to meet all the requirements of the award.
Good news! The monster stack is back for players until July 16, so bettors can now take advantage and win up to 75 Lacs. Besides, bettors can increase the winnings of 3 special weekly events:
On Monday – Monster Stack Events with a prize pool of $250,000
On Monday – Monster Stack High Roller Events with a prize pool of $200,000
On Tuesday – Monster Stack Super High Roller Events with a prize pool of $150,000
* Note – 1 USD = 75 INR; 10 USD = 750 INR and so on.
Therefore, to take advantage of all bonuses and promotions, you must fulfill certain general conditions of the Bodog site.
Bodog offers several convenient payment methods.
Deposit Method
Maestro and Visa – INR 2,000
AstroPay – INR 250
Bitcoin – INR 1,000
Netbanking/UPI – INR 1,000
But other options may also be available depending on your current location. All deposits on the sports betting platform are processed instantly and free of charge, unless you withdraw your deposit immediately without betting, in which case fees may be applied. The minimum deposit limit varies by method.
Withdrawal Method
The withdrawal methods available depend on what you used to deposit to your player account. The minimum amount per withdrawal request is Rs. 1000, while the maximum daily withdrawal amount depends on your withdrawal method.
Bodog registration
Creating a personal account is a vital step towards playing online games on Bodog India, and this process is simple. All you have to do is visit https://www.bodog.com/in/
You will see a "Login" button on the site
If you are not a registered member, click on "Don't have an account? Join now" option under the "Login" button.
Enter your personal information:
Email id and mobile number.
Click on the "Join Now" button and verify your email to get started.
Hence, now you can start to play your favorite games at Bodog. If you face any kind of problem during the process or betting, you can simply contact the tech support for help.
How to bet on Bodog India?
Below is a step-by-step guide on how to bet on Bodog India:
Click on the "Sports" tab in the top navigation bar on the home page.
Select from the list of sports on the page.
Choose from many international and regional matches. You can also employ the filters to select the games by region.
After selecting a match, place a bet on a match by depositing money in your Bodog wallet.
If you want to see the bets you have placed to date, simply press the "My Bets" tab, and all the history will be shown to you.
Hence, placing a bet with Bodog is very simple and quick!
Bodog mobile
If you are a betting lover and tend to place bets from anywhere and anytime, then you can now do it without any trouble or downloads, as the Bodog site comes with a fully functional mobile theme for the players. The mobile version of the website is designed with HTML5, which means that all major tablets, such as Apple iPhone and Android, can easily play Bodog.
Overall, the mobile interface is indistinguishable to the desktop version and takes only a second to run. But no native app is available for players to download.
Bodog has always performed well in providing assistance and support to its new and existing customers.
You can use the live chat, which is available 24/7 for Indian players in several languages , including Tamil, Hindi, Marathi, and Gujarati. It is easy to get answers to your questions from an agent by just clicking on the "chat icon." Fill in the details:
Your question.
Click on "Start Chat," and you'll be connected to someone who can help in seconds.
There is a Help page that covers additional questions related to deposits, General Banking, Bitcoin, General services, Games rules, poker, and or other top questions.
You can also send your questions via email at https://www.bodog.com/in/help/contact-us by filling the details:
Option (Deposit/Withdrawal, Casino, Poker and so on)
Click on "Send," and you will receive an answer in 24 hours.
Hence, No matter which option you choose for the help, the tech support representatives at Bodog work hard to respond quickly and professionally to player questions.
Here we have compiled a list of several pros and cons to inform our readers of all the ins and outs of the Bodog.
Good range of markets
Attractive and easy to use site
Transparent and reliable service
Good bonus and promotions conditions
Bitcoin compatible
Quick withdrawals
No VIP programs
Skrill and Neteller option is not available
Rollover requirement three times on the welcome bonus
Is Bodog accepting Indian Rupees? +
There is certainly nothing to worry about currency exchange when it comes to Bodog. They have a particular Indian site for Indian players that accept deposits, bets, and withdrawals in Indian rupees. Hence, you can say Bodog is a real INR betting site.
Is Bodog safe? +
Bodog is a safe and secure site for all players worldwide. The website complies with international license and regulatory standards. Bodog is a company registered in Curaçao and regulated by the government of Curaçao, is also governed by the Cagayan Economic Zone Authority (CEZA) in the Philippines.
Besides, the information provided by customers on Bodog is protected as they use the advanced 128-bit SSL encryption technology, which makes it extremely difficult for cybercriminals or fraudsters to access customer data.
Bodog India has everything to be one of the favorite online betting sites with sports betting and for a huge selection of slot games to play online and win a huge amount of money. They offer attractive bonuses and promotions that can attract a wide range of customers.
Overall, it is a reliable betting site that has invested in welcoming Indian residents. The live chat service provided by them is available in several languages. Besides, Bodog is an excellent betting site if you are searching for an overall good betting experience.
up to ₹10 000
Website bodog.com
Quick withdrawals Exceptional customer support Good range of markets Attractive and easy to use site Transparent and reliable service Good bonus and promotions conditions Bitcoin compatible
No VIP programs Skrill and Neteller option is not available Rollover requirement three times on the welcome bonus


text:
Practice Area Leads
Ruben van Engelenburg: Tenacious, Driven Problem Solver [July Consultant of the Month]
Ursula Daly: Bring On The Marketing Challenge [June Consultant of the Month]
Client Project: Website Redesign for Wurl
Category Consulting Tips Content Creative Design & Development Data Science & Analytics Finance Future of Work Growth & Digital Positioning & Brand Management Product & Project Management This Is EM
Year 2019 2018 2017 2016 2015 2014 2013 2012 2010 2009
Author Nicole Hanusek Justin Liszanckie Dan Holmes Jessaline Tuason Kathy Jung Ken Chen Leilani Yau Wens Gerdyman Alice Chan Jane Markell Jennifer Roop Kimberly Wiefling Whitney Glockner Black Paula Polley Jocelyn Rasekhi Rachel Plasse Ryan Rigoli Tara Verner Kelly Gherardi Suzy DeLine Adelyn Zhou Daniel Gehant Giovanna O'Grady Mark Harnett Toby Skinner Karl Pontau Colleen McCarthy Ed Sweet Christine Keefer Erica Lee Kevin Cranfill
Workers of the World Delight — A Rallying Cry for Happiness at Work, Just in Time for May Day
Kevin Cranfill
May Day is an international holiday most of us know about, but few of us bother to recognize. It's principally concerned with the welfare of the working classes, pay, benefits, labor conditions, though for many Americans, the holiday is more closely associated with maypole dances, rowdy worker protests, and even the old Soviet Union. Red Square. Military parades. Communism.
I'll spare you the full history of May Day and its quirky, circuitous journey, though the National Museum of American History offers an interesting take. But let's use the occasion to reexamine the nature of work itself.
Source: Gallup
A recent Gallup Poll found that only 34% of Americans are presently "engaged" with their jobs. The study defines "engaged" as being "involved in, enthusiastic about, and committed to" one's workplace, which means a solid majority of Americans are either "not engaged" or "actively disengaged" with their work. The 34% is also tied for the highest percentage recorded since 2000, when Gallup began reporting the figure.
But when nearly seven out of 10 of us are either apathetic about, or find no joy in our jobs, the real question may be how can we do better?
Will Work… for Perks?
Let's start with Silicon Valley. I'll admit there's an irony in using May Day, a holiday associated with the working class, to reflect on the so-called plight of the Silicon Valley worker. Many people think of Silicon Valley jobs as the non plus ultra of the working life. They think of high salaries, unlimited PTO, free lunches, and swag. They are not wrong.
What they may be interested to know is that lots of people who have these jobs actually don't like them.
They're not engaged, they're actively disengaged, or they're just plain grumpy. Not even the perks can perk them up, not because they don't appreciate them, not because they're ungrateful, but because they're perks. A perk is a privilege that enhances the work experience. Perks shouldn't be the primary motivation for dragging our butts into the office each day. They probably shouldn't be the thing we think of first when a friend asks us about our job. Perks are not the point.
I'm not knocking perky, high-paying, full-time positions. I used to have one. I took the job because, well, what's not to like about a high salary, unlimited PTO, free lunches and swag? No job is perfect, and this opportunity had its share of drawbacks, but it also checked lots of boxes I hadn't checked before. And who am I kidding? As somebody who was still relatively new to the Bay Area, but who'd only ever been offered a contract job, the prospect of being a full-fledged, fully credentialed member of the Silicon Valley workforce held a certain allure.
I told myself to give the position one year. Looking back, I'm glad I didn't give it two. It wasn't the perks. They were as good as advertised. It wasn't the people. They were smart, friendly and eager to do work that mattered. What wasn't to like? Pretty much everything else. Endless meetings. Dueling agendas. A mentality that valued status over ability. Organizational charts that put people in a box. Budget battles. Turf battles.
These weren't subjective issues. I liked my manager. I loved my team. The company was admired, and the field was dynamic. The issues were structural, and, based on the experiences of friends and colleagues who earn their livings at other admired companies, in other dynamic fields, and are equally fed up with their jobs, they may well be systemic.
Rat Race, Revisited
Nobody is immune to the blinders of the rat race. At one point or another, we've all lost sight of what matters most because of the demands of the job. It will likely happen again. Similarly, anybody can have a blah day. Every day shouldn't be that way. Mondays shouldn't always be that way. (Mondays have feelings, too.) What to do? We might start by questioning society's winner-takes-all-mentality, the one that demands 'round the clock availability, the one that makes us question our self worth if our meeting calendar isn't full, the one The New York Times says is back. Yippee!
Then there's the dreaded "or else"? Always be online or else. Make at least six figures a year or else. (Make in excess of six figures a year or else.) Conform to society's definition of high-class mobility or else. You take my point. Enjoyment is beside the point. Fulfillment is beside the point. I had one friend who tried to convince me that not everybody needs to be fulfilled by their work. There's a reason it's called work, he reminded me. (He disliked his job, too.)
But is he right? Is joyful work an oxymoron, the stuff of fantasy? Is the illusion of fulfillment the best we can hope for? Could those ho-hum expressions on the faces of so many of our colleagues be trying to tell us something? Could that something be this? Work doesn't have to be an ego-driven dog-eat-dog affair. We don't have to be run by rules that we have no control over. Those who play the high-class mobility game should be prepared to lose it.
Are You Living Your Best Life?
Countless studies have been devoted to the subject of worker happiness. They seem to suggest two core truths about people. Truth 1) Most of us care about what we do. Truth 2) All of us want to be recognized for our efforts. It's about feeling valued. It's about purpose and self-respect. It's not an abyss, but the gap won't close itself, either. The Greater Good Science Center at the University of California identifies four key pillars of happiness at work: Purpose, Engagement, Resilience and Kindness — or PERK. How's that for ironic?
Our jobs as marketers is to spark growth. But what about our own personal growth? If you're one of the lucky 34% who feels "engaged" with your job, take a few minutes this May Day to remind yourself why. (Dance around a maypole if you like.)
If you're on the other side of the divide, the way the majority of us still are, reassess where you are and where you're headed. What type of opportunity would be a better fit? Where does work fit into your life? How can it help you live your best life? Find the answer to these questions and you might just find yourself in another position or even on another career path entirely. Your new job may or may not include a rooftop deck or a bigger breakfast bar, but it will likely feel more enjoyable, more sustainable, and more like what you were put on this earth to do in the first place.
Want to know what it's like to work with a team of experienced marketers who enjoy what they do so much they've been known to mistake Monday for Friday? Get in touch!
A delighted EM Marketing Team
Why Translating Your Full-Time Salary to a Consulting Rate Is Short-Sighted
Erica Lee
Freedom to Work Anywhere, Part 5: How to Bring Out Creativity and Strategic Thinking
This Is EM
Christine Keefer
Freedom to Work Anywhere, Part 4: How to Transition Away from the Bay Area
Justin Liszanckie
Freedom to Work Anywhere, Part 3: How to Successfully Work Remotely
Ed Sweet
Freedom To Work Anywhere, Part 2: How to Make the Most of Your Day
Perks of Being a Consultant: The Freedom to Work Anywhere
EM Marketing: The Future of Work for Marketers
A Pivot in the Employment World
Driven by Uber: The New Workforce in the Future of Work
When Did Having Free Time Become Socially Unacceptable?
info@emmarketing.com
Let's Talk Let's Talk
Looking to be part of something big?
Join us Join us
Copyright © EM Marketing, Inc. All rights reserved.


Brandy Jones, Undergraduate, Sociology, and Member, Executive Committee, M.U.S.I.C. During 2013, URI Student Senate records indicate that multicultural student organizations (MSOs) representing an estimated 21% of the total undergraduate population were allocated $60,487, or 4.3% of the total $976, 413 budgeted from student activity fees paid by students. Despite their historic contributions to campus history as well as their current cultural impact, the MSOs draw almost all of their funding from the contingency category – the most vulnerable of the three categories of funding. Two organizations – Capoeira and the Latin American Students Association – are believed to receive token amounts from the "individual budget line" category – the second most secure level of funding. This workshop will build support and understanding for M.U.S.I.C. in its second appeal for recognition to Student Senate as a council/umbrella organization (a) whose primary purpose is to enhance the performance and status of the other multicultural student organizations; (b) whose own performance and status will be enhanced by access to a "full budget" – the highest priority budget category – bringing with it eligibility for stipends for M.U.S.I.C. officers; c) whose confirmation is expected to lessen the long‐dysfunctional dependence of the MSOs on "all comers" parties to supplement their revenues; and d) whose active advocacy would help to improve the inclusivity and receptivity to cultural difference of Student Senate, making it likely that culturally diverse candidates will compete more and persist longer.


text:
Saint-Pol is een voormalig arrondissement in het departement Pas-de-Calais in de Franse regio Hauts-de-France. Het arrondissement werd in 1801 gevormd als afsplitsing van het arrondissement Arras en op 10 september 1926 opgeheven. De zes kantons werden bij de opheffing opnieuw toegevoegd aan het arrondissement Arras.

Kantons
Het arrondissement was samengesteld uit de volgende kantons:
 kanton Aubigny-en-Artois
 kanton Auxi-le-Château
 kanton Avesnes-le-Comte
 kanton Heuchin
 kanton Saint-Pol-sur-Ternoise
 kanton Le Parcq

Geografie van Pas-de-Calais
Saint-Pol

redpajama_set_name: RedPajamaWikipedia


Zecke has posted a blog entry showing off some patches to QtWebKit that brings QtWebKit Acid3 test to 100/100. Head over and give him thank for taking the time to track down the edge cases.
A little over a year ago one of Trolltech's developers, Jens (the same guy who made our awesome logo) posted a blog entry on labs.trolltech.com with some style code that takes the normal Qt XP style and adds Microsoft Explorer style tool bars on XP and Vista. I have integrated the code into Arora for a new upgrade in our look.
After the announcement of Mozilla's Aurora project the Arora homepage has gotten a number of new visiters, many of them probably looking for screenshots. I have hastily put together a wiki page of screenshots that can show off some of the features in Arora.
I am happy to announce Arora 0.3. Many new features and improvements are in this release. Keeping the goals of the project the biggest feature was the addition of boring disk cache when using the Qt 4.5 snapshot. Cache is a fundimental feaure of a browser and to a simple browser without one isn't much of a browser. It is not just a feature for Arora, but something that is desired by anyone using QtWebkit or QNetworkAccessManager and I have worked hard the past two weeks getting the feature completed. More details can be found on trolltech labs. After the disk cache, the class the makes up the Location bar has been re-written. The new class is fundamentally much cleaner and designed to have the ability to add more widgets in the location bar. A feature that will hopefully be used by extensions in the future.
- Created a new LineEdit that handles adding side widgets in a clean, simple and elegant way.
- Add a way to set the minimum font size in the settings.
- When making the text larger or smaller, use predefined zoom levels.
- Shortcuts in the Edit menu like in Firefox.
- Save the state of inspector across sessions.
- Add option to automatically restore the session on startup.
- Fix segfault caused by the clear private data dialog.
- Only include the sha1 and change number in the version when using git.
- Add QTRY_COMPARE and QTRY_VERIFY for the autotests to use.
And the full list of the authors, now even longer!
P.S. For those of you using WebKit trunk on Windows I have been informed that flash support is in/done and to please report any issues you notice.


Charlie writes that he has continued his extra reading and writing assignments for Bill and he notices a trend in the books Bill gives him. Like liljables May 15, I read this book for the first time fifteen years ago, and now I have the pleasure of reading it again with the Teen Book Club here at Whistler Public Library.
He cannot control his flashbacks of Aunt Helen, who died in a car crash on her way to buy him a birthday gift.
Five out of five stars. The standout performance comes from Ezra Miller, however, capturing our attention from the moment he steps on screen, generating the larger-than-life feeling of the character written in the book.
Knowing that his parents became unexpectedly pregnant at a young age, Charlie realizes that adolescence and early adulthood are often when people have life-changing experiences, like his parents. By declaring that he finally understands the end of the poem, Charlie is saying that he identifies with the author of what might have been a suicide note, indicating to the reader just how rock-bottom Charlie feels.
The story then follows Charlie as he navigates new friendships and experiences the ups-and-downs of being a teenager, all while dealing with some demons of his past. Nothing ever really felt right until I directed the movie from my own book," Chbosky tells Film.
Later that evening, Sam kisses Charlie because she wants his first kiss to be from someone who loves him. I was offered to sell it or auction it and I passed up every opportunity until the moment was right.
Patrick and Brad's relationship is discovered by Brad's abusive father, and Brad disappears from school for a few days. Get a box of tissues, a blanket, a cup of tea. Maybe this was just me, but that really bugged me!
He promises her to only cry for important things from now on. The main character, Charlie, is a shy high school boy who describes his life through a series of letters to a friend of his who remains unnamed.
If you are a sucker for good quotes, this holds many. You also worked steadily on the TV series Jericho, which you co-created. My playlist right now is a fun combination. Eventually, he discovers that his sister is pregnant and agrees to bring her to an abortion clinic without telling anyone.
It captures so much about what it costs and how brave it is to reach out and connect to others, to another. After Patrick told him he was gay, Charlie did research on the gay community.Sep 20, · Watch video · Directed by Stephen Chbosky. With Logan Lerman, Emma Watson, Ezra Miller, Paul Rudd.
The Perks of Being a Wallflower () 8 / Want to share IMDb's rating on your own site? Use the HTML below. Last evening I attended a screening that included a fascinating Q&A with writer/director Stephen Chbosky. It reminded me of how personal and 8/10(K). After 50 pages I would have written that The Perks of Being a Wallflower was poorly written, boring and tasteless.
After pages I would have clapped because really, wow, Stephen Chbosky really did want to tick all the strong issues boxes, haha. /sarcasm. One of the most honest and assured teen dramas in recent memory, writer/director Stephen Chbosky's The Perks of Being a Wallflower feels in many ways like vintage John Hughes, only infused with a bit more substance and transplanted to the mid-'90s.
Filled with expressive, well-rounded characters, directed with restraint but remarkable attention 8/ Stephen Chbosky has been influenced by a lot of angsty classics, like The Graduate and Catcher in the Rye.
Inhe made his own contribution to the canon, releasing The Perks of Being a Wallflower, a young-adult novel whose tough themes helped make it a formidable word-of-mouth success, becoming. - Stephen Chbosky's The Perks of Being a Wallflower The complexities of growing up can be overwhelming, but one must overcome the realities of the wild and poignant roller coaster of youth in order to live a sensible and productive life.
Watch video · Chbosky, who published "Perks of Being a Wallflower" inreveals that there was never any doubt he would direct his coming-of-age opus.