Skip to content
Closed
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
2 changes: 2 additions & 0 deletions ui/src/data-services/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export const API_ROUTES = {
ME: 'users/me',
MEMBERS: (projectId: string) => `projects/${projectId}/members`,
OCCURRENCE_ALGORITHMS: 'occurrences/algorithms',
OCCURRENCE_SETS: 'occurrences/sets',
OCCURRENCES: 'occurrences',
PAGES: 'pages',
PIPELINES: 'ml/pipelines',
Expand All @@ -31,6 +32,7 @@ export const API_ROUTES = {
SUMMARY: 'status/summary',
TAGS: 'tags',
TAXA_LISTS: 'taxa/lists',
TRAINING_DATA: 'ml/training-data',
USERS: 'users',
}

Expand Down
49 changes: 49 additions & 0 deletions ui/src/data-services/hooks/algorithm/useTrainingDataSummary.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { API_ROUTES, API_URL } from 'data-services/constants'
import { useAuthorizedQuery } from '../auth/useAuthorizedQuery'

export interface ServerTrainingDataSummary {
rows: number
classes: number
train: number
test: number
verified_detections_without_embedding: number
}

export interface TrainingDataSummary {
cropsReady: number
species: number
cropsWithoutEmbedding: number
}

const convertServerRecord = (
record: ServerTrainingDataSummary
): TrainingDataSummary => ({
cropsReady: record.rows,
species: record.classes,
cropsWithoutEmbedding: record.verified_detections_without_embedding,
})

export const useTrainingDataSummary = (
projectId?: string,
algorithmKey?: string
): {
summary?: TrainingDataSummary
isLoading: boolean
error?: unknown
} => {
const params = { project_id: projectId ?? '', algorithm: algorithmKey ?? '' }
const queryString = new URLSearchParams(params).toString()

const { data, isLoading, error } =
useAuthorizedQuery<ServerTrainingDataSummary>({
queryKey: [API_ROUTES.TRAINING_DATA, params],
url: `${API_URL}/${API_ROUTES.TRAINING_DATA}/summary/?${queryString}`,
enabled: !!projectId && !!algorithmKey,
})

return {
summary: data ? convertServerRecord(data) : undefined,
isLoading,
error,
}
}
20 changes: 20 additions & 0 deletions ui/src/data-services/hooks/jobs/useCreateJob.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,46 @@
import { useMutation, useQueryClient } from '@tanstack/react-query'
import axios from 'axios'
import { API_ROUTES, API_URL, SUCCESS_TIMEOUT } from 'data-services/constants'
import { ServerJobType } from 'data-services/models/job'
import { getAuthHeader } from 'data-services/utils'
import { useUser } from 'utils/user/userContext'

interface JobFieldValues {
algorithm?: string
delay?: number
jobType?: ServerJobType
name: string
occurrenceSet?: string
projectId: string
pipeline?: string
sourceImage?: string
sourceImages?: string
startNow?: boolean
}

// Each job type reads what it needs out of the free-form params the API stores.
const buildParams = (fieldValues: JobFieldValues) => {
const params: { algorithm_key?: string; occurrence_set_id?: string } = {}

if (fieldValues.algorithm) {
params.algorithm_key = fieldValues.algorithm
}
if (fieldValues.occurrenceSet) {
params.occurrence_set_id = fieldValues.occurrenceSet
}

return Object.keys(params).length ? params : undefined
}

const convertToServerFieldValues = (fieldValues: JobFieldValues) => ({
delay: fieldValues.delay ?? 0,
name: fieldValues.name,
project_id: fieldValues.projectId,
job_type_key: fieldValues.jobType,
pipeline_id: fieldValues.pipeline,
source_image_collection_id: fieldValues.sourceImages,
source_image_single_id: fieldValues.sourceImage,
params: buildParams(fieldValues),
})

export const useCreateJob = (onSuccess?: (id: string) => void) => {
Expand Down
37 changes: 37 additions & 0 deletions ui/src/data-services/models/algorithm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,26 @@ import { Entity } from './entity'

export type ServerAlgorithm = any // TODO: Update this type

export interface ServerAlgorithmEvaluation {
id: number
occurrence_set: { id: number; name: string }
accuracy: number | null
accuracy_by_species: number | null
occurrences_scored: number
occurrences_skipped: number
species_scored: number
created_at: string
}

export interface AlgorithmEvaluation {
id: string
occurrenceSetName: string
accuracy?: number
accuracyBySpecies?: number
occurrencesScored: number
occurrencesSkipped: number
}

export class Algorithm extends Entity {
protected readonly _algorithm: ServerAlgorithm

Expand Down Expand Up @@ -43,4 +63,21 @@ export class Algorithm extends Entity {
? this._algorithm.category_count
: undefined
}

get trainable(): boolean {
return this._algorithm.trainable ?? false
}

get evaluations(): AlgorithmEvaluation[] {
const rows: ServerAlgorithmEvaluation[] = this._algorithm.evaluations ?? []

return rows.map((row) => ({
id: `${row.id}`,
occurrenceSetName: row.occurrence_set.name,
accuracy: row.accuracy ?? undefined,
accuracyBySpecies: row.accuracy_by_species ?? undefined,
occurrencesScored: row.occurrences_scored,
occurrencesSkipped: row.occurrences_skipped,
}))
}
}
6 changes: 6 additions & 0 deletions ui/src/data-services/models/job.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ export const SERVER_JOB_TYPES = [
'data_storage_sync',
'populate_captures_collection',
'data_export',
'train_classifier',
'generate_embeddings',
'evaluate_algorithm',
'unknown',
] as const

Expand Down Expand Up @@ -164,6 +167,9 @@ export class Job extends Entity {
data_storage_sync: 'Data storage sync',
populate_captures_collection: 'Populate captures collection',
data_export: 'Data export',
train_classifier: 'Train classifier',
generate_embeddings: 'Generate embeddings',
evaluate_algorithm: 'Evaluate algorithm',
unknown: 'Unknown',
}[key]

Expand Down
32 changes: 32 additions & 0 deletions ui/src/data-services/models/species-details.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,43 @@ import { ServerSpecies, Species } from './species'

export type ServerSpeciesDetails = ServerSpecies & any // TODO: Update this type

export interface ServerAlgorithmPerformance {
algorithm: { id: number; name: string; key: string; version: number }
occurrence_set: { id: number; name: string }
accuracy: number
occurrences_scored: number
correct: number
overall_accuracy: number | null
overall_accuracy_by_species: number | null
}

export interface AlgorithmPerformance {
algorithmId: string
algorithmName: string
accuracy: number
occurrencesScored: number
correct: number
occurrenceSetName: string
}

export class SpeciesDetails extends Species {
public constructor(species: ServerSpeciesDetails) {
super(species)
}

get algorithmPerformance(): AlgorithmPerformance[] {
const rows = this._species.algorithm_performance ?? []

return rows.map((row: ServerAlgorithmPerformance) => ({
algorithmId: `${row.algorithm.id}`,
algorithmName: row.algorithm.name,
accuracy: row.accuracy,
occurrencesScored: row.occurrences_scored,
correct: row.correct,
occurrenceSetName: row.occurrence_set.name,
}))
}

get commonNameLabel(): string | undefined {
return this._species.common_name_en ?? undefined
}
Expand Down
4 changes: 4 additions & 0 deletions ui/src/data-services/models/species.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,10 @@ export class Species extends Taxon {
return this._species.occurrences_count ?? 0
}

get numTrainingCrops(): number | undefined {
return this._species.training_crops_ready ?? undefined
}

get numVerified(): number {
return this._species.verified_count ?? 0
}
Expand Down
31 changes: 31 additions & 0 deletions ui/src/data-services/models/taxa-list.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,22 @@
import { Entity, ServerEntity } from 'data-services/models/entity'

export type ServerTaxaListBestModel = {
id: number
name: string
accuracy: number | null
accuracy_by_species: number | null
occurrence_set: string
}

export interface TaxaListBestModel {
id: string
name: string
accuracyBySpecies?: number
occurrenceSetName: string
}

export type ServerTaxaList = ServerEntity & {
best_model: ServerTaxaListBestModel | null // Highest scoring algorithm on these taxa
projects: number[] // Array of project IDs
taxa: string // URL to taxa API endpoint (filtered by this taxa list)
taxa_count: number // Number of taxa in list
Expand All @@ -15,6 +31,21 @@ export class TaxaList extends Entity {
this._taxaList = taxaList
}

get bestModel(): TaxaListBestModel | undefined {
const model = this._taxaList.best_model

if (!model) {
return undefined
}

return {
id: `${model.id}`,
name: model.name,
accuracyBySpecies: model.accuracy_by_species ?? undefined,
occurrenceSetName: model.occurrence_set,
}
}

get taxaCount() {
return this._taxaList.taxa_count
}
Expand Down
1 change: 1 addition & 0 deletions ui/src/data-services/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export interface FetchParams {
filters?: { field: string; value?: string; error?: string }[]
withCounts?: boolean
withExampleOccurrences?: boolean
withTrainingCropCounts?: boolean
}

export interface APIValidationError {
Expand Down
3 changes: 3 additions & 0 deletions ui/src/data-services/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ export const getFetchUrl = ({
if (params?.withExampleOccurrences) {
queryParams.with_example_occurrences = String(true)
}
if (params?.withTrainingCropCounts) {
queryParams.with_training_crop_counts = String(true)
}

const baseUrl = `${API_URL}/${collection}`
const queryString = new URLSearchParams(queryParams).toString()
Expand Down
16 changes: 16 additions & 0 deletions ui/src/pages/algorithm-details/algorithm-details-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ import { useNavigate, useParams } from 'react-router-dom'
import { APP_ROUTES } from 'utils/constants'
import { getAppRoute } from 'utils/getAppRoute'
import { STRING, translate } from 'utils/language'
import { AlgorithmEvaluations } from './algorithm-evaluations'
import { AlgorithmTrainingData } from './algorithm-training-data'
import styles from './styles.module.scss'

export const AlgorithmDetailsDialog = ({ id }: { id: string }) => {
Expand Down Expand Up @@ -91,6 +93,20 @@ const AlgorithmDetailsContent = ({ algorithm }: { algorithm: Algorithm }) => (
/>
</FormRow>
</FormSection>
{algorithm.trainable && (
<FormSection title={translate(STRING.FIELD_LABEL_TRAINING_DATA)}>
<FormRow>
<AlgorithmTrainingData algorithm={algorithm} />
</FormRow>
</FormSection>
)}
{algorithm.evaluations.length > 0 && (
<FormSection title={translate(STRING.EVALUATION)}>
<div className={styles.tableContainer}>
<AlgorithmEvaluations algorithm={algorithm} />
</div>
</FormSection>
)}
{algorithm.uri || algorithm.categoryMapURI ? (
<FormSection title={translate(STRING.EXTERNAL_RESOURCES)}>
<div className="flex flex-col items-start gap-3">
Expand Down
Loading