From a9bc5fb83b7a0a1c93050477b5dbf798155701ca Mon Sep 17 00:00:00 2001 From: Daniel Edler Date: Mon, 30 Sep 2019 03:47:25 +0200 Subject: [PATCH] v2.0.0-beta.1 commit 3b17045b449c6632a4e74c92504b46f8c8c2a67a Merge: 8315f4a 5fed4a4 Author: Daniel Edler Date: Mon Sep 30 03:33:10 2019 +0200 Merge branch 'master' into develop commit 8315f4aa01df5486887fa7f59dd9e3513a430fa5 Author: Daniel Edler Date: Mon Sep 30 03:25:05 2019 +0200 fix: don't add x scroll outside if multi input fixes #12 commit 1ceea7f31f90bb78427ae948b43f59f8a866a4d0 Author: Daniel Edler Date: Mon Sep 30 03:03:36 2019 +0200 fix: enable run ancestral states analysis fixes #13 commit 2b41fdc3c5e89e88e10cba4a78774a41a6ec6861 Author: Daniel Edler Date: Mon Sep 30 02:45:34 2019 +0200 improvement: rename model to analysis and fix typo commit a5f99a5d2cc2207b598c8758383846071cef81ec Author: Daniel Edler Date: Mon Sep 30 02:19:38 2019 +0200 fix: move subst. model from alignment to analysis Keep AA Matrix name but also move multistate model. fixes #45 and fixes #44 commit 0c40d98f27685033dd88474b35489b1c25443f68 Author: Daniel Edler Date: Sun Sep 29 22:54:26 2019 +0200 fix: open concatenated alignment Write file before run only on open event. Also let the user open the partition file. fixes #8 commit 774ec0b3fed22390239f687e7bfb5147b0a28447 Author: Daniel Edler Date: Sun Sep 29 21:33:45 2019 +0200 fix: specify correction type for ASC prefix fixes #14 commit 973600052183adc348acc7d4d17286c5ee588f41 Author: Daniel Edler Date: Sun Sep 29 20:00:01 2019 +0200 fix: don't show error message on cancel raxml fixes #11 commit e6f5c7f4a8c8a056aaebc448efadcb8854ca0f7d Author: Daniel Edler Date: Sun Sep 29 18:08:32 2019 +0200 feat: spinning indicator while running fixes #27 commit 14ddc2ba3901b9066df237fb8f5a074a243156ed Author: Daniel Edler Date: Sun Sep 29 17:14:37 2019 +0200 fix: add missing outgroup option in one analysis fixes #42 commit 2cf0a37480bfa78e200fa6b32805d64dc5c4762b Author: Daniel Edler Date: Sun Sep 29 17:01:23 2019 +0200 style: increase font size in console. Fixes #41 --- src/app/AlignmentCard.js | 29 +++-- src/app/App.js | 7 +- src/app/Console.js | 2 +- src/app/Input.js | 3 +- src/app/Model.js | 9 +- src/app/Raxml.js | 6 +- src/app/TreeCard.js | 15 +-- src/app/components/LoadingButton.js | 54 +++++++++ src/app/components/OptionSelect.js | 2 +- src/app/components/SnackbarMessage.js | 4 +- src/app/store/Alignment.js | 87 +++++--------- src/app/store/Run.js | 157 +++++++++++++++++++++++--- src/main/api.js | 17 +-- src/settings/raxml.js | 12 ++ 14 files changed, 288 insertions(+), 116 deletions(-) create mode 100644 src/app/components/LoadingButton.js diff --git a/src/app/AlignmentCard.js b/src/app/AlignmentCard.js index 2872f358..b394c3c9 100644 --- a/src/app/AlignmentCard.js +++ b/src/app/AlignmentCard.js @@ -152,23 +152,27 @@ function AlignmentCard({ className, alignment }) { // { alignment.typecheckingComplete ? 'Checking...' : 'Pending...' } // ); + const individualSubstitutionModel = false; + const Content = alignment.showPartition ? ( ) : (
- - - Substitution model - + { individualSubstitutionModel ? ( + + + Substitution model + + ) : null } { alignment.modelExtra ? ( option.setValue(e.target.value) } diff --git a/src/app/components/SnackbarMessage.js b/src/app/components/SnackbarMessage.js index 49564d44..6459e0ec 100644 --- a/src/app/components/SnackbarMessage.js +++ b/src/app/components/SnackbarMessage.js @@ -21,14 +21,14 @@ const variantIcon = { const useStyles = makeStyles(theme => ({ success: { - backgroundColor: theme.palette.primary.main, + backgroundColor: theme.palette.output.main, }, error: { // backgroundColor: theme.palette.error.main, backgroundColor: '#f2401b', }, info: { - backgroundColor: theme.palette.primary.main, + backgroundColor: theme.palette.input.main, }, warning: { backgroundColor: amber[700], diff --git a/src/app/store/Alignment.js b/src/app/store/Alignment.js index 96c8df81..c6611599 100644 --- a/src/app/store/Alignment.js +++ b/src/app/store/Alignment.js @@ -2,23 +2,14 @@ import { observable, computed, action, runInAction } from 'mobx'; import { ipcRenderer } from 'electron'; import * as ipc from '../../constants/ipc'; import parsePath from 'parse-filepath'; -import * as raxmlSettings from '../../settings/raxml'; import { join } from 'path'; import fs from 'fs'; import util from 'util'; import union from 'lodash/union'; import intersection from 'lodash/intersection'; - -const modelOptions = { - 'protein': raxmlSettings.aminoAcidSubstitutionModelOptions, - 'binary': raxmlSettings.binarySubstitutionModelOptions, - 'mixed': raxmlSettings.mixedSubstitutionModelOptions, - 'multistate': raxmlSettings.multistateSubstitutionModelOptions, - 'dna': raxmlSettings.nucleotideSubstitutionModelOptions, - 'rna': raxmlSettings.nucleotideSubstitutionModelOptions, - 'ambiguousDna': raxmlSettings.nucleotideSubstitutionModelOptions, - 'ambiguousRna': raxmlSettings.nucleotideSubstitutionModelOptions, -}; +import * as raxmlSettings from '../../settings/raxml'; +const { modelOptions } = raxmlSettings; +const writeFile = util.promisify(fs.writeFile); class Alignment { run = null; @@ -26,15 +17,7 @@ class Alignment { @observable error = null; @observable dataType = undefined; - @observable model = ''; @observable aaMatrixName = raxmlSettings.aminoAcidSubstitutionMatrixOptions.default; - @computed get modelFlagName() { - let name = this.model; - if (this.dataType === 'protein') { - name += this.aaMatrixName; - } - return name; - } @observable size = 0; @observable fileFormat = undefined; @@ -58,10 +41,6 @@ class Alignment { // @observable taxons = []; - // TODO: This should change all other multistate models if available, according to documentation: - // If you have several partitions that consist of multi-state characters the model specified via -K will be applied to all models. Thus, it is not possible to assign different models to distinct multi-state partitions! - @observable multistateModel = raxmlSettings.kMultistateSubstitutionModelOptions.default; - // Partition stuff @observable showPartition = false; @observable partitionText = ""; @@ -138,19 +117,6 @@ class Alignment { return 'ok'; } - // @computed - // get loading() { - // return !this.checkRunComplete; - // } - - @computed - get modelOptions() { - if (!this.dataType) { - return []; - } - return modelOptions[this.dataType].options; - } - @computed get modelExtra() { switch (this.dataType) { @@ -161,13 +127,6 @@ class Alignment { value: this.aaMatrixName, onChange: this.onChangeAAMatrixName, }; - case 'multistate': - return { - label: 'Multistate model', - options: raxmlSettings.kMultistateSubstitutionModelOptions.options, - value: this.multistateModel, - onChange: this.onChangeMultistateModel, - }; default: return null; } @@ -181,6 +140,9 @@ class Alignment { // Receive a progress update for one of the alignments being parsed ipcRenderer.on(ipc.ALIGNMENT_PARSE_SUCCESS, (event, { id, alignment }) => { if (id === this.id) { + if (alignment.dataType !== this.run.dataType) { + this.run.substitutionModel.value = modelOptions[alignment.dataType].default; + } runInAction(() => { this.sequences = alignment.sequences; this.fileFormat = alignment.fileFormat; @@ -190,7 +152,6 @@ class Alignment { this.numSequencesParsed = this.numSequences; this.dataType = alignment.dataType; this.typecheckingComplete = alignment.typecheckingComplete; - this.model = modelOptions[alignment.dataType].default; this.loading = false; }); }; @@ -246,12 +207,6 @@ class Alignment { this.aaMatrixName = event.target.value; } - @action - onChangeMultistateModel = (event) => { - console.log('onChangeMultistateModel'); - this.multistateModel = event.target.value; - } - getSequenceCode = (taxon) => { // TODO: Sort sequences on taxon for quicker search, intersection and union const seq = this.sequences.find(seq => seq.taxon === taxon); @@ -376,25 +331,40 @@ class FinalAlignment { } @action - openFile = () => { + openFile = async () => { + await this.writeConcatenatedAlignment(); ipcRenderer.send(ipc.FILE_OPEN, this.path); }; @action - showFileInFolder = () => { + openPartition = async () => { + await this.writeConcatenatedAlignmentAndPartition(); + ipcRenderer.send(ipc.FILE_OPEN, this.partitionFilePath); + }; + + @action + showFileInFolder = async () => { + await this.writeConcatenatedAlignment(); ipcRenderer.send(ipc.FILE_SHOW_IN_FOLDER, this.path); }; @action - openFolder = () => { + openFolder = async () => { + await this.writeConcatenatedAlignmentAndPartition(); ipcRenderer.send(ipc.FOLDER_OPEN, this.dir); }; @action writeConcatenatedAlignmentAndPartition = async () => { + await this.writeConcatenatedAlignment(); + await this.writePartition(); + }; + + @action + writeConcatenatedAlignment = async () => { const { taxons, numSequences } = this; - console.log(`Write concatenated alignment in FASTA format to ${this.path}..`); try { + console.log(`Write concatenated alignment in FASTA format to ${this.path}..`); const writeStream = fs.createWriteStream(this.path); const write = util.promisify(writeStream.write); const end = util.promisify(writeStream.end); @@ -413,16 +383,19 @@ class FinalAlignment { console.error('Error writing concatenated alignment:', err); throw err; } + }; + + @action + writePartition = async () => { try { console.log(`Writing partition to ${this.partitionFilePath}...`); - const writeFile = util.promisify(fs.writeFile); await writeFile(this.partitionFilePath, this.partitionFileContent); } catch (err) { console.error('Error writing partition:', err); throw err; } - }; + } } diff --git a/src/app/store/Run.js b/src/app/store/Run.js index abe772ca..40bce659 100644 --- a/src/app/store/Run.js +++ b/src/app/store/Run.js @@ -9,9 +9,12 @@ import { promisedComputed } from 'computed-async-mobx'; import { join } from 'path'; import filenamify from 'filenamify'; import util from 'electron-util'; +import * as raxmlSettings from '../../settings/raxml'; +const { modelOptions } = raxmlSettings; export const MAX_NUM_CPUS = cpus().length; + const raxmlBinarySuffix = util.platform({ macos: '-Mac', windows: '-Windows.exe', @@ -121,9 +124,9 @@ class NumRepetitions extends Option { @computed get notAvailable() { return !this.run.analysisOption.params.includes(params.reps); } } -//TODO: Another branch length option for FT? ('compute brL' vs 'BS brL' for the rest) +//TODO: Another branch lengths option for FT? ('compute brL' vs 'BS brL' for the rest) class BranchLength extends Option { - constructor(run) { super(run, false, 'BS brL', 'Compute branch length', 'Optimize model parameters and branch lengths for the given input tree'); } + constructor(run) { super(run, false, 'BS brL', 'Compute branch lengths', 'Optimize model parameters and branch lengths for the given input tree'); } @computed get notAvailable() { return !this.run.analysisOption.params.includes(params.brL); } } @@ -151,6 +154,41 @@ class OutGroup extends Option { //TODO: Allow multiple selections } +class SubstitutionModel extends Option { + constructor(run) { super(run, 'GTRGAMMA', 'Substitution model'); } + @computed get options() { + if (!this.run.haveAlignments) { + return []; + } + const modelSettings = modelOptions[this.run.dataType]; + if (!modelSettings) { + return []; + } + return modelSettings.options.map(value => ({ value, title: value })); + } + @computed get notAvailable() { return !this.run.haveAlignments; } + @computed get cmdValue() { + let model = this.value; + if (this.run.dataType === 'protein') { + // model += this.aaMatrixName.value; + model += this.run.alignments[0].aaMatrixName; + } + return model; + } +} + +class AAMatrixName extends Option { + constructor(run) { super(run, 'BLOSUM62', 'Matrix name', 'Amino Acid Substitution Matrix name'); } + options = raxmlSettings.aminoAcidSubstitutionMatrixOptions.options.map(value => ({ value, title: value })); + @computed get notAvailable() { return this.run.dataType !== 'protein'; } +} + +class MultistateModel extends Option { + constructor(run) { super(run, 'GTR', 'Multistate model'); } + options = raxmlSettings.kMultistateSubstitutionModelOptions.options.map(value => ({ value, title: value })); + @computed get notAvailable() { return this.run.dataType !== 'multistate'; } +} + class Tree extends Option { constructor(run) { super(run, '', 'Tree', ''); } @computed get notAvailable() { @@ -218,6 +256,9 @@ class Run { sHlike = new SHlike(this); combinedOutput = new CombinedOutput(this); outGroup = new OutGroup(this); + substitutionModel = new SubstitutionModel(this); + aaMatrixName = new AAMatrixName(this); + multistateModel = new MultistateModel(this); startingTree = new StartingTree(this); tree = new Tree(this); @@ -300,10 +341,28 @@ class Run { finalAlignment = new FinalAlignment(this); + // @observable dataType = 'mixed'; + @computed get dataType() { + const numAlignments = this.alignments.length; + if (numAlignments === 0) { + return 'none'; + } + const firstType = this.alignments[0].dataType; + if (numAlignments === 1) { + return firstType; + } + for (let i = 1; i < numAlignments; ++i) { + if (this.alignments[i].dataType !== firstType) { + return 'mixed'; + } + } + return firstType; + } + @observable error = null; @computed get missing() { - if (!this.tree.notAvailable && !this.tree.value) { + if (!this.tree.notAvailable && !this.tree.filePath) { return 'Missing tree, please load one.'; } return ''; @@ -311,8 +370,10 @@ class Run { @observable running = false; @observable finished = false; + @observable exitCode = 0; @action clearFinished = () => { this.finished = false; + this.exitCode = 0; } atomFinished; @@ -343,7 +404,13 @@ class Run { } first.push('-f', 'E'); first.push('-p', this.seedParsimony); - first.push('-m', this.finalAlignment.modelFlagName); + first.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + first.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + first.push('-K', this.multistateModel.value); + } first.push('-n', this.outputFilenameSafe); first.push('-s', this.finalAlignment.path); if (this.disableCheckUndeterminedSequence) { @@ -363,7 +430,13 @@ class Run { next.push('-T', this.numThreads.value); } next.push('-f', 'e'); - next.push('-m', this.finalAlignment.modelFlagName); + next.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + next.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + next.push('-K', this.multistateModel.value); + } next.push('-t', `${treeFile1}`); next.push('-n', `brL.${this.outputFilenameSafe}`); next.push('-s', `${this.finalAlignment.path}`); @@ -382,7 +455,13 @@ class Run { next.push('-T', this.numThreads.value); } next.push('-f', 'e'); - next.push('-m', this.finalAlignment.modelFlagName); + next.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + next.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + next.push('-K', this.multistateModel.value); + } next.push('-t', `${treeFile2}`); next.push('-n', `sh.${this.outputFilenameSafe}`); next.push('-s', `${this.finalAlignment.path}`); @@ -402,7 +481,13 @@ class Run { first.push('-T', this.numThreads.value); } first.push('-f', 'd'); - first.push('-m', this.finalAlignment.modelFlagName); + first.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + first.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + first.push('-K', this.multistateModel.value); + } first.push('-N', this.numRepetitions.value); if (this.disableCheckUndeterminedSequence) { first.push('-O'); @@ -424,7 +509,13 @@ class Run { next.push('-T', this.numThreads.value); } next.push('-f', 'e'); - next.push('-m', this.finalAlignment.modelFlagName); + next.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + next.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + next.push('-K', this.multistateModel.value); + } next.push('-t', `${treeFile}`); next.push('-n', `sh.${this.outputFilenameSafe}`); next.push('-s', `${this.finalAlignment.path}`); @@ -454,12 +545,21 @@ class Run { first.push('-x', this.seedRapidBootstrap); first.push('-p', this.seedParsimony); first.push('-N', this.numRepetitions.value); - first.push('-m', this.finalAlignment.modelFlagName); + first.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + first.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + first.push('-K', this.multistateModel.value); + } first.push('-n', this.outputFilenameSafe); first.push('-s', this.finalAlignment.path); if (this.disableCheckUndeterminedSequence) { first.push('-O'); } + if (this.outGroup.cmdValue) { + first.push('-o', this.outGroup.cmdValue); + } first.push('-w', `${this.outputDir}`); if (this.alignments.length > 1) { first.push('-q', `${this.finalAlignment.partitionFilePath}`); @@ -491,7 +591,13 @@ class Run { first.push('-T', this.numThreads.value); } first.push('-b', this.seedBootstrap); - first.push('-m', this.finalAlignment.modelFlagName); + first.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + first.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + first.push('-K', this.multistateModel.value); + } if (this.branchLength.value) { first.push('-k'); } @@ -514,7 +620,13 @@ class Run { second.push('-T', this.numThreads.value); } second.push('-f', 'd'); - second.push('-m', this.finalAlignment.modelFlagName); + second.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + second.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + second.push('-K', this.multistateModel.value); + } second.push('-p', this.seedParsimony); second.push('-N', this.numRuns.value); second.push('-n', outputFilenameSafe2); @@ -536,7 +648,13 @@ class Run { third.push('-f', 'b'); third.push('-t', treeFile); third.push('-z', treesFile); - third.push('-m', this.finalAlignment.modelFlagName); + third.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + third.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + third.push('-K', this.multistateModel.value); + } third.push('-n', this.outputFilenameSafe); third.push('-s', this.finalAlignment.path); if (this.disableCheckUndeterminedSequence) { @@ -569,7 +687,13 @@ class Run { first.push('-f', 'A'); first.push('-t', this.tree.filePath); first.push('-s', this.finalAlignment.path); - first.push('-m', this.finalAlignment.modelFlagName); + first.push('-m', this.substitutionModel.cmdValue); + if (this.substitutionModel.value.startsWith('ASC_')) { + first.push('--asc-corr=lewis'); + } + if (!this.multistateModel.notAvailable) { + first.push('-K', this.multistateModel.value); + } first.push('-n', `${this.outputFilenameSafe}`); first.push('-w', `${this.outputDir}`); if (this.alignments.length > 1) { @@ -632,7 +756,7 @@ class Run { @observable code = undefined; @observable createdAt = undefined; @observable data = ''; - @observable dataType = undefined; + // @observable dataType = undefined; @observable flagsrunCode = undefined; @observable flagsrunData = undefined; @observable globalArgs = {}; @@ -792,12 +916,13 @@ class Run { }; @action - onRunFinished = (event, { id, resultDir, resultFilenames }) => { + onRunFinished = (event, { id, resultDir, resultFilenames, exitCode }) => { if (id === this.id) { - console.log(`Process ${id} finished with result filenames ${resultFilenames} in dir ${resultDir}.`); + console.log(`Process ${id} finished with exitCode '${exitCode}' and result filenames ${resultFilenames} in dir ${resultDir}.`); this.resultDir = resultDir; this.atomFinished.reportChanged(); this.finished = true; + this.exitCode = exitCode; this.afterRun(); } }; diff --git a/src/main/api.js b/src/main/api.js index e0492400..9cfec139 100644 --- a/src/main/api.js +++ b/src/main/api.js @@ -131,10 +131,14 @@ ipcMain.on(ipc.RUN_START, async (event, { id, args, binaryName, outputDir, outpu } } + let exitCode = 0; for (const arg of args) { try { console.log(`Run ${binaryName} with args:`, arg) - await runProcess(id, event, binaryPath, arg); + exitCode = await runProcess(id, event, binaryPath, arg); + if (exitCode !== 0) { + break; + } } catch (err) { console.error('Run error:', err); @@ -147,8 +151,7 @@ ipcMain.on(ipc.RUN_START, async (event, { id, args, binaryName, outputDir, outpu const resultFilenames = filenames.filter(filename => filename.endsWith(outputFilename)); - send(event, ipc.RUN_FINISHED, { id, resultDir: outputDir, resultFilenames }); - + send(event, ipc.RUN_FINISHED, { id, resultDir: outputDir, resultFilenames, exitCode }); }); @@ -214,16 +217,16 @@ async function runProcess(id, event, binaryPath, args) { const onQuit = message => (code, signal) => { if (exited) { return; } + console.log(`Process finished with event '${message}' and error/code/signal:`, signal || code); exited = true; - console.log('Exited with error (or code or signal):', code || signal); delete state.processes[id]; if (message === 'error') { return reject(code); // code is an error object on 'error' event } - if (code === 0) { - return resolve(code); + if (!code) { + return resolve(signal || code); // Add code last to get through code 0 } - return reject(new Error(`Exited with code ${code || signal}. Check console output for more information.`)); + return reject(new Error(`Exited with code ${signal || code}. Check console output for more information.`)); } ['error', 'exit', 'close'].forEach(message => { diff --git a/src/settings/raxml.js b/src/settings/raxml.js index 5578b599..f591b114 100644 --- a/src/settings/raxml.js +++ b/src/settings/raxml.js @@ -195,6 +195,18 @@ export const aminoAcidSubstitutionMatrixOptions = { ] }; +export const modelOptions = { + 'protein': aminoAcidSubstitutionModelOptions, + 'binary': binarySubstitutionModelOptions, + 'mixed': mixedSubstitutionModelOptions, + 'multistate': multistateSubstitutionModelOptions, + 'dna': nucleotideSubstitutionModelOptions, + 'rna': nucleotideSubstitutionModelOptions, + 'ambiguousDna': nucleotideSubstitutionModelOptions, + 'ambiguousRna': nucleotideSubstitutionModelOptions, +}; + + // -N export const numberRunsOptions = { argument: 'N',