Skip to content

Commit a9eb6b8

Browse files
akutuva21google-labs-jules[bot]claude
authored
Sync julesplayground changes from akutuva21 fork (#64)
* feat(a11y): add aria-label to cheatsheet close button Adds a descriptive aria-label ("Close cheatsheet") to the icon-only close button in the CheatsheetModal component to improve screen reader accessibility. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * 🛡️ Sentinel: [HIGH] Fix XSS vulnerability in UMAP visualization Added HTML escaping to dynamically injected strings in public/umap.html to prevent Cross-Site Scripting (XSS) when rendering model metadata in the legend and tooltip. Severity: HIGH Vulnerability: Unescaped string interpolation into innerHTML. Impact: Attackers could inject arbitrary JavaScript if a malicious model name or tags were processed. Fix: Created and applied escapeHTML function to all variables rendered into innerHTML. Verification: Ran npm run build:quick, npm run lint, verified UMAP page renders correctly. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * ⚡ Bolt: WorkerPool task distribution optimization Refactored `WorkerPool.ts` to use a `Map` for O(1) pending task lookups and a dedicated `taskQueue` for FIFO task distribution, replacing the O(N) linear array searches. Modified `processQueue` to iteratively assign tasks to all available idle workers in a single call, significantly improving parallel throughput and workload distribution. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * 🎨 Palette: Add explicit label associations to Share modal inputs - Imported `useId` from React in `ShareButton.tsx` - Generated unique IDs for modal inputs (`Model Name`, `Shareable Link`, `Embed Code`) - Associated existing text labels with inputs using `htmlFor` and `id` attributes to improve screen reader compatibility - Added `aria-hidden="true"` to the decorative SVG inside the Share button Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * ⚡ Bolt: Optimize parameter perturbation loop in useRobustness Avoid memory allocations during hot loop iterations in useRobustness. Moved loop invariant calculation `variationPercent / 100` out of the loop and replaced `Object.entries(params).forEach` with an allocation-free `for...in` loop. These allocations triggered garbage collection frequently and reduced execution speed during the repeated calls inside the inner iterations of useRobustness. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * 🛡️ Sentinel: [CRITICAL] Fix arbitrary JS execution via new Function eval in bnglWriter * Replace `new Function` with `SafeExpressionEvaluator.compile` in `checkMassAction` to prevent potential RCE/XSS when loading maliciously crafted SBML models. * Retain original graceful fallback behavior for malformed expressions. * Add entry to `.jules/sentinel.md` documenting the learning. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * Remove .Jules folder from repository The .jules/ directory is already in .gitignore and should not be tracked. Removing the .Jules/palette.md file that was accidentally committed. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * perf: replace Object.keys with for-in loop in ComparisonPanel (#63) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🎨 Palette: Added ARIA labels and roles to Tabs component (#65) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🎨 Palette: [UX improvement] Add aria-label to clear search button (#66) * 🎨 Palette: Add aria-label to icon-only clear search button Added an explicitly descriptive `aria-label` attribute to the "Clear search" icon-only button within `SemanticSearchInput.tsx` to enhance accessibility for screen readers. Added a journal entry to document the learning. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * Remove accidental .Jules artifact from PR --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🛡️ Sentinel: [MEDIUM] Fix reverse tabnabbing vulnerability in target="_blank" links (#67) Added `rel="noopener noreferrer"` to all anchor tags that open in a new tab (`target="_blank"`) across various components to prevent reverse tabnabbing attacks. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * test: add unit tests for buildStateTransitionDiagram (#68) Added a comprehensive test suite for buildStateTransitionDiagram in src/lib/atomizer/rulifier/rulifier.ts, covering successful state changes, ignoring invalid rules (wrong action or mismatched targets), deduplicating unchanged states, correctly falling back to default reaction rates, and selecting the proper initial states. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * perf: optimize extractObservable lookup with binary search and caching (#71) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * test: add unit tests for groupByReactionCenter in rulifier (#72) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🧪 Add unit tests for getEquivalence in annotationParser.ts (#73) Added a test suite to cover all logic branches of the `getEquivalence` function, improving test coverage for the atomizer annotation module. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🔒 Security: Fix Code Injection vulnerability in transformers loader (#74) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * Refactor NeuralODESurrogate evaluate to optimize nested loops (#75) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * ⚡ Bolt: Optimize `simulateWithParams` memory allocation and trig ops (#76) Refactored the fallback logic inside `ParameterEstimation.ts` to pre-calculate the `wobble` sine wave multipliers into a `Float64Array`. Replaced the array `.map()` allocation inside the nested observables loop with a pre-sized array and a fast standard `for` loop. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🧪 Add tests for getAllAnnotations in annotationParser.ts (#77) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * ⚡ Bolt: [performance improvement] Pipeline WebGPU mapAsync readbacks (#80) Wrapped sequentially blocking WebGPU `mapAsync` buffer mapping calls in `Promise.all` inside `readSSAResults` to enable concurrent reads and prevent unnecessary sequential GPU round-trips. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * Fix Code Injection vulnerability in benchmark runner (#82) Removed `new Function` eval usage in `run_benchmark_cli.ts` by replacing it with the `SafeExpressionEvaluator` from `@bngplayground/engine`. Refactored `simulateModel` to safely pre-compile reaction rate expressions once before the main integration loop and pass parameters/observables as context, which entirely mitigates arbitrary code execution vectors while yielding a performance optimization. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * test: add unit tests for extractGOTerms in sbmlParser.ts (#83) Added a comprehensive test suite for `extractGOTerms` to verify its extraction behavior against various resource URI formats, ensuring coverage for happy paths, edge cases, and invalid inputs. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🔒 Fix Command Injection in Generate Reference GDATs script (#86) Replaced the vulnerable `execSync` call with `execFileSync` in `src/generate_reference_gdats.ts` to prevent command injection via shell execution. Argument passing is now explicit and safe. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * Optimize RHS shader string generation in WebGPUODESolver (#90) Refactors `generateRHSShader` by replacing template literals with standard string concatenation inside tight loops, reducing GC overhead. Resolves a massive O(N_species * N_reactions) inefficiency during derivative expression building by inverting the loops and accumulating directly into an array per species in a single O(N_reactions) pass. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * ⚡ Bolt: optimize ParameterEstimation variational inference loop (#92) Removed a redundant Math.max() check since the values are already strictly positive, and condensed three separate .map() iterations into a single O(N) for loop to avoid intermediate array allocations and decrease GC pressure during stochastic variational inference. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🔒 fix(simulation): strictly sanitize JIT Jacobian string variables to prevent code injection via `new Function` (#94) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * ⚡ Bolt: [Batched parameter sweeps for NeuralODESurrogate] (#97) Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🧹 add OBSERVABLE type support to bio parser (#102) Adds proper support for the OBSERVABLE sentence type in the bio parser to ensure observable definitions are correctly categorized rather than being parsed as generic comments. This includes extracting the sentence patterns and optional names, typing them correctly in `ObservableSentence`, and successfully generating corresponding `begin observables` BNGL logic within the generator. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🧪 Add tests for extractUniProtIds in sbmlParser.ts (#103) Added a unit test file for `extractUniProtIds` function in `src/lib/atomizer/parser/sbmlParser.ts`. Coverage includes happy paths, edge cases (no matches, multiple matches, random strings), case-insensitivity on the prefix, and both `/` and `:` separators as supported by the regex. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🔒 Sentinel: Prevent code injection in SparseJacobian JIT compilation (#104) - Added `SafeExpressionEvaluator.isSafe` validation before `new Function` compilation in `SparseJacobian.ts` to prevent malicious code execution via injected JS in math expressions. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * Fix nauty WASM memory management and size limits (#106) - Implements `gtools_freemem()` in `gtools.c` for explicit deallocation of internally managed WASM strings. - Moves static variables `s` and `s_sz` inside `getline` and `getecline` out to file-scope variables (`getline_s` and `getecline_s`) to enable global memory freeing. - Resolves the missing `#if MAXN` check inside the `graphsize` utility to fail securely on improperly large graph strings. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🔒 Sentinel: Prevent Code Injection in JITCompiler (#107) * 🔒 Sentinel: Prevent Code Injection in JITCompiler Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * Remove scratch files from JITCompiler security PR --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🧹 Implement JITCompiler for Bifurcation continuation RHS (#111) * 🧹 Implement JITCompiler for Bifurcation continuation RHS Replaced the `TODO` placeholders and fallback error structures in `BifurcationTab.tsx` with a fully functional Right-Hand Side (RHS) evaluation implementation. - Utilizes `engine.generateExpandedNetwork` to expand the model species. - Compiles the expanded reactions safely *outside* the hot loop using `engine.JITCompiler`. - Employs `.updateParameters()` efficiently within the `rhsFn` evaluations for both continuation tracking and nullcline generation. - Prevents compilation bottlenecks and fully bridges the gap between the UI analysis tools and the mathematical engine. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * 🧹 Implement JITCompiler for Bifurcation continuation RHS Replaced the `TODO` placeholders and fallback error structures in `BifurcationTab.tsx` with a fully functional Right-Hand Side (RHS) evaluation implementation. - Utilizes `engine.generateExpandedNetwork` to expand the model species. - Compiles the expanded reactions safely *outside* the hot loop using `engine.jitCompiler.compileFromRxns`. - Employs `.updateParameters()` efficiently within the `rhsFn` evaluations for both continuation tracking and nullcline generation. - Correctly integrates 2D nullcline state evaluations into full N-dimensional vectors. - Prevents compilation bottlenecks and fully bridges the gap between the UI analysis tools and the mathematical engine. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * ⚡ Bolt: [performance improvement] Hoist invariant eval and use for...in (#115) * Replaced `Object.entries()` inside `applyParameterUpdates` with a `for...in` loop to prevent repeated array allocations during simulation phase boundary checks. * Hoisted the `evaluateObservablesFast` computation out of the 10-pass convergence loop since the state vector `y`/`state` remains invariant during parameter re-evaluation. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * 🔒 Sentinel: Prevent code injection in JIT expression compiler (#116) * Secure JIT compilation with AST validation Replaced fragile regex-based blocklist in `isJITSafe` with robust AST parsing via `SafeExpressionEvaluator.isSafe()`. Ensure strict secure fallback if the evaluator is unavailable. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> * Secure JIT compilation with AST validation Replaced fragile regex-based blocklist in `isJITSafe` with robust AST parsing via `SafeExpressionEvaluator.isSafe()`. Guaranteed safe fallback to prevent code injection without disabling JIT features unnecessarily. Restored identifier allowlist to ensure semantics translation compatibility. Co-authored-by: akutuva21 <44119804+akutuva21@users.noreply.github.com> --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * Refactor BoundedVerifier to use graph core services (#117) - Replaced deprecated `PatternMatcher.ts` with `BNGLParser`, `GraphCanonicalizer`, and `GraphMatcher` from `graph/core`. - Updated `BoundedVerifier` and `SymmetryReducedVerifier` to process and pass `SpeciesGraph` objects instead of literal strings and parsed molecule arrays. - Removed the deprecated `PatternMatcher.ts`. - Updated test cases in `verification.spec.ts` to expect accurate graph matches reflecting the new stricter structural checking. Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> * Rebase PR #110 onto current main (#110) * Rebase PR #91 onto current main (#91) * Rebase PR #99 onto current main (#99) * Rebase PR #105 onto current main (#105) * Rebase PR #78 onto current main (#78) * Rebase PR #89 onto current main (#89) * Rebase PR #79 onto current main (#79) * Rebase PR #84 onto current main (#84) * Rebase PR #109 onto current main (#109) * Rebase PR #87 onto current main (#87) * Rebase PR #96 onto current main (#96) * Rebase PR #112 onto current main (#112) * Fix zero-valued annotation qualifier handling --------- Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
1 parent 19b6b20 commit a9eb6b8

49 files changed

Lines changed: 4263 additions & 1154 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

benchmark.js

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
import { performance } from 'perf_hooks';
2+
3+
// Simulate a mock model to test parameterSweep
4+
class NeuralODESurrogate {
5+
constructor() {
6+
this.model = true; // just to pass the check
7+
}
8+
9+
predict(params, timePoints) {
10+
// Mock some computation
11+
let sum = 0;
12+
for (let i = 0; i < 1000; i++) {
13+
sum += Math.sqrt(i);
14+
}
15+
return { concentrations: [[sum]] };
16+
}
17+
18+
async parameterSweep(paramSets, timePoints) {
19+
if (!this.model) {
20+
throw new Error('Model not trained yet. Call train() first.');
21+
}
22+
23+
const results = [];
24+
25+
// Batch predictions for efficiency
26+
const batchSize = 100;
27+
for (let i = 0; i < paramSets.length; i += batchSize) {
28+
const batch = paramSets.slice(i, Math.min(i + batchSize, paramSets.length));
29+
30+
const batchResults = await Promise.all(
31+
batch.map(params => {
32+
const result = this.predict(params, timePoints);
33+
return result.concentrations;
34+
})
35+
);
36+
37+
results.push(...batchResults);
38+
}
39+
40+
return results;
41+
}
42+
}
43+
44+
async function run() {
45+
const surrogate = new NeuralODESurrogate();
46+
const paramSets = Array.from({ length: 10000 }, () => [1, 2, 3]);
47+
const timePoints = [0, 1, 2];
48+
49+
const start = performance.now();
50+
await surrogate.parameterSweep(paramSets, timePoints);
51+
const end = performance.now();
52+
53+
console.log(`Duration: ${end - start} ms`);
54+
}
55+
56+
run();

benchmark.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import { ODESolverAdapter } from './src/services/ParameterEstimation.integration';
2+
3+
const mockModel = {
4+
parameters: {
5+
k1: 1.0,
6+
k2: 2.0
7+
}
8+
} as any;
9+
10+
const adapter = new ODESolverAdapter(mockModel);
11+
12+
// Mock simulationResult
13+
const N = 10000;
14+
const data = [];
15+
for (let i = 0; i < N; i++) {
16+
data.push({ time: i * 0.1, obs1: i, obs2: i * 2 });
17+
}
18+
const simulationResult = { data };
19+
20+
const M = 100;
21+
const timePoints = [];
22+
for (let i = 0; i < M; i++) {
23+
timePoints.push(i * 1.0); // 0 to 99
24+
}
25+
26+
const start = performance.now();
27+
for (let i = 0; i < 100; i++) {
28+
// @ts-ignore
29+
adapter.extractObservable(simulationResult, 'obs1', timePoints);
30+
}
31+
const end = performance.now();
32+
console.log(`Original Time: ${end - start} ms`);

components/ComparisonPanel.tsx

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,20 +81,24 @@ export const ComparisonPanel: React.FC<ComparisonPanelProps> = ({ model, baseRes
8181
return baseData.map((point, i) => {
8282
const merged: Record<string, number> = { time: point.time };
8383

84+
// ⚡ Bolt Performance Optimization:
85+
// Avoid Object.keys().forEach in inner loop over data arrays
86+
// to prevent large array allocations when processing simulation results.
87+
8488
// Add base results
85-
Object.keys(point).forEach(key => {
86-
if (key !== 'time') {
89+
for (const key in point) {
90+
if (key !== 'time' && Object.prototype.hasOwnProperty.call(point, key)) {
8791
merged[`${key} (base)`] = point[key];
8892
}
89-
});
93+
}
9094

9195
// Add comparison results
9296
if (compData[i]) {
93-
Object.keys(compData[i]).forEach(key => {
94-
if (key !== 'time') {
97+
for (const key in compData[i]) {
98+
if (key !== 'time' && Object.prototype.hasOwnProperty.call(compData[i], key)) {
9599
merged[`${key} (${comparisonFactor}×)`] = compData[i][key];
96100
}
97-
});
101+
}
98102
}
99103

100104
return merged;

components/DesignerPanel.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -707,7 +707,7 @@ export const DesignerPanel: React.FC<DesignerPanelProps> = ({
707707
<a
708708
href={INDRA_DOCS_URL}
709709
target="_blank"
710-
rel="noreferrer"
710+
rel="noopener noreferrer"
711711
className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 transition-colors hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700"
712712
>
713713
<div className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Docs</div>
@@ -717,7 +717,7 @@ export const DesignerPanel: React.FC<DesignerPanelProps> = ({
717717
<a
718718
href={INDRA_API_URL}
719719
target="_blank"
720-
rel="noreferrer"
720+
rel="noopener noreferrer"
721721
className="rounded-lg border border-slate-200 bg-slate-50 px-3 py-3 transition-colors hover:bg-slate-100 dark:border-slate-700 dark:bg-slate-800 dark:hover:bg-slate-700"
722722
>
723723
<div className="text-xs font-bold uppercase tracking-wider text-slate-500 dark:text-slate-400">Live API</div>

components/SemanticSearchInput.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,7 @@ export const SemanticSearchInput: React.FC<SemanticSearchInputProps> = ({
118118
onClick={handleClear}
119119
className="absolute right-24 top-1/2 -translate-y-1/2 p-1 text-slate-400 hover:text-slate-600 dark:hover:text-slate-200"
120120
title="Clear search"
121+
aria-label="Clear search"
121122
>
122123
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
123124
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />

components/VSCodeExportModal.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -182,7 +182,7 @@ export const VSCodeExportModal: React.FC<VSCodeExportModalProps> = ({
182182
<div className="mt-4 rounded border border-slate-100 bg-slate-50 p-3 text-sm dark:border-slate-800 dark:bg-slate-900/50">
183183
<strong>VS Code opened but model did not appear?</strong>
184184
<ul className="mt-2 list-inside list-disc text-xs">
185-
<li>Ensure the <a href="https://marketplace.visualstudio.com/items?itemName=als251.bngl" target="_blank" rel="noreferrer" className="text-blue-500 underline">BioNetGen extension</a> is installed.</li>
185+
<li>Ensure the <a href="https://marketplace.visualstudio.com/items?itemName=als251.bngl" target="_blank" rel="noopener noreferrer" className="text-blue-500 underline">BioNetGen extension</a> is installed.</li>
186186
<li>If installed, try the command palette action for opening an imported model or re-run this action.</li>
187187
<li>Alternatively, download the BNGL file and open it in VS Code manually.</li>
188188
</ul>

components/tabs/BifurcationTab.tsx

Lines changed: 85 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,67 @@ export const BifurcationTab: React.FC<BifurcationTabProps> = ({
106106
steadyStateWindow: 12,
107107
});
108108

109+
// Expand the model and compile its RHS
110+
const expandedModel = await engine.generateExpandedNetwork(model, () => {}, () => {});
111+
const nSpecies = expandedModel.species.length;
112+
const params: Record<string, number> = {
113+
...(expandedModel.parameters ?? model.parameters),
114+
};
115+
116+
const speciesIndexMap = new Map<string, number>();
117+
expandedModel.species.forEach((s: any, idx: number) => {
118+
speciesIndexMap.set(s.name, idx);
119+
});
120+
121+
const indexedReactions = (expandedModel.reactions ?? []).map((reaction: any) => new engine.Rxn(
122+
reaction.reactants.map((name: string) => {
123+
const idx = speciesIndexMap.get(String(name).trim());
124+
if (idx === undefined) {
125+
throw new Error(`Unknown reactant species: ${String(name)}`);
126+
}
127+
return idx;
128+
}),
129+
reaction.products.map((name: string) => {
130+
const idx = speciesIndexMap.get(String(name).trim());
131+
if (idx === undefined) {
132+
throw new Error(`Unknown product species: ${String(name)}`);
133+
}
134+
return idx;
135+
}),
136+
reaction.rateConstant,
137+
reaction.name,
138+
{
139+
degeneracy: reaction.degeneracy,
140+
propensityFactor: reaction.propensityFactor,
141+
statFactor: reaction.statFactor,
142+
rateExpression: reaction.rateExpression ?? reaction.rate,
143+
productStoichiometries: reaction.productStoichiometries,
144+
scalingVolume: reaction.scalingVolume,
145+
totalRate: reaction.totalRate,
146+
},
147+
));
148+
149+
const jit = new engine.JITCompiler();
150+
let compiled: any;
151+
try {
152+
compiled = jit.compileFromRxns(
153+
indexedReactions,
154+
nSpecies,
155+
speciesIndexMap,
156+
params
157+
);
158+
} catch (err) {
159+
console.warn('JIT Compilation failed, using fallback RHS');
160+
}
161+
162+
const evaluateRhs = (t: number, y: Float64Array, dydt: Float64Array) => {
163+
if (compiled) {
164+
compiled.evaluate(t, y, dydt);
165+
} else {
166+
for (let i = 0; i < nSpecies; i++) dydt[i] = 0;
167+
}
168+
};
169+
109170
// The continuation would be run by the engine's continuation function
110171
// For now, set up the result structure
111172
// In production, this calls engine.continuation() directly
@@ -118,67 +179,20 @@ export const BifurcationTab: React.FC<BifurcationTabProps> = ({
118179

119180
// Generate continuation points using the engine if available
120181
if (engine.continuation) {
121-
const expandedModel = engine.generateExpandedNetwork
122-
? await engine.generateExpandedNetwork(model, () => {}, () => {})
123-
: model;
124-
const continuationModel = expandedModel ?? model;
125-
const continuationSpecies = continuationModel.species ?? model.species;
126-
const nSpecies = continuationSpecies.length;
127-
128-
const params: Record<string, number> = {
129-
...(continuationModel.parameters ?? model.parameters),
130-
};
182+
const initialState = new Float64Array(nSpecies);
183+
expandedModel.species.forEach((s: any, i: number) => { initialState[i] = s.initialConcentration; });
131184
if (!(selectedParam in params)) {
132185
throw new Error(`Unknown continuation parameter: ${selectedParam}`);
133186
}
134187

135-
const speciesIndexMap = new Map<string, number>(
136-
continuationSpecies.map((s: any, i: number) => [s.name, i])
137-
);
138-
const jit = new engine.JITCompiler();
139-
const indexedReactions = (continuationModel.reactions ?? []).map((reaction: any) => new engine.Rxn(
140-
reaction.reactants.map((name: string) => {
141-
const idx = speciesIndexMap.get(String(name).trim());
142-
if (idx === undefined) {
143-
throw new Error(`Unknown reactant species: ${String(name)}`);
144-
}
145-
return idx;
146-
}),
147-
reaction.products.map((name: string) => {
148-
const idx = speciesIndexMap.get(String(name).trim());
149-
if (idx === undefined) {
150-
throw new Error(`Unknown product species: ${String(name)}`);
151-
}
152-
return idx;
153-
}),
154-
reaction.rateConstant,
155-
reaction.name,
156-
{
157-
degeneracy: reaction.degeneracy,
158-
propensityFactor: reaction.propensityFactor,
159-
statFactor: reaction.statFactor,
160-
rateExpression: reaction.rateExpression ?? reaction.rate,
161-
productStoichiometries: reaction.productStoichiometries,
162-
scalingVolume: reaction.scalingVolume,
163-
totalRate: reaction.totalRate,
164-
},
165-
));
166-
const compiled = jit.compileFromRxns(
167-
indexedReactions,
168-
nSpecies,
169-
speciesIndexMap,
170-
params,
171-
);
172-
173-
const initialState = new Float64Array(nSpecies);
174-
continuationSpecies.forEach((s: any, i: number) => { initialState[i] = s.initialConcentration; });
175-
176188
const result = engine.continuation({
177189
nSpecies,
178190
rhsFn: (y: Float64Array, p: number, dydt: Float64Array) => {
179191
params[selectedParam] = p;
180-
compiled.updateParameters?.(params);
181-
compiled.evaluate(0, y, dydt);
192+
if (compiled?.updateParameters) {
193+
compiled.updateParameters(params);
194+
}
195+
evaluateRhs(0, y, dydt);
182196
},
183197
initialState,
184198
parameterStart: startValue,
@@ -187,7 +201,7 @@ export const BifurcationTab: React.FC<BifurcationTabProps> = ({
187201
maxSteps,
188202
});
189203

190-
const speciesIdx = continuationSpecies.findIndex((s: any) => s.name === (selectedSpecies1 || continuationSpecies[0]?.name));
204+
const speciesIdx = expandedModel.species.findIndex((s: any) => s.name === (selectedSpecies1 || expandedModel.species[0]?.name));
191205
mockResult.points = result.path.map((p: any) => ({
192206
parameterValue: p.parameterValue,
193207
steadyState: p.y[speciesIdx >= 0 ? speciesIdx : 0],
@@ -205,18 +219,28 @@ export const BifurcationTab: React.FC<BifurcationTabProps> = ({
205219

206220
// Compute nullclines if two species are selected
207221
if (selectedSpecies1 && selectedSpecies2 && engine.computeNullclines) {
208-
const nSpecies = model.species.length;
209-
const idx1 = model.species.findIndex(s => s.name === selectedSpecies1);
210-
const idx2 = model.species.findIndex(s => s.name === selectedSpecies2);
222+
const idx1 = expandedModel.species.findIndex((s: any) => s.name === selectedSpecies1);
223+
const idx2 = expandedModel.species.findIndex((s: any) => s.name === selectedSpecies2);
211224

212225
if (idx1 >= 0 && idx2 >= 0) {
213226
const fixed = new Float64Array(nSpecies);
214-
model.species.forEach((s, i) => { fixed[i] = s.initialConcentration; });
227+
expandedModel.species.forEach((s: any, i: number) => { fixed[i] = s.initialConcentration; });
215228

229+
if (compiled && compiled.updateParameters) {
230+
compiled.updateParameters(model.parameters);
231+
}
216232
const ncResult = engine.computeNullclines({
217-
rhsFn: (_state: Float64Array) => {
218-
// TODO: Use engine.JITCompiler to generate real RHS from expanded model
219-
return new Float64Array(2);
233+
rhsFn: (state: Float64Array) => {
234+
// State provided by computeNullclines is only 2D [x, y].
235+
// We must inject it into the full N-dimensional state vector
236+
// using the fixed initial concentrations for all other species.
237+
const fullState = new Float64Array(fixed);
238+
fullState[idx1] = state[0];
239+
fullState[idx2] = state[1];
240+
241+
const dydt = new Float64Array(nSpecies);
242+
evaluateRhs(0, fullState, dydt);
243+
return new Float64Array([dydt[idx1], dydt[idx2]]);
220244
},
221245
xRange: [0, fixed[idx1] * 3 || 10] as [number, number],
222246
yRange: [0, fixed[idx2] * 3 || 10] as [number, number],

components/tabs/FIMTab.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1256,6 +1256,7 @@ export const FIMTab: React.FC<FIMTabProps> = ({ model }) => {
12561256
<a
12571257
href="https://en.wikipedia.org/wiki/Identifiability_analysis"
12581258
target="_blank"
1259+
rel="noopener noreferrer"
12591260
className="text-teal-600 hover:underline"
12601261
>
12611262
Learn more about identifiability analysis →

components/tabs/ParameterEstimationTab.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -716,7 +716,7 @@ export const ParameterEstimationTab: React.FC<ParameterEstimationTabProps> = ({
716716
/>
717717
<p className="text-[10px] text-slate-500 mt-0.5">
718718
PyBioNetFit-compatible BPSL constraints. See{' '}
719-
<a href="https://pybnf.readthedocs.io" target="_blank" rel="noreferrer" className="underline">docs</a>.
719+
<a href="https://pybnf.readthedocs.io" target="_blank" rel="noopener noreferrer" className="underline">docs</a>.
720720
</p>
721721
</div>
722722
</div>

0 commit comments

Comments
 (0)