/** * Composable for generating dilution series used in plate-based experiments. * Provides geometric series generation from presets (2x, 3x, ½ log, 10x) * and utility functions for series manipulation. */ export interface LevelEntry { value: number replicates: number } export interface DilutionPreset { label: string factor: number | 'half-log' } export const DEFAULT_PRESETS: DilutionPreset[] = [ { label: '2x', factor: 2 }, { label: '3x', factor: 3 }, { label: '½ log', factor: 'half-log' }, { label: '10x', factor: 10 }, ] export const DEFAULT_UNITS = ['µM', 'nM', 'mM', 'pM'] function resolveFactor(factor: number | 'half-log'): number { return factor === 'half-log' ? Math.sqrt(10) : factor } function roundToSignificant(value: number, digits: number = 4): number { if (value === 0) return 0 const magnitude = Math.floor(Math.log10(Math.abs(value))) + 1 const factor = Math.pow(10, digits - magnitude) return Math.round(value * factor) / factor } export function generateDilutionSeries( startConcentration: number, count: number, factor: number | 'half-log', defaultReplicates: number = 1, ): LevelEntry[] { if (count < 1 || startConcentration <= 0) return [] const divisor = resolveFactor(factor) const levels: LevelEntry[] = [] for (let i = 0; i < count; i++) { levels.push({ value: roundToSignificant(startConcentration / Math.pow(divisor, i)), replicates: defaultReplicates, }) } return levels } export interface UseReagentSeriesReturn { generateSeries: ( start: number, count: number, factor: number | 'half-log', defaultReplicates?: number, ) => LevelEntry[] sortLevels: (levels: LevelEntry[], order?: 'asc' | 'desc') => LevelEntry[] totalPositions: (levels: LevelEntry[]) => number } /** Generates and sorts geometric dilution series (2x, 3x, ½ log, 10x) for plate-based experiments. */ export function useReagentSeries(): UseReagentSeriesReturn { function generateSeries( start: number, count: number, factor: number | 'half-log', defaultReplicates: number = 1, ): LevelEntry[] { return generateDilutionSeries(start, count, factor, defaultReplicates) } function sortLevels(levels: LevelEntry[], order: 'asc' | 'desc' = 'desc'): LevelEntry[] { return [...levels].sort((a, b) => order === 'desc' ? b.value - a.value : a.value - b.value, ) } function totalPositions(levels: LevelEntry[]): number { return levels.reduce((sum, l) => sum + l.replicates, 0) } return { generateSeries, sortLevels, totalPositions } }