From 135635ac0673835fda9ea7eda009e327752aee71 Mon Sep 17 00:00:00 2001 From: David Sehnal <david.sehnal@gmail.com> Date: Sat, 23 Sep 2017 14:47:54 +0200 Subject: [PATCH] Changed data model, updated GRO parser to handle velocities, arbitrary precision, multiple models --- README.md | 6 +- package-lock.json | Bin 0 -> 131693 bytes package.json | 9 +- src/{data => reader/cif}/data.ts | 3 - src/{data => reader/cif}/schema.ts | 3 - src/reader/common/column.ts | 43 ++++ src/reader/common/spec/fixed-column.spec.ts | 50 +++++ src/reader/common/text/column/__token.ts | 114 ++++++++++ src/reader/common/text/column/fixed.ts | 53 +++++ src/reader/common/text/token-field.ts | 114 ---------- src/reader/common/text/tokenizer.ts | 61 ++++-- src/reader/gro/format.ts | 14 -- src/reader/gro/parser.ts | 206 +++++++++--------- src/reader/gro/schema.d.ts | 40 ++++ src/reader/gro/schema.ts | 40 ---- .../spec/cif.spec.ts} | 5 +- src/reader/spec/gro.spec.ts | 115 +++++----- src/script.ts | 84 ++++--- 18 files changed, 550 insertions(+), 410 deletions(-) create mode 100644 package-lock.json rename src/{data => reader/cif}/data.ts (95%) rename src/{data => reader/cif}/schema.ts (96%) create mode 100644 src/reader/common/column.ts create mode 100644 src/reader/common/spec/fixed-column.spec.ts create mode 100644 src/reader/common/text/column/__token.ts create mode 100644 src/reader/common/text/column/fixed.ts delete mode 100644 src/reader/common/text/token-field.ts delete mode 100644 src/reader/gro/format.ts create mode 100644 src/reader/gro/schema.d.ts delete mode 100644 src/reader/gro/schema.ts rename src/{data/spec/schema.spec.ts => reader/spec/cif.spec.ts} (95%) diff --git a/README.md b/README.md index 5b2a0416a..30294cdc0 100644 --- a/README.md +++ b/README.md @@ -43,8 +43,4 @@ TODO ---- -- write about unittest (AR) -- gro reader - - read more than one block - - read velocities - - detect number of decimal places +- write about unittest (AR) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000000000000000000000000000000000000..e211546c2e17aecf72e79eabc20961af48563ceb GIT binary patch literal 131693 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 literal 0 HcmV?d00001 diff --git a/package.json b/package.json index c1de25f15..ddb398263 100644 --- a/package.json +++ b/package.json @@ -15,8 +15,13 @@ "script": "./node_modules/.bin/rollup build/js/src/script.js -e fs -f cjs -o build/js/script.js" }, "jest": { - "moduleFileExtensions": [ "ts", "js" ], - "transform": { "\\.ts$": "<rootDir>/node_modules/ts-jest/preprocessor.js" }, + "moduleFileExtensions": [ + "ts", + "js" + ], + "transform": { + "\\.ts$": "<rootDir>/node_modules/ts-jest/preprocessor.js" + }, "testRegex": "\\.spec\\.ts$" }, "author": "", diff --git a/src/data/data.ts b/src/reader/cif/data.ts similarity index 95% rename from src/data/data.ts rename to src/reader/cif/data.ts index 849a0954e..6d85c6a83 100644 --- a/src/data/data.ts +++ b/src/reader/cif/data.ts @@ -56,9 +56,6 @@ export interface Field { int(row: number): number, float(row: number): number, - /** The 'intrinsic value' of the field, e.g., array, binary data, ... */ - value(row: number): any, - presence(row: number): ValuePresence, areValuesEqual(rowA: number, rowB: number): boolean, diff --git a/src/data/schema.ts b/src/reader/cif/schema.ts similarity index 96% rename from src/data/schema.ts rename to src/reader/cif/schema.ts index c9f1a1a86..9db6cb797 100644 --- a/src/data/schema.ts +++ b/src/reader/cif/schema.ts @@ -67,7 +67,6 @@ export namespace Field { export function str(spec?: Spec) { return createSchema(spec, Str); } export function int(spec?: Spec) { return createSchema(spec, Int); } export function float(spec?: Spec) { return createSchema(spec, Float); } - export function value<T>(spec?: Spec): Schema<T> { return createSchema(spec, Value); } function create<T>(field: Data.Field, value: (row: number) => T, toArray: Field<T>['toArray']): Field<T> { return { isDefined: field.isDefined, value, presence: field.presence, areValuesEqual: field.areValuesEqual, stringEquals: field.stringEquals, toArray }; @@ -76,14 +75,12 @@ export namespace Field { function Str(field: Data.Field) { return create(field, field.str, field.toStringArray); } function Int(field: Data.Field) { return create(field, field.int, field.toIntArray); } function Float(field: Data.Field) { return create(field, field.float, field.toFloatArray); } - function Value(field: Data.Field) { return create(field, field.value, () => { throw Error('not supported'); }); } const DefaultUndefined: Data.Field = { isDefined: false, str: row => null, int: row => 0, float: row => 0, - value: row => null, presence: row => Data.ValuePresence.NotSpecified, areValuesEqual: (rowA, rowB) => true, diff --git a/src/reader/common/column.ts b/src/reader/common/column.ts new file mode 100644 index 000000000..6a2cd768b --- /dev/null +++ b/src/reader/common/column.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +export type ArrayType = string[] | number[] | Float32Array | Float64Array | Int8Array | Int16Array | Int32Array | Uint8Array | Uint16Array | Uint32Array +export type ColumnType = typeof ColumnType.str | typeof ColumnType.int | typeof ColumnType.float + +export namespace ColumnType { + export const str = { '@type': '' as string, kind: 'str' as 'str' }; + export const int = { '@type': 0 as number, kind: 'int' as 'int' }; + export const float = { '@type': 0 as number, kind: 'float' as 'float' }; +} + +export interface Column<T> { + readonly isColumnDefined: boolean, + readonly rowCount: number, + value(row: number): T, + toArray(ctor?: (size: number) => ArrayType, startRow?: number, endRowExclusive?: number): ReadonlyArray<T> +} + +export function UndefinedColumn<T extends ColumnType>(rowCount: number, type: T): Column<T['@type']> { + const value: Column<T['@type']>['value'] = type.kind === 'str' ? row => '' : row => 0; + return { + isColumnDefined: false, + rowCount, + value, + toArray(ctor, s, e) { + const { array } = createArray(rowCount, ctor, s, e); + for (let i = 0, _i = array.length; i < _i; i++) array[i] = value(0) + return array; + } + } +} + +/** A helped function for Column.toArray */ +export function createArray(rowCount: number, ctor?: (size: number) => ArrayType, start?: number, end?: number) { + const c = typeof ctor !== 'undefined' ? ctor : (s: number) => new Array(s); + const s = typeof start !== 'undefined' ? Math.max(Math.min(start, rowCount - 1), 0) : 0; + const e = typeof end !== 'undefined' ? Math.min(end, rowCount) : rowCount; + return { array: c(e - s) as any[], start: s, end: e }; +} \ No newline at end of file diff --git a/src/reader/common/spec/fixed-column.spec.ts b/src/reader/common/spec/fixed-column.spec.ts new file mode 100644 index 000000000..4aa5e3f24 --- /dev/null +++ b/src/reader/common/spec/fixed-column.spec.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import FixedColumn from '../text/column/fixed' +import { ColumnType } from '../../common/column' + +const lines = [ + '1.123 abc', + '1.00 a', + '1.1 bcd ', + '', + ' 5' +] + +const data = lines.join('\n'); + +const linesTokens = (function () { + const tokens: number[] = []; + let last = 0; + for (const l of lines) { + tokens.push(last, last + l.length); + last += l.length + 1; + } + if (tokens[tokens.length - 1] > data.length) tokens[tokens.length - 1] = data.length; + return tokens; +}()); + +describe('fixed text column', () => { + const col = FixedColumn({ data, lines: linesTokens, rowCount: lines.length }); + const col1 = col(0, 5, ColumnType.float); + const col2 = col(5, 4, ColumnType.str); + it('number', () => { + expect(col1.value(0)).toBe(1.123); + expect(col1.value(1)).toBe(1.0); + expect(col1.value(2)).toBe(1.1); + expect(col1.value(3)).toBe(0); + expect(col1.value(4)).toBe(5); + }) + it('str', () => { + expect(col2.value(0)).toBe('abc'); + expect(col2.value(1)).toBe('a'); + expect(col2.value(2)).toBe('bc'); + expect(col2.value(3)).toBe(''); + expect(col2.value(4)).toBe(''); + }) +}); diff --git a/src/reader/common/text/column/__token.ts b/src/reader/common/text/column/__token.ts new file mode 100644 index 000000000..87326c26d --- /dev/null +++ b/src/reader/common/text/column/__token.ts @@ -0,0 +1,114 @@ +// /* +// * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. +// * +// * @author David Sehnal <david.sehnal@gmail.com> +// */ + +// import * as Data from '../../../../data/data' +// import { parseInt as fastParseInt, parseFloat as fastParseFloat } from '../number-parser' +// import { Tokens } from '../tokenizer' +// import ShortStringPool from '../../../../utils/short-string-pool' + +// export function createTokenFields(data: string, fields: string[], tokens: Tokens): { [name: string]: Data.Field } { +// const fi: TokenFieldInfo = { data, fieldCount: fields.length, tokens: tokens.indices }; +// const categoryFields = Object.create(null); +// for (let i = 0; i < fi.fieldCount; ++i) { +// categoryFields[fields[i]] = TokenField(fi, i); +// } +// return categoryFields; +// } + +// export interface TokenFieldInfo { +// data: string, +// tokens: ArrayLike<number>, +// fieldCount: number, +// isCif?: boolean +// } + +// export function TokenField(info: TokenFieldInfo, index: number): Data.Field { +// const { data, tokens, fieldCount, isCif = false } = info; +// const stringPool = ShortStringPool.create(); + +// const str: Data.Field['str'] = isCif ? row => { +// const i = (row * fieldCount + index) * 2; +// const ret = ShortStringPool.get(stringPool, data.substring(tokens[i], tokens[i + 1])); +// if (ret === '.' || ret === '?') return null; +// return ret; +// } : row => { +// const i = (row * fieldCount + index) * 2; +// return ShortStringPool.get(stringPool, data.substring(tokens[i], tokens[i + 1])); +// }; + +// const int: Data.Field['int'] = row => { +// const i = (row * fieldCount + index) * 2; +// return fastParseInt(data, tokens[i], tokens[i + 1]) || 0; +// }; + +// const float: Data.Field['float'] = row => { +// const i = (row * fieldCount + index) * 2; +// return fastParseFloat(data, tokens[i], tokens[i + 1]) || 0; +// }; + +// const presence: Data.Field['presence'] = isCif ? row => { +// const i = 2 * (row * fieldCount + index); +// const s = tokens[i]; +// if (tokens[i + 1] - s !== 1) return Data.ValuePresence.Present; +// const v = data.charCodeAt(s); +// if (v === 46 /* . */) return Data.ValuePresence.NotSpecified; +// if (v === 63 /* ? */) return Data.ValuePresence.Unknown; +// return Data.ValuePresence.Present; +// } : row => { +// const i = 2 * (row * fieldCount + index); +// return tokens[i] === tokens[i + 1] ? Data.ValuePresence.NotSpecified : Data.ValuePresence.Present +// }; + +// return { +// isDefined: true, +// str, +// int, +// float, +// value: str, +// presence, +// areValuesEqual: (rowA, rowB) => { +// const aI = (rowA * fieldCount + index) * 2, aS = tokens[aI]; +// const bI = (rowB * fieldCount + index) * 2, bS = tokens[bI]; +// const len = tokens[aI + 1] - aS; +// if (len !== tokens[bI + 1] - bS) return false; +// for (let i = 0; i < len; i++) { +// if (data.charCodeAt(i + aS) !== data.charCodeAt(i + bS)) { +// return false; +// } +// } +// return true; +// }, +// stringEquals: (row, value) => { +// const aI = (row * fieldCount + index) * 2; +// const s = tokens[aI]; +// if (!value) return presence(row) !== Data.ValuePresence.Present; +// const len = value.length; +// if (len !== tokens[aI + 1] - s) return false; +// for (let i = 0; i < len; i++) { +// if (data.charCodeAt(i + s) !== value.charCodeAt(i)) return false; +// } +// return true; +// }, +// toStringArray: (startRow, endRowExclusive, ctor) => { +// const count = endRowExclusive - startRow; +// const ret = ctor(count) as any; +// for (let i = 0; i < count; i++) { ret[i] = str(startRow + i); } +// return ret; +// }, +// toIntArray: (startRow, endRowExclusive, ctor) => { +// const count = endRowExclusive - startRow; +// const ret = ctor(count) as any; +// for (let i = 0; i < count; i++) { ret[i] = int(startRow + i); } +// return ret; +// }, +// toFloatArray: (startRow, endRowExclusive, ctor) => { +// const count = endRowExclusive - startRow; +// const ret = ctor(count) as any; +// for (let i = 0; i < count; i++) { ret[i] = float(startRow + i); } +// return ret; +// } +// } +// } \ No newline at end of file diff --git a/src/reader/common/text/column/fixed.ts b/src/reader/common/text/column/fixed.ts new file mode 100644 index 000000000..e7df1e074 --- /dev/null +++ b/src/reader/common/text/column/fixed.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Column, ColumnType, createArray } from '../../column' +import { trimStr } from '../tokenizer' +import { parseIntSkipLeadingWhitespace, parseFloatSkipLeadingWhitespace } from '../number-parser' + +export interface FixedColumnInfo { + data: string, + lines: ArrayLike<number>, + rowCount: number +} + +export default function FixedColumnProvider(info: FixedColumnInfo) { + return function<T extends ColumnType>(offset: number, width: number, type: T) { + return FixedColumn(info, offset, width, type); + } +} + +function getArrayValues(value: (row: number) => any, target: any[], start: number) { + for (let i = 0, _e = target.length; i < _e; i++) target[i] = value(start + i); + return target; +} + +export function FixedColumn<T extends ColumnType>(info: FixedColumnInfo, offset: number, width: number, type: T): Column<T['@type']> { + const { data, lines, rowCount } = info; + const { kind } = type; + + const value: Column<T['@type']>['value'] = kind === 'str' ? row => { + let s = lines[2 * row] + offset, e = s + width, le = lines[2 * row + 1]; + if (s >= le) return ''; + if (e > le) e = le; + return trimStr(data, s, e); + } : kind === 'int' ? row => { + const s = lines[2 * row] + offset, e = s + width; + return parseIntSkipLeadingWhitespace(data, s, e); + } : row => { + const s = lines[2 * row] + offset, e = s + width; + return parseFloatSkipLeadingWhitespace(data, s, e); + } + return { + isColumnDefined: true, + rowCount, + value, + toArray(ctor, s, e) { + const { array, start } = createArray(rowCount, ctor, s, e); + return getArrayValues(value, array, start); + } + }; +} \ No newline at end of file diff --git a/src/reader/common/text/token-field.ts b/src/reader/common/text/token-field.ts deleted file mode 100644 index 849d7cb84..000000000 --- a/src/reader/common/text/token-field.ts +++ /dev/null @@ -1,114 +0,0 @@ -/* - * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. - * - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import * as Data from '../../../data/data' -import { parseInt as fastParseInt, parseFloat as fastParseFloat } from './number-parser' -import { Tokens } from './tokenizer' -import ShortStringPool from '../../../utils/short-string-pool' - -export function createTokenFields(data: string, fields: string[], tokens: Tokens): { [name: string]: Data.Field } { - const fi: TokenFieldInfo = { data, fieldCount: fields.length, tokens: tokens.indices }; - const categoryFields = Object.create(null); - for (let i = 0; i < fi.fieldCount; ++i) { - categoryFields[fields[i]] = TokenField(fi, i); - } - return categoryFields; -} - -export interface TokenFieldInfo { - data: string, - tokens: ArrayLike<number>, - fieldCount: number, - isCif?: boolean -} - -export function TokenField(info: TokenFieldInfo, index: number): Data.Field { - const { data, tokens, fieldCount, isCif = false } = info; - const stringPool = ShortStringPool.create(); - - const str: Data.Field['str'] = isCif ? row => { - const i = (row * fieldCount + index) * 2; - const ret = ShortStringPool.get(stringPool, data.substring(tokens[i], tokens[i + 1])); - if (ret === '.' || ret === '?') return null; - return ret; - } : row => { - const i = (row * fieldCount + index) * 2; - return ShortStringPool.get(stringPool, data.substring(tokens[i], tokens[i + 1])); - }; - - const int: Data.Field['int'] = row => { - const i = (row * fieldCount + index) * 2; - return fastParseInt(data, tokens[i], tokens[i + 1]) || 0; - }; - - const float: Data.Field['float'] = row => { - const i = (row * fieldCount + index) * 2; - return fastParseFloat(data, tokens[i], tokens[i + 1]) || 0; - }; - - const presence: Data.Field['presence'] = isCif ? row => { - const i = 2 * (row * fieldCount + index); - const s = tokens[i]; - if (tokens[i + 1] - s !== 1) return Data.ValuePresence.Present; - const v = data.charCodeAt(s); - if (v === 46 /* . */) return Data.ValuePresence.NotSpecified; - if (v === 63 /* ? */) return Data.ValuePresence.Unknown; - return Data.ValuePresence.Present; - } : row => { - const i = 2 * (row * fieldCount + index); - return tokens[i] === tokens[i + 1] ? Data.ValuePresence.NotSpecified : Data.ValuePresence.Present - }; - - return { - isDefined: true, - str, - int, - float, - value: str, - presence, - areValuesEqual: (rowA, rowB) => { - const aI = (rowA * fieldCount + index) * 2, aS = tokens[aI]; - const bI = (rowB * fieldCount + index) * 2, bS = tokens[bI]; - const len = tokens[aI + 1] - aS; - if (len !== tokens[bI + 1] - bS) return false; - for (let i = 0; i < len; i++) { - if (data.charCodeAt(i + aS) !== data.charCodeAt(i + bS)) { - return false; - } - } - return true; - }, - stringEquals: (row, value) => { - const aI = (row * fieldCount + index) * 2; - const s = tokens[aI]; - if (!value) return presence(row) !== Data.ValuePresence.Present; - const len = value.length; - if (len !== tokens[aI + 1] - s) return false; - for (let i = 0; i < len; i++) { - if (data.charCodeAt(i + s) !== value.charCodeAt(i)) return false; - } - return true; - }, - toStringArray: (startRow, endRowExclusive, ctor) => { - const count = endRowExclusive - startRow; - const ret = ctor(count) as any; - for (let i = 0; i < count; i++) { ret[i] = str(startRow + i); } - return ret; - }, - toIntArray: (startRow, endRowExclusive, ctor) => { - const count = endRowExclusive - startRow; - const ret = ctor(count) as any; - for (let i = 0; i < count; i++) { ret[i] = int(startRow + i); } - return ret; - }, - toFloatArray: (startRow, endRowExclusive, ctor) => { - const count = endRowExclusive - startRow; - const ret = ctor(count) as any; - for (let i = 0; i < count; i++) { ret[i] = float(startRow + i); } - return ret; - } - } -} \ No newline at end of file diff --git a/src/reader/common/text/tokenizer.ts b/src/reader/common/text/tokenizer.ts index 79cabe100..b957a35b1 100644 --- a/src/reader/common/text/tokenizer.ts +++ b/src/reader/common/text/tokenizer.ts @@ -6,7 +6,7 @@ * @author Alexander Rose <alexander.rose@weirdbyte.de> */ -export interface State<Info = any, TokenType = any> { +export interface State<TokenType = any> { data: string position: number @@ -16,12 +16,10 @@ export interface State<Info = any, TokenType = any> { currentTokenStart: number currentTokenEnd: number - currentTokenType: TokenType, - - info: Info + currentTokenType: TokenType } -export function State<Info, TokenType>(data: string, info?: Info, initialTokenType?: TokenType): State<Info, TokenType> { +export function State<TokenType>(data: string, initialTokenType?: TokenType): State<TokenType> { return { data, position: 0, @@ -29,37 +27,53 @@ export function State<Info, TokenType>(data: string, info?: Info, initialTokenTy currentLineNumber: 1, currentTokenStart: 0, currentTokenEnd: 0, - currentTokenType: initialTokenType!, - info: info! + currentTokenType: initialTokenType! }; } +export function getTokenString(state: State) { + return state.data.substring(state.currentTokenStart, state.currentTokenEnd); +} + /** * Eat everything until a newline occurs. */ export function eatLine(state: State) { + const { data } = state; while (state.position < state.length) { - switch (state.data.charCodeAt(state.position)) { + switch (data.charCodeAt(state.position)) { case 10: // \n - state.currentTokenEnd = state.position - ++state.position - ++state.currentLineNumber - return + state.currentTokenEnd = state.position; + ++state.position; + ++state.currentLineNumber; + return; case 13: // \r - state.currentTokenEnd = state.position - ++state.position - ++state.currentLineNumber - if (state.data.charCodeAt(state.position) === 10) { - ++state.position + state.currentTokenEnd = state.position; + ++state.position; + ++state.currentLineNumber; + if (data.charCodeAt(state.position) === 10) { + ++state.position; } - return + return; default: - ++state.position + ++state.position; + break; } } state.currentTokenEnd = state.position; } +/** Sets the current token start to the current position */ +export function markStart(state: State) { + state.currentTokenStart = state.position; +} + +/** Sets the current token start to current position and moves to the next line. */ +export function markLine(state: State) { + state.currentTokenStart = state.position; + eatLine(state); +} + /** * Eat everything until a whitespace/newline occurs. */ @@ -129,6 +143,15 @@ export function trim(state: State, start: number, end: number) { state.position = end; } +export function trimStr(data: string, start: number, end: number) { + let s = start, e = end - 1; + let c = data.charCodeAt(s); + while ((c === 9 || c === 32) && s <= e) c = data.charCodeAt(++s); + c = data.charCodeAt(e); + while ((c === 9 || c === 32) && e >= s) c = data.charCodeAt(--e); + return data.substring(s, e + 1); +} + export interface Tokens { indicesLenMinus2: number, count: number, diff --git a/src/reader/gro/format.ts b/src/reader/gro/format.ts deleted file mode 100644 index ce24a7978..000000000 --- a/src/reader/gro/format.ts +++ /dev/null @@ -1,14 +0,0 @@ -/* - * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import schema from './schema' -import parse from './parser' - -export default { - parse, - schema -}; \ No newline at end of file diff --git a/src/reader/gro/parser.ts b/src/reader/gro/parser.ts index 7c41e81bb..f4560630e 100644 --- a/src/reader/gro/parser.ts +++ b/src/reader/gro/parser.ts @@ -5,77 +5,71 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import { State as TokenizerState, Tokens, eatLine, skipWhitespace, eatValue, trim } from '../common/text/tokenizer' -import { parseInt } from '../common/text/number-parser' -import { createTokenFields } from '../common/text/token-field' -import * as Data from '../../data/data' +import { State as TokenizerState, Tokens, markLine, getTokenString } from '../common/text/tokenizer' +import FixedColumn from '../common/text/column/fixed' +import { ColumnType, UndefinedColumn } from '../common/column' +import * as Schema from './schema' import Result from '../result' -interface StateInfo { - numberOfAtoms: number - hasVelocities: boolean - numberOfDecimalPlaces: number +interface State { + tokenizer: TokenizerState, + header: Schema.Header, + numberOfAtoms: number, } -type State = TokenizerState<StateInfo> +function createEmptyHeader(): Schema.Header { + return { + title: '', + timeInPs: 0, + hasVelocities: false, + precision: { position: 0, velocity: 0 }, + box: [0, 0, 0] + }; +} -function createState(data: string): State { - return TokenizerState(data, { numberOfAtoms: 0, hasVelocities: false, numberOfDecimalPlaces: 3 }); +function createState(tokenizer: TokenizerState): State { + return { + tokenizer, + header: createEmptyHeader(), + numberOfAtoms: 0 + }; } /** * title string (free format string, optional time in ps after 't=') */ -function handleTitleString(state: State, tokens: Tokens) { - eatLine(state) - // console.log('title', state.data.substring(state.currentTokenStart, state.currentTokenEnd)) - let start = state.currentTokenStart - let end = state.currentTokenEnd - let valueStart = state.currentTokenStart - let valueEnd = start - - while (valueEnd < end && !isTime(state.data, valueEnd)) ++valueEnd; +function handleTitleString(state: State) { + const { tokenizer, header } = state; + markLine(tokenizer); - if (isTime(state.data, valueEnd)) { - let timeStart = valueEnd + 2 + let line = getTokenString(tokenizer); - while (valueEnd > start && isSpaceOrComma(state.data, valueEnd - 1)) --valueEnd; - Tokens.add(tokens, valueStart, valueEnd) // title + // skip potential empty lines... + if (line.trim().length === 0) { + markLine(tokenizer); + line = getTokenString(tokenizer); + } - while (timeStart < end && state.data.charCodeAt(timeStart) === 32) ++timeStart; - while (valueEnd > timeStart && state.data.charCodeAt(valueEnd - 1) === 32) --valueEnd; - Tokens.add(tokens, timeStart, end) // time + const timeOffset = line.lastIndexOf('t='); + if (timeOffset >= 0) { + header.timeInPs = parseFloat(line.substring(timeOffset + 2)); + header.title = line.substring(0, timeOffset).trim(); + if (header.title && header.title[header.title.length - 1] === ',') { + header.title = header.title.substring(0, header.title.length - 1); + } } else { - Tokens.add(tokens, valueStart, valueEnd) // title - Tokens.add(tokens, valueEnd, valueEnd) // empty token for time + header.title = line; } } -function isSpaceOrComma(data: string, position: number): boolean { - const c = data.charCodeAt(position); - return c === 32 || c === 44 -} - -function isTime(data: string, position: number): boolean { - // T/t - const c = data.charCodeAt(position); - if (c !== 84 && c !== 116) return false; - // = - if (data.charCodeAt(position + 1) !== 61) return false; - - return true; -} - /** * number of atoms (free format integer) */ -function handleNumberOfAtoms(state: State, tokens: Tokens) { - skipWhitespace(state) - state.currentTokenStart = state.position - eatValue(state) - state.info.numberOfAtoms = parseInt(state.data, state.currentTokenStart, state.currentTokenEnd) - Tokens.add(tokens, state.currentTokenStart, state.currentTokenEnd) - eatLine(state) +function handleNumberOfAtoms(state: State) { + const { tokenizer } = state; + markLine(tokenizer); + const line = getTokenString(tokenizer); + state.numberOfAtoms = parseInt(line); } /** @@ -94,33 +88,46 @@ function handleNumberOfAtoms(state: State, tokens: Tokens) { * position (in nm, x y z in 3 columns, each 8 positions with 3 decimal places) * velocity (in nm/ps (or km/s), x y z in 3 columns, each 8 positions with 4 decimal places) */ -function handleAtoms(state: State) { - const fieldSizes = [ 5, 5, 5, 5, 8, 8, 8, 8, 8, 8 ]; - const fields = [ 'residueNumber', 'residueName', 'atomName', 'atomNumber', 'x', 'y', 'z' ] - if (state.info.hasVelocities) { - fields.push('vx', 'vy', 'vz') - } - - const fieldCount = fields.length - const tokens = Tokens.create(state.info.numberOfAtoms * 2 * fieldCount) +function handleAtoms(state: State): Schema.Atoms { + const { tokenizer, numberOfAtoms } = state; + const lineTokens = Tokens.create(numberOfAtoms * 2); - let start: number; - let end: number; - - for (let i = 0, _i = state.info.numberOfAtoms; i < _i; ++i) { - state.currentTokenStart = state.position; - end = state.currentTokenStart; - for (let j = 0; j < fieldCount; ++j) { - start = end; - end = start + fieldSizes[j]; - - trim(state, start, end); - Tokens.addUnchecked(tokens, state.currentTokenStart, state.currentTokenEnd); - } - eatLine(state) + for (let i = 0; i < numberOfAtoms; i++) { + markLine(tokenizer); + Tokens.addUnchecked(lineTokens, tokenizer.currentTokenStart, tokenizer.currentTokenEnd); } - return Data.Category(state.info.numberOfAtoms, createTokenFields(state.data, fields, tokens)); + const lines = lineTokens.indices; + const positionSample = tokenizer.data.substring(lines[0], lines[1]).substring(20); + const precisions = positionSample.match(/\.\d+/g)!; + const hasVelocities = precisions.length === 6; + state.header.hasVelocities = hasVelocities; + state.header.precision.position = precisions[0].length - 1; + state.header.precision.velocity = hasVelocities ? precisions[3].length - 1 : 0; + + const pO = 20; + const pW = state.header.precision.position + 5; + const vO = pO + 3 * pW; + const vW = state.header.precision.velocity + 4; + + const col = FixedColumn({ data: tokenizer.data, lines, rowCount: state.numberOfAtoms }); + const undef = UndefinedColumn(state.numberOfAtoms, ColumnType.float); + + const ret = { + count: state.numberOfAtoms, + residueNumber: col(0, 5, ColumnType.int), + residueName: col(5, 5, ColumnType.str), + atomName: col(10, 5, ColumnType.str), + atomNumber: col(15, 5, ColumnType.int), + x: col(pO, pW, ColumnType.float), + y: col(pO + pW, pW, ColumnType.float), + z: col(pO + 2 * pW, pW, ColumnType.float), + vx: hasVelocities ? col(vO, vW, ColumnType.float) : undef, + vy: hasVelocities ? col(vO + vW, vW, ColumnType.float) : undef, + vz: hasVelocities ? col(vO + 2 * vW, vW, ColumnType.float) : undef, + }; + + return ret; } /** @@ -129,35 +136,32 @@ function handleAtoms(state: State) { * the last 6 values may be omitted (they will be set to zero). * Gromacs only supports boxes with v1(y)=v1(z)=v2(z)=0. */ -function handleBoxVectors(state: State, tokens: Tokens) { - // just read the first three values, ignore any remaining - for (let i = 0; i < 3; ++i) { - skipWhitespace(state); - state.currentTokenStart = state.position; - eatValue(state); - Tokens.add(tokens, state.currentTokenStart, state.currentTokenEnd); - } +function handleBoxVectors(state: State) { + const { tokenizer } = state; + markLine(tokenizer); + const values = getTokenString(tokenizer).trim().split(/\s+/g); + state.header.box = [+values[0], +values[1], +values[2]]; } -function parseInternal(data: string): Result<Data.File> { - const state = createState(data); - - const headerFields = ['title', 'timeInPs', 'numberOfAtoms', 'boxX', 'boxY', 'boxZ']; - const headerTokens = Tokens.create(2 * headerFields.length); - - handleTitleString(state, headerTokens); - handleNumberOfAtoms(state, headerTokens); - const atoms = handleAtoms(state); - handleBoxVectors(state, headerTokens); - - const block = Data.Block({ - header: Data.Category(1, createTokenFields(data, headerFields, headerTokens)), - atoms - }); +function parseInternal(data: string): Result<Schema.File> { + const tokenizer = TokenizerState(data); + + const structures: Schema.Structure[] = []; + while (tokenizer.position < data.length) { + const state = createState(tokenizer); + handleTitleString(state); + handleNumberOfAtoms(state); + const atoms = handleAtoms(state); + handleBoxVectors(state); + structures.push({ header: state.header, atoms }); + } - return Result.success(Data.File([block])); + const result: Schema.File = { structures }; + return Result.success(result); } -export default function parse(data: string) { +export function parse(data: string) { return parseInternal(data); -} \ No newline at end of file +} + +export default parse; \ No newline at end of file diff --git a/src/reader/gro/schema.d.ts b/src/reader/gro/schema.d.ts new file mode 100644 index 000000000..1d49d9641 --- /dev/null +++ b/src/reader/gro/schema.d.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. + * + * @author Alexander Rose <alexander.rose@weirdbyte.de> + * @author David Sehnal <david.sehnal@gmail.com> + */ + +import { Column } from '../common/column' + +export interface Header { + title: string, + timeInPs: number, + /** number of decimal places */ + precision: { position: number, velocity: number }, + hasVelocities: boolean, + box: [number, number, number] +} + +export interface Atoms { + count: number, + residueNumber: Column<number>, + residueName: Column<string>, + atomName: Column<string>, + atomNumber: Column<number>, + x: Column<number>, + y: Column<number>, + z: Column<number>, + vx: Column<number>, + vy: Column<number>, + vz: Column<number> +} + +export interface Structure { + header: Readonly<Header>, + atoms: Readonly<Atoms> +} + +export interface File { + structures: Structure[] +} \ No newline at end of file diff --git a/src/reader/gro/schema.ts b/src/reader/gro/schema.ts deleted file mode 100644 index d8e9c576a..000000000 --- a/src/reader/gro/schema.ts +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright (c) 2017 molio contributors, licensed under MIT, See LICENSE file for more info. - * - * @author Alexander Rose <alexander.rose@weirdbyte.de> - * @author David Sehnal <david.sehnal@gmail.com> - */ - -import * as Schema from '../../data/schema' -import * as Data from '../../data/data' - -const str = Schema.Field.str() -const int = Schema.Field.int() -const float = Schema.Field.float() - -const header = { - 'title': str, - 'timeInPs': float, - 'numberOfAtoms': int, - 'boxX': float, - 'boxY': float, - 'boxZ': float -} - -const atoms = { - 'residueNumber': int, - 'residueName': str, - 'atomName': str, - 'atomNumber': int, - 'x': float, - 'y': float, - 'z': float, - 'vx': float, - 'vy': float, - 'vz': float -} - -const schema = { header, atoms }; -export default function (block: Data.Block) { - return Schema.apply(schema, block); -} \ No newline at end of file diff --git a/src/data/spec/schema.spec.ts b/src/reader/spec/cif.spec.ts similarity index 95% rename from src/data/spec/schema.spec.ts rename to src/reader/spec/cif.spec.ts index 131468eb1..bbd81a321 100644 --- a/src/data/spec/schema.spec.ts +++ b/src/reader/spec/cif.spec.ts @@ -4,8 +4,8 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import * as Data from '../data' -import * as Schema from '../schema' +import * as Data from '../cif/data' +import * as Schema from '../cif/schema' function Field(values: any[]): Data.Field { return { @@ -13,7 +13,6 @@ function Field(values: any[]): Data.Field { str: row => '' + values[row], int: row => +values[row] || 0, float: row => +values[row] || 0, - value: row => values[row], presence: row => Data.ValuePresence.Present, areValuesEqual: (rowA, rowB) => values[rowA] === values[rowB], diff --git a/src/reader/spec/gro.spec.ts b/src/reader/spec/gro.spec.ts index dc2423731..67dd4b21d 100644 --- a/src/reader/spec/gro.spec.ts +++ b/src/reader/spec/gro.spec.ts @@ -5,7 +5,7 @@ * @author David Sehnal <david.sehnal@gmail.com> */ -import Gro from '../gro/format' +import Gro from '../gro/parser' const groString = `MD of 2 waters, t= 4.2 6 @@ -15,82 +15,77 @@ const groString = `MD of 2 waters, t= 4.2 2WATER OW1 4 1.275 0.053 0.622 0.2519 0.3140 -0.1734 2WATER HW2 5 1.337 0.002 0.680 -1.0641 -1.1349 0.0257 2WATER HW3 6 1.326 0.120 0.568 1.9427 -0.8216 -0.0244 - 1.82060 1.82060 1.82060` + 1.82060 2.82060 3.82060` const groStringHighPrecision = `Generated by trjconv : 2168 system t= 15.00000 3 1ETH C1 1 2.735383 2.672010 1.450194 0.2345 -0.1622 0.2097 1ETH H11 2 0.015804 2.716597 1.460588 0.8528 -0.7984 0.6605 1ETH H12 3 2.744822 2.565544 1.409227 -2.3812 2.8618 1.8101 - 1.82060 1.82060 1.82060` + 1.82060 2.82060 3.82060` describe('gro reader', () => { it('basic', () => { - const parsed = Gro.parse(groString) + const parsed = Gro(groString) if (parsed.isError) { console.log(parsed) - } else { - const groFile = parsed.result; - const data = Gro.schema(groFile.blocks[0]); - - const { header, atoms } = data; - if (header._isDefined) { - expect(header.title.value(0)).toBe('MD of 2 waters') - expect(header.timeInPs.value(0)).toBeCloseTo(4.2) - expect(header.numberOfAtoms.value(0)).toBe(6) - - expect(header.boxX.value(0)).toBeCloseTo(1.82060) - expect(header.boxY.value(0)).toBeCloseTo(1.82060) - expect(header.boxZ.value(0)).toBeCloseTo(1.82060) - } else { - console.error('no header') - } - - if (atoms._rowCount === 6) { - expect(atoms.x.value(0)).toBeCloseTo(0.126); - expect(atoms.y.value(0)).toBeCloseTo(1.624); - expect(atoms.z.value(0)).toBeCloseTo(1.679); - - // TODO: check velocities when they are parsed. - } else { - console.error('no atoms'); - } + return; } - }) + + const groFile = parsed.result; + const data = groFile.structures[0]; + + const { header, atoms } = data; + expect(header.title).toBe('MD of 2 waters') + expect(header.timeInPs).toBeCloseTo(4.2) + expect(header.hasVelocities).toBe(true); + expect(header.precision.position).toBe(3); + expect(header.precision.velocity).toBe(4); + expect(header.box[0]).toBeCloseTo(1.82060, 0.00001) + expect(header.box[1]).toBeCloseTo(2.82060, 0.00001) + expect(header.box[2]).toBeCloseTo(3.82060, 0.00001) + + expect(atoms.count).toBe(6); + + expect(atoms.x.value(0)).toBeCloseTo(0.126, 0.001); + expect(atoms.y.value(0)).toBeCloseTo(1.624, 0.001); + expect(atoms.z.value(0)).toBeCloseTo(1.679, 0.001); + + expect(atoms.vx.value(5)).toBeCloseTo(1.9427, 0.0001); + expect(atoms.vy.value(5)).toBeCloseTo(-0.8216, 0.0001); + expect(atoms.vz.value(5)).toBeCloseTo(-0.0244, 0.0001); + }); it('high precision', () => { - const parsed = Gro.parse(groStringHighPrecision) + const parsed = Gro(groStringHighPrecision); if (parsed.isError) { console.log(parsed) - } else { - const groFile = parsed.result; - const data = Gro.schema(groFile.blocks[0]); - - const { header, atoms } = data; - if (header._isDefined) { - expect(header.title.value(0)).toBe('Generated by trjconv : 2168 system') - expect(header.timeInPs.value(0)).toBeCloseTo(15) - expect(header.numberOfAtoms.value(0)).toBe(3) - - expect(header.boxX.value(0)).toBeCloseTo(1.82060) - expect(header.boxY.value(0)).toBeCloseTo(1.82060) - expect(header.boxZ.value(0)).toBeCloseTo(1.82060) - } else { - console.error('no header') - } - - if (atoms._rowCount === 3) { - // TODO: test when high-prec parser is available - // expect(atoms.x.value(1)).toBeCloseTo(0.015804, 0.00001); - // expect(atoms.y.value(1)).toBeCloseTo(2.716597, 0.00001); - // expect(atoms.z.value(1)).toBeCloseTo(1.460588, 0.00001); - - // TODO: check velocities when they are parsed. - } else { - console.error('no atoms'); - } + return; } - }) + + const groFile = parsed.result; + const data = groFile.structures[0]; + + const { header, atoms } = data; + expect(header.title).toBe('Generated by trjconv : 2168 system') + expect(header.timeInPs).toBeCloseTo(15) + expect(header.hasVelocities).toBe(true); + expect(header.precision.position).toBe(6); + expect(header.precision.velocity).toBe(4); + expect(header.box[0]).toBeCloseTo(1.82060, 0.00001) + expect(header.box[1]).toBeCloseTo(2.82060, 0.00001) + expect(header.box[2]).toBeCloseTo(3.82060, 0.00001) + + expect(atoms.count).toBe(3); + + expect(atoms.x.value(1)).toBeCloseTo(0.015804, 0.000001); + expect(atoms.y.value(1)).toBeCloseTo(2.716597, 0.000001); + expect(atoms.z.value(1)).toBeCloseTo(1.460588, 0.000001); + + expect(atoms.vx.value(0)).toBeCloseTo(0.2345, 0.0001); + expect(atoms.vy.value(0)).toBeCloseTo(-0.1622, 0.0001); + expect(atoms.vz.value(0)).toBeCloseTo(0.2097, 0.0001); + }); }); diff --git a/src/script.ts b/src/script.ts index 2febe153c..92b27d329 100644 --- a/src/script.ts +++ b/src/script.ts @@ -7,72 +7,64 @@ // import * as util from 'util' import * as fs from 'fs' -import Gro from './reader/gro/format' +import Gro from './reader/gro/parser' //const file = '1crn.gro' // const file = 'water.gro' // const file = 'test.gro' const file = 'md_1u19_trj.gro' -fs.readFile(`./examples/${file}`, 'utf8', function (err,data) { +fs.readFile(`./examples/${file}`, 'utf8', function (err,input) { if (err) { return console.log(err); } // console.log(data); console.time('parse') - const parsed = Gro.parse(data) + const parsed = Gro(input) console.timeEnd('parse') if (parsed.isError) { console.log(parsed) - } else { - const groFile = parsed.result - const data = Gro.schema(groFile.blocks[0]) + return; + } - // const header = groFile.blocks[0].getCategory('header') - const { header, atoms } = data; - if (header._rowCount !== 1) { - console.log('title', header.title.value(0)) - console.log('timeInPs', header.timeInPs.value(0)) - console.log('numberOfAtoms', header.numberOfAtoms.value(0)) - console.log('boxX', header.boxX.value(0)) - console.log('boxY', header.boxY.value(0)) - console.log('boxZ', header.boxZ.value(0)) - } else { - console.error('no header') - } + const groFile = parsed.result - if (atoms._rowCount > 0) { - console.log(`'${atoms.residueNumber.value(1)}'`) - console.log(`'${atoms.residueName.value(1)}'`) - console.log(`'${atoms.atomName.value(1)}'`) - console.log(atoms.z.value(1)) - console.log(`'${atoms.z.value(1)}'`) + console.log('structure count: ', groFile.structures.length); - const n = atoms._rowCount - console.log('rowCount', n) + const data = groFile.structures[0]; - console.time('getFloatArray x') - const x = atoms.x.toArray(0, n, x => new Float32Array(x))! - console.timeEnd('getFloatArray x') - console.log(x.length, x[0], x[x.length-1]) + // const header = groFile.blocks[0].getCategory('header') + const { header, atoms } = data; + console.log(JSON.stringify(header, null, 2)); + console.log('number of atoms:', atoms.count); - console.time('getFloatArray y') - const y = atoms.y.toArray(0, n, x => new Float32Array(x))! - console.timeEnd('getFloatArray y') - console.log(y.length, y[0], y[y.length-1]) + console.log(`'${atoms.residueNumber.value(1)}'`) + console.log(`'${atoms.residueName.value(1)}'`) + console.log(`'${atoms.atomName.value(1)}'`) + console.log(atoms.z.value(1)) + console.log(`'${atoms.z.value(1)}'`) - console.time('getFloatArray z') - const z = atoms.z.toArray(0, n, x => new Float32Array(x))! - console.timeEnd('getFloatArray z') - console.log(z.length, z[0], z[z.length-1]) + const n = atoms.count; + console.log('rowCount', n) - console.time('getIntArray residueNumber') - const residueNumber = atoms.residueNumber.toArray(0, n, x => new Int32Array(x))! - console.timeEnd('getIntArray residueNumber') - console.log(residueNumber.length, residueNumber[0], residueNumber[residueNumber.length-1]) - } else { - console.error('no atoms') - } - } + console.time('getFloatArray x') + const x = atoms.x.toArray(x => new Float32Array(x))! + console.timeEnd('getFloatArray x') + console.log(x.length, x[0], x[x.length-1]) + + console.time('getFloatArray y') + const y = atoms.y.toArray(x => new Float32Array(x))! + console.timeEnd('getFloatArray y') + console.log(y.length, y[0], y[y.length-1]) + + console.time('getFloatArray z') + const z = atoms.z.toArray(x => new Float32Array(x))! + console.timeEnd('getFloatArray z') + console.log(z.length, z[0], z[z.length-1]) + + console.time('getIntArray residueNumber') + const residueNumber = atoms.residueNumber.toArray(x => new Int32Array(x))! + console.timeEnd('getIntArray residueNumber') + console.log(residueNumber.length, residueNumber[0], residueNumber[residueNumber.length-1]) }); -- GitLab