diff --git a/src/apps/cif2bcif/converter.ts b/src/apps/cif2bcif/converter.ts index b663a301e60b8a7119875dbfac6b9fd3e850efaa..1a3a08221bdb79e9808e69dad4651aa141e2cd21 100644 --- a/src/apps/cif2bcif/converter.ts +++ b/src/apps/cif2bcif/converter.ts @@ -9,10 +9,11 @@ import CIF, { Category } from 'mol-io/reader/cif' import * as Encoder from 'mol-io/writer/cif' import * as fs from 'fs' import classify from './field-classifier' +import { Run } from 'mol-task' async function getCIF(path: string) { const str = fs.readFileSync(path, 'utf8'); - const parsed = await CIF.parseText(str)(); + const parsed = await Run(CIF.parseText(str)); if (parsed.isError) { throw new Error(parsed.toString()); } diff --git a/src/apps/combine-mmcif/index.ts b/src/apps/combine-mmcif/index.ts index dd13d652deb5bebb6cbc153ed86bdf8d061ac8cd..d107f81132f4110c1c4d01152711ba7f8a9155c5 100644 --- a/src/apps/combine-mmcif/index.ts +++ b/src/apps/combine-mmcif/index.ts @@ -13,11 +13,11 @@ require('util.promisify').shim(); const readFile = util.promisify(fs.readFile); const writeFile = util.promisify(fs.writeFile); +import { Run, Progress } from 'mol-task' import { Database, Table, DatabaseCollection } from 'mol-data/db' import CIF from 'mol-io/reader/cif' // import { CCD_Schema } from 'mol-io/reader/cif/schema/ccd' import * as Encoder from 'mol-io/writer/cif' -import Computation from 'mol-util/computation' import { mmCIF_Schema, mmCIF_Database } from 'mol-io/reader/cif/schema/mmcif'; import { CCD_Schema } from 'mol-io/reader/cif/schema/ccd'; import { BIRD_Schema } from 'mol-io/reader/cif/schema/bird'; @@ -48,10 +48,6 @@ export async function ensureDataAvailable() { await ensureAvailable(BIRD_PATH, BIRD_URL) } -function showProgress(tag: string, p: Computation.Progress) { - console.log(`[${tag}] ${p.message} ${p.isIndeterminate ? '' : (p.current / p.max * 100).toFixed(2) + '% '}(${p.elapsedMs | 0}ms)`) -} - export async function readFileAsCollection<S extends Database.Schema>(path: string, schema: S) { const parsed = await parseCif(await readFile(path, 'utf8')) return CIF.toDatabaseCollection(schema, parsed.result) @@ -80,14 +76,10 @@ export async function getBIRD() { } async function parseCif(data: string|Uint8Array) { - const comp = CIF.parse(data) - const ctx = Computation.observable({ - updateRateMs: 250, - observer: p => showProgress(`cif parser ${typeof data === 'string' ? 'string' : 'binary'}`, p) - }); - console.time('parse cif') - const parsed = await comp(ctx); - console.timeEnd('parse cif') + const comp = CIF.parse(data); + console.time('parse cif'); + const parsed = await Run(comp, p => console.log(Progress.format(p)), 250); + console.timeEnd('parse cif'); if (parsed.isError) throw parsed; return parsed } diff --git a/src/apps/schema-generator/schema-from-mmcif-dic.ts b/src/apps/schema-generator/schema-from-mmcif-dic.ts index 14859b34217695e44b43084a672debc175984e86..711fa67749f3e636f840f7c7e5505d9cfaa141f7 100644 --- a/src/apps/schema-generator/schema-from-mmcif-dic.ts +++ b/src/apps/schema-generator/schema-from-mmcif-dic.ts @@ -14,11 +14,12 @@ import CIF from 'mol-io/reader/cif' import { generateSchema } from './util/cif-dic' import { generate } from './util/generate' import { Filter, mergeFilters } from './util/json-schema' +import { Run } from 'mol-task'; async function runGenerateSchema(name: string, fieldNamesPath?: string, minCount = 0, typescript = false, out?: string) { await ensureMmcifDicAvailable() const comp = CIF.parseText(fs.readFileSync(MMCIF_DIC_PATH, 'utf8')) - const parsed = await comp(); + const parsed = await Run(comp); if (parsed.isError) throw parsed // console.log(fieldNamesPath, minCount) @@ -47,7 +48,7 @@ async function runGenerateSchema(name: string, fieldNamesPath?: string, minCount async function getFieldNamesFilter(fieldNamesPath: string): Promise<Filter> { const fieldNamesStr = fs.readFileSync(fieldNamesPath, 'utf8') - const parsed = await Csv(fieldNamesStr, { noColumnNames: true })(); + const parsed = await Run(Csv(fieldNamesStr, { noColumnNames: true })); if (parsed.isError) throw parser.error const csvFile = parsed.result; @@ -68,7 +69,7 @@ async function getFieldNamesFilter(fieldNamesPath: string): Promise<Filter> { async function getUsageCountsFilter(minCount: number): Promise<Filter> { const usageCountsStr = fs.readFileSync(MMCIF_USAGE_COUNTS_PATH, 'utf8') - const parsed = await Csv(usageCountsStr, { delimiter: ' ' })(); + const parsed = await Run(Csv(usageCountsStr, { delimiter: ' ' })); if (parsed.isError) throw parser.error const csvFile = parsed.result; diff --git a/src/apps/structure-info/index.ts b/src/apps/structure-info/index.ts index 59f4664f3c050637cac2f418c30168da6cba26cc..7a911acb48122191c26689017d9fdde7ea2f5857 100644 --- a/src/apps/structure-info/index.ts +++ b/src/apps/structure-info/index.ts @@ -10,20 +10,12 @@ require('util.promisify').shim(); // import { Table } from 'mol-data/db' import CIF from 'mol-io/reader/cif' -import Computation from 'mol-util/computation' import { Model } from 'mol-model/structure' - -function showProgress(tag: string, p: Computation.Progress) { - console.log(`[${tag}] ${p.message} ${p.isIndeterminate ? '' : (p.current / p.max * 100).toFixed(2) + '% '}(${p.elapsedMs | 0}ms)`) -} +import { Run, Progress } from 'mol-task' async function parseCif(data: string|Uint8Array) { - const comp = CIF.parse(data) - const ctx = Computation.observable({ - updateRateMs: 250, - observer: p => showProgress(`cif parser ${typeof data === 'string' ? 'string' : 'binary'}`, p) - }); - const parsed = await comp(ctx); + const comp = CIF.parse(data); + const parsed = await Run(comp, p => console.log(Progress.format(p)), 250); if (parsed.isError) throw parsed; return parsed } diff --git a/src/examples/computation.ts b/src/examples/task.ts similarity index 54% rename from src/examples/computation.ts rename to src/examples/task.ts index 8068bff3191764eacce29e8cc12b56377410daee..41025f042c48eb1252f14483ba94e1f10c771470 100644 --- a/src/examples/computation.ts +++ b/src/examples/task.ts @@ -4,7 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { Task, Run, Progress, Scheduler, now } from 'mol-task' +import { Task, Run, Progress, Scheduler, now, MultistepTask, chunkedSubtask } from 'mol-task' export async function test1() { const t = Task.create('test', async () => 1); @@ -13,11 +13,16 @@ export async function test1() { } function messageTree(root: Progress.Node, prefix = ''): string { - if (!root.children.length) return `${prefix}${root.progress.taskName}: ${root.progress.message}`; + const p = root.progress; + if (!root.children.length) { + if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}`; + return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}`; + } const newPrefix = prefix + ' |_ '; const subTree = root.children.map(c => messageTree(c, newPrefix)); - return `${prefix}${root.progress.taskName}: ${root.progress.message}\n${subTree.join('\n')}`; + if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}\n${subTree.join('\n')}`; + return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}\n${subTree.join('\n')}`; } function createTask<T>(delayMs: number, r: T): Task<T> { @@ -38,7 +43,7 @@ export function abortAfter(delay: number) { }); } -function testTree() { +export function testTree() { return Task.create('test o', async ctx => { await Scheduler.delay(250); if (ctx.shouldUpdate) await ctx.update({ message: 'hi! 1' }); @@ -60,6 +65,40 @@ function testTree() { }); } +export type ChunkedState = { i: number, current: number, total: number } + +function processChunk(n: number, state: ChunkedState): number { + const toProcess = Math.min(state.current + n, state.total); + const start = state.current; + for (let i = start; i < toProcess; i++) { + for (let j = 0; j < 1000000; j++) { + state.i += (i * j + 1 + state.i) % 1023; + state.i = state.i % 1000; + } + } + state.current = toProcess; + return toProcess - start; +} + +export const ms = MultistepTask('ms-task', ['step 1', 'step 2', 'step 3'], async (p: { i: number }, step, ctx) => { + await step(0); + + const child = Task.create('chunked', async ctx => { + const s = await chunkedSubtask(ctx, 25, { i: 0, current: 0, total: 125 }, processChunk, (ctx, s, p) => ctx.update('chunk test ' + p)) + return s.i; + }); + + await ctx.runChild(child); + await Scheduler.delay(250); + await step(1); + await chunkedSubtask(ctx, 25, { i: 0, current: 0, total: 80 }, processChunk, (ctx, s, p) => ctx.update('chunk test ' + p)) + await Scheduler.delay(250); + await step(2); + await Scheduler.delay(250); + return p.i + 3; +}) + + export function abortingObserver(p: Progress) { console.log(messageTree(p.root)); if (now() - p.root.progress.startedTime > 1000) { @@ -67,11 +106,16 @@ export function abortingObserver(p: Progress) { } } +export function logP(p: Progress) { console.log(messageTree(p.root)); } + async function test() { try { //const r = await Run(testTree(), p => console.log(messageTree(p.root)), 250); - const r = await Run(testTree(), abortingObserver, 250); - console.log(r); + //const r = await Run(testTree(), abortingObserver, 250); + //console.log(r); + + const m = await Run(ms({ i: 10 }), logP); + console.log(m); } catch (e) { console.error(e); } diff --git a/src/mol-io/reader/_spec/csv.spec.ts b/src/mol-io/reader/_spec/csv.spec.ts index cd14e30e425101170e3a4d3c48a5d530fd6c9e5c..b074b5bc7d51a2df190180065852e4f7dec8c75d 100644 --- a/src/mol-io/reader/_spec/csv.spec.ts +++ b/src/mol-io/reader/_spec/csv.spec.ts @@ -5,6 +5,7 @@ */ import Csv from '../csv/parser' +import { Run } from 'mol-task'; const csvStringBasic = `StrCol,IntCol,FloatCol # comment @@ -23,7 +24,7 @@ string2\t42\t2.44` describe('csv reader', () => { it('basic', async () => { - const parsed = await Csv(csvStringBasic)(); + const parsed = await Run(Csv(csvStringBasic)); if (parsed.isError) return; const csvFile = parsed.result; @@ -45,7 +46,7 @@ describe('csv reader', () => { }); it('advanced', async () => { - const parsed = await Csv(csvStringAdvanced)(); + const parsed = await Run(Csv(csvStringAdvanced)); if (parsed.isError) return; const csvFile = parsed.result; @@ -62,7 +63,7 @@ describe('csv reader', () => { }); it('tabs', async () => { - const parsed = await Csv(tabString, { delimiter: '\t' })(); + const parsed = await Run(Csv(tabString, { delimiter: '\t' })); if (parsed.isError) return; const csvFile = parsed.result; diff --git a/src/mol-io/reader/_spec/gro.spec.ts b/src/mol-io/reader/_spec/gro.spec.ts index 888f23c83369fa8fc27e4adec486aab86003e7ab..6254be8f9d45d287f643e2d50993e8b62eb11bbe 100644 --- a/src/mol-io/reader/_spec/gro.spec.ts +++ b/src/mol-io/reader/_spec/gro.spec.ts @@ -6,6 +6,7 @@ */ import Gro from '../gro/parser' +import { Run } from 'mol-task'; const groString = `MD of 2 waters, t= 4.2 6 @@ -26,7 +27,7 @@ const groStringHighPrecision = `Generated by trjconv : 2168 system t= 15.00000 describe('gro reader', () => { it('basic', async () => { - const parsed = await Gro(groString)(); + const parsed = await Run(Gro(groString)); if (parsed.isError) { console.log(parsed) @@ -57,7 +58,7 @@ describe('gro reader', () => { }); it('high precision', async () => { - const parsed = await Gro(groStringHighPrecision)() + const parsed = await Run(Gro(groStringHighPrecision)); if (parsed.isError) { console.log(parsed) diff --git a/src/mol-io/reader/_spec/mol2.spec.ts b/src/mol-io/reader/_spec/mol2.spec.ts index c329f61a4e5c66a28588d8424eb4327dbafb25ff..43244780636af6fbcd75010522ba9405b75ca2ff 100644 --- a/src/mol-io/reader/_spec/mol2.spec.ts +++ b/src/mol-io/reader/_spec/mol2.spec.ts @@ -1,5 +1,6 @@ import Mol2 from '../mol2/parser' +import { Run } from 'mol-task'; const Mol2String = `@<TRIPOS>MOLECULE 5816 @@ -246,7 +247,7 @@ GASTEIGER describe('mol2 reader', () => { it('basic', async () => { - const parsed = await Mol2(Mol2String)(); + const parsed = await Run(Mol2(Mol2String)); if (parsed.isError) { throw new Error(parsed.message); } @@ -297,7 +298,7 @@ describe('mol2 reader', () => { }); it('multiblocks', async () => { - const parsed = await Mol2(Mol2StringMultiBlocks)(); + const parsed = await Run(Mol2(Mol2StringMultiBlocks)); if (parsed.isError) { throw new Error(parsed.message); } @@ -348,7 +349,7 @@ describe('mol2 reader', () => { }); it('minimal', async () => { - const parsed = await Mol2(Mol2StringMinimal)(); + const parsed = await Run(Mol2(Mol2StringMinimal)); if (parsed.isError) { throw new Error(parsed.message); } diff --git a/src/mol-io/reader/cif/binary/parser.ts b/src/mol-io/reader/cif/binary/parser.ts index 4f1b705eb62810531f2d5665def1b88b189e82d4..bd53cd0ad2d569049d6145eb35481bb3187e1fae 100644 --- a/src/mol-io/reader/cif/binary/parser.ts +++ b/src/mol-io/reader/cif/binary/parser.ts @@ -9,7 +9,7 @@ import { EncodedCategory, EncodedFile } from '../../../common/binary-cif' import Field from './field' import Result from '../../result' import decodeMsgPack from '../../../common/msgpack/decode' -import Computation from 'mol-util/computation' +import { Task } from 'mol-task' function checkVersions(min: number[], current: number[]) { for (let i = 0; i < 2; i++) { @@ -37,7 +37,7 @@ function Category(data: EncodedCategory): Data.Category { } export default function parse(data: Uint8Array) { - return Computation.create<Result<Data.File>>(async ctx => { + return Task.create<Result<Data.File>>('Parse BinaryCIF', async ctx => { const minVersion = [0, 3]; try { diff --git a/src/mol-io/reader/cif/text/parser.ts b/src/mol-io/reader/cif/text/parser.ts index 8f6a466e7f7161692d898d1c5825449e42090df8..bbd4b39c83edaaa82b95b720511b9fc0baac7cb1 100644 --- a/src/mol-io/reader/cif/text/parser.ts +++ b/src/mol-io/reader/cif/text/parser.ts @@ -26,7 +26,7 @@ import * as Data from '../data-model' import Field from './field' import { Tokens, TokenBuilder } from '../../common/text/tokenizer' import Result from '../../result' -import Computation from 'mol-util/computation' +import { Task, RuntimeContext, chunkedSubtask } from 'mol-task' /** * Types of supported mmCIF tokens. @@ -42,18 +42,18 @@ const enum CifTokenType { } interface TokenizerState { - data: string; + data: string, - position: number; - length: number; - isEscaped: boolean; + position: number, + length: number, + isEscaped: boolean, - lineNumber: number; - tokenType: CifTokenType; - tokenStart: number; - tokenEnd: number; + lineNumber: number, + tokenType: CifTokenType, + tokenStart: number, + tokenEnd: number, - chunker: Computation.Chunker + runtimeCtx: RuntimeContext } /** @@ -387,7 +387,7 @@ function moveNext(state: TokenizerState) { while (state.tokenType === CifTokenType.Comment) moveNextInternal(state); } -function createTokenizer(data: string, ctx: Computation.Context): TokenizerState { +function createTokenizer(data: string, runtimeCtx: RuntimeContext): TokenizerState { return { data, length: data.length, @@ -398,7 +398,7 @@ function createTokenizer(data: string, ctx: Computation.Context): TokenizerState lineNumber: 1, isEscaped: false, - chunker: Computation.chunker(ctx, 1000000) + runtimeCtx }; } @@ -468,7 +468,7 @@ interface LoopReadState { tokenCount: number } -function readLoopChunk(state: LoopReadState, chunkSize: number) { +function readLoopChunk(chunkSize: number, state: LoopReadState) { const { tokenizer, tokens, fieldCount } = state; let tokenCount = state.tokenCount; let counter = 0; @@ -481,12 +481,14 @@ function readLoopChunk(state: LoopReadState, chunkSize: number) { return counter; } -function readLoopChunks(state: LoopReadState) { - return state.tokenizer.chunker.process( - chunkSize => readLoopChunk(state, chunkSize), - update => update({ message: 'Parsing...', current: state.tokenizer.position, max: state.tokenizer.data.length })); +function updateLoopChunk(ctx: RuntimeContext, state: LoopReadState) { + return ctx.update({ message: 'Parsing...', current: state.tokenizer.position, max: state.tokenizer.data.length }); } +// const readLoopChunks = ChunkedSubtask(1000000, +// (size, state: LoopReadState) => readLoopChunk(state, size), +// (ctx, state) => ctx.update({ message: 'Parsing...', current: state.tokenizer.position, max: state.tokenizer.data.length })); + /** * Reads a loop. */ @@ -514,7 +516,7 @@ async function handleLoop(tokenizer: TokenizerState, ctx: FrameContext): Promise tokens }; - await readLoopChunks(state); + await chunkedSubtask(tokenizer.runtimeCtx, 1000000, state, readLoopChunk, updateLoopChunk); if (state.tokenCount % fieldCount !== 0) { return { @@ -560,9 +562,9 @@ function result(data: Data.File) { * * @returns CifParserResult wrapper of the result. */ -async function parseInternal(data: string, ctx: Computation.Context) { +async function parseInternal(data: string, runtimeCtx: RuntimeContext) { const dataBlocks: Data.Block[] = []; - const tokenizer = createTokenizer(data, ctx); + const tokenizer = createTokenizer(data, runtimeCtx); let blockHeader = ''; let blockCtx = FrameContext(); @@ -574,7 +576,7 @@ async function parseInternal(data: string, ctx: Computation.Context) { let saveCtx = FrameContext(); let saveFrame: Data.Frame = Data.SafeFrame(saveCtx.categoryNames, saveCtx.categories, ''); - ctx.update({ message: 'Parsing...', current: 0, max: data.length }); + runtimeCtx.update({ message: 'Parsing...', current: 0, max: data.length }); moveNext(tokenizer); while (tokenizer.tokenType !== CifTokenType.End) { @@ -641,7 +643,7 @@ async function parseInternal(data: string, ctx: Computation.Context) { } export default function parse(data: string) { - return Computation.create<Result<Data.File>>(async ctx => { + return Task.create<Result<Data.File>>('Parse CIF', async ctx => { return await parseInternal(data, ctx); }); } \ No newline at end of file diff --git a/src/mol-io/reader/common/text/tokenizer.ts b/src/mol-io/reader/common/text/tokenizer.ts index 15a8492c74ffd939be708e62e4858a8129291106..60f14c1b135bfca7d215f3a3d8939997b4ec2ae5 100644 --- a/src/mol-io/reader/common/text/tokenizer.ts +++ b/src/mol-io/reader/common/text/tokenizer.ts @@ -6,7 +6,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -import Computation from 'mol-util/computation' +import { chunkedSubtask, RuntimeContext } from 'mol-task' export interface Tokenizer { data: string, @@ -109,17 +109,17 @@ export namespace Tokenizer { } /** Advance the state by the given number of lines and return line starts/ends as tokens. */ - export async function readLinesAsync(state: Tokenizer, count: number, chunker: Computation.Chunker): Promise<Tokens> { + export async function readLinesAsync(state: Tokenizer, count: number, ctx: RuntimeContext, initialLineCount = 100000): Promise<Tokens> { const { length } = state; const lineTokens = TokenBuilder.create(state, count * 2); let linesAlreadyRead = 0; - await chunker.process(chunkSize => { + await chunkedSubtask(ctx, initialLineCount, state, (chunkSize, state) => { const linesToRead = Math.min(count - linesAlreadyRead, chunkSize); readLinesChunk(state, linesToRead, lineTokens); linesAlreadyRead += linesToRead; return linesToRead; - }, update => update({ message: 'Parsing...', current: state.position, max: length })); + }, (ctx, state) => ctx.update({ message: 'Parsing...', current: state.position, max: length })); return lineTokens; } diff --git a/src/mol-io/reader/csv/parser.ts b/src/mol-io/reader/csv/parser.ts index 3d80e906e9c57eb219614cc171a243a0949a746a..66a7a9e1783c3107a9b618cbb0fbbaf0fdd4a6e2 100644 --- a/src/mol-io/reader/csv/parser.ts +++ b/src/mol-io/reader/csv/parser.ts @@ -9,7 +9,7 @@ import { Tokens, TokenBuilder, Tokenizer } from '../common/text/tokenizer' import * as Data from './data-model' import Field from './field' import Result from '../result' -import Computation from 'mol-util/computation' +import { Task, RuntimeContext, chunkedSubtask, } from 'mol-task' const enum CsvTokenType { Value = 0, @@ -22,7 +22,7 @@ interface State { tokenizer: Tokenizer, tokenType: CsvTokenType; - chunker: Computation.Chunker, + runtimeCtx: RuntimeContext, tokens: Tokens[], fieldCount: number, @@ -38,7 +38,7 @@ interface State { noColumnNamesRecord: boolean } -function State(data: string, ctx: Computation.Context, opts: CsvOptions): State { +function State(data: string, runtimeCtx: RuntimeContext, opts: CsvOptions): State { const tokenizer = Tokenizer(data) return { @@ -46,7 +46,7 @@ function State(data: string, ctx: Computation.Context, opts: CsvOptions): State tokenizer, tokenType: CsvTokenType.End, - chunker: Computation.chunker(ctx, 100000), + runtimeCtx, tokens: [], fieldCount: 0, @@ -206,7 +206,7 @@ function moveNext(state: State) { return newRecord } -function readRecordsChunk(state: State, chunkSize: number) { +function readRecordsChunk(chunkSize: number, state: State) { if (state.tokenType === CsvTokenType.End) return 0 let newRecord = moveNext(state); @@ -225,9 +225,8 @@ function readRecordsChunk(state: State, chunkSize: number) { } function readRecordsChunks(state: State) { - return state.chunker.process( - chunkSize => readRecordsChunk(state, chunkSize), - update => update({ message: 'Parsing...', current: state.tokenizer.position, max: state.data.length })); + return chunkedSubtask(state.runtimeCtx, 100000, state, readRecordsChunk, + (ctx, state) => ctx.update({ message: 'Parsing...', current: state.tokenizer.position, max: state.data.length })); } function addColumn (state: State) { @@ -261,7 +260,7 @@ async function handleRecords(state: State): Promise<Data.Table> { return Data.Table(state.recordCount, state.columnNames, columns) } -async function parseInternal(data: string, ctx: Computation.Context, opts: CsvOptions): Promise<Result<Data.File>> { +async function parseInternal(data: string, ctx: RuntimeContext, opts: CsvOptions): Promise<Result<Data.File>> { const state = State(data, ctx, opts); ctx.update({ message: 'Parsing...', current: 0, max: data.length }); @@ -279,7 +278,7 @@ interface CsvOptions { export function parse(data: string, opts?: Partial<CsvOptions>) { const completeOpts = Object.assign({}, { quote: '"', comment: '#', delimiter: ',', noColumnNames: false }, opts) - return Computation.create<Result<Data.File>>(async ctx => { + return Task.create<Result<Data.File>>('Parse CSV', async ctx => { return await parseInternal(data, ctx, completeOpts); }); } diff --git a/src/mol-io/reader/gro/parser.ts b/src/mol-io/reader/gro/parser.ts index d1c71ec315c3f98d47f45692838c9dfa462d4142..eee90135b4a588a565b25ee1f5354f150d987f3b 100644 --- a/src/mol-io/reader/gro/parser.ts +++ b/src/mol-io/reader/gro/parser.ts @@ -10,13 +10,13 @@ import Tokenizer from '../common/text/tokenizer' import FixedColumn from '../common/text/column/fixed' import * as Schema from './schema' import Result from '../result' -import Computation from 'mol-util/computation' +import { Task, RuntimeContext } from 'mol-task' interface State { tokenizer: Tokenizer, header: Schema.Header, numberOfAtoms: number, - chunker: Computation.Chunker + runtimeCtx: RuntimeContext } function createEmptyHeader(): Schema.Header { @@ -29,12 +29,12 @@ function createEmptyHeader(): Schema.Header { }; } -function State(tokenizer: Tokenizer, ctx: Computation.Context): State { +function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext): State { return { tokenizer, header: createEmptyHeader(), numberOfAtoms: 0, - chunker: Computation.chunker(ctx, 100000) // 100000 lines is the default chunk size for this reader + runtimeCtx }; } @@ -90,7 +90,7 @@ function handleNumberOfAtoms(state: State) { */ async function handleAtoms(state: State): Promise<Schema.Atoms> { const { tokenizer, numberOfAtoms } = state; - const lines = await Tokenizer.readLinesAsync(tokenizer, numberOfAtoms, state.chunker); + const lines = await Tokenizer.readLinesAsync(tokenizer, numberOfAtoms, state.runtimeCtx, 100000); const positionSample = tokenizer.data.substring(lines.indices[0], lines.indices[1]).substring(20); const precisions = positionSample.match(/\.\d+/g)!; @@ -137,7 +137,7 @@ function handleBoxVectors(state: State) { state.header.box = [+values[0], +values[1], +values[2]]; } -async function parseInternal(data: string, ctx: Computation.Context): Promise<Result<Schema.File>> { +async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.File>> { const tokenizer = Tokenizer(data); ctx.update({ message: 'Parsing...', current: 0, max: data.length }); @@ -156,7 +156,7 @@ async function parseInternal(data: string, ctx: Computation.Context): Promise<Re } export function parse(data: string) { - return Computation.create<Result<Schema.File>>(async ctx => { + return Task.create<Result<Schema.File>>('Parse GRO', async ctx => { return await parseInternal(data, ctx); }); } diff --git a/src/mol-io/reader/mol2/parser.ts b/src/mol-io/reader/mol2/parser.ts index f807f4ff7b78d1c3f7a051fbba88a0a69c6216de..32126acb9050dbf33e490f2d9ffed2ca98a5f76c 100644 --- a/src/mol-io/reader/mol2/parser.ts +++ b/src/mol-io/reader/mol2/parser.ts @@ -6,7 +6,7 @@ */ // NOTES -//When want to created undefined string column, must use +// When want to created undefined string column, must use // undefStr = UndefinedColumn(molecule.num_atoms, ColumnType.str) // but not // const undefPooledStr = UndefinedColumn(molecule.num_atoms, ColumnType.pooledStr); @@ -16,14 +16,14 @@ import { TokenBuilder, Tokenizer } from '../common/text/tokenizer' import TokenColumn from '../common/text/column/token' import * as Schema from './schema' import Result from '../result' -import Computation from 'mol-util/computation' +import { Task, RuntimeContext, chunkedSubtask } from 'mol-task' const { skipWhitespace, eatValue, markLine, getTokenString, readLine } = Tokenizer; interface State { tokenizer: Tokenizer, molecule: Schema.Molecule, - chunker: Computation.Chunker + runtimeCtx: RuntimeContext } function createEmptyMolecule(): Schema.Molecule { @@ -36,16 +36,16 @@ function createEmptyMolecule(): Schema.Molecule { num_sets: 0, mol_type: '', charge_type: '', - status_bits:'', + status_bits: '', mol_comment: '' }; } -function State(tokenizer: Tokenizer, ctx: Computation.Context): State { +function State(tokenizer: Tokenizer, runtimeCtx: RuntimeContext): State { return { tokenizer, molecule: createEmptyMolecule(), - chunker: Computation.chunker(ctx, 100000) + runtimeCtx }; } @@ -54,7 +54,7 @@ const reWhitespace = /\s+/g; function handleMolecule(state: State) { const { tokenizer, molecule } = state; - while(getTokenString(tokenizer) !== '@<TRIPOS>MOLECULE'){ + while (getTokenString(tokenizer) !== '@<TRIPOS>MOLECULE') { markLine(tokenizer); } @@ -84,10 +84,10 @@ function handleMolecule(state: State) { molecule.mol_comment = getTokenString(tokenizer) } -function isStatus_bit(aString: String): Boolean{ - if(aString.includes('DSPMOD') || aString.includes('TYPECOL') || aString.includes('CAP') - || aString.includes('BACKBONE') || aString.includes('DICT') || aString.includes('ESSENTIAL') - || aString.includes('WATER') || aString.includes('DIRECT')){ +function isStatus_bit(aString: String): Boolean { + if (aString.includes('DSPMOD') || aString.includes('TYPECOL') || aString.includes('CAP') + || aString.includes('BACKBONE') || aString.includes('DICT') || aString.includes('ESSENTIAL') + || aString.includes('WATER') || aString.includes('DIRECT')) { return true; } return false; @@ -101,7 +101,7 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> { let hasStatus_bit = false; // skip empty lines and '@<TRIPOS>ATOM' - while(getTokenString(tokenizer) != '@<TRIPOS>ATOM'){ + while (getTokenString(tokenizer) !== '@<TRIPOS>ATOM') { markLine(tokenizer); } @@ -113,17 +113,17 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> { // optional columns are in order "integer string float string". // Use this to find out which column is missing or empty - for(let i = 6; i < firstLineLength; i++){ - if(!isNaN(Number(firstLineArray[i]))){ - if(firstLineArray[i].indexOf('.') == -1){ + for (let i = 6; i < firstLineLength; i++) { + if (!isNaN(Number(firstLineArray[i]))) { + if (firstLineArray[i].indexOf('.') === -1) { hasSubst_id = true; - }else{ + } else { hasCharge = true; } - }else if(isNaN(Number(firstLineArray[i]))){ - if(!isStatus_bit(firstLineArray[i])){ + } else if (isNaN(Number(firstLineArray[i]))) { + if (!isStatus_bit(firstLineArray[i])) { hasSubst_name = true; - }else{ + } else { hasStatus_bit = true; } } @@ -131,7 +131,7 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> { // required columns const atom_idTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2); - const atom_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2);; + const atom_nameTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2); const xTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2); const yTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2); const zTokens = TokenBuilder.create(tokenizer, molecule.num_atoms * 2); @@ -160,28 +160,28 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> { const undefStr = Column.Undefined(molecule.num_atoms, Column.Schema.str); let numOfColumn = 6; - if(hasSubst_id){numOfColumn++} - if(hasSubst_name){numOfColumn++} - if(hasCharge){numOfColumn++} - if(hasStatus_bit){numOfColumn++} + if (hasSubst_id) { numOfColumn++ } + if (hasSubst_name) { numOfColumn++ } + if (hasCharge) { numOfColumn++ } + if (hasStatus_bit) { numOfColumn++ } tokenizer.position = initialTokenizerPosition; tokenizer.lineNumber = initialTokenizerLineNumber; const { length } = tokenizer; let linesAlreadyRead = 0; - await state.chunker.process(chunkSize => { + await chunkedSubtask(state.runtimeCtx, 100000, void 0, chunkSize => { const linesToRead = Math.min(molecule.num_atoms - linesAlreadyRead, chunkSize); - for(let i = 0; i < linesToRead; i++){ + for (let i = 0; i < linesToRead; i++) { let subst_idWritten = false; let subst_nameWritten = false; let chargeWritten = false; let status_bitWritten = false; - for(let j = 0; j < numOfColumn; j++){ + for (let j = 0; j < numOfColumn; j++) { skipWhitespace(tokenizer); tokenizer.tokenStart = tokenizer.position; eatValue(tokenizer); - switch(j){ + switch (j) { case 0: TokenBuilder.addUnchecked(atom_idTokens, tokenizer.tokenStart, tokenizer.tokenEnd); break; @@ -201,16 +201,16 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> { TokenBuilder.addUnchecked(atom_typeTokens, tokenizer.tokenStart, tokenizer.tokenEnd); break; default: - if(hasSubst_id === true && subst_idWritten === false){ + if (hasSubst_id === true && subst_idWritten === false) { TokenBuilder.addUnchecked(subst_idTokens, tokenizer.tokenStart, tokenizer.tokenEnd); subst_idWritten = true; - }else if(hasSubst_name === true && subst_nameWritten === false){ + } else if (hasSubst_name === true && subst_nameWritten === false) { TokenBuilder.addUnchecked(subst_nameTokens, tokenizer.tokenStart, tokenizer.tokenEnd); subst_nameWritten = true; - }else if(hasCharge === true && chargeWritten === false){ + } else if (hasCharge === true && chargeWritten === false) { TokenBuilder.addUnchecked(chargeTokens, tokenizer.tokenStart, tokenizer.tokenEnd); chargeWritten = true; - }else if(hasStatus_bit === true && status_bitWritten === false){ + } else if (hasStatus_bit === true && status_bitWritten === false) { TokenBuilder.addUnchecked(status_bitTokens, tokenizer.tokenStart, tokenizer.tokenEnd); status_bitWritten = true; } @@ -219,7 +219,7 @@ async function handleAtoms(state: State): Promise<Schema.Atoms> { } linesAlreadyRead += linesToRead; return linesToRead; - }, update => update({ message: 'Parsing...', current: tokenizer.position, max: length })); + }, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length })); const ret = { count: molecule.num_atoms, @@ -243,7 +243,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> { const { tokenizer, molecule } = state; let hasStatus_bit = false; - while(getTokenString(tokenizer) !== '@<TRIPOS>BOND'){ + while (getTokenString(tokenizer) !== '@<TRIPOS>BOND') { markLine(tokenizer); } @@ -252,7 +252,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> { const firstLine = readLine(tokenizer); const firstLineArray = firstLine.trim().split(/\s+/g) const firstLineLength = firstLineArray.length; - if(firstLineLength === 5){ + if (firstLineLength === 5) { hasStatus_bit = true; } @@ -273,21 +273,21 @@ async function handleBonds(state: State): Promise<Schema.Bonds> { const undefStr = Column.Undefined(molecule.num_bonds, Column.Schema.str); let numberOfColumn = 4; - if(hasStatus_bit){numberOfColumn++} + if (hasStatus_bit) { numberOfColumn++ } tokenizer.position = initialTokenizerPosition; tokenizer.lineNumber = initialTokenizerLineNumber; const { length } = tokenizer; let linesAlreadyRead = 0; - await state.chunker.process(chunkSize => { + await chunkedSubtask(state.runtimeCtx, 100000, void 0, chunkSize => { const linesToRead = Math.min(molecule.num_bonds - linesAlreadyRead, chunkSize); - for(let i = 0; i < linesToRead; i++){ - for(let j = 0; j < numberOfColumn; j++){ + for (let i = 0; i < linesToRead; i++) { + for (let j = 0; j < numberOfColumn; j++) { skipWhitespace(tokenizer); tokenizer.tokenStart = tokenizer.position; eatValue(tokenizer); - switch(j){ + switch (j) { case 0: TokenBuilder.addUnchecked(bond_idTokens, tokenizer.tokenStart, tokenizer.tokenEnd); break; @@ -308,7 +308,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> { } linesAlreadyRead += linesToRead; return linesToRead; - }, update => update({ message: 'Parsing...', current: tokenizer.position, max: length })); + }, ctx => ctx.update({ message: 'Parsing...', current: tokenizer.position, max: length })); const ret = { count: molecule.num_bonds, @@ -324,7 +324,7 @@ async function handleBonds(state: State): Promise<Schema.Bonds> { return ret; } -async function parseInternal(data: string, ctx: Computation.Context): Promise<Result<Schema.File>> { +async function parseInternal(data: string, ctx: RuntimeContext): Promise<Result<Schema.File>> { const tokenizer = Tokenizer(data); ctx.update({ message: 'Parsing...', current: 0, max: data.length }); @@ -342,7 +342,7 @@ async function parseInternal(data: string, ctx: Computation.Context): Promise<Re } export function parse(data: string) { - return Computation.create<Result<Schema.File>>(async ctx => { + return Task.create<Result<Schema.File>>('Parse MOL2', async ctx => { return await parseInternal(data, ctx); }); } diff --git a/src/mol-script/compiler.ts b/src/mol-script/compiler.ts new file mode 100644 index 0000000000000000000000000000000000000000..758322a6341b800bf710e47d6df3bf3011084a57 --- /dev/null +++ b/src/mol-script/compiler.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Expression from './expression' +import Environment from './runtime/environment' +import RuntimeExpression from './runtime/expression' +import SymbolRuntime, { RuntimeArguments } from './runtime/symbol' + +export type CompiledExpression<C, T> = (ctx: C) => T + +type Compiler<C> = <T>(expression: Expression) => CompiledExpression<C, T> +function Compiler<C>(envProvider: (ctx?: C) => Environment): Compiler<C> { + const env = envProvider(void 0); + return expression => wrap(envProvider, compile(env, expression).runtime); +} + +type CompileResult = { isConst: boolean, runtime: RuntimeExpression } + +namespace CompileResult { + export function Const(value: any): CompileResult { return { isConst: true, runtime: RuntimeExpression.constant(value) } } + export function Dynamic(runtime: RuntimeExpression): CompileResult { return { isConst: false, runtime } } +} + +function wrap<C, T>(envProvider: (ctx?: C) => Environment, runtime: RuntimeExpression<C, T>) { + return (ctx: C) => runtime(envProvider(ctx)); +} + +function noRuntimeFor(symbol: string) { + throw new Error(`Could not find runtime for symbol '${symbol}'.`); +} + +function applySymbolStatic(runtime: SymbolRuntime, args: RuntimeArguments) { + return CompileResult.Dynamic(env => runtime(env, args)) +} + +function applySymbolDynamic(head: RuntimeExpression, args: RuntimeArguments) { + return CompileResult.Dynamic(env => { + const value = head(env); + const symbol = env.symbolTable[value]; + if (!symbol) noRuntimeFor(value); + return symbol.runtime(env, args); + }) +} + +function apply(env: Environment, head: CompileResult, args: RuntimeArguments, constArgs: boolean): CompileResult { + if (head.isConst) { + const value = head.runtime(env); + const symbol = env.symbolTable[value]; + if (!symbol) throw new Error(`Could not find runtime for symbol '${value}'.`); + if (symbol.attributes.isStatic && constArgs) return CompileResult.Const(symbol.runtime(env, args)); + return applySymbolStatic(symbol.runtime, args); + } + return applySymbolDynamic(head.runtime, args); +} + +function compile(env: Environment, expression: Expression): CompileResult { + if (Expression.isLiteral(expression)) { + return CompileResult.Const(expression); + } + + const head = compile(env, expression.head); + if (!expression.args) { + return apply(env, head, [], true); + } else if (Expression.isArgumentsArray(expression.args)) { + const args = []; + let constArgs = false; + for (const arg of expression.args) { + const compiled = compile(env, arg); + constArgs = constArgs && compiled.isConst; + args.push(compiled.runtime); + } + return apply(env, head, args as any, constArgs); + } else { + const args = Object.create(null); + let constArgs = false; + for (const key of Object.keys(expression.args)) { + const compiled = compile(env, expression.args[key]); + constArgs = constArgs && compiled.isConst; + args[key] = compiled.runtime; + } + return apply(env, head, args, constArgs); + } +} + +export default Compiler \ No newline at end of file diff --git a/src/mol-script/container.ts b/src/mol-script/container.ts new file mode 100644 index 0000000000000000000000000000000000000000..f03655aa965ff19c85da5e3c4ffb3641b8e26e7b --- /dev/null +++ b/src/mol-script/container.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Expression from './expression' + +interface Container { + source?: string, + version: string, + expression: Expression +} + +export default Container \ No newline at end of file diff --git a/src/mol-script/core-symbols.ts b/src/mol-script/core-symbols.ts new file mode 100644 index 0000000000000000000000000000000000000000..67dd55ca4c4c077738192a299eefefa5aab20b79 --- /dev/null +++ b/src/mol-script/core-symbols.ts @@ -0,0 +1,184 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Type from './type' +import Symbol, { Arguments, Argument } from './symbol' +import { symbol, normalizeTable, symbolList } from './helpers' + +export namespace Types { + export type List<T = any> = ArrayLike<T> + export type Set<T = any> = { has(e: T): boolean } + + export const AnyVar = Type.Variable('a', Type.Any); + export const AnyValueVar = Type.Variable('a', Type.Any); + export const ConstrainedVar = Type.Variable('a', Type.Any, true); + + export const Regex = Type.Value<RegExp>('Core', 'Regex'); + + export const Set = <T extends Type>(t?: T) => Type.Container<Set<T['@type']>>('Core', 'Set', t || AnyValueVar); + export const List = <T extends Type>(t?: T) => Type.Container<List<T['@type']>>('Core', 'List', t || AnyVar); + export const Fn = <T extends Type>(t?: T, alias?: string) => Type.Container<(env: any) => T['@type']>('Core', 'Fn', t || AnyVar, alias); + export const Flags = <T extends Type>(t: T, alias?: string) => Type.Container<number>('Core', 'Flags', t, alias); + + export const BitFlags = Flags(Type.Num, 'BitFlags'); +} + +function unaryOp<T extends Type>(type: T, description?: string) { + return symbol(Arguments.Dictionary({ 0: Argument(type) }), type, description); +} + +function binOp<T extends Type>(type: T, description?: string) { + return symbol(Arguments.List(type, { nonEmpty: true }), type, description); +} + +function binRel<A extends Type, T extends Type>(src: A, target: T, description?: string) { + return symbol(Arguments.Dictionary({ + 0: Argument(src), + 1: Argument(src) + }), target, description); +} + +const type = { + '@header': 'Types', + bool: symbol(Arguments.Dictionary({ 0: Argument(Type.AnyValue) }), Type.Bool, 'Convert a value to boolean.'), + num: symbol(Arguments.Dictionary({ 0: Argument(Type.AnyValue) }), Type.Num, 'Convert a value to number.'), + str: symbol(Arguments.Dictionary({ 0: Argument(Type.AnyValue) }), Type.Str, 'Convert a value to string.'), + regex: symbol( + Arguments.Dictionary({ + 0: Argument(Type.Str, { description: 'Expression' }), + 1: Argument(Type.Str, { isOptional: true, description: `Flags, e.g. 'i' for ignore case` }) + }), Types.Regex, 'Creates a regular expression from a string using the ECMAscript syntax.'), + + list: symbol(Arguments.List(Types.AnyVar), Types.List()), + set: symbol(Arguments.List(Types.AnyValueVar), Types.Set()), + bitflags: symbol(Arguments.Dictionary({ 0: Argument(Type.Num) }), Types.BitFlags, 'Interpret a number as bitflags.'), + compositeKey: symbol(Arguments.List(Type.AnyValue), Type.AnyValue), +}; + +const logic = { + '@header': 'Logic', + not: unaryOp(Type.Bool), + and: binOp(Type.Bool), + or: binOp(Type.Bool), +}; + +const ctrl = { + '@header': 'Control', + eval: symbol(Arguments.Dictionary({ 0: Argument(Types.Fn(Types.AnyVar)) }), Types.AnyVar, 'Evaluate a function.'), + fn: symbol(Arguments.Dictionary({ 0: Argument(Types.AnyVar) }), Types.Fn(Types.AnyVar), 'Wrap an expression to a "lazy" function.'), + if: symbol(Arguments.Dictionary({ + 0: Argument(Type.Bool, { description: 'Condition' }), + 1: Argument(Type.Variable('a', Type.Any), { description: 'If true' }), + 2: Argument(Type.Variable('b', Type.Any), { description: 'If false' }) + }), Type.Union([Type.Variable('a', Type.Any), Type.Variable('b', Type.Any)])), + assoc: symbol(Arguments.Dictionary({ + 0: Argument(Type.Str, { description: 'Name' }), + 1: Argument(Type.Variable('a', Type.Any), {description: 'Value to assign' }) + }), Type.Variable('a', Type.Any)) +}; + +const rel = { + '@header': 'Relational', + eq: binRel(Type.Variable('a', Type.AnyValue, true), Type.Bool), + neq: binRel(Type.Variable('a', Type.AnyValue, true), Type.Bool), + lt: binRel(Type.Num, Type.Bool), + lte: binRel(Type.Num, Type.Bool), + gr: binRel(Type.Num, Type.Bool), + gre: binRel(Type.Num, Type.Bool), + inRange: symbol(Arguments.Dictionary({ + 0: Argument(Type.Num, { description: 'Value to test' }), + 1: Argument(Type.Num, { description: 'Minimum value' }), + 2: Argument(Type.Num, { description: 'Maximum value' }) + }), Type.Bool, 'Check if the value of the 1st argument is >= 2nd and <= 3rd.'), +}; + +const math = { + '@header': 'Math', + add: binOp(Type.Num), + sub: binOp(Type.Num), + mult: binOp(Type.Num), + div: binRel(Type.Num, Type.Num), + pow: binRel(Type.Num, Type.Num), + mod: binRel(Type.Num, Type.Num), + + min: binOp(Type.Num), + max: binOp(Type.Num), + + floor: unaryOp(Type.Num), + ceil: unaryOp(Type.Num), + roundInt: unaryOp(Type.Num), + abs: unaryOp(Type.Num), + sqrt: unaryOp(Type.Num), + sin: unaryOp(Type.Num), + cos: unaryOp(Type.Num), + tan: unaryOp(Type.Num), + asin: unaryOp(Type.Num), + acos: unaryOp(Type.Num), + atan: unaryOp(Type.Num), + sinh: unaryOp(Type.Num), + cosh: unaryOp(Type.Num), + tanh: unaryOp(Type.Num), + exp: unaryOp(Type.Num), + log: unaryOp(Type.Num), + log10: unaryOp(Type.Num), + atan2: binRel(Type.Num, Type.Num) +}; + +const str = { + '@header': 'Strings', + concat: binOp(Type.Str), + match: symbol(Arguments.Dictionary({ 0: Argument(Types.Regex), 1: Argument(Type.Str) }), Type.Bool) +}; + +const list = { + '@header': 'Lists', + getAt: symbol(Arguments.Dictionary({ 0: Argument(Types.List()), 1: Argument(Type.Num) }), Types.AnyVar) +}; + +const set = { + '@header': 'Sets', + has: symbol(Arguments.Dictionary({ 0: Argument(Types.Set(Types.ConstrainedVar)), 1: Argument(Types.ConstrainedVar) }), Type.Bool, 'Check if the the 1st argument includes the value of the 2nd.'), + isSubset: symbol(Arguments.Dictionary({ 0: Argument(Types.Set(Types.ConstrainedVar)), 1: Argument(Types.Set(Types.ConstrainedVar)) }), Type.Bool, 'Check if the the 1st argument is a subset of the 2nd.') +}; + +const flags = { + '@header': 'Flags', + hasAny: symbol(Arguments.Dictionary({ + 0: Argument(Types.Flags(Types.ConstrainedVar)), + 1: Argument(Types.Flags(Types.ConstrainedVar)) + }), Type.Bool, 'Check if the the 1st argument has at least one of the 2nd one\'s flags.'), + hasAll: symbol(Arguments.Dictionary({ + 0: Argument(Types.Flags(Types.ConstrainedVar)), + 1: Argument(Types.Flags(Types.ConstrainedVar)) + }), Type.Bool, 'Check if the the 1st argument has all 2nd one\'s flags.'), +} + +const table = { + '@header': 'Language Primitives', + type, + logic, + ctrl, + rel, + math, + str, + list, + set, + flags +} + +normalizeTable(table); + +export const SymbolList = symbolList(table); + +export const SymbolMap = (function() { + const map: { [id: string]: Symbol | undefined } = Object.create(null); + for (const s of SymbolList) map[s.id] = s; + return map; +})(); + +export default table; + + diff --git a/src/mol-script/expression-formatter.ts b/src/mol-script/expression-formatter.ts new file mode 100644 index 0000000000000000000000000000000000000000..71e5ca6bfebdc638ff0210f0ab8d33ddf275826e --- /dev/null +++ b/src/mol-script/expression-formatter.ts @@ -0,0 +1,127 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Expression from './expression' + +const { isLiteral, isArgumentsArray } = Expression; + +export default function format(e: Expression) { + const writer = new Writer(); + _format(e, writer); + return writer.getStr(); +} + +class Writer { + private value: string[] = []; + private currentLineLength = 0; + private prefixLength = 0; + private _prefix: string = ''; + private localPrefix: string = ''; + + private setLocal() { + this.localPrefix = ' '; + } + + newline() { + this.value.push(`\n${this._prefix}${this.localPrefix}`); + this.currentLineLength = 0; + } + + push() { + this.value.push('('); + this.currentLineLength = 0; + this.localPrefix = ''; + this.prefixLength += 2; + this._prefix = new Array(this.prefixLength + 1).join(' '); + } + + pop() { + this.value.push(')'); + this.prefixLength -= 2; + this._prefix = new Array(this.prefixLength + 1).join(' '); + } + + append(str: string) { + if (!this.currentLineLength) { + this.value.push(str); + this.currentLineLength = str.length; + } else if (this.currentLineLength + this.prefixLength + this.localPrefix.length + str.length < 80) { + this.value.push(str); + this.currentLineLength += str.length; + } else { + this.setLocal(); + this.newline(); + this.value.push(str); + this.currentLineLength = str.length; + } + } + + whitespace() { + if (this.currentLineLength + this.prefixLength + this.localPrefix.length + 1 < 80) { + this.value.push(' '); + } + } + + getStr() { + return this.value.join(''); + } +} + +function _format(e: Expression, writer: Writer) { + if (isLiteral(e)) { + if (typeof e === 'string' && (/\s/.test(e) || !e.length)) writer.append(`\`${e}\``); + else writer.append(`${e}`); + return; + } + + writer.push(); + _format(e.head, writer); + + if (!e.args) { + writer.pop(); + return; + } + + if (isArgumentsArray(e.args)) { + let prevLiteral = true; + for (const a of e.args) { + if (isLiteral(a)) { + if (prevLiteral) writer.whitespace(); + else writer.newline(); + prevLiteral = true; + } else { + prevLiteral = false; + writer.newline(); + } + _format(a, writer); + } + writer.pop(); + return; + } + + const keys = Object.keys(e.args); + if (!keys.length) { + writer.pop(); + return; + } + + if (keys.length === 1 && isLiteral(e.args[keys[0]])) { + writer.whitespace() + writer.append(`:${keys[0]}`); + writer.whitespace(); + _format(e.args[keys[0]], writer); + writer.pop(); + return; + } + + for (const a of keys) { + writer.newline(); + writer.append(`:${a}`); + writer.whitespace(); + _format(e.args[a], writer); + } + writer.pop(); +} \ No newline at end of file diff --git a/src/mol-script/expression.ts b/src/mol-script/expression.ts new file mode 100644 index 0000000000000000000000000000000000000000..c9ffdcb5b79b770be5b142be2a5a1adac3aeaaa2 --- /dev/null +++ b/src/mol-script/expression.ts @@ -0,0 +1,24 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +type Expression = + | Expression.Literal + | Expression.Apply + +namespace Expression { + export type Literal = string | number | boolean + export type Arguments = Expression[] | { [name: string]: Expression } + export interface Apply { readonly head: Expression, readonly args?: Arguments } + + export function Apply(head: Expression, args?: Arguments): Apply { return args ? { head, args } : { head }; } + + export function isArgumentsArray(e: Arguments): e is Expression[] { return e instanceof Array; } + export function isArgumentsMap(e: Arguments): e is { [name: string]: Expression } { return !(e instanceof Array); } + export function isLiteral(e: Expression): e is Expression.Literal { return !isApply(e); } + export function isApply(e: Expression): e is Expression.Apply { return !! e && !!(e as Expression.Apply).head && typeof e === 'object'; } +} + +export default Expression \ No newline at end of file diff --git a/src/mol-script/helpers.ts b/src/mol-script/helpers.ts new file mode 100644 index 0000000000000000000000000000000000000000..09ced0a60a313962856c2f911a47ab3d645f4486 --- /dev/null +++ b/src/mol-script/helpers.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Type from './type' +import Symbol, { Arguments, isSymbol } from './symbol' + +export function symbol<A extends Arguments, T extends Type<S>, S>(args: A, type: T, description?: string) { + return Symbol('', args, type, description); +} + +export function normalizeTable(table: any) { + _normalizeTable('', '', table); +} + +export function symbolList(table: any): Symbol[] { + const list: Symbol[] = []; + _symbolList(table, list); + return list; +} + +function formatKey(key: string) { + const regex = /([a-z])([A-Z])([a-z]|$)/g; + // do this twice because 'xXxX' + return key.replace(regex, (s, a, b, c) => `${a}-${b.toLocaleLowerCase()}${c}`).replace(regex, (s, a, b, c) => `${a}-${b.toLocaleLowerCase()}${c}`); +} + +function _normalizeTable(namespace: string, key: string, obj: any) { + if (isSymbol(obj)) { + obj.info.namespace = namespace; + obj.info.name = obj.info.name || formatKey(key); + obj.id = `${obj.info.namespace}.${obj.info.name}`; + return; + } + const currentNs = `${obj['@namespace'] || formatKey(key)}`; + const newNs = namespace ? `${namespace}.${currentNs}` : currentNs; + for (const childKey of Object.keys(obj)) { + if (typeof obj[childKey] !== 'object' && !isSymbol(obj[childKey])) continue; + _normalizeTable(newNs, childKey, obj[childKey]); + } +} + +function _symbolList(obj: any, list: Symbol[]) { + if (isSymbol(obj)) { + list.push(obj); + return; + } + for (const childKey of Object.keys(obj)) { + if (typeof obj[childKey] !== 'object' && !isSymbol(obj[childKey])) continue; + _symbolList(obj[childKey], list); + } +} \ No newline at end of file diff --git a/src/mol-script/runtime/environment.ts b/src/mol-script/runtime/environment.ts new file mode 100644 index 0000000000000000000000000000000000000000..24e7e8b3407dda4a879fccdbbc5f7d61d83a5d17 --- /dev/null +++ b/src/mol-script/runtime/environment.ts @@ -0,0 +1,14 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { SymbolRuntimeTable } from './symbol' + +interface Environment<T = any> { + readonly symbolTable: SymbolRuntimeTable, + readonly context: T +} + +export default Environment \ No newline at end of file diff --git a/src/mol-script/runtime/expression.ts b/src/mol-script/runtime/expression.ts new file mode 100644 index 0000000000000000000000000000000000000000..c2e5a71c63e25a50955a8b5cd747452f67465d00 --- /dev/null +++ b/src/mol-script/runtime/expression.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Environment from './environment' + +type RuntimeExpression<C = any, T = any> = (env: Environment<C>) => T + +export interface ExpressionInfo { + isConst?: boolean +} + +namespace RuntimeExpression { + export function constant<C, T>(c: T): RuntimeExpression<C, T> { + return env => c; + } + + export function func<C, T>(f: (env: Environment<C>) => T): RuntimeExpression<C, T> { + return env => f(env); + } +} + +export default RuntimeExpression \ No newline at end of file diff --git a/src/mol-script/runtime/symbol.ts b/src/mol-script/runtime/symbol.ts new file mode 100644 index 0000000000000000000000000000000000000000..15b4493ad51ee1a7b98e66922ad740e411f02b56 --- /dev/null +++ b/src/mol-script/runtime/symbol.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Environment from './environment' +import RuntimeExpression from './expression' + +export type RuntimeArguments = ArrayLike<RuntimeExpression> | { [name: string]: RuntimeExpression | undefined } + +type SymbolRuntime = (env: Environment, args: RuntimeArguments) => any + +namespace SymbolRuntime { + export interface Info { + readonly runtime: SymbolRuntime, + readonly attributes: Attributes + } + + export interface Attributes { isStatic: boolean } +} + +function SymbolRuntime(symbol: Symbol, attributes: Partial<SymbolRuntime.Attributes> = {}) { + const { isStatic = false } = attributes; + return (runtime: SymbolRuntime): SymbolRuntime.Info => { + return ({ runtime, attributes: { isStatic } }); + }; +} + +export type SymbolRuntimeTable = { readonly [id: string]: SymbolRuntime.Info } + +export default SymbolRuntime \ No newline at end of file diff --git a/src/mol-script/symbol.ts b/src/mol-script/symbol.ts new file mode 100644 index 0000000000000000000000000000000000000000..34630be3fa2e936aef935d3ac68e19c024628c0b --- /dev/null +++ b/src/mol-script/symbol.ts @@ -0,0 +1,84 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import Type from './type' +import Expression from './expression' + +export type Argument<T extends Type = Type> = { + type: T, + isOptional: boolean, + isRest: boolean, + defaultValue: T['@type'] | undefined, + description: string | undefined +} +export function Argument<T extends Type>(type: T, params?: { description?: string, defaultValue?: T['@type'], isOptional?: boolean, isRest?: boolean }): Argument<T> { + const { description = void 0, isOptional = false, isRest = false, defaultValue = void 0 } = params || {} + return { type, isOptional, isRest, defaultValue, description }; +} + +export type Arguments<T extends { [key: string]: any } = {}> = + | Arguments.List<T> + | Arguments.Dictionary<T> + +export namespace Arguments { + export const None: Arguments = Dictionary({}); + + export interface Dictionary<T extends { [key: string]: any } = {}, Traits = {}> { + kind: 'dictionary', + map: { [P in keyof T]: Argument<T[P]> }, + '@type': T + } + export function Dictionary<Map extends { [key: string]: Argument<any> }>(map: Map): Arguments<{ [P in keyof Map]: Map[P]['type']['@type'] }> { + return { kind: 'dictionary', map, '@type': 0 as any }; + } + + export interface List<T extends { [key: string]: any } = {}, Traits = {}> { + kind: 'list', + type: Type, + nonEmpty: boolean, + '@type': T + } + + export function List<T extends Type>(type: T, params?: { nonEmpty?: boolean }): Arguments<{ [key: string]: T['@type'] }> { + const { nonEmpty = false } = params || { } + return { kind: 'list', type, nonEmpty, '@type': 0 as any }; + } +} + +export type ExpressionArguments<T> = { [P in keyof T]?: Expression } | { [index: number]: Expression } + +interface Symbol<A extends Arguments = Arguments, T extends Type = Type> { + (args?: ExpressionArguments<A['@type']>): Expression, + info: { + namespace: string, + name: string, + description?: string + }, + args: A + type: T, + id: string, +} + +function Symbol<A extends Arguments, T extends Type>(name: string, args: A, type: T, description?: string) { + const symbol: Symbol<A, T> = function(args: ExpressionArguments<A['@type']>) { + return Expression.Apply(symbol.id, args as any); + } as any; + symbol.info = { namespace: '', name, description }; + symbol.id = ''; + symbol.args = args; + symbol.type = type; + return symbol; +} + +export function isSymbol(x: any): x is Symbol { + const s = x as Symbol; + return typeof s === 'function' && !!s.info && !!s.args && typeof s.info.namespace === 'string' && !!s.type; +} + +export type SymbolMap = { [id: string]: Symbol | undefined } + +export default Symbol + diff --git a/src/mol-script/type.ts b/src/mol-script/type.ts new file mode 100644 index 0000000000000000000000000000000000000000..e51a1237cffe2847eb05e0cd8eaaa4a280f07dc1 --- /dev/null +++ b/src/mol-script/type.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2018 Mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +type Type<T = any> = + | Type.Any | Type.AnyValue | Type.Variable<T> | Type.Value<T> + | Type.Container<T> | Type.Union<T> | Type.OneOf<T> + +namespace Type { + export interface Any { kind: 'any', '@type': any } + export interface Variable<T> { kind: 'variable', name: string, type: Type, isConstraint: boolean, '@type': any } + export interface AnyValue { kind: 'any-value', '@type': any } + export interface Value<T> { kind: 'value', namespace: string, name: string, parent?: Value<any>, '@type': T } + export interface Identifier { kind: 'identifier', '@type': string } + export interface Container<T> { kind: 'container', namespace: string, name: string, alias?: string, child: Type, '@type': T } + export interface Union<T> { kind: 'union', types: Type[], '@type': T } + export interface OneOf<T> { kind: 'oneof', type: Value<T>, namespace: string, name: string, values: { [v: string]: boolean | undefined }, '@type': T } + + export function Variable<T = any>(name: string, type: Type, isConstraint?: boolean): Variable<T> { return { kind: 'variable', name, type: type, isConstraint } as any; } + export function Value<T>(namespace: string, name: string, parent?: Value<any>): Value<T> { return { kind: 'value', namespace, name, parent } as any; } + export function Container<T = any>(namespace: string, name: string, child: Type, alias?: string): Container<T> { return { kind: 'container', namespace, name, child, alias } as any; } + export function Union<T = any>(types: Type[]): Union<T> { return { kind: 'union', types } as any; } + export function OneOf<T = any>(namespace: string, name: string, type: Value<T>, values: any[]): OneOf<T> { + const vals = Object.create(null); + for (const v of values) vals[v] = true; + return { kind: 'oneof', namespace, name, type, values: vals } as any; + } + + export const Any: Any = { kind: 'any' } as any; + export const AnyValue: AnyValue = { kind: 'any-value' } as any; + + export const Num = Value<number>('', 'Number'); + export const Str = Value<string>('', 'String'); + export const Bool = OneOf<boolean>('', 'Bool', Str as any, ['true', 'false']); + + export function oneOfValues({ values }: OneOf<any>) { return Object.keys(values).sort(); } +} + +export default Type \ No newline at end of file diff --git a/src/mol-task/execution/observable.ts b/src/mol-task/execution/observable.ts index 8c184c180944438d788b63a0000311d596a8c9f8..60d6c4e291d4e3c6fd5d37de1312604fb6652c1b 100644 --- a/src/mol-task/execution/observable.ts +++ b/src/mol-task/execution/observable.ts @@ -127,6 +127,8 @@ function notifyObserver(info: ProgressInfo, time: number) { } class ObservableRuntimeContext implements RuntimeContext { + isSynchronous = false; + isExecuting = true; lastUpdatedTime = 0; @@ -158,9 +160,9 @@ class ObservableRuntimeContext implements RuntimeContext { } else { if (typeof update.canAbort !== 'undefined') progress.canAbort = update.canAbort; if (typeof update.current !== 'undefined') progress.current = update.current; - if (typeof update.isIndeterminate !== 'undefined') progress.isIndeterminate = update.isIndeterminate; if (typeof update.max !== 'undefined') progress.max = update.max; if (typeof update.message !== 'undefined') progress.message = update.message; + progress.isIndeterminate = typeof progress.current === 'undefined' || typeof progress.max === 'undefined'; } } diff --git a/src/mol-task/execution/progress.ts b/src/mol-task/execution/progress.ts index 513c262c67574879b6bf17f952f2569f00d8ebd1..4bf829dcb94c35de288a0fdf92b61a89213f8c0c 100644 --- a/src/mol-task/execution/progress.ts +++ b/src/mol-task/execution/progress.ts @@ -19,6 +19,21 @@ namespace Progress { } export interface Observer { (progress: Progress): void } + + function _format(root: Progress.Node, prefix = ''): string { + const p = root.progress; + if (!root.children.length) { + if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}`; + return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}`; + } + + const newPrefix = prefix + ' |_ '; + const subTree = root.children.map(c => _format(c, newPrefix)); + if (p.isIndeterminate) return `${prefix}${p.taskName}: ${p.message}\n${subTree.join('\n')}`; + return `${prefix}${p.taskName}: [${p.current}/${p.max}] ${p.message}\n${subTree.join('\n')}`; + } + + export function format(p: Progress) { return _format(p.root); } } export { Progress } \ No newline at end of file diff --git a/src/mol-task/execution/runtime-context.ts b/src/mol-task/execution/runtime-context.ts index e8812c55f733fbbd70779ec98bc5664f656024f8..d2529a3eb3599d9ece65c7183b7a36cdc0117b33 100644 --- a/src/mol-task/execution/runtime-context.ts +++ b/src/mol-task/execution/runtime-context.ts @@ -8,6 +8,7 @@ import { Task } from '../task' interface RuntimeContext { readonly shouldUpdate: boolean, + readonly isSynchronous: boolean, // Idiomatic usage: // if (ctx.needsYield) await ctx.yield({ ... }); diff --git a/src/mol-task/execution/synchronous.ts b/src/mol-task/execution/synchronous.ts index 74b1180b51a7274afa5887aef5d9e3e6eeba243a..4ca40a04a5ccc91024b08ca3bcf6093ac8643e34 100644 --- a/src/mol-task/execution/synchronous.ts +++ b/src/mol-task/execution/synchronous.ts @@ -8,7 +8,9 @@ import { Task } from '../task' import { RuntimeContext } from './runtime-context' class SynchronousRuntimeContext implements RuntimeContext { - shouldUpdate: boolean = false; + shouldUpdate = false; + isSynchronous = true; + update(progress: string | Partial<RuntimeContext.ProgressUpdate>, dontNotify?: boolean): Promise<void> | void { } runChild<T>(task: Task<T>, progress?: string | Partial<RuntimeContext.ProgressUpdate>): Promise<T> { return ExecuteSynchronous(task); } } diff --git a/src/mol-task/index.ts b/src/mol-task/index.ts index 66449f0fb16c3e632963118f26e69d1604e83eed..830153c1cfaa3cd6512b083dd988b90b825bdb08 100644 --- a/src/mol-task/index.ts +++ b/src/mol-task/index.ts @@ -11,6 +11,8 @@ import { ExecuteObservable } from './execution/observable' import { Progress } from './execution/progress' import { now } from './util/now' import { Scheduler } from './util/scheduler' +import { MultistepTask } from './util/multistep' +import { chunkedSubtask } from './util/chunked' // Run the task without the ability to observe its progress. function Run<T>(task: Task<T>): Promise<T>; @@ -21,4 +23,4 @@ function Run<T>(task: Task<T>, observer?: Progress.Observer, updateRateMs?: numb return ExecuteSynchronous(task); } -export { Task, RuntimeContext, Progress, Run, now, Scheduler } \ No newline at end of file +export { Task, RuntimeContext, Progress, Run, now, Scheduler, MultistepTask, chunkedSubtask } \ No newline at end of file diff --git a/src/mol-task/util.ts b/src/mol-task/util.ts deleted file mode 100644 index a3ce20ff33975a7f205ce180c2f377ab65e4715b..0000000000000000000000000000000000000000 --- a/src/mol-task/util.ts +++ /dev/null @@ -1,96 +0,0 @@ - -// enum TaskState { -// Pending, -// Running, -// Aborted, -// Completed, -// Failed -// } - -interface TaskState { - -} - -namespace TaskState { - export interface Pending { kind: 'Pending' } - export interface Running { kind: 'Running', } - - export interface Progress { - message: string, - isIndeterminate: boolean, - current: number, - max: number, - elapsedMs: number - } -} - -type ExecutionContext = { - run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>, - subscribe(o: (p: string, compId: number) => void): void, - - requestAbort(compId: number): void -} - -namespace ExecutionContext { - // export interface Synchronous extends ExecutionContext { - // run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>, - // } - - // export interface Observable extends ExecutionContext { - // run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>, - // } - export const Sync: ExecutionContext = 0 as any; -} - -interface RuntimeContext { - run<T>(c: Computation<T>, params?: { updateRateMs: number }): Promise<T>, - yield(name: string): Promise<void> | void -} - -// if no context is specified, use the synchronous one. -interface Computation<T> { (ctx: RuntimeContext): Promise<T>, _id: number } - -function create<T>(c: (ctx: RuntimeContext) => Promise<T>): Computation<T> { return 0 as any; } -function constant<T>(c: T) { return create(async ctx => c); } - -type MultistepFn<P, T> = (params: P, step: (s: number) => Promise<void> | void, ctx: RuntimeContext) => Promise<T> -type ComputationProvider<P, T> = (params: P) => Computation<T> -function MultistepComputation<P, T>(name: string, steps: string[], f: MultistepFn<P, T>): ComputationProvider<P, T> { - return params => create(async ctx => f(params, n => ctx.yield(steps[n]), ctx)); -} - -// if total count is specified, could automatically provide percentage -type UniformlyChunkedFn<S> = (chunkSize: number, state: S, totalCount?: number) => number -type UniformlyChunkedProvider<S> = (ctx: RuntimeContext, state: S) => Promise<S> -function UniformlyChunked<S>(label: string, initialChunk: number, f: UniformlyChunkedFn<S>): UniformlyChunkedProvider<S> { - // TODO: track average time required for single element and then determine chunk size based on that. - return 0 as any; -} - -type LineReaderState = { str: string, position: number, lines: string[] } -const uniformPart = UniformlyChunked('Reading lines', 1000000, (size, state: LineReaderState) => { - state.position += size; - state.lines.push(''); - return 0 /* number of lines read */; -}); - -function readLines(str: string): Computation<string[]> { - return create(async ctx => { - const state = (await uniformPart(ctx, { str, position: 0, lines: [] })); - return state.lines; - }); -} - -const prependHiToLines = MultistepComputation('Hi prepend', ['Parse input', 'Prepend Hi'], async (p: string, step, ctx) => { - await step(0); - const lines = await readLines(p)(ctx); - await step(1); - const ret = lines.map(l => 'Hi ' + l); - return ret; -}); - - -(async function() { - const r = await ExecutionContext.Sync.run(prependHiToLines('1\n2'), { updateRateMs: 150 }); - console.log(r) -}()) diff --git a/src/mol-task/util/chunked.ts b/src/mol-task/util/chunked.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..d091e57066cbda2c53c38d14b0d18a2df53d4c8f 100644 --- a/src/mol-task/util/chunked.ts +++ b/src/mol-task/util/chunked.ts @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { now } from './now' +import { RuntimeContext } from '../execution/runtime-context' + +type UniformlyChunkedFn<S> = (chunkSize: number, state: S) => number + +async function chunkedSubtask<S>(ctx: RuntimeContext, initialChunk: number, state: S, + f: UniformlyChunkedFn<S>, update: (ctx: RuntimeContext, state: S, processed: number) => Promise<void> | void): Promise<S> { + let chunkSize = Math.max(initialChunk, 0); + let globalProcessed = 0, globalTime = 0; + + if (ctx.isSynchronous) { + f(Number.MAX_SAFE_INTEGER, state); + return state; + } + + let start = now(); + let lastSize = 0, currentTime = 0; + + while ((lastSize = f(chunkSize, state)) > 0) { + globalProcessed += lastSize; + + const delta = now() - start; + currentTime += delta; + globalTime += delta; + + if (ctx.shouldUpdate) { + await update(ctx, state, globalProcessed); + + chunkSize = Math.round(currentTime * globalProcessed / globalTime) + 1; + start = now(); + currentTime = 0; + } + } + if (ctx.shouldUpdate) { + await update(ctx, state, globalProcessed); + } + return state; +} + +export { chunkedSubtask } \ No newline at end of file diff --git a/src/mol-task/util/multistep.ts b/src/mol-task/util/multistep.ts index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..dba09e8485627d5e3c08bbf703ca6af7994df750 100644 --- a/src/mol-task/util/multistep.ts +++ b/src/mol-task/util/multistep.ts @@ -0,0 +1,17 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Task } from '../task' +import { RuntimeContext } from '../execution/runtime-context' + +export type MultistepFn<P, T> = + (params: P, step: (s: number) => Promise<void> | void, ctx: RuntimeContext) => Promise<T> + +function MultistepTask<P, T>(name: string, steps: string[], f: MultistepFn<P, T>, onAbort?: () => void) { + return (params: P) => Task.create(name, async ctx => f(params, n => ctx.update({ message: `${steps[n]}`, current: n + 1, max: steps.length }), ctx), onAbort); +} + +export { MultistepTask } \ No newline at end of file diff --git a/src/mol-util/computation.ts b/src/mol-util/computation.ts deleted file mode 100644 index 3d956de70054e16b858a96c080393ac582517ab7..0000000000000000000000000000000000000000 --- a/src/mol-util/computation.ts +++ /dev/null @@ -1,283 +0,0 @@ -/** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * Adapted from https://github.com/dsehnal/LiteMol - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import Scheduler from './scheduler' -import timeNow from './time' - -interface Computation<A> { - (ctx?: Computation.Context): Promise<A> -} - -namespace Computation { - export let PRINT_ERRORS_TO_CONSOLE = false; - - export function create<A>(computation: (ctx: Context) => Promise<A>) { - return ComputationImpl(computation); - } - - export function resolve<A>(a: A) { - return create<A>(_ => Promise.resolve(a)); - } - - export function reject<A>(reason: any) { - return create<A>(_ => Promise.reject(reason)); - } - - export interface Params { - updateRateMs?: number, - observer?: ProgressObserver - } - - export const Aborted = 'Aborted'; - - export interface Progress { - message: string, - isIndeterminate: boolean, - current: number, - max: number, - elapsedMs: number, - requestAbort?: () => void - } - - export interface ProgressUpdate { - message?: string, - abort?: boolean | (() => void), - current?: number, - max?: number - } - - export interface Context { - readonly isSynchronous: boolean, - /** Also checks if the computation was aborted. If so, throws. */ - readonly requiresUpdate: boolean, - requestAbort(): void, - - subscribe(onProgress: ProgressObserver): { dispose: () => void }, - /** Also checks if the computation was aborted. If so, throws. */ - update(info: ProgressUpdate): Promise<void> | void - } - - export type ProgressObserver = (progress: Readonly<Progress>) => void; - - const emptyDisposer = { dispose: () => { } } - - /** A context without updates. */ - export const synchronous: Context = { - isSynchronous: true, - requiresUpdate: false, - requestAbort() { }, - subscribe(onProgress) { return emptyDisposer; }, - update(info) { } - } - - export function observable(params?: Partial<Params>) { - const ret = new ObservableContext(params && params.updateRateMs); - if (params && params.observer) ret.subscribe(params.observer); - return ret; - } - - export const now = timeNow; - - /** A utility for splitting large computations into smaller parts. */ - export interface Chunker { - setNextChunkSize(size: number): void, - /** nextChunk must return the number of actually processed chunks. */ - process(nextChunk: (chunkSize: number) => number, update: (updater: Context['update']) => void, nextChunkSize?: number): Promise<void> - } - - export function chunker(ctx: Context, nextChunkSize: number): Chunker { - return new ChunkerImpl(ctx, nextChunkSize); - } -} - -const DefaulUpdateRateMs = 150; - -function ComputationImpl<A>(computation: (ctx: Computation.Context) => Promise<A>): Computation<A> { - return (ctx?: Computation.Context) => { - const context: ObservableContext = ctx ? ctx : Computation.synchronous as any; - return new Promise<A>(async (resolve, reject) => { - try { - if (context.started) context.started(); - const result = await computation(context); - resolve(result); - } catch (e) { - if (Computation.PRINT_ERRORS_TO_CONSOLE) console.error(e); - reject(e); - } finally { - if (context.finished) context.finished(); - } - }); - } -} - -class ObservableContext implements Computation.Context { - readonly updateRate: number; - readonly isSynchronous: boolean = false; - private level = 0; - private startedTime = 0; - private abortRequested = false; - private lastUpdated = 0; - private observers: Computation.ProgressObserver[] | undefined = void 0; - private progress: Computation.Progress = { message: 'Working...', current: 0, max: 0, elapsedMs: 0, isIndeterminate: true, requestAbort: void 0 }; - - private checkAborted() { - if (this.abortRequested) throw Computation.Aborted; - } - - private abortRequester = () => { this.abortRequested = true }; - - subscribe = (obs: Computation.ProgressObserver) => { - if (!this.observers) this.observers = []; - this.observers.push(obs); - return { - dispose: () => { - if (!this.observers) return; - for (let i = 0; i < this.observers.length; i++) { - if (this.observers[i] === obs) { - this.observers[i] = this.observers[this.observers.length - 1]; - this.observers.pop(); - return; - } - } - } - }; - } - - requestAbort() { - try { - if (this.abortRequester) { - this.abortRequester.call(null); - } - } catch (e) { } - } - - update({ message, abort, current, max }: Computation.ProgressUpdate): Promise<void> | void { - this.checkAborted(); - - const time = Computation.now(); - - if (typeof abort === 'boolean') { - this.progress.requestAbort = abort ? this.abortRequester : void 0; - } else { - if (abort) this.abortRequester = abort; - this.progress.requestAbort = abort ? this.abortRequester : void 0; - } - - if (typeof message !== 'undefined') this.progress.message = message; - this.progress.elapsedMs = time - this.startedTime; - if (isNaN(current!)) { - this.progress.isIndeterminate = true; - } else { - this.progress.isIndeterminate = false; - this.progress.current = current!; - if (!isNaN(max!)) this.progress.max = max!; - } - - if (this.observers) { - const p = { ...this.progress }; - for (let i = 0, _i = this.observers.length; i < _i; i++) { - Scheduler.immediate(this.observers[i], p); - } - } - - this.lastUpdated = time; - - return Scheduler.immediatePromise(); - } - - get requiresUpdate() { - this.checkAborted(); - if (this.isSynchronous) return false; - return Computation.now() - this.lastUpdated > this.updateRate; - } - - started() { - if (!this.level) { - this.startedTime = Computation.now(); - this.lastUpdated = this.startedTime; - } - this.level++; - } - - finished() { - this.level--; - if (this.level < 0) { - throw new Error('Bug in code somewhere, Computation.resolve/reject called too many times.'); - } - if (!this.level) this.observers = void 0; - } - - constructor(updateRate?: number) { - this.updateRate = updateRate || DefaulUpdateRateMs; - } -} - -class ChunkerImpl implements Computation.Chunker { - private processedSinceUpdate = 0; - private updater: Computation.Context['update']; - - private computeChunkSize(delta: number) { - if (!delta) { - this.processedSinceUpdate = 0; - return this.nextChunkSize; - } - const rate = (this.context as ObservableContext).updateRate || DefaulUpdateRateMs; - const ret = Math.round(this.processedSinceUpdate * rate / delta + 1); - this.processedSinceUpdate = 0; - return ret; - } - - private getNextChunkSize() { - const ctx = this.context as ObservableContext; - // be smart if the computation is synchronous and process the whole chunk at once. - if (ctx.isSynchronous) return Number.MAX_SAFE_INTEGER; - return this.nextChunkSize; - } - - setNextChunkSize(size: number) { - this.nextChunkSize = size; - } - - async process(nextChunk: (size: number) => number, update: (updater: Computation.Context['update']) => Promise<void> | void, nextChunkSize?: number) { - if (typeof nextChunkSize !== 'undefined') this.setNextChunkSize(nextChunkSize); - this.processedSinceUpdate = 0; - - // track time for the actual computation and exclude the "update time" - let chunkStart = Computation.now(); - let lastChunkSize: number; - let chunkCount = 0; - let totalSize = 0; - let updateCount = 0; - while ((lastChunkSize = nextChunk(this.getNextChunkSize())) > 0) { - chunkCount++; - this.processedSinceUpdate += lastChunkSize; - totalSize += lastChunkSize; - if (this.context.requiresUpdate) { - let time = Computation.now(); - await update(this.updater); - this.nextChunkSize = updateCount > 0 - ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1)) - : this.computeChunkSize(time - chunkStart) - updateCount++; - chunkStart = Computation.now(); - } - } - if (this.context.requiresUpdate) { - let time = Computation.now(); - await update(this.updater); - this.nextChunkSize = updateCount > 0 - ? Math.round((totalSize + this.computeChunkSize(time - chunkStart)) / (chunkCount + 1)) - : this.computeChunkSize(time - chunkStart) - } - } - - constructor(public context: Computation.Context, private nextChunkSize: number) { - this.updater = this.context.update.bind(this.context); - } -} - -export default Computation; \ No newline at end of file diff --git a/src/mol-util/index.ts b/src/mol-util/index.ts index caa3e33f456784b6a966406074d5726e92c1b190..16d00860d8b51105c8e5fd8ac951091612ad895e 100644 --- a/src/mol-util/index.ts +++ b/src/mol-util/index.ts @@ -6,13 +6,12 @@ */ import BitFlags from './bit-flags' -import Computation from './computation' -import Scheduler from './scheduler' import StringBuilder from './string-builder' -import Time from './time' import UUID from './uuid' +import Mask from './mask' -export { BitFlags, Computation, Scheduler, StringBuilder, Time, UUID } +export * from './value-cell' +export { BitFlags, StringBuilder, UUID, Mask } export function arrayEqual<T>(arr1: T[], arr2: T[]) { const length = arr1.length diff --git a/src/mol-util/scheduler.ts b/src/mol-util/scheduler.ts deleted file mode 100644 index e118ec97cbf221e6b21d7126687f39bcc78753e7..0000000000000000000000000000000000000000 --- a/src/mol-util/scheduler.ts +++ /dev/null @@ -1,207 +0,0 @@ -/** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -/** - * setImmediate polyfill adapted from https://github.com/YuzuJS/setImmediate - * Copyright (c) 2012 Barnesandnoble.com, llc, Donavon West, and Domenic Denicola - * MIT license. - */ - -function createImmediateActions() { - type Callback = (...args: any[]) => void; - type Task = { callback: Callback, args: any[] } - - const tasksByHandle: { [handle: number]: Task } = { }; - const doc = typeof document !== 'undefined' ? document : void 0; - - let currentlyRunningATask = false; - let nextHandle = 1; // Spec says greater than zero - let registerImmediate: ((handle: number) => void); - - function setImmediate(callback: Callback, ...args: any[]) { - // Callback can either be a function or a string - if (typeof callback !== 'function') { - callback = new Function('' + callback) as Callback; - } - // Store and register the task - const task = { callback: callback, args: args }; - tasksByHandle[nextHandle] = task; - registerImmediate(nextHandle); - return nextHandle++; - } - - function clearImmediate(handle: number) { - delete tasksByHandle[handle]; - } - - function run(task: Task) { - const callback = task.callback; - const args = task.args; - switch (args.length) { - case 0: - callback(); - break; - case 1: - callback(args[0]); - break; - case 2: - callback(args[0], args[1]); - break; - case 3: - callback(args[0], args[1], args[2]); - break; - default: - callback.apply(undefined, args); - break; - } - } - - function runIfPresent(handle: number) { - // From the spec: 'Wait until any invocations of this algorithm started before this one have completed.' - // So if we're currently running a task, we'll need to delay this invocation. - if (currentlyRunningATask) { - // Delay by doing a setTimeout. setImmediate was tried instead, but in Firefox 7 it generated a - // 'too much recursion' error. - setTimeout(runIfPresent, 0, handle); - } else { - const task = tasksByHandle[handle]; - if (task) { - currentlyRunningATask = true; - try { - run(task); - } finally { - clearImmediate(handle); - currentlyRunningATask = false; - } - } - } - } - - function installNextTickImplementation() { - registerImmediate = function(handle) { - process.nextTick(function () { runIfPresent(handle); }); - }; - } - - function canUsePostMessage() { - // The test against `importScripts` prevents this implementation from being installed inside a web worker, - // where `global.postMessage` means something completely different and can't be used for this purpose. - const global = typeof window !== 'undefined' ? window as any : void 0; - if (global && global.postMessage && !global.importScripts) { - let postMessageIsAsynchronous = true; - const oldOnMessage = global.onmessage; - global.onmessage = function() { - postMessageIsAsynchronous = false; - }; - global.postMessage('', '*'); - global.onmessage = oldOnMessage; - return postMessageIsAsynchronous; - } - } - - function installPostMessageImplementation() { - // Installs an event handler on `global` for the `message` event: see - // * https://developer.mozilla.org/en/DOM/window.postMessage - // * http://www.whatwg.org/specs/web-apps/current-work/multipage/comms.html#crossDocumentMessages - - const messagePrefix = 'setImmediate$' + Math.random() + '$'; - const global = typeof window !== 'undefined' ? window as any : void 0; - const onGlobalMessage = function(event: any) { - if (event.source === global && - typeof event.data === 'string' && - event.data.indexOf(messagePrefix) === 0) { - runIfPresent(+event.data.slice(messagePrefix.length)); - } - }; - - if (window.addEventListener) { - window.addEventListener('message', onGlobalMessage, false); - } else { - (window as any).attachEvent('onmessage', onGlobalMessage); - } - - registerImmediate = function(handle) { - window.postMessage(messagePrefix + handle, '*'); - }; - } - - function installMessageChannelImplementation() { - const channel = new MessageChannel(); - channel.port1.onmessage = function(event) { - const handle = event.data; - runIfPresent(handle); - }; - - registerImmediate = function(handle) { - channel.port2.postMessage(handle); - }; - } - - function installReadyStateChangeImplementation() { - const html = doc!.documentElement; - registerImmediate = function(handle) { - // Create a <script> element; its readystatechange event will be fired asynchronously once it is inserted - // into the document. Do so, thus queuing up the task. Remember to clean up once it's been called. - let script = doc!.createElement('script') as any; - script.onreadystatechange = function () { - runIfPresent(handle); - script.onreadystatechange = null; - html.removeChild(script); - script = null; - }; - html.appendChild(script); - }; - } - - function installSetTimeoutImplementation() { - registerImmediate = function(handle) { - setTimeout(runIfPresent, 0, handle); - }; - } - - // Don't get fooled by e.g. browserify environments. - if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') { - // For Node.js before 0.9 - installNextTickImplementation(); - } else if (canUsePostMessage()) { - // For non-IE10 modern browsers - installPostMessageImplementation(); - } else if (typeof MessageChannel !== 'undefined') { - // For web workers, where supported - installMessageChannelImplementation(); - } else if (doc && 'onreadystatechange' in doc.createElement('script')) { - // For IE 6–8 - installReadyStateChangeImplementation(); - } else { - // For older browsers - installSetTimeoutImplementation(); - } - - return { - setImmediate, - clearImmediate - }; -} - -const immediateActions = (function () { - if (typeof setImmediate !== 'undefined') { - if (typeof window !== 'undefined') { - return { setImmediate: (handler: any, ...args: any[]) => window.setImmediate(handler, ...args as any), clearImmediate: (handle: any) => window.clearImmediate(handle) }; - } else return { setImmediate, clearImmediate } - } - return createImmediateActions(); -}()); - -function resolveImmediate(res: () => void) { - immediateActions.setImmediate(res); -} - -export default { - immediate: immediateActions.setImmediate, - clearImmediate: immediateActions.clearImmediate, - - immediatePromise() { return new Promise<void>(resolveImmediate); } -}; diff --git a/src/mol-util/time.ts b/src/mol-util/time.ts deleted file mode 100644 index a0ee3abfdeccd9b60ee442d460e49b2f496f7813..0000000000000000000000000000000000000000 --- a/src/mol-util/time.ts +++ /dev/null @@ -1,24 +0,0 @@ -/** - * Copyright (c) 2017 mol* contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -declare var process: any; -declare var window: any; - -const now: () => number = (function () { - if (typeof window !== 'undefined' && window.performance) { - const perf = window.performance; - return () => perf.now(); - } else if (typeof process !== 'undefined' && process.hrtime !== 'undefined') { - return () => { - let t = process.hrtime(); - return t[0] * 1000 + t[1] / 1000000; - }; - } else { - return () => +new Date(); - } -}()); - -export default now; \ No newline at end of file diff --git a/src/mol-util/uuid.ts b/src/mol-util/uuid.ts index 471d349695e2ff619c8166a5066fbffc539ef652..b3dc51a34c377aa27e53e7971456f061b163bb2d 100644 --- a/src/mol-util/uuid.ts +++ b/src/mol-util/uuid.ts @@ -4,7 +4,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import now from './time' +import { now } from 'mol-task' interface UUID extends String { '@type': 'uuid' } diff --git a/src/mol-util/value-cell.ts b/src/mol-util/value-cell.ts new file mode 100644 index 0000000000000000000000000000000000000000..762bcbf7e723970b2f755d6658cddc3efba52675 --- /dev/null +++ b/src/mol-util/value-cell.ts @@ -0,0 +1,24 @@ +/** + * Copyright (c) 2018 mol* contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +/** A mutable value cell. */ +interface ValueCell<T> { value: T } +/** Create a mutable value cell. */ +function ValueCell<T>(value: T): ValueCell<T> { return { value }; } + +/** An immutable value box that also holds a version of the attribute. */ +interface ValueBox<T> { readonly version: number, readonly value: T } +/** Create a new box with the specified value and version = 0 */ +function ValueBox<T>(value: T): ValueBox<T> +/** Create a new box by updating the value of an old box and incrementing the version number. */ +function ValueBox<T>(box: ValueBox<T>, value: T): ValueBox<T> +function ValueBox<T>(boxOrValue: T | ValueBox<T>, value?: T): ValueBox<T> { + if (arguments.length === 2) return { version: (boxOrValue as ValueBox<T>).version + 1, value: value! }; + return { version: 0, value: boxOrValue as T }; +} + +export { ValueCell, ValueBox }; + diff --git a/src/perf-tests/structure.ts b/src/perf-tests/structure.ts index 01df5dc9d5f2a7b13045f1fe7180b04bef13708e..98111e1d358200da7964a89f6d72f829b0e8e891 100644 --- a/src/perf-tests/structure.ts +++ b/src/perf-tests/structure.ts @@ -15,6 +15,7 @@ import { Structure, Model, Queries as Q, Atom, AtomGroup, AtomSet, Selection, Sy import { Segmentation } from 'mol-data/int' import to_mmCIF from 'mol-model/structure/export/mmcif' +import { Run } from 'mol-task'; require('util.promisify').shim(); const readFileAsync = util.promisify(fs.readFile); @@ -59,7 +60,7 @@ export async function readCIF(path: string) { console.time('parse'); const comp = typeof input === 'string' ? CIF.parseText(input) : CIF.parseBinary(input); - const parsed = await comp(); + const parsed = await Run(comp); console.timeEnd('parse'); if (parsed.isError) { throw parsed;