Download src/shared/lab.ts from tostido/weed-sim: direct link, hf CLI and curl.
- Browser
- Download file 11 kB
-
https://huggingface.co/spaces/tostido/weed-sim/resolve/main/src/shared/lab.ts
- Command line
-
hf download hf://spaces/tostido/weed-sim/src/shared/lab.ts
-
curl -L -o lab.ts https://huggingface.co/spaces/tostido/weed-sim/resolve/main/src/shared/lab.ts
11 kB
| import { colorFamily, hueFromRgb } from './color'; | |
| import { recombineProfiles } from './breeder'; | |
| import { bloomScoreFromPalette, generatePhenotype } from './phenotype'; | |
| import { canAttempt } from './state'; | |
| import type { | |
| BloomPredictionSummary, | |
| ColorPattern, | |
| CrossPrediction, | |
| DistributionSummary, | |
| GermplasmRecord, | |
| LabPayload, | |
| Rgb, | |
| SeedProfile, | |
| TraitVariable, | |
| } from './types'; | |
| export const TRAIT_VARIABLES: TraitVariable[] = [ | |
| { key: 'THC', label: 'THC', unit: '%', min: 0, max: 35, scale: 'numeric' }, | |
| { key: 'CBD', label: 'CBD', unit: '%', min: 0, max: 20, scale: 'numeric' }, | |
| { key: 'Yield', label: 'Yield', unit: 'g', min: 30, max: 140, scale: 'numeric' }, | |
| { key: 'GrowTime', label: 'Grow Time', unit: 'd', min: 40, max: 80, scale: 'numeric' }, | |
| { key: 'BudHue', label: 'Bud Hue', unit: 'deg', min: 0, max: 360, scale: 'circular' }, | |
| { key: 'LeafHue', label: 'Leaf Hue', unit: 'deg', min: 0, max: 360, scale: 'circular' }, | |
| { key: 'Stability', label: 'Stability', unit: '', min: 0, max: 1, scale: 'numeric' }, | |
| { key: 'Volatility', label: 'Volatility', unit: '', min: 0, max: 1, scale: 'numeric' }, | |
| { key: 'Bloom', label: 'Bloom', unit: '', min: 0, max: 100, scale: 'numeric' }, | |
| ]; | |
| function generation(seed: SeedProfile, byId: Map<string, SeedProfile>, cache: Map<string, number>): number { | |
| const cached = cache.get(seed.seedId); | |
| if (cached !== undefined) return cached; | |
| const parentGenerations = seed.lineage | |
| .map((parentId) => (parentId ? byId.get(parentId) : undefined)) | |
| .filter((parent): parent is SeedProfile => Boolean(parent)) | |
| .map((parent) => generation(parent, byId, cache)); | |
| const value = parentGenerations.length ? Math.max(...parentGenerations) + 1 : 0; | |
| cache.set(seed.seedId, value); | |
| return value; | |
| } | |
| function pedigree(seed: SeedProfile, byId: Map<string, SeedProfile>): string { | |
| const [parentOneId, parentTwoId] = seed.lineage; | |
| const parentOne = parentOneId ? byId.get(parentOneId) : undefined; | |
| const parentTwo = parentTwoId ? byId.get(parentTwoId) : undefined; | |
| if (parentOne && parentTwo) return `${parentOne.strainName} / ${parentTwo.strainName}`; | |
| if (parentOne) return `${parentOne.strainName} (clone)`; | |
| return 'founder'; | |
| } | |
| function meanStability(seed: SeedProfile): number { | |
| const values = Object.values(seed.stabilities).filter((value) => Number.isFinite(value)); | |
| return values.length ? Math.round((values.reduce((sum, value) => sum + value, 0) / values.length) * 1000) / 1000 : 0; | |
| } | |
| export function germplasmRecords(seeds: SeedProfile[]): GermplasmRecord[] { | |
| const byId = new Map(seeds.map((seed) => [seed.seedId, seed])); | |
| const cache = new Map<string, number>(); | |
| return seeds.map((seed) => { | |
| const budHue = hueFromRgb(seed.budColor); | |
| const leafHue = hueFromRgb(seed.leafColor); | |
| const stability = meanStability(seed); | |
| const budBloom = bloomScoreFromPalette(seed.budPalette, seed.budPattern); | |
| const leafBloom = bloomScoreFromPalette(seed.leafPalette, seed.leafPattern); | |
| return { | |
| germplasmDbId: seed.seedId, | |
| germplasmName: seed.strainName, | |
| germplasmType: seed.type, | |
| generation: generation(seed, byId, cache), | |
| pedigree: pedigree(seed, byId), | |
| parents: seed.lineage, | |
| isStarter: seed.isStarter, | |
| stage: seed.growthStage, | |
| THC: seed.thc, | |
| CBD: seed.cbd, | |
| Yield: seed.yield, | |
| GrowTime: seed.growTime, | |
| BudHue: budHue, | |
| LeafHue: leafHue, | |
| Stability: stability, | |
| Volatility: Math.round((1 - stability) * 1000) / 1000, | |
| Bloom: budBloom.score, | |
| budColor: seed.budColor, | |
| leafColor: seed.leafColor, | |
| budPalette: seed.budPalette, | |
| leafPalette: seed.leafPalette, | |
| budPattern: seed.budPattern, | |
| leafPattern: seed.leafPattern, | |
| budBloom, | |
| leafBloom, | |
| budFamily: colorFamily(budHue), | |
| alleles: seed.alleles, | |
| attemptsUsed: seed.attemptsUsed, | |
| maxAttempts: seed.maxAttempts, | |
| canAttempt: canAttempt(seed), | |
| }; | |
| }); | |
| } | |
| function countFacet(records: GermplasmRecord[], key: keyof GermplasmRecord): Record<string, number> { | |
| const counts: Record<string, number> = {}; | |
| for (const record of records) { | |
| const value = String(record[key]); | |
| counts[value] = (counts[value] ?? 0) + 1; | |
| } | |
| return Object.fromEntries(Object.entries(counts).sort(([left], [right]) => left.localeCompare(right))); | |
| } | |
| export function facets(records: GermplasmRecord[]): Record<string, Record<string, number>> { | |
| return { | |
| germplasmType: countFacet(records, 'germplasmType'), | |
| generation: countFacet(records, 'generation'), | |
| stage: countFacet(records, 'stage'), | |
| budFamily: countFacet(records, 'budFamily'), | |
| budPattern: countFacet(records, 'budPattern'), | |
| }; | |
| } | |
| export function labPayload(seeds: SeedProfile[]): LabPayload { | |
| const records = germplasmRecords(seeds); | |
| return { | |
| variables: TRAIT_VARIABLES, | |
| records, | |
| facets: facets(records), | |
| count: records.length, | |
| }; | |
| } | |
| function downsample<T>(values: T[], cap: number): T[] { | |
| if (values.length <= cap) return values; | |
| const step = values.length / cap; | |
| const out: T[] = []; | |
| for (let index = 0; index < cap; index += 1) out.push(values[Math.floor(index * step)] as T); | |
| return out; | |
| } | |
| function summarize(values: number[], keepValues = true, cap = 80): DistributionSummary { | |
| const sorted = [...values].sort((left, right) => left - right); | |
| const total = sorted.reduce((sum, value) => sum + value, 0); | |
| return { | |
| min: sorted[0] ?? 0, | |
| max: sorted[sorted.length - 1] ?? 0, | |
| mean: sorted.length ? Math.round((total / sorted.length) * 100) / 100 : 0, | |
| median: sorted[Math.floor(sorted.length / 2)] ?? 0, | |
| values: keepValues ? downsample(values, cap) : [], | |
| }; | |
| } | |
| function incrementPattern(target: Record<ColorPattern, number>, pattern: ColorPattern): void { | |
| target[pattern] = (target[pattern] ?? 0) + 1; | |
| } | |
| function bloomHint(summary: { score: DistributionSummary; volatility: DistributionSummary; colorCount: DistributionSummary; maxColors: number; patterns: Record<ColorPattern, number> }): string { | |
| const topPattern = Object.entries(summary.patterns).sort((left, right) => right[1] - left[1])[0]?.[0] ?? 'solid'; | |
| if (summary.maxColors >= 5 || summary.score.max >= 82) return `wild bloom lane: up to ${summary.maxColors} colors, ${topPattern} dominant`; | |
| if (summary.colorCount.mean >= 3.2 || summary.score.mean >= 64) return `bloom-positive: ${topPattern} bias, mean ${summary.colorCount.mean.toFixed(1)} colors`; | |
| if (summary.volatility.mean >= 0.42) return `volatile but unproven: color spread needs a lucky roll`; | |
| return `stable expression: safer traits, lower bloom odds`; | |
| } | |
| function bloomSummary(scores: number[], volatilities: number[], colorCounts: number[], patterns: Record<ColorPattern, number>, keepValues = true): BloomPredictionSummary { | |
| const summary = { | |
| score: summarize(scores, keepValues), | |
| volatility: summarize(volatilities, keepValues), | |
| colorCount: summarize(colorCounts, keepValues), | |
| maxColors: Math.max(0, ...colorCounts), | |
| patterns, | |
| hint: '', | |
| }; | |
| return { ...summary, hint: bloomHint(summary) }; | |
| } | |
| export function predictCross(parentOne: SeedProfile, parentTwo: SeedProfile, sampleCount = 200, summaryOnly = false): CrossPrediction { | |
| const n = Math.max(20, Math.min(1000, sampleCount)); | |
| const thc: number[] = []; | |
| const cbd: number[] = []; | |
| const yieldValues: number[] = []; | |
| const growTime: number[] = []; | |
| const budCloud: Rgb[] = []; | |
| const leafCloud: Rgb[] = []; | |
| const budBloomScores: number[] = []; | |
| const leafBloomScores: number[] = []; | |
| const budColorCounts: number[] = []; | |
| const leafColorCounts: number[] = []; | |
| const budVolatility: number[] = []; | |
| const leafVolatility: number[] = []; | |
| const budPatterns: Record<ColorPattern, number> = { solid: 0, speckled: 0, split: 0, mosaic: 0, rimmed: 0, polycolor: 0 }; | |
| const leafPatterns: Record<ColorPattern, number> = { solid: 0, speckled: 0, split: 0, mosaic: 0, rimmed: 0, polycolor: 0 }; | |
| for (let index = 0; index < n; index += 1) { | |
| const { alleles, stabilities } = recombineProfiles(parentOne, parentTwo); | |
| const phenotype = generatePhenotype(alleles, stabilities); | |
| thc.push(Math.round(phenotype.thc * 100) / 100); | |
| cbd.push(Math.round(phenotype.cbd * 100) / 100); | |
| yieldValues.push(Math.round(phenotype.yield * 10) / 10); | |
| growTime.push(Math.round(phenotype.growTime * 10) / 10); | |
| const budBloom = bloomScoreFromPalette(phenotype.budPaletteRgb, phenotype.budPattern); | |
| const leafBloom = bloomScoreFromPalette(phenotype.leafPaletteRgb, phenotype.leafPattern); | |
| budCloud.push(phenotype.budColorRgb); | |
| leafCloud.push(phenotype.leafColorRgb); | |
| budBloomScores.push(budBloom.score); | |
| leafBloomScores.push(leafBloom.score); | |
| budColorCounts.push(budBloom.colorCount); | |
| leafColorCounts.push(leafBloom.colorCount); | |
| budVolatility.push(Math.round((1 - stabilities.BudColor) * 1000) / 1000); | |
| leafVolatility.push(Math.round((1 - stabilities.LeafColor) * 1000) / 1000); | |
| incrementPattern(budPatterns, phenotype.budPattern); | |
| incrementPattern(leafPatterns, phenotype.leafPattern); | |
| } | |
| const keepValues = !summaryOnly; | |
| return { | |
| n, | |
| parents: [parentOne.strainName, parentTwo.strainName], | |
| THC: summarize(thc, keepValues), | |
| CBD: summarize(cbd, keepValues), | |
| Yield: summarize(yieldValues, keepValues), | |
| GrowTime: summarize(growTime, keepValues), | |
| budCloud: summaryOnly ? [] : downsample(budCloud, 80), | |
| leafCloud: summaryOnly ? [] : downsample(leafCloud, 80), | |
| budBloom: bloomSummary(budBloomScores, budVolatility, budColorCounts, budPatterns, keepValues), | |
| leafBloom: bloomSummary(leafBloomScores, leafVolatility, leafColorCounts, leafPatterns, keepValues), | |
| }; | |
| } | |
| function safeLabel(name: string): string { | |
| return name.replaceAll(',', '_').replaceAll('(', '[').replaceAll(')', ']').replaceAll(':', '-'); | |
| } | |
| export function toNewick(seeds: SeedProfile[]): string { | |
| const byId = new Map(seeds.map((seed) => [seed.seedId, seed])); | |
| const children = new Map<string, string[]>(); | |
| const hasParent = new Set<string>(); | |
| for (const seed of seeds) { | |
| for (const parentId of seed.lineage) { | |
| if (parentId && byId.has(parentId)) { | |
| children.set(parentId, [...(children.get(parentId) ?? []), seed.seedId]); | |
| hasParent.add(seed.seedId); | |
| } | |
| } | |
| } | |
| const seen = new Set<string>(); | |
| const render = (nodeId: string): string => { | |
| const node = byId.get(nodeId); | |
| if (!node) return ''; | |
| const kids = (children.get(nodeId) ?? []).filter((kid) => !seen.has(kid)); | |
| for (const kid of kids) seen.add(kid); | |
| const label = `${safeLabel(node.strainName)}#${node.seedId.slice(0, 6)}`; | |
| if (!kids.length) return label; | |
| return `(${kids.map(render).join(',')})${label}`; | |
| }; | |
| const roots = seeds.map((seed) => seed.seedId).filter((seedId) => !hasParent.has(seedId)); | |
| return `(${roots.filter((root) => !seen.has(root)).map(render).join(',')});`; | |
| } | |