Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix values provided by H5Grove #836

Merged
merged 7 commits into from
Nov 3, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 20 additions & 8 deletions packages/app/src/providers/h5grove/h5grove-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,20 @@ import type {
GroupWithChildren,
UnresolvedEntity,
} from '@h5web/shared';
import { assertDataset, buildEntityPath, EntityKind } from '@h5web/shared';
import {
hasScalarShape,
hasArrayShape,
assertDataset,
buildEntityPath,
EntityKind,
assertNonNullShape,
} from '@h5web/shared';
import { isString } from 'lodash';

import { ProviderApi } from '../api';
import type { ValuesStoreParams } from '../models';
import { ProviderError } from '../models';
import { convertDtype, handleAxiosError } from '../utils';
import { convertDtype, flattenValue, handleAxiosError } from '../utils';
import type {
H5GroveAttribute,
H5GroveAttrValuesResponse,
Expand Down Expand Up @@ -41,17 +48,22 @@ export class H5GroveApi extends ProviderApi {
public async getValue(
params: ValuesStoreParams
): Promise<H5GroveDataResponse> {
const { path } = params;
const { path, selection } = params;
const entity = await this.getEntity(path);
assertDataset(entity);
assertNonNullShape(entity);

const DTypedArray = typedArrayFromDType(entity.type);
if (!DTypedArray) {
return this.fetchData(params);
if (DTypedArray) {
const buffer = await this.fetchBinaryData(params);
const array = new DTypedArray(buffer);
return hasScalarShape(entity) ? array[0] : [...array];
}

const buffer = await this.fetchBinaryData(params);
return new DTypedArray(buffer);
const value = await this.fetchData(params);
return hasArrayShape(entity)
? flattenValue(value, entity, selection)
: value;
axelboc marked this conversation as resolved.
Show resolved Hide resolved
}
loichuder marked this conversation as resolved.
Show resolved Hide resolved

private async fetchEntity(path: string): Promise<H5GroveEntityResponse> {
Expand Down Expand Up @@ -114,7 +126,7 @@ export class H5GroveApi extends ProviderApi {
params: ValuesStoreParams
): Promise<ArrayBuffer> {
const { data } = await this.cancellableFetchValue<ArrayBuffer>(
`/data/`,
'/data/',
params,
{ ...params, format: 'bin' },
'arraybuffer'
Expand Down
12 changes: 4 additions & 8 deletions packages/app/src/providers/hsds/hsds-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ import {
import { ProviderApi } from '../api';
import type { ValuesStoreParams } from '../models';
import { ProviderError } from '../models';
import { handleAxiosError } from '../utils';
import { flattenValue, handleAxiosError } from '../utils';
import type {
HsdsDatasetResponse,
HsdsDatatypeResponse,
Expand All @@ -33,7 +33,7 @@ import type {
HsdsId,
} from './models';
import {
assertHsdsDataset,
assertNonNullHsdsDataset,
isHsdsGroup,
convertHsdsShape,
convertHsdsType,
Expand Down Expand Up @@ -111,18 +111,14 @@ export class HsdsApi extends ProviderApi {
const { path } = params;

const entity = await this.getEntity(path);
assertHsdsDataset(entity);
assertNonNullHsdsDataset(entity);

const value = await this.fetchValue(entity.id, params);

// https://github.com/HDFGroup/hsds/issues/88
// HSDS does not reduce the number of dimensions when selecting indices
// Therefore the flattening must be done on all dimensions regardless of the selection
if (hasArrayShape(entity)) {
return (value as unknown[]).flat(entity.shape.length - 1);
}

return value;
return hasArrayShape(entity) ? flattenValue(value, entity) : value;
}

private async fetchRootId(): Promise<HsdsId> {
Expand Down
18 changes: 13 additions & 5 deletions packages/app/src/providers/hsds/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,16 @@ import type {
ComplexType,
DType,
Attribute,
ScalarShape,
ArrayShape,
} from '@h5web/shared';
import {
isGroup,
isDataset,
DTypeClass,
Endianness,
hasNonNullShape,
} from '@h5web/shared';
import { isGroup, isDataset, DTypeClass, Endianness } from '@h5web/shared';

import type {
HsdsType,
Expand All @@ -34,11 +42,11 @@ export function assertHsdsEntity(entity: Entity): asserts entity is HsdsEntity {
}
}

export function assertHsdsDataset(
export function assertNonNullHsdsDataset(
entity: HsdsEntity
): asserts entity is HsdsEntity<Dataset> {
if (!isHsdsDataset(entity)) {
throw new Error('Expected entity to be HSDS dataset');
): asserts entity is HsdsEntity<Dataset<ScalarShape | ArrayShape>> {
if (!isHsdsDataset(entity) || !hasNonNullShape(entity)) {
throw new Error('Expected entity to be HSDS dataset with non-null shape');
}
}

Expand Down
15 changes: 13 additions & 2 deletions packages/app/src/providers/utils.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DType } from '@h5web/shared';
import { Endianness, DTypeClass } from '@h5web/shared';
import type { ArrayShape, Dataset, DType } from '@h5web/shared';
import { Endianness, DTypeClass, assertArray } from '@h5web/shared';
import axios from 'axios';

import type { ProviderError } from './models';
Expand Down Expand Up @@ -81,6 +81,17 @@ export function convertDtype(dtype: string): DType {
}
}

export function flattenValue(
value: unknown,
dataset: Dataset<ArrayShape>,
selection?: string
): unknown[] {
assertArray(value);
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will throw if we ask the API to select a scalar, so at least we'll know (in the unlikely event we write code that does this inadvertently).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that given our usecases, we will never get a scalar from an array dataset.

Still it bothers me to prevent it for the following reasons:

  • Here and in assertDatasetValue usage locations, we have access to selection. So we could assert according to the selection.
  • The type inference in useDatasetValue could make use of dimMapping to have a Value<D> corresponding to the gotten slice.

Anyway, this will do for now.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure, it's all doable, but adding this level of type safety to useDatasetValue means that we then have to assert that the returned value is not scalar inside the relevant containers, which leads to exactly the same result as the assertion that the value returned by the provider is an array regardless of selection.

If anything, it would make more sense to prevent useDatasetValue from being called with a dimMapping that only contains numbers.

Copy link
Contributor Author

@axelboc axelboc Nov 3, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Either way, we still have implicit coupling between useDatasetValue and the provider APIs' getValue methods... Perhaps a better solution would be to rethink the signature of getValue to make this coupling more explicit - e.g. by letting the method accept a dataset rather than an entity, and an optional dimension mapping array with at least one axis in it.

const slicedDims = selection?.split(',').filter((s) => s.includes(':'));
const dims = slicedDims || dataset.shape;
return value.flat(dims.length - 1);
}

export async function handleAxiosError<T>(
func: () => Promise<T>,
getErrorToThrow: (
Expand Down
28 changes: 21 additions & 7 deletions packages/app/src/vis-packs/core/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {
ScaleType,
getBounds,
getValidDomainForScale,
assertDatasetValue,
assertNonNullShape,
} from '@h5web/shared';
import type { NdArray } from 'ndarray';
import { useContext, useMemo } from 'react';
Expand Down Expand Up @@ -45,11 +47,17 @@ export function useDatasetValue(
return undefined;
}

// Dataset with null shape has no value to fetch
assertNonNullShape(dataset);

// If `dimMapping` is not provided or has no slicing dimension, the entire dataset will be fetched
return valuesStore.get({
const value = valuesStore.get({
path: dataset.path,
selection: getSliceSelection(dimMapping),
});

assertDatasetValue(value, dataset);
axelboc marked this conversation as resolved.
Show resolved Hide resolved
return value;
}

export function useDatasetValues<D extends Dataset>(
Expand All @@ -60,7 +68,15 @@ export function useDatasetValues(datasets: Dataset[]): Record<string, unknown> {
const { valuesStore } = useContext(ProviderContext);

return Object.fromEntries(
datasets.map(({ name, path }) => [name, valuesStore.get({ path })])
datasets.map((dataset) => {
assertNonNullShape(dataset);

const { name, path } = dataset;
const value = valuesStore.get({ path });
assertDatasetValue(value, dataset);

return [name, value];
})
);
}

Expand Down Expand Up @@ -95,14 +111,12 @@ export const useCombinedDomain = createMemo(getCombinedDomain);
const useBaseArray = createMemo(getBaseArray);
const useApplyMapping = createMemo(applyMapping);

export function useMappedArray<T extends unknown[] | undefined>(
value: T,
export function useMappedArray<T, U extends T[] | undefined>(
value: U,
dims: number[],
mapping: DimensionMapping,
autoScale?: boolean
): T extends (infer U)[]
? [NdArray<U[]>, NdArray<U[]>]
: [undefined, undefined];
): U extends T[] ? [NdArray<U>, NdArray<U>] : [undefined, undefined];
loichuder marked this conversation as resolved.
Show resolved Hide resolved

export function useMappedArray<T>(
value: T[] | undefined,
Expand Down
12 changes: 6 additions & 6 deletions packages/app/src/vis-packs/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,10 @@ import { getAttributeValue } from '../../utils';

export const DEFAULT_DOMAIN: Domain = [0.1, 1];

export function getBaseArray<T extends unknown[] | undefined>(
value: T,
export function getBaseArray<T, U extends T[] | undefined>(
value: U,
rawDims: number[]
): T extends (infer U)[] ? NdArray<U[]> : undefined;
): U extends T[] ? NdArray<U> : undefined;

export function getBaseArray<T>(
value: T[] | undefined,
Expand All @@ -22,10 +22,10 @@ export function getBaseArray<T>(
return value && ndarray(value, rawDims);
}

export function applyMapping<T extends NdArray<unknown[]> | undefined>(
baseArray: T,
export function applyMapping<T, U extends NdArray<T[]> | undefined>(
baseArray: U,
mapping: (number | Axis | ':')[]
): T extends NdArray<infer U> ? NdArray<U> : undefined;
): U extends NdArray<T[]> ? U : undefined;

export function applyMapping<T>(
baseArray: NdArray<T[]> | undefined,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,25 +1,21 @@
import type { H5WebComplex } from '@h5web/shared';
import {
assertComplexType,
assertGroupWithChildren,
assertMinDims,
} from '@h5web/shared';
import { assertGroupWithChildren, assertMinDims } from '@h5web/shared';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import VisBoundary from '../../VisBoundary';
import MappedComplexVis from '../../core/complex/MappedComplexVis';
import { useDimMappingState } from '../../hooks';
import type { VisContainerProps } from '../../models';
import NxValuesFetcher from '../NxValuesFetcher';
import { getNxData, getDatasetLabel } from '../utils';
import { getNxData, getDatasetLabel, assertComplexSignal } from '../utils';

function NxComplexImageContainer(props: VisContainerProps) {
const { entity } = props;
assertGroupWithChildren(entity);

const nxData = getNxData(entity);
assertComplexSignal(nxData);

const { signalDataset, silxStyle } = nxData;
assertComplexType(signalDataset);
assertMinDims(signalDataset, 2);

const { shape: dims } = signalDataset;
Expand All @@ -40,7 +36,7 @@ function NxComplexImageContainer(props: VisContainerProps) {
const { signal, axisMapping, title } = nxValues;
return (
<MappedComplexVis
value={signal as H5WebComplex[]}
value={signal}
dims={dims}
dimMapping={dimMapping}
axisMapping={axisMapping}
Expand Down
13 changes: 5 additions & 8 deletions packages/app/src/vis-packs/nexus/containers/NxImageContainer.tsx
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import {
assertGroupWithChildren,
assertNumericType,
assertMinDims,
} from '@h5web/shared';
import { assertGroupWithChildren, assertMinDims } from '@h5web/shared';

import DimensionMapper from '../../../dimension-mapper/DimensionMapper';
import VisBoundary from '../../VisBoundary';
import MappedHeatmapVis from '../../core/heatmap/MappedHeatmapVis';
import { useDimMappingState } from '../../hooks';
import type { VisContainerProps } from '../../models';
import NxValuesFetcher from '../NxValuesFetcher';
import { getNxData, getDatasetLabel } from '../utils';
import { getNxData, getDatasetLabel, assertNumericSignal } from '../utils';

function NxImageContainer(props: VisContainerProps) {
const { entity } = props;
assertGroupWithChildren(entity);

const nxData = getNxData(entity);
assertNumericSignal(nxData);

const { signalDataset, silxStyle } = nxData;
assertNumericType(signalDataset);
assertMinDims(signalDataset, 2);

const { shape: dims } = signalDataset;
Expand All @@ -39,7 +36,7 @@ function NxImageContainer(props: VisContainerProps) {
const { signal, axisMapping, title } = nxValues;
return (
<MappedHeatmapVis
value={signal as number[]}
value={signal}
dims={dims}
dimMapping={dimMapping}
axisMapping={axisMapping}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { RgbVis } from '@h5web/lib';
import {
assertGroupWithChildren,
assertNumericType,
assertNumDims,
DTypeClass,
} from '@h5web/shared';
Expand All @@ -11,15 +10,16 @@ import VisBoundary from '../../VisBoundary';
import { useRgbVisConfig } from '../../core/rgb/config';
import type { VisContainerProps } from '../../models';
import NxValuesFetcher from '../NxValuesFetcher';
import { getNxData, getDatasetLabel } from '../utils';
import { getNxData, getDatasetLabel, assertNumericSignal } from '../utils';

function NxRgbContainer(props: VisContainerProps) {
const { entity } = props;
assertGroupWithChildren(entity);

const nxData = getNxData(entity);
assertNumericSignal(nxData);

const { signalDataset } = nxData;
assertNumericType(signalDataset);
assertNumDims(signalDataset, 3);

const { shape: dims } = signalDataset;
Expand All @@ -37,7 +37,7 @@ function NxRgbContainer(props: VisContainerProps) {
const { signal, title } = nxValues;
return (
<RgbVis
value={signal as number[]}
value={signal}
dims={dims}
floatFormat={signalDataset.type.class === DTypeClass.Float}
title={title || getDatasetLabel(signalDataset)}
Expand Down
13 changes: 13 additions & 0 deletions packages/app/src/vis-packs/nexus/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import {
assertArray,
assertArrayShape,
assertComplexType,
assertDataset,
assertDefined,
assertNumericOrComplexType,
Expand Down Expand Up @@ -182,3 +183,15 @@ export function getDatasetLabel(dataset: Dataset): string {

return dataset.name;
}

export function assertNumericSignal(
nxData: NxData
): asserts nxData is NxData<NumericType> {
assertNumericType(nxData.signalDataset);
}

export function assertComplexSignal(
nxData: NxData
): asserts nxData is NxData<ComplexType> {
assertComplexType(nxData.signalDataset);
}
Loading