diff --git a/client/src/app/core/utils/counter.util.ts b/client/src/app/core/utils/counter.util.ts new file mode 100644 index 00000000..7110bb08 --- /dev/null +++ b/client/src/app/core/utils/counter.util.ts @@ -0,0 +1,12 @@ +// keeps an internal count that is incremented at each get call +class Counter { + + private _count = 0; + + getCount() { + return this._count++; + } +} + +// singleton +export const counter = new Counter(); diff --git a/client/src/app/core/utils/index.ts b/client/src/app/core/utils/index.ts index aa9fdb99..ca80a4d3 100644 --- a/client/src/app/core/utils/index.ts +++ b/client/src/app/core/utils/index.ts @@ -1,5 +1,6 @@ import { arrayFlatten } from './array-flatten.util'; import { arrayIsEqual } from './array-is-equal.util'; +import { counter } from './counter.util'; import { elementIsVisible } from './element-is-visible.util'; import { memoizeArray } from './memoize-array.util'; import { saveFile } from './save-file.util'; @@ -8,6 +9,7 @@ import { saveFile } from './save-file.util'; export const utils: any[] = [ arrayFlatten, arrayIsEqual, + counter, elementIsVisible, memoizeArray, saveFile, @@ -16,6 +18,7 @@ export const utils: any[] = [ export * from './array-flatten.util'; export * from './array-is-equal.util'; +export * from './counter.util'; export * from './element-is-visible.util'; export * from './memoize-array.util'; export * from './save-file.util'; diff --git a/client/src/app/gene/components/viewers/micro/micro.shim.ts b/client/src/app/gene/components/viewers/micro/micro.shim.ts index ed246f0e..b50d1171 100644 --- a/client/src/app/gene/components/viewers/micro/micro.shim.ts +++ b/client/src/app/gene/components/viewers/micro/micro.shim.ts @@ -84,7 +84,7 @@ function _tracksToData(tracks, genes) { export function microShim(clusterID, queryTracks, tracks, genes) { // create data const filteredTracks = tracks.filter((t) => t.cluster == clusterID); - const data = _tracksToData(tracks, genes); + const data = _tracksToData(filteredTracks, genes); // identify bold tracks const bold = []; filteredTracks.forEach((t, i) => { diff --git a/client/src/app/gene/gene-routing.module.ts b/client/src/app/gene/gene-routing.module.ts index 81b21cab..9575a954 100644 --- a/client/src/app/gene/gene-routing.module.ts +++ b/client/src/app/gene/gene-routing.module.ts @@ -7,9 +7,13 @@ import { GeneComponent, HeaderLeftComponent, HeaderRightComponent } export const routes: Routes = [ { - path: ':source/:gene', + path: '', component: GeneComponent, }, + //{ + // path: ':source/:gene', + // component: GeneComponent, + //}, { path: '', component: HeaderLeftComponent, diff --git a/client/src/app/gene/models/params/clustering.model.ts b/client/src/app/gene/models/params/clustering.model.ts index ad7508f3..351d8376 100644 --- a/client/src/app/gene/models/params/clustering.model.ts +++ b/client/src/app/gene/models/params/clustering.model.ts @@ -11,7 +11,7 @@ export class ClusteringParams implements Params { constructor( public linkage: string = 'average', // TODO: remove magic string - public threshold: number = 10) { + public threshold: number = 20) { } formControls(): any { diff --git a/client/src/app/gene/models/shims/family-count-map.ts b/client/src/app/gene/models/shims/family-count-map.ts new file mode 100644 index 00000000..26872018 --- /dev/null +++ b/client/src/app/gene/models/shims/family-count-map.ts @@ -0,0 +1,14 @@ +export type FamilyCountMap = {[key: string]: number}; + + +export function familyCountMap(families: string[]): FamilyCountMap { + const reducer = (accumulator, family) => { + if (!(family in accumulator)) { + accumulator[family] = 0; + } + accumulator[family] += 1; + return accumulator; + }; + const map = families.reduce(reducer, {}); + return map; +} diff --git a/client/src/app/gene/models/shims/index.ts b/client/src/app/gene/models/shims/index.ts index f829a0e8..d1eb245a 100644 --- a/client/src/app/gene/models/shims/index.ts +++ b/client/src/app/gene/models/shims/index.ts @@ -1,5 +1,6 @@ export * from './block-index-map'; export * from './endpoint-genes'; +export * from './family-count-map'; export * from './gene-map'; export * from './macro-blocks'; export * from './name-source-id'; diff --git a/client/src/app/gene/store/actions/chromosome.actions.ts b/client/src/app/gene/store/actions/chromosome.actions.ts index d7cc7959..7cf9edc7 100644 --- a/client/src/app/gene/store/actions/chromosome.actions.ts +++ b/client/src/app/gene/store/actions/chromosome.actions.ts @@ -1,4 +1,5 @@ import { Action } from '@ngrx/store'; +import { counter } from '@gcv/core/utils'; import { Track } from '@gcv/gene/models'; export const GET = '[CHROMOSOME] GET'; @@ -7,6 +8,7 @@ export const GET_FAILURE = '[CHROMOSOME] GET_FAILURE'; export class Get implements Action { readonly type = GET; + readonly id = counter.getCount(); constructor(public payload: {name: string, source: string}) { } } diff --git a/client/src/app/gene/store/actions/gene.actions.ts b/client/src/app/gene/store/actions/gene.actions.ts index bde885d9..4e582ffe 100644 --- a/client/src/app/gene/store/actions/gene.actions.ts +++ b/client/src/app/gene/store/actions/gene.actions.ts @@ -1,12 +1,15 @@ import { Action } from '@ngrx/store'; +import { counter } from '@gcv/core/utils'; import { Gene, Track } from '@gcv/gene/models'; export const GET = '[GENE] GET'; export const GET_SUCCESS = '[GENE] GET_SUCCESS'; export const GET_FAILURE = '[GENE] GET_FAILURE'; + export class Get implements Action { readonly type = GET; + readonly id = counter.getCount(); constructor(public payload: {names: string[], source: string}) { } } diff --git a/client/src/app/gene/store/effects/chromosome.effects.ts b/client/src/app/gene/store/effects/chromosome.effects.ts index bb2ef947..780363ab 100644 --- a/client/src/app/gene/store/effects/chromosome.effects.ts +++ b/client/src/app/gene/store/effects/chromosome.effects.ts @@ -3,11 +3,13 @@ import { Injectable } from '@angular/core'; // store import { Effect, Actions, ofType } from '@ngrx/effects'; import { of } from 'rxjs'; -import { catchError, filter, map, mergeMap, switchMap } from 'rxjs/operators'; +import { catchError, filter, map, mergeMap, switchMap, withLatestFrom } + from 'rxjs/operators'; import { Store } from '@ngrx/store'; import * as chromosomeActions from '@gcv/gene/store/actions/chromosome.actions'; import * as fromRoot from '@gcv/gene/store/reducers'; import * as fromChromosome from '@gcv/gene/store/selectors/chromosome/'; +import { TrackID, trackID } from '@gcv/gene/store/utils'; // app import { ChromosomeService } from '@gcv/gene/services'; @@ -24,15 +26,28 @@ export class ChromosomeEffects { getSelected$ = this.store .select(fromChromosome.getUnloadedSelectedChromosomeIDs).pipe( filter((ids) => ids.length > 0), - switchMap((ids) => ids.map((id) => new chromosomeActions.Get(id))), + mergeMap((ids) => ids.map((id) => new chromosomeActions.Get(id))), ); // get chromosome via the chromosome service @Effect() getChromosome$ = this.actions$.pipe( ofType(chromosomeActions.GET), - map((action: chromosomeActions.Get) => action.payload), - mergeMap(({name, source}) => { + map((action: chromosomeActions.Get) => { + return {action: action.id, ...action.payload}; + }), + withLatestFrom(this.store.select(fromChromosome.getLoading)), + mergeMap(([{action, name, source}, loading]) => { + // get loaded/loading genes + const actionTrackID = + ({name, source, action}) => `${trackID(name, source)}:${action}`; + const loadingIDs = new Set(loading.map(actionTrackID)); + // only load chromosome if action is loading + const id = actionTrackID({action, name, source}); + const chromosomeID = trackID(name, source); + if (!loadingIDs.has(id)) { + return []; + } return this.chromosomeService.getChromosome(name, source).pipe( map((chromosome) => { return new chromosomeActions.GetSuccess({chromosome}); diff --git a/client/src/app/gene/store/effects/gene.effects.ts b/client/src/app/gene/store/effects/gene.effects.ts index 54475feb..66bdc89e 100644 --- a/client/src/app/gene/store/effects/gene.effects.ts +++ b/client/src/app/gene/store/effects/gene.effects.ts @@ -29,34 +29,45 @@ export class GeneEffects { getSelected$ = this.store.select(fromGene.getUnloadedSelectedGeneIDs).pipe( filter((ids) => ids.length > 0), switchMap((ids) => { - const id2action = ({name, source}) => { - return new geneActions.Get({names: [name], source}); + // bin ids by source + const reducer = (accumulator, {name, source}) => { + if (!(source in accumulator)) { + accumulator[source] = []; + } + accumulator[source].push(name); + return accumulator; }; - const actions = ids.map(id2action); + const geneBins = ids.reduce(reducer, {}); + // make a gene request action for each source bin + const actions = Object.entries(geneBins) + .map(([source, names]: [string, string[]]) => { + return new geneActions.Get({names, source}); + }); return actions; }), ); - // TODO: when loading multi route, take loading and failed genes into - // consideration - // get genes via the gene service @Effect() getGenes$ = this.actions$.pipe( ofType(geneActions.GET), - map((action: geneActions.Get) => action.payload), - withLatestFrom(this.store.select(fromGene.getGeneState)), - concatMap(([{names, source}, state]) => { + map((action: geneActions.Get) => ({action: action.id, ...action.payload})), + withLatestFrom(this.store.select(fromGene.getLoading)), + concatMap(([{action, names, source}, loading]) => { // get loaded/loading genes - const loadingIDs = new Set(state.loading.map(geneID)); - const loadedIDs = new Set(state.loaded.map(geneID)); - // filter out genes that are already loaded or loading - const filteredNamess = names.filter((n) => { - const gID = geneID(n, source); - return !loadingIDs.has(gID) && !loadedIDs.has(gID); + const actionGeneID = + ({name, source, action}) => `${geneID(name, source)}:${action}`; + const loadingIDs = new Set(loading.map(actionGeneID)); + // only keep genes whose action is loaded + const filteredNames = names.filter((name) => { + const id = actionGeneID({name, source, action}); + return loadingIDs.has(id); }); // get the genes - return this.geneService.getGenes(names, source).pipe( + if (filteredNames.length == 0) { + return []; + } + return this.geneService.getGenes(filteredNames, source).pipe( map((genes: Gene[]) => new geneActions.GetSuccess({genes})), catchError((error) => of(new geneActions.GetFailure({names, source}))) ); @@ -66,9 +77,7 @@ export class GeneEffects { // get all genes for the selected micro-tracks @Effect() getMicroTrackGenes$ = this.store.select(fromMicroTracks.getAllMicroTracks).pipe( - switchMap((tracks) => { - return geneActions.tracksToGetGeneActions(tracks); - }), + switchMap((tracks) => geneActions.tracksToGetGeneActions(tracks)), ); } diff --git a/client/src/app/gene/store/reducers/chromosome.reducer.ts b/client/src/app/gene/store/reducers/chromosome.reducer.ts index 834bacc5..cf568985 100644 --- a/client/src/app/gene/store/reducers/chromosome.reducer.ts +++ b/client/src/app/gene/store/reducers/chromosome.reducer.ts @@ -1,17 +1,16 @@ // A chromosome is an instance of Track that represents an entire chromosome // as an ordered list of genes and a corresponding list of gene families. This -// file provides an NgRx reducer and selectors for storing and accessing -// chromosome data. Specifically, a chromosome is loaded as a Track for each -// gene provided by the user. These Tracks are stored by the chromosome reducer -// and made available via selectors. This includes a selector that provides the -// neighborhood each user provided gene occurs is as a slice of the gene's -// chromosome. +// file provides an NgRx reducer for storing chromosome data. Specifically, a +// chromosome is loaded as a Track for each gene provided by the user. These +// Tracks are stored by the chromosome reducer and made available via selectors. +// This includes a selector that provides the neighborhood each user provided +// gene occurs is as a slice of the gene's chromosome - a micro-track. // NgRx import { createEntityAdapter, EntityState } from '@ngrx/entity'; // store import * as chromosomeActions from '@gcv/gene/store/actions/chromosome.actions'; -import { TrackID, trackID } from '@gcv/gene/store/utils'; +import { ActionID, TrackID, trackID } from '@gcv/gene/store/utils'; // app import { Track } from '@gcv/gene/models'; @@ -23,17 +22,14 @@ const adapter = createEntityAdapter({ selectId: (e) => trackID(e.name, e.source) }); -// TODO: is loaded even necessary or can it be derived from entity ids and // selectedChromosomeIDs selector? export interface State extends EntityState { failed: TrackID[]; - loaded: TrackID[]; - loading: TrackID[]; + loading: (TrackID & ActionID)[]; } const initialState: State = adapter.getInitialState({ failed: [], - loaded: [], loading: [], }); @@ -43,9 +39,20 @@ export function reducer( ): State { switch (action.type) { case chromosomeActions.GET: + const loadingIDs = new Set(state.loading.map(trackID)); + const id = trackID(action.payload); + const loading = []; + if (!loadingIDs.has(id) && !(id in state.entities)) { + loading.push({action: action.id, ...action.payload}); + } + const failed = state.failed.filter(({name, source}) => { + const fID = trackID(name, source); + return fID === id; + }); return { ...state, - loading: state.loading.concat([action.payload]), + loading: state.loading.concat(loading), + failed, }; case chromosomeActions.GET_SUCCESS: { @@ -55,7 +62,6 @@ export function reducer( chromosome, { ...state, - loaded: state.loaded.concat(id), loading: state.loading.filter(({name, source}) => { return !(name === id.name && source === id.source); }), diff --git a/client/src/app/gene/store/reducers/gene.reducer.ts b/client/src/app/gene/store/reducers/gene.reducer.ts index a3d89cca..7fdab43f 100644 --- a/client/src/app/gene/store/reducers/gene.reducer.ts +++ b/client/src/app/gene/store/reducers/gene.reducer.ts @@ -1,8 +1,8 @@ // A Gene is a second class citizen in the GCV, that is, other than dictating // what chromosomes are loaded, all visualizations, algorithms, and auxiliary // models are derived from the gene families of the Track model. As such, genes -// are loaded on an as needed basis. This file contains an NgRx reducer and -// selectors for storing and accessing Genes. +// are loaded on an as needed basis. This file contains an NgRx reducer for +// storing Genes. // NgRx import { createEntityAdapter, EntityState } from '@ngrx/entity'; @@ -10,6 +10,7 @@ import { createEntityAdapter, EntityState } from '@ngrx/entity'; import * as geneActions from '@gcv/gene/store/actions/gene.actions'; // app import { Gene } from '@gcv/gene/models'; +import { ActionID } from '@gcv/gene/store/utils'; declare var Object: any; // because TypeScript doesn't support Object.values @@ -32,17 +33,14 @@ const adapter = createEntityAdapter({ selectId: (e) => geneID(e.name, e.source) }); -// TODO: is loaded even necessary or can it be derived from entity ids and // selectedGeneIDs selector? export interface State extends EntityState { failed: GeneID[]; - loaded: GeneID[]; - loading: GeneID[]; + loading: (GeneID & ActionID)[]; } const initialState: State = adapter.getInitialState({ failed: [], - loaded: [], loading: [], }); @@ -53,10 +51,24 @@ export function reducer( switch (action.type) { case geneActions.GET: const source = action.payload.source; - const loading = action.payload.names.map((name) => ({name, source})); + const loadingIDs = new Set(state.loading.map(geneID)); + const loading = action.payload.names + .filter((name) => { + const id = geneID(name, source); + return !loadingIDs.has(id) && !(id in state.entities); + }) + .map((name) => { + return {name, source, action: action.id}; + }); + const newLoadingIDs = new Set(loading.map(geneID)); + const failed = state.failed.filter(({name, source}) => { + const id = geneID(name, source); + return !newLoadingIDs.has(id); + }); return { ...state, loading: state.loading.concat(loading), + failed, }; case geneActions.GET_SUCCESS: { @@ -72,7 +84,6 @@ export function reducer( genes, { ...state, - loaded: state.loaded.concat(loaded), loading, }, ); diff --git a/client/src/app/gene/store/selectors/chromosome/chromosome-state.selector.ts b/client/src/app/gene/store/selectors/chromosome/chromosome-state.selector.ts index a2165bd2..f1495a0c 100644 --- a/client/src/app/gene/store/selectors/chromosome/chromosome-state.selector.ts +++ b/client/src/app/gene/store/selectors/chromosome/chromosome-state.selector.ts @@ -10,3 +10,9 @@ export const getChromosomeState = createSelector( fromModule.getGeneModuleState, state => state[chromosomeFeatureKey] ); + + +export const getLoading = createSelector( + getChromosomeState, + state => state.loading, +); diff --git a/client/src/app/gene/store/selectors/chromosome/clustered-chromosomes.selector.ts b/client/src/app/gene/store/selectors/chromosome/clustered-chromosomes.selector.ts index e8b02341..c651a065 100644 --- a/client/src/app/gene/store/selectors/chromosome/clustered-chromosomes.selector.ts +++ b/client/src/app/gene/store/selectors/chromosome/clustered-chromosomes.selector.ts @@ -10,7 +10,8 @@ import { Track } from '@gcv/gene/models'; import { ClusterMixin } from '@gcv/gene/models/mixins'; -export const getSelectedChromosomesForCluster = (id: number) => createSelector( +export const getSelectedChromosomesForCluster = +(id: number) => createSelector( getSelectedChromosomes, getSelectedMicroTracksForCluster(id), (chromosomes: Track[], tracks: (Track | ClusterMixin)[]): Track[] => { diff --git a/client/src/app/gene/store/selectors/chromosome/selected-chromosomes.selector.ts b/client/src/app/gene/store/selectors/chromosome/selected-chromosomes.selector.ts index 8369a3ae..73a11367 100644 --- a/client/src/app/gene/store/selectors/chromosome/selected-chromosomes.selector.ts +++ b/client/src/app/gene/store/selectors/chromosome/selected-chromosomes.selector.ts @@ -83,7 +83,7 @@ export const getUnloadedSelectedChromosomeIDs = createSelector( getSelectedChromosomeIDs, (state: State, ids: TrackID[]): TrackID[] => { const loadingIDs = new Set(state.loading.map(trackID)); - const loadedIDs = new Set(state.loaded.map(trackID)); + const loadedIDs = new Set(state.ids as string[]); const unloadedIDs = ids.filter((id) => { const idString = trackID(id); return !loadingIDs.has(idString) && !loadedIDs.has(idString); diff --git a/client/src/app/gene/store/selectors/gene/gene-state.selector.ts b/client/src/app/gene/store/selectors/gene/gene-state.selector.ts index f9d29684..49b3d217 100644 --- a/client/src/app/gene/store/selectors/gene/gene-state.selector.ts +++ b/client/src/app/gene/store/selectors/gene/gene-state.selector.ts @@ -1,11 +1,19 @@ // NgRx -import { createSelector } from '@ngrx/store'; +import { createSelector, createSelectorFactory } from '@ngrx/store'; // store import * as fromModule from '@gcv/gene/store/reducers'; import { geneFeatureKey } from '@gcv/gene/store/reducers/gene.reducer'; +// app +import { memoizeArray } from '@gcv/core/utils'; export const getGeneState = createSelector( fromModule.getGeneModuleState, state => state[geneFeatureKey] ); + + +export const getLoading = createSelectorFactory(memoizeArray)( + getGeneState, + state => state.loading, +); diff --git a/client/src/app/gene/store/selectors/gene/selected-genes.selector.ts b/client/src/app/gene/store/selectors/gene/selected-genes.selector.ts index 8eb8ff76..d715e95d 100644 --- a/client/src/app/gene/store/selectors/gene/selected-genes.selector.ts +++ b/client/src/app/gene/store/selectors/gene/selected-genes.selector.ts @@ -32,7 +32,7 @@ export const getUnloadedSelectedGeneIDs = createSelector( getSelectedGeneIDs, (state: State, ids: GeneID[]): GeneID[] => { const loadingIDs = new Set(state.loading.map(geneID)); - const loadedIDs = new Set(state.loaded.map(geneID)); + const loadedIDs = new Set(state.ids as string[]); const unloadedIDs = ids.filter((id) => { const idString = geneID(id); return !loadingIDs.has(idString) && !loadedIDs.has(idString); diff --git a/client/src/app/gene/store/selectors/micro-tracks/clustered-and-aligned-micro-tracks.selector.ts b/client/src/app/gene/store/selectors/micro-tracks/clustered-and-aligned-micro-tracks.selector.ts index d1364307..acf543a9 100644 --- a/client/src/app/gene/store/selectors/micro-tracks/clustered-and-aligned-micro-tracks.selector.ts +++ b/client/src/app/gene/store/selectors/micro-tracks/clustered-and-aligned-micro-tracks.selector.ts @@ -14,13 +14,13 @@ import { regexpFactory } from '@gcv/gene/algorithms/utils'; import { Track } from '@gcv/gene/models'; import { AlignmentParams, ClusteringParams } from '@gcv/gene/models/params'; import { AlignmentMixin, ClusterMixin } from '@gcv/gene/models/mixins'; +import { familyCountMap } from '@gcv/gene/models/shims'; // clustered // clusters micro tracks based on their families -// TODO: update params to work with new clusterer // TODO: only cluster when selectedLoaded emits true export const getClusteredSelectedMicroTracks = createSelector( getSelectedMicroTracks, @@ -38,12 +38,13 @@ export const getClusteredSelectedMicroTracks = createSelector( clusterfck.hcluster(tracks, metric, params.linkage, params.threshold); const recurrence = (cluster) => { const elements = []; - if ('left' in cluster || 'right' in cluster) { - if ('left' in cluster) { + if ('left' in cluster && 'right' in cluster) { + if (cluster['left']['size'] >= cluster['right']['size']) { elements.push(...recurrence(cluster['left'])); - } - if ('right' in cluster) { elements.push(...recurrence(cluster['right'])); + } else { + elements.push(...recurrence(cluster['right'])); + elements.push(...recurrence(cluster['left'])); } } else { elements.push(cluster['value']); @@ -111,23 +112,44 @@ export const getClusteredAndAlignedSelectedMicroTracks = createSelector( // prepare the data const trackFamilies = tracks.map((t) => t.families); const l = trackFamilies[0].length; - const flattenedTracks = arrayFlatten(trackFamilies); - const characters = new Set(flattenedTracks); + const flattenedTracks: string[] = arrayFlatten(trackFamilies); + //const characters = new Set(flattenedTracks); + const familyCounts = familyCountMap(flattenedTracks); + const characters = new Set(); const omit = new Set(); + Object.entries(familyCounts).forEach(([family, count]) => { + if (count == 1 || family == '') { + omit.add(family); + } else { + characters.add(family); + } + }); // construct and train the model - const hmm = new GCV.graph.MSAHMM(l, characters); - hmm.train(trackFamilies, {reverse: true, omit, surgery: true}); - // align the tracks - const alignments = trackFamilies.map((f) => hmm.align(f)); - const mixin = (t, i) => { + const mixinFactory = (alignments) => (t, i) => { const t2 = Object.create(t); t2.alignment = alignments[i]; return t2; }; - const alignedTracks = tracks.map(mixin); - const consensus = hmm.consensus(); - accumulator.consensuses[i] = hmm.consensus(); - accumulator.tracks.push(...alignedTracks); + // msa via hmm + if (characters.size > 0) { + const hmm = new GCV.graph.MSAHMM(l, characters); + hmm.train(trackFamilies, {reverse: true, omit}); + // align the tracks + const alignments = trackFamilies.map((f,i) => { + return hmm.align(f, {inversions: false}); + }); + const alignedTracks = tracks.map(mixinFactory(alignments)); + accumulator.consensuses[i] = hmm.consensus(); + accumulator.tracks.push(...alignedTracks); + // edge case where all families are orphans or singletons + } else { + const alignments = trackFamilies.map((families) => { + return families.map((f, i) => i); + }); + const alignedTracks = tracks.map(mixinFactory(alignments)); + accumulator.consensuses[i] = []; + accumulator.tracks.push(...alignedTracks); + } return accumulator; }; const clusteredAlignments = { diff --git a/client/src/app/gene/store/selectors/router/index.ts b/client/src/app/gene/store/selectors/router/index.ts index 90a0af43..c3a7e291 100644 --- a/client/src/app/gene/store/selectors/router/index.ts +++ b/client/src/app/gene/store/selectors/router/index.ts @@ -4,6 +4,8 @@ import { createSelector } from '@ngrx/store'; import * as fromModule from '@gcv/gene/store/reducers'; import { initialState } from '@gcv/gene/store/reducers/router.reducer'; // app +import { AppConfig } from '@gcv/app.config'; +import { arrayFlatten } from '@gcv/core/utils'; import { AlignmentParams, BlockParams, ClusteringParams, QueryParams, SourceParams } from '@gcv/gene/models/params'; import { instantiateAndPopulate } from '@gcv/gene/utils'; @@ -23,43 +25,21 @@ export const getParams = createSelector( (route) => route.state.params, ); -// TODO: get multi route genes too export const getRouteGenes = createSelector( getRouterState, (route): {name: string, source: string}[] => { // TODO: can this be handled upstream? if (route === undefined || route.state === undefined || - route.state.params === undefined || - route.state.params.gene === undefined || - route.state.params.source === undefined) { + route.state.params === undefined) { return []; } + const sources = AppConfig.SERVERS.map((s) => s.id); const params = route.state.params; - return [{name: params.gene, source: params.source}]; - }, -); - -export const getSearchRoute = createSelector( - getRouterState, - // TODO: 'properly' set initial state - // https://github.com/ngrx/platform/issues/662 - (route=initialState): {gene: string, source: string} => { - const params = route.state.params; - return {source: params.source, gene: params.gene}; - }, -); - -export const getSearchRouteSource = createSelector( - getRouterState, - (route) => route.state.params.source, -); - -export const getMultiRoute = createSelector( - getRouterState, - (route) => { - const params = route.state.params; - return {genes: params.genes}; + const selectedGenes = sources + .filter((source) => source in params) + .map((source) => params[source].map((name) => ({source, name}))); + return arrayFlatten(selectedGenes); }, ); diff --git a/client/src/app/gene/store/utils/action-id.ts b/client/src/app/gene/store/utils/action-id.ts new file mode 100644 index 00000000..dcf45a44 --- /dev/null +++ b/client/src/app/gene/store/utils/action-id.ts @@ -0,0 +1 @@ +export type ActionID = {action: number}; diff --git a/client/src/app/gene/store/utils/index.ts b/client/src/app/gene/store/utils/index.ts index cddfaaf9..86b1b28e 100644 --- a/client/src/app/gene/store/utils/index.ts +++ b/client/src/app/gene/store/utils/index.ts @@ -1 +1,2 @@ +export * from './action-id'; export * from './track-id'; diff --git a/client/src/app/gene/utils/custom-router-state-serializer.util.ts b/client/src/app/gene/utils/custom-router-state-serializer.util.ts index 7bb9e524..112b9a61 100644 --- a/client/src/app/gene/utils/custom-router-state-serializer.util.ts +++ b/client/src/app/gene/utils/custom-router-state-serializer.util.ts @@ -1,5 +1,8 @@ +// Angular import { Params, RouterStateSnapshot } from '@angular/router'; import { RouterStateSerializer } from '@ngrx/router-store'; +// app +import { AppConfig } from '@gcv/app.config'; import * as fromRouter from '@gcv/gene/store/reducers/router.reducer'; // Returns an object including only the URL, params, and query params instead of @@ -15,13 +18,15 @@ implements RouterStateSerializer { } let { url, root: { queryParams } } = routerState; - let { params } = route; + const params = Object.assign({}, route.params); - // convert route params into expected types - if (params.genes) { - params = Object.assign({}, params); - params.genes = params.genes.split(','); - } + // convert route params into expected types (gene lists) + const sources = AppConfig.SERVERS.map((s) => s.id); + sources.forEach((source) => { + if (source in params) { + params[source] = params[source].split(','); + } + }); // convert route query params into expected types queryParams = Object.assign({}, queryParams); diff --git a/client/src/assets/js/gcv/graph/msa-hmm.ts b/client/src/assets/js/gcv/graph/msa-hmm.ts index 051e24e8..1e0ef71b 100644 --- a/client/src/assets/js/gcv/graph/msa-hmm.ts +++ b/client/src/assets/js/gcv/graph/msa-hmm.ts @@ -456,7 +456,7 @@ export class MSAHMM extends Directed { } while (reversePath[j].startsWith("d") && j >= 0) { if (reverse) { - path.push(reverse[j]); + path.push(reversePath[j]); } j--; } @@ -610,7 +610,7 @@ export class MSAHMM extends Directed { */ train(sequences, options: { - omit?: Set, // TODO: should omitting be left to the user? + omit?: Set, reverse?: boolean, surgery?: boolean, }={}) @@ -618,7 +618,7 @@ export class MSAHMM extends Directed { // parse optional parameters this._setOption(options, "omit", new Set()); this._setOption(options, "reverse", true); - this._setOption(options, "surgery", false); + this._setOption(options, "surgery", true); // train the model const filter = (sequence) => sequence.filter((s) => !options.omit.has(s)); sequences.forEach((s, i) => { @@ -668,7 +668,7 @@ export class MSAHMM extends Directed { } insertion = 0; }; - let prev = "a"; + let prev = "a0"; let prevIndex = -1; for (let i = 1; i < path.length; i++) { const n = path[i]; @@ -688,6 +688,8 @@ export class MSAHMM extends Directed { } else if (n.startsWith("i")) { insertion++; } + prev = n; + prevIndex = n; } return alignment; } @@ -714,19 +716,23 @@ export class MSAHMM extends Directed { // compute Viterbi paths and their emission probabilities let forward = sequence; - const forwardPath = this.viterbi(forward); + let forwardPath = this.viterbi(forward); let forwardEmissions = this.sequenceEmissions(forward, forwardPath); let reverse = (options.reverse || options.inversions) ? [...forward].reverse() : []; - const reversePath = this.viterbi(reverse); + let reversePath = this.viterbi(reverse); let reverseEmissions = this.sequenceEmissions(reverse, reversePath); // swap orientations depending on score const reversed = options.reverse && forwardPath.probability < reversePath.probability; if (reversed) { - [forward, forwardEmissions, reverseEmissions] = - [reverse, reverseEmissions, forwardEmissions]; + [forward, reverse, + forwardPath, reversePath, + forwardEmissions, reverseEmissions] = + [reverse, forward, + reversePath, forwardPath, + reverseEmissions, forwardEmissions]; } // compute inversions @@ -740,8 +746,7 @@ export class MSAHMM extends Directed { // convert path to hmm state independent alignment let alignment = this.pathToAlignment(alignmentPath); if (reversed) { - const l = sequence.length-1; - alignment = alignment.map((x) => l-x); + alignment.reverse(); } return alignment; diff --git a/client/src/assets/js/gcv/metrics/levenshtein.ts b/client/src/assets/js/gcv/metrics/levenshtein.ts index f7329055..6547cac7 100644 --- a/client/src/assets/js/gcv/metrics/levenshtein.ts +++ b/client/src/assets/js/gcv/metrics/levenshtein.ts @@ -1,17 +1,32 @@ -export const levenshtein = (s: T[], t: T[]): number => { - const sLen = s.length; - const tLen = t.length; - return levenshteinRecurrence(s, sLen, t, tLen); +import { matrix } from '../common'; + + +export const levenshtein = (a: T[], b: T[]): number => { + const i = a.length; + const j = b.length; + const t = matrix(i+1, j+1, null); + return levenshteinRecurrence(a, i, b, j, t); } -const levenshteinRecurrence = (s: T[], sLen: number, t: T[], tLen: number): number => { - // base case: empty strings - if (sLen == 0) return tLen; - if (tLen == 0) return sLen; - // test if last characters of the strings match - let cost = (s[sLen-1] == t[tLen-1] ? 0 : 1); - // return minimum of delete char from s, delete char from t, and delete char from both - return Math.min(levenshteinRecurrence(s, sLen-1, t, tLen)+1, - levenshteinRecurrence(s, sLen, t, tLen-1)+1, - levenshteinRecurrence(s, sLen-1, t, tLen-1)+cost); + +const levenshteinRecurrence = +(a: T[], i: number, b: T[], j: number, t: number[]): number => { + // use memoized data if possible + if (t[i][j] === null) { + // base case: empty strings + if (i == 0) { + t[i][j] = j; + } else if (j == 0) { + t[i][j] = i; + // test if last characters of the strings match + } else { + let cost = (a[i-1] == b[j-1] ? 0 : 1); + // return minimum of delete char from a, delete char from b, and delete + // char from both + t[i][j] = Math.min(levenshteinRecurrence(a, i-1, b, j, t)+1, + levenshteinRecurrence(a, i, b, j-1, t)+1, + levenshteinRecurrence(a, i-1, b, j-1, t)+cost); + } + } + return t[i][j]; }