From d650341f57a944ced861769b484f401adebd4628 Mon Sep 17 00:00:00 2001 From: Ilya Matiach Date: Thu, 21 Dec 2023 10:49:03 -0500 Subject: [PATCH] announce search results in RAI Vision dashboard toolbar for accessibility --- .../Controls/DataCharacteristics.tsx | 4 +- .../Controls/DataCharacteristicsHelper.ts | 4 +- .../Controls/ImageList.tsx | 4 +- .../Controls/TableList.tsx | 3 +- .../Controls/TableListHelper.ts | 1 + .../Controls/TabsView.tsx | 52 +++++++++++++ .../Controls/ToolBar.tsx | 2 + .../IVisionExplanationDashboardState.ts | 1 + .../VisionExplanationDashboard.tsx | 35 ++++++++- ...x => VisionExplanationDashboardHeader.tsx} | 75 ++++++++++++------- .../VisionExplanationDashboardHelper.ts | 2 + .../utils/getFilteredData.ts | 9 ++- .../utils/searchTextUtils.ts | 75 +++++++++++++++++++ libs/localization/src/lib/en.json | 5 ++ 14 files changed, 236 insertions(+), 36 deletions(-) rename libs/interpret-vision/src/lib/VisionExplanationDashboard/{VisionExplanationDashboardCommon.tsx => VisionExplanationDashboardHeader.tsx} (67%) create mode 100644 libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/searchTextUtils.ts diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristics.tsx b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristics.tsx index d91edbc365..270acff11e 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristics.tsx +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristics.tsx @@ -186,7 +186,8 @@ export class DataCharacteristics extends React.Component< const filteredItems = getFilteredDataFromSearch( this.props.searchValue, this.props.items, - this.props.taskType + this.props.taskType, + this.props.onSearchUpdated ); this.setState( processItems( @@ -262,7 +263,6 @@ export class DataCharacteristics extends React.Component< showBackArrow[index] = true; this.setState({ renderStartIndex, showBackArrow }); }; - private loadPrevItems = (index: number) => (): void => { const { renderStartIndex, showBackArrow } = this.state; renderStartIndex[index] -= this.state.columnCount[index]; diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristicsHelper.ts b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristicsHelper.ts index 7b2ebb427b..1b141782d9 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristicsHelper.ts +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/DataCharacteristicsHelper.ts @@ -13,6 +13,7 @@ export interface IDataCharacteristicsProps extends ISearchable { imageDim: number; numRows: number; taskType: string; + onSearchUpdated: (successCount: number, errorCount: number) => void; selectItem(item: IVisionListItem): void; } @@ -188,7 +189,7 @@ export function processItems( showBackArrow.push(false); columnCount.push(0); }); - return { + const result = { columnCount, dropdownOptionsPredicted, dropdownOptionsTrue, @@ -201,6 +202,7 @@ export function processItems( selectedKeysTrue, showBackArrow }; + return result; } export function getLabelVisibility( diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ImageList.tsx b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ImageList.tsx index 9ba2954797..6e7454e067 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ImageList.tsx +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ImageList.tsx @@ -26,6 +26,7 @@ export interface IImageListProps extends ISearchable { imageDim: number; selectItem: (item: IVisionListItem) => void; taskType: string; + onSearchUpdated: (successCount: number, errorCount: number) => void; } export interface IImageListState { @@ -93,7 +94,8 @@ export class ImageList extends React.Component< filteredItems = getFilteredDataFromSearch( searchValue, filteredItems, - this.props.taskType + this.props.taskType, + this.props.onSearchUpdated ); } return filteredItems; diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableList.tsx b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableList.tsx index 7ae14b44b7..b839929e60 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableList.tsx +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableList.tsx @@ -187,7 +187,8 @@ export class TableList extends React.Component< const filteredItems = getFilteredDataFromSearch( searchValue, items, - this.props.taskType + this.props.taskType, + this.props.onSearchUpdated ); return filteredItems; } diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableListHelper.ts b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableListHelper.ts index 9df1c0624a..658cad8c23 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableListHelper.ts +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TableListHelper.ts @@ -15,6 +15,7 @@ export interface ITableListProps extends ISearchable { selectItem: (item: IVisionListItem) => void; updateSelectedIndices: (indices: number[]) => void; taskType: string; + onSearchUpdated: (successCount: number, errorCount: number) => void; } export interface ITableListState { diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TabsView.tsx b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TabsView.tsx index c4f0187bcb..2dfd57a8cf 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TabsView.tsx +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/TabsView.tsx @@ -7,8 +7,10 @@ import { ErrorCohort, DatasetTaskType } from "@responsible-ai/core-ui"; +import { localization } from "@responsible-ai/localization"; import React from "react"; +import { updateSearchTextAriaLabel } from "../utils/searchTextUtils"; import { visionExplanationDashboardStyles } from "../VisionExplanationDashboard.styles"; import { TitleBarOptions, @@ -31,6 +33,7 @@ export interface ITabsViewProps { selectedItem: IVisionListItem | undefined; selectedKey: string; onItemSelect: (item: IVisionListItem) => void; + onSearchUpdated: (searchResultsAriaLabel: string) => void; updateSelectedIndices: (indices: number[]) => void; selectedCohort: ErrorCohort; setSelectedCohort: (cohort: ErrorCohort) => void; @@ -39,6 +42,8 @@ export interface ITabsViewProps { export interface ITabViewState { items: IVisionListItem[]; + errorInstancesCount?: number; + successInstancesCount?: number; } const stackTokens = { @@ -62,6 +67,17 @@ export class TabsView extends React.Component { items: this.props.errorInstances.concat(...this.props.successInstances) }); } + if ( + this.props.searchValue !== prevProps.searchValue && + (!this.props.searchValue || this.props.searchValue === "") + ) { + this.setState({ + errorInstancesCount: undefined, + successInstancesCount: undefined + }); + const label = localization.InterpretVision.Search.defaultSearchLabel; + this.props.onSearchUpdated(label); + } } public render(): React.ReactNode { @@ -78,6 +94,7 @@ export class TabsView extends React.Component { items={this.state.items} imageDim={this.props.imageDim} numRows={this.props.numRows} + onSearchUpdated={this.onSearchCountUpdated} searchValue={this.props.searchValue} selectItem={this.props.onItemSelect} taskType={this.props.taskType} @@ -94,6 +111,7 @@ export class TabsView extends React.Component { successInstances={this.props.successInstances} imageDim={this.props.imageDim} otherMetadataFieldNames={this.props.otherMetadataFieldNames} + onSearchUpdated={this.onSearchCountUpdated} searchValue={this.props.searchValue} selectItem={this.props.onItemSelect} updateSelectedIndices={this.props.updateSelectedIndices} @@ -122,6 +140,7 @@ export class TabsView extends React.Component { { { { ); } } + + private onSearchCountUpdated = ( + successCount: number, + errorCount: number + ): void => { + updateSearchTextAriaLabel( + this.props.onSearchUpdated, + successCount, + errorCount, + this.props.searchValue + ); + }; + + private updateSuccessErrorInstancesAriaLabel = (): void => { + this.onSearchCountUpdated( + this.state.successInstancesCount ?? 0, + this.state.errorInstancesCount ?? 0 + ); + }; + + private onSearchUpdatedError = (_: number, errorCount: number): void => { + this.setState({ errorInstancesCount: errorCount }, () => { + this.updateSuccessErrorInstancesAriaLabel(); + }); + }; + + private onSearchUpdatedSuccess = (successCount: number): void => { + this.setState({ successInstancesCount: successCount }, () => { + this.updateSuccessErrorInstancesAriaLabel(); + }); + }; } diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ToolBar.tsx b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ToolBar.tsx index f2c3f03297..ddcee76cd7 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ToolBar.tsx +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Controls/ToolBar.tsx @@ -22,6 +22,7 @@ export interface IToolBarProps { newValue?: string ) => void; selectedCohort: ErrorCohort; + searchResultsAriaLabel: string; setSelectedCohort: (cohort: ErrorCohort) => void; } @@ -68,6 +69,7 @@ export class ToolBar extends React.Component { placeholder={localization.InterpretVision.Dashboard.search} value={this.props.searchValue} onChange={this.props.onSearch} + ariaLabel={this.props.searchResultsAriaLabel} /> diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Interfaces/IVisionExplanationDashboardState.ts b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Interfaces/IVisionExplanationDashboardState.ts index c67b93651f..6681e286d4 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/Interfaces/IVisionExplanationDashboardState.ts +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/Interfaces/IVisionExplanationDashboardState.ts @@ -12,6 +12,7 @@ export interface IVisionExplanationDashboardState { otherMetadataFieldNames: string[]; numRows: number; panelOpen: boolean; + searchResultsAriaLabel: string; searchValue: string; selectedIndices: number[]; selectedItem: IVisionListItem | undefined; diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboard.tsx b/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboard.tsx index 2d7ccc03d2..1963148323 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboard.tsx +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboard.tsx @@ -17,7 +17,7 @@ import { TabsView } from "./Controls/TabsView"; import { IVisionExplanationDashboardProps } from "./Interfaces/IVisionExplanationDashboardProps"; import { IVisionExplanationDashboardState } from "./Interfaces/IVisionExplanationDashboardState"; import { visionExplanationDashboardStyles } from "./VisionExplanationDashboard.styles"; -import { VisionExplanationDashboardCommon } from "./VisionExplanationDashboardCommon"; +import { VisionExplanationDashboardHeader } from "./VisionExplanationDashboardHeader"; import { preprocessData, getItems, @@ -35,10 +35,12 @@ export class VisionExplanationDashboard extends React.Component< defaultModelAssessmentContext; private originalErrorInstances: IVisionListItem[] = []; private originalSuccessInstances: IVisionListItem[] = []; + public constructor(props: IVisionExplanationDashboardProps) { super(props); this.state = defaultState; } + public componentDidMount(): void { const data = preprocessData(this.props, this.context.dataset); if (!data) { @@ -48,6 +50,7 @@ export class VisionExplanationDashboard extends React.Component< this.originalSuccessInstances = data.successInstances; this.setState(data); } + public componentDidUpdate(prevProps: IVisionExplanationDashboardProps): void { if (this.props.selectedCohort !== prevProps.selectedCohort) { this.setState( @@ -59,6 +62,7 @@ export class VisionExplanationDashboard extends React.Component< ); } } + public render(): React.ReactNode { const classNames = visionExplanationDashboardStyles(); const imageStyles = imageListStyles(); @@ -69,11 +73,22 @@ export class VisionExplanationDashboard extends React.Component< id="VisionDataExplorer" tokens={{ childrenGap: "l1", padding: "m 40px" }} > - ); } + public updateSelectedIndices = (indices: number[]): void => { this.setState({ selectedIndices: indices }); }; + public addCohortWrapper = (name: string, switchCohort: boolean): void => { this.context.addCohort( getCohort(name, this.state.selectedIndices, this.context.jointDataset), @@ -128,15 +146,22 @@ export class VisionExplanationDashboard extends React.Component< switchCohort ); }; + public onPanelClose = (): void => { this.setState({ panelOpen: !this.state.panelOpen }); }; + public onSearch = ( _event?: React.ChangeEvent, newValue?: string ): void => { this.setState({ searchValue: newValue || "" }); }; + + public onSearchUpdated = (searchResultsAriaLabel: string): void => { + this.setState({ searchResultsAriaLabel }); + }; + public onItemSelect = (item: IVisionListItem): void => { this.setState({ panelOpen: !this.state.panelOpen, selectedItem: item }); const index = item.index; @@ -165,6 +190,7 @@ export class VisionExplanationDashboard extends React.Component< }); } }; + public onItemSelectObjectDetection = ( item: IVisionListItem, selectedObject = -1 @@ -202,6 +228,7 @@ export class VisionExplanationDashboard extends React.Component< }); } }; + /* For onSliderChange, the max imageDims per tab (400 and 100) are selected arbitrary to look like the Figma. For handleLinkClick, the default are half the max values chosen in onSliderChange. */ public onSliderChange = (value: number): void => { @@ -214,12 +241,14 @@ export class VisionExplanationDashboard extends React.Component< this.setState({ imageDim: Math.floor((value / 100) * 100) }); } }; + public onNumRowsSelect = ( _event: React.FormEvent, item: IDropdownOption | undefined ): void => { this.setState({ numRows: Number(item?.text) }); }; + public handleLinkClick = (item?: PivotItem): void => { if (item && item.props.itemKey !== undefined) { this.setState({ selectedKey: item.props.itemKey }); diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardCommon.tsx b/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardHeader.tsx similarity index 67% rename from libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardCommon.tsx rename to libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardHeader.tsx index 5dc283f534..34616ef293 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardCommon.tsx +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardHeader.tsx @@ -7,9 +7,15 @@ import { Stack, Slider, Separator, - Text + Text, + PivotItem, + IDropdownOption } from "@fluentui/react"; -import { DatasetTaskType, IVisionListItem } from "@responsible-ai/core-ui"; +import { + DatasetTaskType, + ErrorCohort, + IVisionListItem +} from "@responsible-ai/core-ui"; import { localization } from "@responsible-ai/localization"; import React from "react"; @@ -18,33 +24,49 @@ import { IDatasetExplorerTabStyles } from "./Controls/ImageList.styles"; import { PageSizeSelectors } from "./Controls/PageSizeSelectors"; import { Pivots } from "./Controls/Pivots"; import { ToolBar } from "./Controls/ToolBar"; -import { VisionExplanationDashboard } from "./VisionExplanationDashboard"; import { IVisionExplanationDashboardStyles } from "./VisionExplanationDashboard.styles"; import { VisionDatasetExplorerTabOptions } from "./VisionExplanationDashboardHelper"; -export interface IVisionExplanationDashboardCommonProps { - thisdashboard: VisionExplanationDashboard; +export interface IVisionExplanationDashboardHeaderProps { + cohorts: ErrorCohort[]; + selectedKey: string; + searchValue: string; + selectedCohort: ErrorCohort; + searchResultsAriaLabel: string; imageStyles: IProcessedStyleSet; classNames: IProcessedStyleSet; taskType: string; + selectedIndices: number[]; + handleLinkClick: (item?: PivotItem) => void; + setSelectedCohort: (cohort: ErrorCohort) => void; + onSliderChange: (value: number) => void; + onNumRowsSelect: ( + _event: React.FormEvent, + item: IDropdownOption | undefined + ) => void; + addCohortWrapper: (name: string, switchCohort: boolean) => void; + onSearch: ( + _event?: React.ChangeEvent, + newValue?: string + ) => void; } -export interface IVisionExplanationDashboardCommonState { +export interface IVisionExplanationDashboardHeaderState { item: IVisionListItem | undefined; metadata: Array> | undefined; } -export class VisionExplanationDashboardCommon extends React.Component< - IVisionExplanationDashboardCommonProps, - IVisionExplanationDashboardCommonState +export class VisionExplanationDashboardHeader extends React.Component< + IVisionExplanationDashboardHeaderProps, + IVisionExplanationDashboardHeaderState > { public render(): React.ReactNode { return ( @@ -52,11 +74,12 @@ export class VisionExplanationDashboardCommon extends React.Component< @@ -75,23 +98,23 @@ export class VisionExplanationDashboardCommon extends React.Component< label={localization.InterpretVision.Dashboard.thumbnailSize} defaultValue={50} showValue={false} - onChange={this.props.thisdashboard.onSliderChange} + onChange={this.props.onSliderChange} disabled={ - this.props.thisdashboard.state.selectedKey === + this.props.selectedKey === VisionDatasetExplorerTabOptions.ClassView } /> - {this.props.thisdashboard.state.selectedKey === + {this.props.selectedKey === VisionDatasetExplorerTabOptions.ClassView && ( )} - {this.props.thisdashboard.state.selectedKey === + {this.props.selectedKey === VisionDatasetExplorerTabOptions.ImageExplorerView && this.props.taskType !== DatasetTaskType.ObjectDetection && ( - {this.props.thisdashboard.state.selectedKey === + {this.props.selectedKey === VisionDatasetExplorerTabOptions.TableView && ( )} diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardHelper.ts b/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardHelper.ts index d90259150a..cbdfe9d96c 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardHelper.ts +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/VisionExplanationDashboardHelper.ts @@ -202,6 +202,8 @@ export const defaultState: IVisionExplanationDashboardState = { numRows: 3, otherMetadataFieldNames: ["mean_pixel_value"], panelOpen: false, + searchResultsAriaLabel: + localization.InterpretVision.Search.defaultSearchLabel, searchValue: "", selectedIndices: [], selectedItem: undefined, diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/getFilteredData.ts b/libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/getFilteredData.ts index 64e461a726..1d5c69e29d 100644 --- a/libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/getFilteredData.ts +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/getFilteredData.ts @@ -3,12 +3,15 @@ import { DatasetTaskType, IVisionListItem } from "@responsible-ai/core-ui"; +import { updateSearchSuccessErrorCounts } from "./searchTextUtils"; + export function getFilteredDataFromSearch( searchVal: string, items: IVisionListItem[], - taskType: string + taskType: string, + onSearchUpdated: (successCount: number, errorCount: number) => void ): IVisionListItem[] { - return items.filter((item) => { + const filteredItems = items.filter((item) => { const predOrIncorrectY = taskType === DatasetTaskType.ObjectDetection ? item.odIncorrect @@ -27,6 +30,8 @@ export function getFilteredDataFromSearch( ); return predOrIncorrectYIncludesSearchVal || trueOrCorrectYIncludesSearchVal; }); + updateSearchSuccessErrorCounts(onSearchUpdated, filteredItems, taskType); + return filteredItems; } export function includesSearchVal( diff --git a/libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/searchTextUtils.ts b/libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/searchTextUtils.ts new file mode 100644 index 0000000000..49e028b7a6 --- /dev/null +++ b/libs/interpret-vision/src/lib/VisionExplanationDashboard/utils/searchTextUtils.ts @@ -0,0 +1,75 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +import { + DatasetTaskType, + IVisionListItem, + NoLabel +} from "@responsible-ai/core-ui"; +import { localization } from "@responsible-ai/localization"; + +export function getSearchTextAriaLabel( + successCount: number, + totalCount: number, + searchValue: string +): string { + const failureCount = totalCount - successCount; + if (!searchValue) { + return localization.InterpretVision.Search.defaultSearchLabel; + } + if (totalCount === 0) { + return localization.formatString( + localization.InterpretVision.Search.emptySearchResultsAriaLabel, + searchValue + ); + } + return localization.formatString( + localization.InterpretVision.Search.searchResultsAriaLabel, + totalCount, + searchValue, + successCount, + failureCount + ); +} + +export function getCorrectCountForItems( + items: IVisionListItem[], + taskType: string +): number { + let count = 0; + items.forEach((itemEntry) => { + if (taskType === DatasetTaskType.ObjectDetection) { + // For object detection, we define correct images as + // those with no incorrect bounding boxes + count += itemEntry.odIncorrect === NoLabel ? 1 : 0; + } else { + count += itemEntry.predictedY === itemEntry.trueY ? 1 : 0; + } + }); + return count; +} + +export function updateSearchSuccessErrorCounts( + onSearchUpdated: (successCount: number, errorCount: number) => void, + examples: IVisionListItem[], + taskType: string +): void { + const successCount = getCorrectCountForItems(examples, taskType); + const errorCount = examples.length - successCount; + onSearchUpdated(successCount, errorCount); +} + +export function updateSearchTextAriaLabel( + onSearchUpdated: (searchResultsAriaLabel: string) => void, + successCount: number, + errorCount: number, + searchValue: string +): void { + const totalCount = successCount + errorCount; + const searchResultsAriaLabel = getSearchTextAriaLabel( + successCount, + totalCount, + searchValue + ); + onSearchUpdated(searchResultsAriaLabel); +} diff --git a/libs/localization/src/lib/en.json b/libs/localization/src/lib/en.json index b277c47213..ef4c723cad 100644 --- a/libs/localization/src/lib/en.json +++ b/libs/localization/src/lib/en.json @@ -1489,6 +1489,11 @@ "titleBarError": "Error instances", "titleBarSuccess": "Success instances", "trueY": "Ground truth: " + }, + "Search": { + "defaultSearchLabel": "Search for image instances", + "searchResultsAriaLabel": "{0} results found for the entered keyword {1} where the number of success instances is {2} and the number of error instances is {3}", + "emptySearchResultsAriaLabel": "No results found for the entered keyword {0}" } }, "ModelAssessment": {