diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..c4732a3 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.git +.venv +bin/results +bin/simulation_cpu/results +bin/simulation_cpu_postprocessing/results +**/*.o +**/__pycache__ +*.pyc diff --git a/.gitignore b/.gitignore index d20aa13..d35ab9e 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,9 @@ bin/simulation_cpu/responses/*.json bin/simulation_cpu_postprocessing/initial_values_zip bin/simulation_cpu_postprocessing/requests/*filled* bin/simulation_cpu_postprocessing/responses/*.json +.DS_Store +bin/simulation_cpu/.docker-test-param.txt +.bench_samples/ *.tex *.pdf *.aux @@ -33,3 +36,53 @@ bin/simulation_cpu_postprocessing/responses/*.json *.swp logfile results + +# CiPA drug parameter data pulled from FDA/CiPA GitHub repo (new drugs beyond quinidine) +bin/simulation_cpu/chantest_hill/bepridil/ +bin/simulation_cpu/chantest_hill/chlorpromazine/ +bin/simulation_cpu/chantest_hill/cisapride/ +bin/simulation_cpu/chantest_hill/diltiazem/ +bin/simulation_cpu/chantest_hill/dofetilide/ +bin/simulation_cpu/chantest_hill/mexiletine/ +bin/simulation_cpu/chantest_hill/ondansetron/ +bin/simulation_cpu/chantest_hill/ranolazine/ +bin/simulation_cpu/chantest_hill/sotalol/ +bin/simulation_cpu/chantest_hill/terfenadine/ +bin/simulation_cpu/chantest_hill/verapamil/ +bin/simulation_cpu/chantest_hill/CiPA_training_drugs_Cmax.csv +bin/simulation_cpu/chantest_hill/CiPA_drug_concentrations.csv +bin/simulation_cpu/chantest_herg/bepridil/ +bin/simulation_cpu/chantest_herg/chlorpromazine/ +bin/simulation_cpu/chantest_herg/cisapride/ +bin/simulation_cpu/chantest_herg/diltiazem/ +bin/simulation_cpu/chantest_herg/dofetilide/ +bin/simulation_cpu/chantest_herg/mexiletine/ +bin/simulation_cpu/chantest_herg/ondansetron/ +bin/simulation_cpu/chantest_herg/ranolazine/ +bin/simulation_cpu/chantest_herg/sotalol/ +bin/simulation_cpu/chantest_herg/terfenadine/ +bin/simulation_cpu/chantest_herg/verapamil/ +bin/simulation_cpu_postprocessing/chantest_hill/bepridil/ +bin/simulation_cpu_postprocessing/chantest_hill/chlorpromazine/ +bin/simulation_cpu_postprocessing/chantest_hill/cisapride/ +bin/simulation_cpu_postprocessing/chantest_hill/diltiazem/ +bin/simulation_cpu_postprocessing/chantest_hill/dofetilide/ +bin/simulation_cpu_postprocessing/chantest_hill/mexiletine/ +bin/simulation_cpu_postprocessing/chantest_hill/ondansetron/ +bin/simulation_cpu_postprocessing/chantest_hill/ranolazine/ +bin/simulation_cpu_postprocessing/chantest_hill/sotalol/ +bin/simulation_cpu_postprocessing/chantest_hill/terfenadine/ +bin/simulation_cpu_postprocessing/chantest_hill/verapamil/ +bin/simulation_cpu_postprocessing/chantest_hill/CiPA_training_drugs_Cmax.csv +bin/simulation_cpu_postprocessing/chantest_hill/CiPA_drug_concentrations.csv +bin/simulation_cpu_postprocessing/chantest_herg/bepridil/ +bin/simulation_cpu_postprocessing/chantest_herg/chlorpromazine/ +bin/simulation_cpu_postprocessing/chantest_herg/cisapride/ +bin/simulation_cpu_postprocessing/chantest_herg/diltiazem/ +bin/simulation_cpu_postprocessing/chantest_herg/dofetilide/ +bin/simulation_cpu_postprocessing/chantest_herg/mexiletine/ +bin/simulation_cpu_postprocessing/chantest_herg/ondansetron/ +bin/simulation_cpu_postprocessing/chantest_herg/ranolazine/ +bin/simulation_cpu_postprocessing/chantest_herg/sotalol/ +bin/simulation_cpu_postprocessing/chantest_herg/terfenadine/ +bin/simulation_cpu_postprocessing/chantest_herg/verapamil/ \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..2c64242 --- /dev/null +++ b/README.md @@ -0,0 +1,593 @@ +# DrugSimulation + +A high-performance, MPI-parallel in-silico cardiac drug safety simulation framework based on the [CiPA (Comprehensive in vitro Proarrhythmia Assay)](https://cipaproject.org/) initiative. The tool simulates the electrophysiological effects of drugs on human ventricular cardiomyocytes across multiple cell models and drug concentrations, then extracts quantitative biomarkers (CiPA features) for cardiotoxicity assessment. + +--- + +## Table of Contents + +- [Overview](#overview) +- [Cell Models](#cell-models) +- [Prerequisites](#prerequisites) +- [Building](#building) +- [Project Structure](#project-structure) +- [Configuration (`param.txt`)](#configuration-paramtxt) +- [Input Files](#input-files) +- [Running a Simulation Locally on Your Machine](#running-a-simulation-locally-on-your-machine) +- [Running in Dockerized Linux (Recommended for macOS)](#running-in-dockerized-linux-recommended-for-macos) +- [Running a Simulation on an HPC Cluster (PBS/Torque)](#running-a-simulation-on-an-hpc-cluster-pbstorque) +- [Post-processing & Reporting](#post-processing--reporting) +- [Output Files](#output-files) +- [Python Scripts](#python-scripts) +- [AI Report Generation](#ai-report-generation) + +--- + +## Overview + +DrugSimulation runs a multi-pace action-potential (AP) simulation for each drug sample and concentration. It computes steady-state electrical behaviour over up to 1000 paces, then extracts the following CiPA biomarker features from the final paces: + +| Feature | Description | +|---|---| +| `qNet` | Net charge (area under net current) | +| `qInward` | Inward charge | +| `INaLAUC` | Area under the late sodium current | +| `ICaLAUC` | Area under the L-type calcium current | +| `APD90` / `APD50` | Action potential duration at 90% / 50% repolarisation | +| `APDTri` | AP triangulation (APD90 − APD50) | +| `VmMax` / `VmRest` | Peak and resting membrane potential | +| `dVmdtMax` | Maximum upstroke velocity | +| `dVmdtMaxRepol` | Maximum repolarisation rate | +| `CaTD90` / `CaTD50` | Calcium transient duration at 90% / 50% | +| `CaTDTri` | Calcium transient triangulation | +| `CaMax` / `CaRest` | Peak and resting intracellular calcium | + +MPI is used to distribute samples across CPU cores. The simulation supports both a full in-silico run and a faster postprocessing-only mode that reads pre-computed steady-state values. + +--- + +## Cell Models + +Select the cardiac cell model at compile time using one of the preprocessor macros below: + +| Macro | Binary suffix | Cell model | +|---|---|---| +| *(none)* | `ORd-static` | O'Hara–Rudy 2011 (default) | +| `-DCIPAORDV1` | `CiPAORdv1.0` | O'Hara–Rudy CiPA v1.0 (2017) | +| `-DTOR_ORD` | `ToR-ORd` | Tomek–O'Hara–Rudy (ToR-ORd) | +| `-DTOR_ORD_DYNCL` | `ToR-ORd-dynCl` | ToR-ORd with dynamic chloride | +| `-DGRANDI` | `Grandi` | Grandi 2011 atrial | +| `-DORD_STATIC_LAND` | `ORd-static_Land` | O'Hara–Rudy + Land contraction | +| `-DCIPAORDV1_LAND` | `CiPAORdv1.0_Land` | CiPAORdv1.0 + Land contraction | +| `-DTOR_ORD_LAND` | `ToR-ORd_Land` | ToR-ORd + Land contraction | + +Add `-DPOSTPROCESSING` to any of the above to build the postprocessing variant. + +--- + +## Prerequisites + +| Dependency | Notes | +|---|---| +| C++11 compiler with MPI (`mpicxx` / `mpicc`) | e.g. OpenMPI or MPICH | +| [libCML](../libCML/) | Cardiac Modelling Library (expected at `../libCML/`) | +| [SUNDIALS CVode 5.7.0](https://computing.llnl.gov/projects/sundials) | ODE solver (expected at `../libCML/libs/sundials-5.7.0/`) | +| `libcurl` | Used by the report generator | +| `libjson-c` | Used by the report generator | +| Python ≥ 3.6 | For post-processing scripts | +| Python packages | See [`bin/scripts/requirements.txt`](bin/scripts/requirements.txt) | + +Install Python dependencies: + +```bash +pip install -r bin/scripts/requirements.txt +``` + +--- + +## Building + +Native builds are intended for Linux environments that have matching `libCML` and SUNDIALS static archives. On macOS, use the Dockerized Linux workflow below because the bundled archives under `../libCML/libs/sundials-5.7.0/lib64` are Linux target files and will not link natively on macOS. + +### Build all variants at once + +```bash +bash compile_binaries.sh +``` + +This builds all eight cell-model variants plus their postprocessing counterparts and places the binaries in `bin/`. + +### Build a single variant manually + +```bash +# Example: CiPAORdv1.0 +make all EXTRA_CXXFLAGS+="-std=c++11 -DCIPAORDV1" + +# Example: ToR-ORd postprocessing +make all EXTRA_CXXFLAGS+="-std=c++11 -DTOR_ORD -DPOSTPROCESSING" + +# Clean build artefacts for a variant +make EXTRA_CXXFLAGS+="-DCIPAORDV1" clean +``` + +Compiled binaries are written to `bin/drugsim_`. + +--- + +## Project Structure + +``` +DrugSimulation/ +├── main.cpp # Entry point – MPI init, parameter loading, simulation dispatch +├── Makefile # Build system +├── compile_binaries.sh # Builds all cell-model variants in one step +├── modules/ +│ ├── drug_simulation_bench.cpp/hpp # Top-level simulation orchestrator (MPI work distribution) +│ ├── insilico.cpp/hpp # Single-sample in-silico AP simulation loop +│ ├── postprocessing.cpp/hpp # Feature extraction from pre-computed steady states +│ ├── report_drug.cpp/hpp # Invokes the AI report-generation pipeline +│ └── show_param_logs.cpp/hpp # Prints parameter summary to stdout +└── bin/ + ├── drugsim_ # Compiled binaries (git-ignored) + ├── scripts/ # Shared utility scripts + │ ├── combine_and_average_features.py # Merges per-core feature CSVs; computes per-concentration averages + │ ├── compute_mean_ci.py # Bootstrap median + 95% CI across concentrations + │ ├── plot_features.py # Dose–response feature plots + │ ├── plot_time_series.py # Time-series plots (Vm, dVm/dt, Cai, currents) + │ ├── convert_params.sh # Converts param.txt keys to camelCase + │ ├── create_concs_directories.sh # Creates result sub-directories per concentration + │ ├── create_escaped_b64_file.sh # Escapes & Base64-encodes files for API upload + │ ├── unzip_files.sh / zip_files.sh # Archive helpers for result files + │ └── requirements.txt # Python package pinned versions + ├── simulation_cpu/ # Ready-to-run working directory for a standard CPU simulation + │ ├── param.txt # Simulation parameters (edit this before running) + │ ├── exec_bash.sh # Launch simulation locally via mpiexec + │ ├── exec_pbs.sh # Launch simulation on a PBS/Torque HPC cluster + │ ├── kill_bash.sh # Kill a running local simulation + │ ├── generate_report.sh # Run post-processing and generate figures + │ ├── exec_perplexity_report_generator.sh # AI narrative report via Perplexity API + │ ├── chantest_hill/ # IC50 / Hill coefficient input files per drug + │ ├── chantest_herg/ # hERG-specific parameter files per drug + │ ├── population/ # Conductance variability (virtual population) files + │ ├── initial_states/ # Pre-computed steady-state initial conditions + │ ├── kitox_error_data/ # Reference IC50 data + │ └── requests/ # Perplexity API prompt templates + └── simulation_cpu_postprocessing/ # Working directory for postprocessing-only runs + └── (same layout as simulation_cpu/) +``` + +--- + +## Configuration (`param.txt`) + +Edit `bin/simulation_cpu/param.txt` before running. Key parameters: + +| Parameter | Example | Description | +|---|---|---| +| `user_name` | `marcell` | Label used in output filenames | +| `number_pacing` | `1000` | Total number of paces to simulate | +| `number_pacing_write` | `10` | Number of final paces written to disk | +| `cycle_length` | `2000` | Pacing cycle length (ms) | +| `cell_model` | `CiPAORdv1.0_endo` | Cell model + cell type (`endo`, `epi`, `myo`) | +| `stimulus_duration` | `0.5` | Stimulus current duration (ms) | +| `stimulus_amplitude_scale` | `1.0` | Scaling factor for stimulus amplitude | +| `solver_type` | `CVode` | ODE solver: `CVode` or `Euler` | +| `time_step_min` | `0.5` | Minimum adaptive time step for CVode (ms) | +| `time_step_max` | `2.0` | Maximum adaptive time step for CVode (ms) | +| `writing_step` | `1.0` | Output writing interval (ms) | +| `drug_name` | `quinidine` | Drug name (used in filenames) | +| `drug_concentrations` | `3237,6474,9711,12948` | Comma-separated concentrations to simulate (µM) | +| `hill_file` | `./chantest_hill/quinidine/IC50_samples30.csv` | IC50 & Hill coefficient samples | +| `herg_file` | `./chantest_herg/quinidine/boot_pars10.csv` | hERG bootstrapped parameters | +| `is_cvar` | `0` | Enable conductance variability (`1` = on) | +| `cvar_file` | `./population/indi_1sample.csv` | Conductance variability population file | +| `number_of_cpu` | `10` | Number of MPI processes for `mpiexec` | + +--- + +## Input Files + +### IC50 / Hill coefficients (`hill_file`) + +CSV file where each row is one virtual sample. Columns define per-channel IC50 and Hill coefficient pairs: + +``` +ICaL_IC50, ICaL_h, IK1_IC50, IK1_h, IKs_IC50, IKs_h, +INa_IC50, INa_h, INaL_IC50, INaL_h, Ito_IC50, Ito_h, +hERG_IC50, hERG_h +``` + +### hERG parameters (`herg_file`) + +CSV file with bootstrapped hERG kinetic parameters for the CiPAORdv1.0 model. + +### Conductance variability (`cvar_file`) + +CSV file providing per-sample scaling factors for ionic conductances (e.g., `GNa_fast`, `GKr`, `PCa`, …). Enables virtual population simulations. + +--- + +## Running a Simulation Locally on Your Machine + +This is a complete end-to-end walkthrough for running a simulation on your local machine. + +### Step 1 — Build the binaries + +From the repository root, build all variants: + +```bash +bash compile_binaries.sh +``` + +Or build only the variant you need (example: CiPAORdv1.0): + +```bash +make all EXTRA_CXXFLAGS+="-std=c++11 -DCIPAORDV1" +``` + +Binaries are placed in `bin/`. + +### Step 2 — Install Python dependencies + +```bash +pip install -r bin/scripts/requirements.txt +``` + +### Step 3 — Configure the simulation + +```bash +cd bin/simulation_cpu +``` + +Open `param.txt` and set the parameters for your run. The most important ones: + +```ini +user_name = your_name +cell_model = CiPAORdv1.0_endo # choose model + cell type (endo/epi/myo) +drug_name = quinidine +drug_concentrations = 3237,6474,9711,12948 +hill_file = ./chantest_hill/quinidine/IC50_samples30.csv +herg_file = ./chantest_herg/quinidine/boot_pars10.csv +number_pacing = 1000 +number_of_cpu = 4 # set to the number of cores you want to use +``` + +> **Tip:** Set `number_of_cpu` to the number of physical/logical cores available on your machine. Run `nproc` (Linux) or `sysctl -n hw.logicalcpu` (macOS) to check. + +### Step 4 — Launch the simulation + +```bash +bash exec_bash.sh +``` + +`exec_bash.sh` will: +1. Read `param.txt` to determine the cell model, drug, concentrations, and CPU count. +2. Verify that `number_of_cpu` does not exceed the machine's available cores. +3. Create result sub-directories under `./results//`. +4. Run `mpiexec -np ../drugsim_ -input_deck param.txt`, logging all output to `./results/logfile`. +5. Zip time-series, feature, and initial-values CSVs per concentration. +6. Automatically call `generate_report.sh` to produce plots and summary statistics. + +Progress is printed to the terminal; detailed logs are in `./results/logfile`. + +### Step 5 — Monitor and stop + +Tail the log in a separate terminal: + +```bash +tail -f ./results/logfile +``` + +To kill a running simulation cleanly: + +```bash +bash kill_bash.sh +``` + +### Step 6 — View results + +After completion, results are in `./results//`: + +``` +results/ +├── 0.00/ # control (no drug) +├── 3237.00/ # Cmax1 +│ ├── quinidine_3237.00_time_series_smp0.csv +│ ├── quinidine_3237.00_features_core0.csv +│ └── … +├── quinidine-median-ci.csv # bootstrap median + 95% CI across concentrations +└── logfile +``` + +Open the generated plots (`.png` / `.pdf`) in the concentration directories, or run `generate_report.sh` again separately if you want to regenerate figures without re-running the full simulation. + +--- + +## Running in Dockerized Linux (Recommended for macOS) + +This repository includes a Linux container workflow that preserves the expected original layout where `libCML` is a sibling directory. + +The Docker service is pinned to `linux/amd64` so it matches the bundled SUNDIALS archives under `../libCML/libs/sundials-5.7.0/lib64`. + +Expected host layout: + +```text +/Users/iganarendra/ + DrugSimulation/ + libCML/ +``` + +### 1. Build the container image + +From the repository root: + +```bash +docker compose up -d --build +``` + +### 2. Build and run simulation in one command (inside container) + +```bash +docker compose exec -T drugsim-linux env VARIANT_FLAG=-DCIPAORDV1 NP=10 RUN_SIM=1 \ + bash docker/linux/build_and_run.sh +``` + +What this does: +1. Builds `libCML/build/libcml.a` inside Linux. +2. Builds DrugSimulation binary using bundled Linux SUNDIALS libs from `../libCML/libs/sundials-5.7.0/lib64`. +3. Runs `mpiexec` on `bin/drugsim_CiPAORdv1.0` with `param.txt` from `bin/simulation_cpu/`. +4. Writes runtime logs to `bin/simulation_cpu/results/logfile.docker`. + +### 3. Build only (skip simulation) + +```bash +docker compose exec -T drugsim-linux env RUN_SIM=0 VARIANT_FLAG=-DCIPAORDV1 \ + bash docker/linux/build_and_run.sh +``` + +### 3a. Run a short pacing test (smoke test) without editing `param.txt` + +```bash +docker compose exec -T drugsim-linux env VARIANT_FLAG=-DCIPAORDV1 NP=1 PACING_OVERRIDE=5 PACING_WRITE_OVERRIDE=5 TIME_STEP_MIN_OVERRIDE=0.001 SOLVER_OVERRIDE=Euler \ + RUN_SIM=1 bash docker/linux/build_and_run.sh +``` + +This creates a temporary parameter file inside the container run, leaves your checked-in `bin/simulation_cpu/param.txt` unchanged, and runs the full simulation flow with only 5 paces using the Euler solver. + +### 4. Supported variant flags + +- `-DCIPAORDV1` +- `-DTOR_ORD` +- `-DTOR_ORD_DYNCL` +- `-DGRANDI` +- `-DORD_STATIC_LAND` +- `-DCIPAORDV1_LAND` +- `-DTOR_ORD_LAND` +- empty string (`""`) for ORd-static default + +### 5. Rollback / cleanup + +Container-only cleanup (safe): + +```bash +docker compose down --remove-orphans +docker image prune -f +``` + +Reset build artifacts in both repos: + +```bash +cd /Users/iganarendra/DrugSimulation && make clean || true +cd /Users/iganarendra/libCML && make clean || true +``` + +Revert dockerization file changes in this repo: + +```bash +cd /Users/iganarendra/DrugSimulation +git restore --source=HEAD --staged --worktree -- Makefile README.md docker-compose.yml docker/ +``` + +--- + +## NEW: Run a Hardware Performance Benchmark + +Use `docker/linux/benchmark.sh` to run a reproducible timing sweep inside the Linux container. + +The benchmark auto-discovers drugs from `bin/simulation_cpu/chantest_hill/`, then sweeps: + +- drug +- solver (`CVode` and `Euler` by default) +- MPI process count (`NP_LIST`) +- repeat index (`REPEATS`) + +Each run calls `docker/linux/run_only.sh`, times the full simulation wall-clock duration, and appends one row to a benchmark CSV. + +### Default benchmark profile + +To keep the benchmark fast and comparable, the script uses short simulation overrides unless you change them: + +- `PACING=5` +- `PACING_WRITE=5` +- `TIME_STEP_MIN=0.001` +- `SOLVERS="CVode,Euler"` +- `NP_LIST="1"` +- `REPEATS=1` +- `VARIANT_FLAG=-DCIPAORDV1` + +It also builds the selected variant once before the sweep unless `SKIP_BUILD=1` is set. + +### Basic usage + +From the repository root, ready to run, just copy and paste to your terminal: + +```bash +bash docker/linux/benchmark.sh +``` + +**The script will start `drugsim-linux` automatically if needed!** + +### Common examples + +Single-process baseline: + +```bash +bash docker/linux/benchmark.sh +``` + +Compare 1, 2, 4, and 8 MPI ranks: + +```bash +NP_LIST="1,2,4,8" REPEATS=3 bash docker/linux/benchmark.sh +``` + +Benchmark only selected drugs: + +```bash +DRUGS="quinidine,bepridil" NP_LIST="1,4" REPEATS=2 bash docker/linux/benchmark.sh +``` + +Benchmark only one solver: + +```bash +SOLVERS="Euler" NP_LIST="1,4" bash docker/linux/benchmark.sh +``` + +Reuse an already-built binary: + +```bash +SKIP_BUILD=1 NP_LIST="1,4" bash docker/linux/benchmark.sh +``` + +Preview the sweep without running it: + +```bash +DRY_RUN=1 NP_LIST="1,4" bash docker/linux/benchmark.sh +``` + +### Useful environment variables + +| Variable | Default | Meaning | +|---|---|---| +| `DRUGS` | auto-discovered | Comma- or space-separated drug list | +| `SOLVERS` | `CVode,Euler` | Solvers to benchmark | +| `NP_LIST` | `1` | MPI process counts to test | +| `REPEATS` | `1` | Repeats per combination | +| `VARIANT_FLAG` | `-DCIPAORDV1` | Cell-model variant built once for the sweep | +| `PACING` | `5` | Override for `number_pacing` | +| `PACING_WRITE` | `5` | Override for `number_pacing_write` | +| `TIME_STEP_MIN` | `0.001` | Override for `time_step_min` | +| `CONC` | unset | Override `drug_concentrations` for every run | +| `HILL_SAMPLES` | `30` | Uses `chantest_hill//IC50_samples.csv` | +| `HERG_SAMPLES` | `10` | Uses `chantest_herg//boot_pars.csv` | +| `SKIP_BUILD` | `0` | Set to `1` to skip the one-time build step | +| `FAIL_FAST` | `0` | Set to `1` to stop on the first failed run | +| `OUT_DIR` | `benchmark_results/` | Output directory for this sweep | +| `DRY_RUN` | `0` | Set to `1` to print commands only | + +Run `bash docker/linux/benchmark.sh --help` to print the full built-in usage text. + +### Output files + +Each sweep writes a new directory under `benchmark_results//` containing: + +- `results.csv` — one row per run with elapsed time, exit code, selected parameters, and hardware metadata +- `summary.txt` — mean and standard deviation of elapsed time grouped by `drug`, `solver`, and `np` +- `environment.txt` — host and container details captured once per sweep +- `logs/__np_rep.log` — stdout/stderr for each individual run + +The benchmark stamps every CSV row with host OS, CPU model, visible RAM, container architecture, and whether Docker is running under CPU emulation. This is especially important on Apple Silicon hosts because the Docker service is intentionally pinned to `linux/amd64`; emulated timings should not be compared against native Linux runs. + +--- + +## Running a Simulation on an HPC Cluster (PBS/Torque) + +1. Navigate to the working directory: + ```bash + cd bin/simulation_cpu + ``` + +2. Edit `param.txt` (same as above). + +3. Submit to the queue: + ```bash + qsub exec_pbs.sh + ``` + +The PBS script requests 1 node with 10 processors (`ppn=10`) by default. Adjust `#PBS -l nodes=1:ppn=N` and `number_of_cpu` in `param.txt` to match your allocation. + +To stop a running local simulation: +```bash +bash kill_bash.sh +``` + +--- + +## Post-processing & Reporting + +From the `simulation_cpu` working directory, after a simulation completes: + +```bash +bash generate_report.sh +``` + +This script performs four steps: + +1. **Time-series plots** — calls `plot_time_series.py` to generate plots of Vm, dVm/dt, Cai, and key ionic currents for each concentration. +2. **Feature plots** — calls `plot_features.py` to produce dose–response plots for all 16 CiPA biomarkers. +3. **Feature aggregation** — calls `combine_and_average_features.py` to merge per-core CSV fragments and compute per-concentration sample averages. +4. **Median & confidence intervals** — calls `compute_mean_ci.py` to compute bootstrap median and 95% CI across concentrations, writing `-median-ci.csv`. + +--- + +## Output Files + +Results are written under `./results//`: + +| File pattern | Contents | +|---|---| +| `__time_series_smp.csv` | Full time-series (Vm, Cai, currents) for sample N | +| `__features_core.csv` | CiPA feature values computed by MPI core K | +| `__initial_values_smp.csv` | Steady-state initial conditions for sample N | +| `--features-all.csv` | Combined features for all samples at a concentration | +| `sample-averages-across-concentrations.csv` | Mean feature value per sample across concentrations | +| `-median-ci.csv` | Bootstrap median + 95% CI for each feature | +| `-depol-failure-count.csv` | Count of depolarisation failures per concentration | + +--- + +## Python Scripts + +All Python utilities live in [`bin/scripts/`](bin/scripts/). + +| Script | Purpose | +|---|---| +| `plot_time_series.py` | Plot Vm, dVm/dt, Cai, INaL, ICaL, IKr, etc. vs. time | +| `plot_features.py` | Plot 16 CiPA features as dose–response curves | +| `combine_and_average_features.py` | Merge per-core feature CSVs; compute sample averages | +| `compute_mean_ci.py` | Bootstrap median and 95% CI from averaged samples | +| `convert_params.sh` | Convert `param.txt` keys to camelCase for API use | +| `create_concs_directories.sh` | Create output directory tree per concentration | +| `create_escaped_b64_file.sh` | Escape special characters and Base64-encode a file | +| `zip_files.sh` / `unzip_files.sh` | Zip/unzip simulation output files | + +--- + +## AI Report Generation + +DrugSimulation includes an optional pipeline to automatically generate a scientific manuscript (LaTeX/PDF) using the [Perplexity](https://www.perplexity.ai/) Sonar Pro API. + +After running `generate_report.sh`, execute: + +```bash +bash exec_perplexity_report_generator.sh +``` + +This pipeline: +1. Converts and Base64-encodes the parameter file, median-CI CSV, time-series CSVs, and a LaTeX template. +2. Sends structured prompts to the Perplexity API for each manuscript section (Methods, Results parts 1–7, Discussion) in both English and Korean. +3. Assembles the API responses into a complete LaTeX document. + +Prompt templates are stored in [`bin/simulation_cpu/requests/`](bin/simulation_cpu/requests/). Responses are saved to `bin/simulation_cpu_postprocessing/responses/`. + +> **Note:** A Perplexity API key must be configured in the environment before running the report generator. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1601e96 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,13 @@ +services: + drugsim-linux: + platform: linux/amd64 + build: + context: . + dockerfile: docker/linux/Dockerfile + container_name: drugsim-linux + working_dir: /workspace/DrugSimulation + stdin_open: true + tty: true + volumes: + - .:/workspace/DrugSimulation + - ../libCML:/workspace/libCML diff --git a/docker/linux/Dockerfile b/docker/linux/Dockerfile new file mode 100644 index 0000000..faec0a1 --- /dev/null +++ b/docker/linux/Dockerfile @@ -0,0 +1,21 @@ +FROM ubuntu:22.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + bash \ + build-essential \ + ca-certificates \ + cmake \ + git \ + libcurl4-openssl-dev \ + libjson-c-dev \ + libopenmpi-dev \ + make \ + openmpi-bin \ + pkg-config \ + && rm -rf /var/lib/apt/lists/* + +WORKDIR /workspace/DrugSimulation + +CMD ["/bin/bash"] diff --git a/docker/linux/benchmark.sh b/docker/linux/benchmark.sh new file mode 100755 index 0000000..795a911 --- /dev/null +++ b/docker/linux/benchmark.sh @@ -0,0 +1,425 @@ +#!/usr/bin/env bash +# Hardware-performance benchmark harness for DrugSimulation. +# +# Sweeps the drugsim container across drugs x solvers x MPI process counts, +# holding every other hyperparameter fixed, and records wall-clock elapsed +# time for each run to a CSV so runs are comparable across hardware. Every +# row is tagged with host + container hardware info (CPU, RAM, arch, whether +# Docker is emulating a foreign architecture) so CSVs from different machines +# can be compared or concatenated without losing track of what ran where. +# +# Run from the repository root (or anywhere; it cd's to the repo root itself): +# bash docker/linux/benchmark.sh +# +# Everything is configured via environment variables (same style as +# build_and_run.sh / run_only.sh). See `bash docker/linux/benchmark.sh -h`. +set -u -o pipefail + +usage() { + cat <<'EOF' +Usage: [ENV=value ...] bash docker/linux/benchmark.sh + +Sweeps DRUGS x SOLVERS x NP_LIST, running each combination REPEATS times via +`docker compose exec ... run_only.sh`, and appends one row per run to a CSV +under OUT_DIR. All other simulation hyperparameters are held fixed across the +whole sweep so timings are comparable. + +Environment variables (defaults shown): + DRUGS (auto-discovered from chantest_hill/*) comma/space separated + SOLVERS CVode,Euler comma/space separated + NP_LIST 1 comma/space separated MPI process counts + REPEATS 1 repeats per combination + VARIANT_FLAG -DCIPAORDV1 cell-model build variant (fixed for the whole sweep) + PACING 5 number_pacing override + PACING_WRITE 5 number_pacing_write override + TIME_STEP_MIN 0.001 time_step_min override + CONC_LEVELS 1x,4x which columns of CMAX_TABLE to use as drug_concentrations (or "all" for 1x,2x,3x,4x) + CMAX_TABLE /chantest_hill/CiPA_drug_concentrations.csv per-drug Cmax table (drug,therapeutic_Cmax_nM,conc_1x..conc_4x,CiPA_risk_category) + CONC (unset -> look up each drug's own concentrations in CMAX_TABLE; set to force the SAME concentrations for every drug instead) + HILL_SAMPLES 30 -> chantest_hill//IC50_samples.csv + HERG_SAMPLES 10 -> chantest_herg//boot_pars.csv + SAMPLES 1 rows of hill/herg to actually use per run (truncated from the + HILL_SAMPLES/HERG_SAMPLES files above); "all" disables truncation. + MPI distributes samples round-robin across NP ranks, so with the + default SAMPLES=1 only rank 0 does simulation work regardless of + NP -- that isolates a single-core, hardware-only timing free of + virtual-population noise. To measure multi-core scaling instead, + set SAMPLES to match each NP value (one sample per core). + SIM_DIR bin/simulation_cpu + SKIP_BUILD 0 set 1 to reuse an already-built binary + FAIL_FAST 0 set 1 to stop the sweep on first failure + OUT_DIR benchmark_results/ where results.csv/summary.txt/logs/ are written + DRY_RUN 0 set 1 to print the commands without running them + +Hardware/environment info (host OS/arch/CPU/RAM, container OS/arch/CPU/RAM, +whether Docker is running inside a VM, whether the container is CPU-emulated) +is auto-detected once per sweep, printed up front, written to +OUT_DIR/environment.txt, and appended as extra columns on every results.csv +row -- no configuration needed. + +Example - sweep all drugs across both solvers at 1 and 4 MPI ranks, 3 repeats: + NP_LIST="1,4" REPEATS=3 bash docker/linux/benchmark.sh + +Example - just two drugs, Euler only, for a quick sanity check: + DRUGS="quinidine,bepridil" SOLVERS="Euler" bash docker/linux/benchmark.sh +EOF +} + +if [[ "${1:-}" == "-h" || "${1:-}" == "--help" ]]; then + usage + exit 0 +fi + +REPO_ROOT="$(git -C "$(dirname "${BASH_SOURCE[0]}")" rev-parse --show-toplevel)" +cd "$REPO_ROOT" + +SIM_DIR="${SIM_DIR:-bin/simulation_cpu}" +VARIANT_FLAG="${VARIANT_FLAG:--DCIPAORDV1}" +SOLVERS="${SOLVERS:-CVode,Euler}" +NP_LIST="${NP_LIST:-1}" +REPEATS="${REPEATS:-1}" +PACING="${PACING:-5}" +PACING_WRITE="${PACING_WRITE:-5}" +TIME_STEP_MIN="${TIME_STEP_MIN:-0.001}" +CONC_LEVELS="${CONC_LEVELS:-1x,4x}" +CMAX_TABLE="${CMAX_TABLE:-$SIM_DIR/chantest_hill/CiPA_drug_concentrations.csv}" +CONC="${CONC:-}" +HILL_SAMPLES="${HILL_SAMPLES:-30}" +HERG_SAMPLES="${HERG_SAMPLES:-10}" +SAMPLES="${SAMPLES:-1}" +SKIP_BUILD="${SKIP_BUILD:-0}" +FAIL_FAST="${FAIL_FAST:-0}" +DRY_RUN="${DRY_RUN:-0}" +OUT_DIR="${OUT_DIR:-benchmark_results/$(date +%Y%m%d_%H%M%S)}" + +# --- Hardware / environment detection -------------------------------------- +# Captured once per sweep (hardware doesn't change mid-run) and stamped onto +# every CSV row, so results from different machines stay self-describing. +norm_arch() { + case "$1" in + arm64 | aarch64) echo "arm64" ;; + x86_64 | amd64) echo "x86_64" ;; + *) echo "$1" ;; + esac +} + +gather_host_info() { + HOST_OS="$(uname -s)" + HOST_ARCH="$(uname -m)" + case "$HOST_OS" in + Darwin) + HOST_CPU_MODEL="$(sysctl -n machdep.cpu.brand_string 2>/dev/null)" + HOST_CPU_CORES="$(sysctl -n hw.logicalcpu 2>/dev/null)" + local ram_bytes + ram_bytes="$(sysctl -n hw.memsize 2>/dev/null)" + HOST_RAM_GB="$(awk -v b="${ram_bytes:-0}" 'BEGIN { printf "%.1f", b / 1024 / 1024 / 1024 }')" + ;; + Linux) + HOST_CPU_MODEL="$(awk -F: '/model name/ { gsub(/^ /, "", $2); print $2; exit }' /proc/cpuinfo 2>/dev/null)" + HOST_CPU_CORES="$(nproc 2>/dev/null || getconf _NPROCESSORS_ONLN 2>/dev/null)" + local ram_kb + ram_kb="$(awk '/MemTotal/ { print $2 }' /proc/meminfo 2>/dev/null)" + HOST_RAM_GB="$(awk -v kb="${ram_kb:-0}" 'BEGIN { printf "%.1f", kb / 1024 / 1024 }')" + ;; + *) + HOST_CPU_MODEL="unknown" + HOST_CPU_CORES="unknown" + HOST_RAM_GB="unknown" + ;; + esac + HOST_CPU_MODEL="${HOST_CPU_MODEL:-unknown}" + HOST_CPU_MODEL="${HOST_CPU_MODEL//,/}" # keep CSV columns intact + HOST_CPU_CORES="${HOST_CPU_CORES:-unknown}" + HOST_RAM_GB="${HOST_RAM_GB:-unknown}" +} + +# Requires the drugsim-linux container to already be running. +gather_container_info() { + DOCKER_OS="$(docker info --format '{{.OperatingSystem}}' 2>/dev/null)" + if [[ "$DOCKER_OS" == *"Docker Desktop"* ]]; then + DOCKER_RUNTIME="docker-desktop-vm" + else + DOCKER_RUNTIME="linux-native" + fi + + CONTAINER_ARCH="$(docker compose exec -T drugsim-linux uname -m 2>/dev/null | tr -d '\r\n')" + CONTAINER_CPU_CORES="$(docker compose exec -T drugsim-linux nproc 2>/dev/null | tr -d '\r\n')" + local mem_kb + mem_kb="$(docker compose exec -T drugsim-linux awk '/MemTotal/ { print $2 }' /proc/meminfo 2>/dev/null | tr -d '\r\n')" + CONTAINER_RAM_GB="$(awk -v kb="${mem_kb:-0}" 'BEGIN { printf "%.1f", kb / 1024 / 1024 }')" + + if [[ -n "$CONTAINER_ARCH" && "$(norm_arch "$HOST_ARCH")" != "$(norm_arch "$CONTAINER_ARCH")" ]]; then + EMULATED="yes" + else + EMULATED="no" + fi + + CONTAINER_ARCH="${CONTAINER_ARCH:-unknown}" + CONTAINER_CPU_CORES="${CONTAINER_CPU_CORES:-unknown}" + CONTAINER_RAM_GB="${CONTAINER_RAM_GB:-unknown}" +} + +# Placeholders so results.csv columns line up even in DRY_RUN mode, where the +# container is never contacted. +DOCKER_RUNTIME="n/a (dry run)" +CONTAINER_ARCH="n/a" +CONTAINER_CPU_CORES="n/a" +CONTAINER_RAM_GB="n/a" +EMULATED="n/a" + +gather_host_info + +# --- Per-drug concentration lookup ------------------------------------------ +# Each drug has its own Cmax, so reusing one drug's concentrations for every +# drug in the sweep understates/overstates the real workload. CMAX_TABLE maps +# drug -> conc_1x..conc_4x (uM); CONC_LEVELS picks which of those columns to +# combine into drug_concentrations for each run. +resolve_conc_levels() { + if [[ "$CONC_LEVELS" == "all" ]]; then + echo "1x 2x 3x 4x" + else + split_list "$CONC_LEVELS" + fi +} + +lookup_drug_conc() { + local drug="$1" + local levels + levels="$(resolve_conc_levels)" + [[ -f "$CMAX_TABLE" ]] || return 1 + awk -F',' -v drug="$drug" -v levels="$levels" ' + NR == 1 { + for (i = 1; i <= NF; i++) col[$i] = i + next + } + $1 == drug { + n = split(levels, lv, " ") + out = "" + ok = 1 + for (j = 1; j <= n; j++) { + idx = col["conc_" lv[j]] + val = (idx != "") ? $(idx) : "" + if (val == "") { ok = 0; break } + out = (out == "") ? val : out "," val + } + if (ok) { print out; found = 1 } + } + END { exit (found ? 0 : 1) } + ' "$CMAX_TABLE" +} + +if [[ -z "${DRUGS:-}" ]]; then + DRUGS="$(find "$SIM_DIR/chantest_hill" -mindepth 1 -maxdepth 1 -type d -exec basename {} \; | sort | tr '\n' ',')" +fi + +# Accept comma- and/or space-separated lists. +split_list() { echo "$1" | tr ',' ' '; } +read -r -a DRUG_ARR <<< "$(split_list "$DRUGS")" +read -r -a SOLVER_ARR <<< "$(split_list "$SOLVERS")" +read -r -a NP_ARR <<< "$(split_list "$NP_LIST")" + +if [[ ${#DRUG_ARR[@]} -eq 0 || ${#SOLVER_ARR[@]} -eq 0 || ${#NP_ARR[@]} -eq 0 ]]; then + echo "DRUGS, SOLVERS and NP_LIST must each resolve to at least one value." >&2 + exit 2 +fi + +mkdir -p "$OUT_DIR/logs" +CSV_FILE="$OUT_DIR/results.csv" +echo "timestamp_utc,variant_flag,drug,solver,np,repeat,pacing,pacing_write,time_step_min,concentrations,hill_file,herg_file,samples,elapsed_seconds,exit_code,log_file,host_os,host_arch,host_cpu_model,host_cpu_cores,host_ram_gb,docker_runtime,container_arch,container_cpu_cores,container_ram_gb,emulated" > "$CSV_FILE" + +# Truncated hill/herg files live here when SAMPLES limits the population size +# (see the per-drug loop below); gitignored, regenerated fresh each run. +SAMPLES_TMP_DIR="$SIM_DIR/.bench_samples" +mkdir -p "$SAMPLES_TMP_DIR" + +echo "Output directory: $OUT_DIR" +echo "Drugs: ${DRUG_ARR[*]}" +echo "Solvers: ${SOLVER_ARR[*]}" +echo "NP: ${NP_ARR[*]}" +echo "Repeats: $REPEATS" +if [[ "$SAMPLES" == "all" ]]; then + echo "Samples: all (untruncated, HILL_SAMPLES=${HILL_SAMPLES}/HERG_SAMPLES=${HERG_SAMPLES})" +else + echo "Samples: ${SAMPLES} row(s) per drug (truncated from HILL_SAMPLES=${HILL_SAMPLES}/HERG_SAMPLES=${HERG_SAMPLES})" +fi +if [[ -n "$CONC" ]]; then + echo "Conc: fixed for all drugs: $CONC" +else + echo "Conc: per-drug, levels=${CONC_LEVELS} from ${CMAX_TABLE}" +fi +echo + +if [[ "$DRY_RUN" != "1" ]]; then + if ! docker compose ps --status running --services 2>/dev/null | grep -qx "drugsim-linux"; then + echo "drugsim-linux container is not running, starting it (docker compose up -d)..." + docker compose up -d + fi + + gather_container_info + + if [[ "$SKIP_BUILD" != "1" ]]; then + echo "Building binary for VARIANT_FLAG=$VARIANT_FLAG once before the sweep (RUN_SIM=0)..." + docker compose exec -T drugsim-linux env \ + "VARIANT_FLAG=${VARIANT_FLAG}" "RUN_SIM=0" \ + bash docker/linux/build_and_run.sh + build_status=$? + if [[ $build_status -ne 0 ]]; then + echo "Build failed (exit $build_status); aborting sweep." >&2 + exit $build_status + fi + echo + fi +fi + +echo "Hardware / environment:" +echo " Host: ${HOST_OS} ${HOST_ARCH} | ${HOST_CPU_MODEL} | ${HOST_CPU_CORES} logical cores | ${HOST_RAM_GB} GB RAM" +echo " Container: ${DOCKER_RUNTIME} | arch=${CONTAINER_ARCH} (emulated=${EMULATED}) | ${CONTAINER_CPU_CORES} cores visible | ${CONTAINER_RAM_GB} GB RAM visible" +if [[ "$EMULATED" == "yes" ]]; then + echo " WARNING: container arch differs from host arch -- this run is CPU-emulated (e.g. Rosetta/QEMU)." + echo " Timings will NOT reflect native hardware performance; do not compare them against native runs." +fi +echo + +{ + echo "host_os=${HOST_OS}" + echo "host_arch=${HOST_ARCH}" + echo "host_cpu_model=${HOST_CPU_MODEL}" + echo "host_cpu_cores=${HOST_CPU_CORES}" + echo "host_ram_gb=${HOST_RAM_GB}" + echo "docker_runtime=${DOCKER_RUNTIME}" + echo "container_arch=${CONTAINER_ARCH}" + echo "container_cpu_cores=${CONTAINER_CPU_CORES}" + echo "container_ram_gb=${CONTAINER_RAM_GB}" + echo "emulated=${EMULATED}" +} > "$OUT_DIR/environment.txt" + +total=$(( ${#DRUG_ARR[@]} * ${#SOLVER_ARR[@]} * ${#NP_ARR[@]} * REPEATS )) +count=0 +failures=0 + +for drug in "${DRUG_ARR[@]}"; do + hill_file="./chantest_hill/${drug}/IC50_samples${HILL_SAMPLES}.csv" + herg_file="./chantest_herg/${drug}/boot_pars${HERG_SAMPLES}.csv" + samples_used="all" + + if [[ "$SAMPLES" != "all" && "$SAMPLES" != "0" ]]; then + src_hill="$SIM_DIR/chantest_hill/${drug}/IC50_samples${HILL_SAMPLES}.csv" + src_herg="$SIM_DIR/chantest_herg/${drug}/boot_pars${HERG_SAMPLES}.csv" + avail_hill=$(($(wc -l < "$src_hill") - 1)) + avail_herg=$(($(wc -l < "$src_herg") - 1)) + samples_used=$((avail_hill < avail_herg ? avail_hill : avail_herg)) + if ((SAMPLES < samples_used)); then + samples_used=$SAMPLES + elif ((SAMPLES > samples_used)); then + echo "WARNING: SAMPLES=${SAMPLES} exceeds available rows for ${drug} (hill=${avail_hill}, herg=${avail_herg}); using ${samples_used} instead. Bump HILL_SAMPLES/HERG_SAMPLES to unlock more." >&2 + fi + + hill_file="./.bench_samples/${drug}_hill_n${samples_used}.csv" + herg_file="./.bench_samples/${drug}_herg_n${samples_used}.csv" + head -n "$((samples_used + 1))" "$src_hill" > "$SAMPLES_TMP_DIR/${drug}_hill_n${samples_used}.csv" + head -n "$((samples_used + 1))" "$src_herg" > "$SAMPLES_TMP_DIR/${drug}_herg_n${samples_used}.csv" + fi + + if [[ -n "$CONC" ]]; then + conc_override="$CONC" + elif conc_override="$(lookup_drug_conc "$drug")"; then + : + else + conc_override="" + echo "WARNING: no concentrations for drug=${drug} (levels=${CONC_LEVELS}) in ${CMAX_TABLE}; falling back to param.txt's own concentrations for this drug." >&2 + fi + + for solver in "${SOLVER_ARR[@]}"; do + for np in "${NP_ARR[@]}"; do + for ((rep = 1; rep <= REPEATS; rep++)); do + count=$((count + 1)) + label="drug=${drug} solver=${solver} np=${np} rep=${rep}" + printf '[%d/%d] %s\n' "$count" "$total" "$label" + + run_id="${drug}_${solver}_np${np}_rep${rep}" + log_file="$OUT_DIR/logs/${run_id}.log" + + cmd=(docker compose exec -T drugsim-linux env + "VARIANT_FLAG=${VARIANT_FLAG}" + "NP=${np}" + "PACING_OVERRIDE=${PACING}" + "PACING_WRITE_OVERRIDE=${PACING_WRITE}" + "TIME_STEP_MIN_OVERRIDE=${TIME_STEP_MIN}" + "SOLVER_OVERRIDE=${solver}" + "DRUG_NAME_OVERRIDE=${drug}" + "HILL_FILE_OVERRIDE=${hill_file}" + "HERG_FILE_OVERRIDE=${herg_file}") + [[ -n "$conc_override" ]] && cmd+=("CONC_OVERRIDE=${conc_override}") + cmd+=(bash docker/linux/run_only.sh) + + if [[ "$DRY_RUN" == "1" ]]; then + printf ' DRY RUN: %s\n' "${cmd[*]}" + continue + fi + + timestamp="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + TIMEFORMAT='%R' + time_file="$(mktemp)" + { time "${cmd[@]}" >"$log_file" 2>&1; } 2>"$time_file" + exit_code=$? + elapsed="$(cat "$time_file")" + rm -f "$time_file" + + conc_used="${conc_override:-}" + conc_used_csv="${conc_used//,/;}" # concentrations are comma-joined; swap to ';' so they don't split into extra CSV fields + echo "${timestamp},${VARIANT_FLAG},${drug},${solver},${np},${rep},${PACING},${PACING_WRITE},${TIME_STEP_MIN},${conc_used_csv},${hill_file},${herg_file},${samples_used},${elapsed},${exit_code},${log_file},${HOST_OS},${HOST_ARCH},${HOST_CPU_MODEL},${HOST_CPU_CORES},${HOST_RAM_GB},${DOCKER_RUNTIME},${CONTAINER_ARCH},${CONTAINER_CPU_CORES},${CONTAINER_RAM_GB},${EMULATED}" >> "$CSV_FILE" + + if [[ $exit_code -ne 0 ]]; then + failures=$((failures + 1)) + echo " FAILED (exit $exit_code) after ${elapsed}s -- see $log_file" + if [[ "$FAIL_FAST" == "1" ]]; then + echo "FAIL_FAST=1, stopping sweep." >&2 + exit $exit_code + fi + else + echo " OK in ${elapsed}s" + fi + done + done + done +done + +if [[ "$DRY_RUN" == "1" ]]; then + echo "Dry run complete: $total combinations would run." + exit 0 +fi + +echo +echo "Sweep complete: $((count - failures))/$total runs succeeded ($failures failed)." +echo "Raw results: $CSV_FILE" +echo "Environment: $OUT_DIR/environment.txt" + +summary_file="$OUT_DIR/summary.txt" +awk -F',' ' + NR == 1 { next } + { + key = $3 "|" $4 "|" $5 + if ($15 == "0") { + n[key]++ + sum[key] += $14 + sumsq[key] += $14 * $14 + } else { + fail[key]++ + } + seen[key] = 1 + } + END { + printf "%-22s %-8s %-4s %-6s %-12s %-12s %-6s\n", "drug", "solver", "np", "n_ok", "mean_s", "stdev_s", "fail" + for (k in seen) { + split(k, a, "|") + cnt = n[k] + 0 + mean = (cnt > 0) ? sum[k] / cnt : 0 + var = (cnt > 1) ? (sumsq[k] - cnt * mean * mean) / (cnt - 1) : 0 + sd = (var > 0) ? sqrt(var) : 0 + printf "%-22s %-8s %-4s %-6d %-12.3f %-12.3f %-6d\n", a[1], a[2], a[3], cnt, mean, sd, (fail[k] + 0) + } + } +' "$CSV_FILE" | sort | tee "$summary_file" + +echo +echo "Summary: $summary_file" diff --git a/docker/linux/build_and_run.sh b/docker/linux/build_and_run.sh new file mode 100755 index 0000000..cce5fd1 --- /dev/null +++ b/docker/linux/build_and_run.sh @@ -0,0 +1,140 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Tunables (can be overridden by environment variables) +VARIANT_FLAG="${VARIANT_FLAG:--DCIPAORDV1}" +SIM_DIR="${SIM_DIR:-bin/simulation_cpu}" +NP="${NP:-10}" +RUN_SIM="${RUN_SIM:-1}" +PARAM_FILE="${PARAM_FILE:-param.txt}" +PACING_OVERRIDE="${PACING_OVERRIDE:-}" +PACING_WRITE_OVERRIDE="${PACING_WRITE_OVERRIDE:-}" +SOLVER_OVERRIDE="${SOLVER_OVERRIDE:-}" +TIME_STEP_MIN_OVERRIDE="${TIME_STEP_MIN_OVERRIDE:-}" +HILL_FILE_OVERRIDE="${HILL_FILE_OVERRIDE:-}" +HERG_FILE_OVERRIDE="${HERG_FILE_OVERRIDE:-}" +DRUG_NAME_OVERRIDE="${DRUG_NAME_OVERRIDE:-}" +CONC_OVERRIDE="${CONC_OVERRIDE:-}" + +variant_suffix_from_flag() { + case "$1" in + "") + echo "ORd-static" + ;; + "-DCIPAORDV1") + echo "CiPAORdv1.0" + ;; + "-DTOR_ORD") + echo "ToR-ORd" + ;; + "-DTOR_ORD_DYNCL") + echo "ToR-ORd-dynCl" + ;; + "-DGRANDI") + echo "Grandi" + ;; + "-DORD_STATIC_LAND") + echo "ORd-static_Land" + ;; + "-DCIPAORDV1_LAND") + echo "CiPAORdv1.0_Land" + ;; + "-DTOR_ORD_LAND") + echo "ToR-ORd_Land" + ;; + *) + echo "" + ;; + esac +} + +BIN_SUFFIX="$(variant_suffix_from_flag "$VARIANT_FLAG")" +if [[ -z "$BIN_SUFFIX" ]]; then + echo "Unsupported VARIANT_FLAG: $VARIANT_FLAG" + exit 2 +fi + +echo "[1/3] Building libCML static library in Linux container..." +cd /workspace/libCML +make clean || true +make build/libcml.a + +echo "[2/3] Building DrugSimulation binary for variant: $BIN_SUFFIX" +cd /workspace/DrugSimulation +make clean || true +make all \ + EXTRA_CXXFLAGS+="-std=c++11 $VARIANT_FLAG" + +if [[ "$RUN_SIM" != "1" ]]; then + echo "RUN_SIM=$RUN_SIM, skipping simulation run." + exit 0 +fi + +echo "[3/3] Running simulation with mpiexec in $SIM_DIR" +cd "/workspace/DrugSimulation/$SIM_DIR" + +PARAM_TO_USE="$PARAM_FILE" +TEMP_PARAM_FILE="" + +if [[ -n "$PACING_OVERRIDE" || -n "$PACING_WRITE_OVERRIDE" || -n "$SOLVER_OVERRIDE" || -n "$TIME_STEP_MIN_OVERRIDE" || -n "$HILL_FILE_OVERRIDE" || -n "$HERG_FILE_OVERRIDE" || -n "$DRUG_NAME_OVERRIDE" || -n "$CONC_OVERRIDE" ]]; then + TEMP_PARAM_FILE=".docker-test-param.txt" + cp "$PARAM_FILE" "$TEMP_PARAM_FILE" + + if [[ -n "$PACING_OVERRIDE" ]]; then + sed -E -i "s#^(number_pacing[[:space:]]*=[[:space:]]*)[0-9]+#\1${PACING_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$PACING_WRITE_OVERRIDE" ]]; then + sed -E -i "s#^(number_pacing_write[[:space:]]*=[[:space:]]*)[0-9]+#\1${PACING_WRITE_OVERRIDE}#" "$TEMP_PARAM_FILE" + elif [[ -n "$PACING_OVERRIDE" ]]; then + sed -E -i "s#^(number_pacing_write[[:space:]]*=[[:space:]]*)[0-9]+#\1${PACING_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$SOLVER_OVERRIDE" ]]; then + sed -E -i "s#^(solver_type[[:space:]]*=[[:space:]]*).* *//?#\1${SOLVER_OVERRIDE} // #" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$TIME_STEP_MIN_OVERRIDE" ]]; then + # Euler uses this as a fixed integration step for the whole run (time_step_max is ignored + # by the Euler branch), so this needs to be small (e.g. 0.005) to stay numerically stable. + sed -E -i "s#^(time_step_min[[:space:]]*=[[:space:]]*)[0-9.]+#\1${TIME_STEP_MIN_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$HILL_FILE_OVERRIDE" ]]; then + sed -E -i "s#^(hill_file[[:space:]]*=[[:space:]]*)\S+#\1${HILL_FILE_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$HERG_FILE_OVERRIDE" ]]; then + sed -E -i "s#^(herg_file[[:space:]]*=[[:space:]]*)\S+#\1${HERG_FILE_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$DRUG_NAME_OVERRIDE" ]]; then + sed -E -i "s#^(drug_name[[:space:]]*=[[:space:]]*)\S+#\1${DRUG_NAME_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$CONC_OVERRIDE" ]]; then + sed -E -i "s#^(drug_concentrations[[:space:]]*=[[:space:]]*)\S+#\1${CONC_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + PARAM_TO_USE="$TEMP_PARAM_FILE" + trap 'rm -f "$TEMP_PARAM_FILE"' EXIT +fi + +DRUG_CONCENTRATIONS_RAW="$(grep '^drug_concentrations' "$PARAM_TO_USE" | cut -d'=' -f2 | sed 's#//.*##' | xargs || true)" +mkdir -p results/0.00 +if [[ -n "$DRUG_CONCENTRATIONS_RAW" ]]; then + IFS=',' read -r -a concs <<< "$DRUG_CONCENTRATIONS_RAW" + for conc in "${concs[@]}"; do + conc_trimmed="$(echo "$conc" | xargs)" + if [[ -n "$conc_trimmed" ]]; then + conc_dir="$(printf "%.2f" "$conc_trimmed")" + mkdir -p "results/$conc_dir" + fi + done +fi + +echo "Using parameter file: $PARAM_TO_USE" +mpiexec --allow-run-as-root -np "$NP" "../drugsim_${BIN_SUFFIX}" -input_deck "$PARAM_TO_USE" \ + | tee results/logfile.docker + +echo "Simulation finished. Logs: $SIM_DIR/results/logfile.docker" diff --git a/docker/linux/run_only.sh b/docker/linux/run_only.sh new file mode 100755 index 0000000..60bc248 --- /dev/null +++ b/docker/linux/run_only.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Tunables (can be overridden by environment variables) +VARIANT_FLAG="${VARIANT_FLAG:--DCIPAORDV1}" +SIM_DIR="${SIM_DIR:-bin/simulation_cpu}" +NP="${NP:-10}" +PARAM_FILE="${PARAM_FILE:-param.txt}" +PACING_OVERRIDE="${PACING_OVERRIDE:-}" +PACING_WRITE_OVERRIDE="${PACING_WRITE_OVERRIDE:-}" +SOLVER_OVERRIDE="${SOLVER_OVERRIDE:-}" +TIME_STEP_MIN_OVERRIDE="${TIME_STEP_MIN_OVERRIDE:-}" +HILL_FILE_OVERRIDE="${HILL_FILE_OVERRIDE:-}" +HERG_FILE_OVERRIDE="${HERG_FILE_OVERRIDE:-}" +DRUG_NAME_OVERRIDE="${DRUG_NAME_OVERRIDE:-}" +CONC_OVERRIDE="${CONC_OVERRIDE:-}" + +variant_suffix_from_flag() { + case "$1" in + "") + echo "ORd-static" + ;; + "-DCIPAORDV1") + echo "CiPAORdv1.0" + ;; + "-DTOR_ORD") + echo "ToR-ORd" + ;; + "-DTOR_ORD_DYNCL") + echo "ToR-ORd-dynCl" + ;; + "-DGRANDI") + echo "Grandi" + ;; + "-DORD_STATIC_LAND") + echo "ORd-static_Land" + ;; + "-DCIPAORDV1_LAND") + echo "CiPAORdv1.0_Land" + ;; + "-DTOR_ORD_LAND") + echo "ToR-ORd_Land" + ;; + *) + echo "" + ;; + esac +} + +BIN_SUFFIX="$(variant_suffix_from_flag "$VARIANT_FLAG")" +if [[ -z "$BIN_SUFFIX" ]]; then + echo "Unsupported VARIANT_FLAG: $VARIANT_FLAG" + exit 2 +fi + +echo "[1/1] Running simulation with mpiexec in $SIM_DIR" +cd "/workspace/DrugSimulation/$SIM_DIR" + +if [[ ! -x "../drugsim_${BIN_SUFFIX}" ]]; then + echo "Missing binary ../drugsim_${BIN_SUFFIX}" + echo "Build first with docker/linux/build_and_run.sh or make in the container." + exit 3 +fi + +PARAM_TO_USE="$PARAM_FILE" +TEMP_PARAM_FILE="" + +if [[ -n "$PACING_OVERRIDE" || -n "$PACING_WRITE_OVERRIDE" || -n "$SOLVER_OVERRIDE" || -n "$TIME_STEP_MIN_OVERRIDE" || -n "$HILL_FILE_OVERRIDE" || -n "$HERG_FILE_OVERRIDE" || -n "$DRUG_NAME_OVERRIDE" || -n "$CONC_OVERRIDE" ]]; then + TEMP_PARAM_FILE=".docker-test-param.txt" + cp "$PARAM_FILE" "$TEMP_PARAM_FILE" + + if [[ -n "$PACING_OVERRIDE" ]]; then + sed -E -i "s#^(number_pacing[[:space:]]*=[[:space:]]*)[0-9]+#\1${PACING_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$PACING_WRITE_OVERRIDE" ]]; then + sed -E -i "s#^(number_pacing_write[[:space:]]*=[[:space:]]*)[0-9]+#\1${PACING_WRITE_OVERRIDE}#" "$TEMP_PARAM_FILE" + elif [[ -n "$PACING_OVERRIDE" ]]; then + sed -E -i "s#^(number_pacing_write[[:space:]]*=[[:space:]]*)[0-9]+#\1${PACING_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$SOLVER_OVERRIDE" ]]; then + sed -E -i "s#^(solver_type[[:space:]]*=[[:space:]]*).* *//?#\1${SOLVER_OVERRIDE} // #" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$TIME_STEP_MIN_OVERRIDE" ]]; then + # Euler uses this as a fixed integration step for the whole run (time_step_max is ignored + # by the Euler branch), so this needs to be small (e.g. 0.005) to stay numerically stable. + sed -E -i "s#^(time_step_min[[:space:]]*=[[:space:]]*)[0-9.]+#\1${TIME_STEP_MIN_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$HILL_FILE_OVERRIDE" ]]; then + sed -E -i "s#^(hill_file[[:space:]]*=[[:space:]]*)\S+#\1${HILL_FILE_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$HERG_FILE_OVERRIDE" ]]; then + sed -E -i "s#^(herg_file[[:space:]]*=[[:space:]]*)\S+#\1${HERG_FILE_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$DRUG_NAME_OVERRIDE" ]]; then + sed -E -i "s#^(drug_name[[:space:]]*=[[:space:]]*)\S+#\1${DRUG_NAME_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + if [[ -n "$CONC_OVERRIDE" ]]; then + sed -E -i "s#^(drug_concentrations[[:space:]]*=[[:space:]]*)\S+#\1${CONC_OVERRIDE}#" "$TEMP_PARAM_FILE" + fi + + PARAM_TO_USE="$TEMP_PARAM_FILE" + trap 'rm -f "$TEMP_PARAM_FILE"' EXIT +fi + +DRUG_CONCENTRATIONS_RAW="$(grep '^drug_concentrations' "$PARAM_TO_USE" | cut -d'=' -f2 | sed 's#//.*##' | xargs || true)" +mkdir -p results/0.00 +if [[ -n "$DRUG_CONCENTRATIONS_RAW" ]]; then + IFS=',' read -r -a concs <<< "$DRUG_CONCENTRATIONS_RAW" + for conc in "${concs[@]}"; do + conc_trimmed="$(echo "$conc" | xargs)" + if [[ -n "$conc_trimmed" ]]; then + conc_dir="$(printf "%.2f" "$conc_trimmed")" + mkdir -p "results/$conc_dir" + fi + done +fi + +echo "Using parameter file: $PARAM_TO_USE" +mpiexec --allow-run-as-root -np "$NP" "../drugsim_${BIN_SUFFIX}" -input_deck "$PARAM_TO_USE" \ + | tee results/logfile.docker + +echo "Simulation finished. Logs: $SIM_DIR/results/logfile.docker" diff --git a/docs/SOLUTION2_MACOS_SELF_GUIDE.md b/docs/SOLUTION2_MACOS_SELF_GUIDE.md new file mode 100644 index 0000000..0de0cda --- /dev/null +++ b/docs/SOLUTION2_MACOS_SELF_GUIDE.md @@ -0,0 +1,423 @@ +# Solution 2 (macOS Native) Self-Execution and Rollback Guide + +## Goal +Build and run DrugSimulation on macOS using one consistent clang-based toolchain, while keeping safe rollback points after every risky step. + +This guide assumes: +- DrugSimulation repo path: `/Users/iganarendra/DrugSimulation` +- libCML repo path: `/Users/iganarendra/libCML` +- macOS with Xcode command line tools installed + +--- + +## High-Level Decision Tree + +1. If both repos are clean, continue. +2. Else, snapshot/stash/backup first, then continue. +3. Build SUNDIALS 5.x from source inside libCML. +4. If SUNDIALS build fails, stop and fix dependency/toolchain issue before touching simulation code. +5. Patch `tm` enum collisions in libCML (clang compatibility). +6. Build libCML with clang. +7. If libCML build fails, rollback patch set and retry from a clean point. +8. Build DrugSimulation with matching clang path. +9. If link fails with ABI/symbol mismatch, verify both sides were truly rebuilt with clang and SUNDIALS 5.x. +10. Run simulation (skip Python/report stage). +11. If runtime fails, inspect logs and rollback only the minimal layer needed. + +--- + +## Step 0: Preflight Checks + +Run: + +```bash +xcode-select -p +clang++ --version +cmake --version +make --version +mpicxx --version +``` + +If `xcode-select -p` fails: + +```bash +xcode-select --install +``` + +If `cmake` is missing: + +```bash +brew install cmake +``` + +Rollback impact: none (tool installation only). + +--- + +## Step 1: Safety Snapshot (Mandatory) + +### 1.1 Check git status in both repos + +```bash +cd /Users/iganarendra/DrugSimulation +git status --short + +cd /Users/iganarendra/libCML +git status --short +``` + +### 1.2 Branch by condition + +- If status is clean in both repos: + - Create safety tags/branches anyway. +- Else: + - Stash or commit before proceeding. + +Recommended: + +```bash +cd /Users/iganarendra/DrugSimulation +git checkout -b backup/pre-solution2-$(date +%Y%m%d-%H%M%S) + +git add -A +git commit -m "backup: pre solution2 attempt" || true + +git stash push -u -m "pre-solution2-drugsim" || true + +cd /Users/iganarendra/libCML +git checkout -b backup/pre-solution2-$(date +%Y%m%d-%H%M%S) + +git add -A +git commit -m "backup: pre solution2 attempt" || true + +git stash push -u -m "pre-solution2-libcml" || true +``` + +### 1.3 Optional file-system backup (extra safety) + +```bash +cd /Users/iganarendra +mkdir -p backup_solution2 + +tar -czf backup_solution2/DrugSimulation-pre-solution2.tar.gz DrugSimulation +tar -czf backup_solution2/libCML-pre-solution2.tar.gz libCML +``` + +Rollback impact: complete restore possible even if git history gets messy. + +--- + +## Step 2: Build Native SUNDIALS 5.x for macOS (inside libCML) + +Reason: bundled prebuilt archives are Linux ELF, not usable on macOS. + +### 2.1 Configure build dir + +```bash +cd /Users/iganarendra/libCML/libs/sundials-5.7.0 +rm -rf build-macos-clang +mkdir -p build-macos-clang +cd build-macos-clang +``` + +### 2.2 CMake configure + +```bash +cmake .. \ + -DCMAKE_BUILD_TYPE=Release \ + -DCMAKE_C_COMPILER=clang \ + -DCMAKE_CXX_COMPILER=clang++ \ + -DBUILD_SHARED_LIBS=OFF \ + -DEXAMPLES_ENABLE=OFF \ + -DSUNDIALS_BUILD_WITH_MONITORING=OFF +``` + +If configure fails: +- If message indicates missing CMake modules/toolchain: + - confirm Xcode tools and CMake install, then rerun. +- Else: + - stop here and fix before moving on. + +### 2.3 Build + +```bash +cmake --build . -j$(sysctl -n hw.ncpu) +``` + +### 2.4 Verify generated static libs are Mach-O + +```bash +find . -name "libsundials*.a" | head -n 10 +file src/cvode/libsundials_cvode.a +file src/nvector/serial/libsundials_nvecserial.a +``` + +Expected: output contains `current ar archive random library` on macOS (not ELF object references from Linux builds). + +Rollback for Step 2: + +```bash +cd /Users/iganarendra/libCML/libs/sundials-5.7.0 +rm -rf build-macos-clang +``` + +--- + +## Step 3: Patch `tm` Enum Collisions in libCML (clang fix) + +Reason: some model headers use `tm` as enum identifier; clang conflicts with `struct tm` contexts more strictly. + +### 3.1 Locate collisions + +```bash +cd /Users/iganarendra/libCML +rg -n "\btm\b" cellmodels | head -n 200 +``` + +### 3.2 Apply minimal rename pattern + +Use one consistent replacement for enum token, for example `tm_state`. + +Important rules: +- Rename only enum member identifiers and direct references. +- Do not rename unrelated variable names unless compilation requires it. +- Keep changes scoped to files that fail or are clearly part of the collision set. + +### 3.3 Verify no accidental broad replacement + +```bash +git diff --stat +git diff -- cellmodels +``` + +If diff looks too broad: + +```bash +git restore --source=HEAD --staged --worktree -- cellmodels +``` + +Then redo with targeted edits. + +Rollback for Step 3: + +```bash +cd /Users/iganarendra/libCML +git restore --source=HEAD --staged --worktree -- . +# or selective restore for touched files only +``` + +--- + +## Step 4: Build libCML with clang + native SUNDIALS includes/libs + +### 4.1 Clean old build artifacts + +```bash +cd /Users/iganarendra/libCML +rm -rf build +mkdir -p build +``` + +### 4.2 Build command (template) + +Adjust include/lib paths if your SUNDIALS build output differs. + +```bash +make \ + CXX=clang++ \ + CC=clang \ + CXXFLAGS="-fPIC -fpermissive -std=c++11 -I./libs/sundials-5.7.0/include -I./libs/sundials-5.7.0/build-macos-clang/include" \ + LDFLAGS="-L./libs/sundials-5.7.0/build-macos-clang/src/cvode -L./libs/sundials-5.7.0/build-macos-clang/src/nvector/serial" \ + build/libcml.a +``` + +If build fails: +- If failure is still `tm`-related: + - go back to Step 3 and finish renames in remaining files. +- If failure is missing SUNDIALS headers: + - verify include path and generated headers in build folder. +- If failure is unrelated model source issue: + - fix one model source at a time, rebuild. + +### 4.3 Verify archive exists and is current + +```bash +ls -lh build/libcml.a +``` + +Rollback for Step 4: + +```bash +cd /Users/iganarendra/libCML +rm -rf build +# optionally restore source edits if needed +# git restore --source=HEAD --staged --worktree -- . +``` + +--- + +## Step 5: Wire DrugSimulation to Correct CML/SUNDIALS Artifacts + +Goal: ensure DrugSimulation links against the newly rebuilt clang-compatible `libcml.a` and matching native SUNDIALS static libs. + +### 5.1 Build clean + +```bash +cd /Users/iganarendra/DrugSimulation +make clean || true +``` + +### 5.2 Build with explicit linker overrides (preferred, less invasive) + +Use your Makefile variable names as applicable: + +```bash +make CXX=mpicxx \ + LDFLAGS="-g ../libCML/build/libcml.a \ + ../libCML/libs/sundials-5.7.0/build-macos-clang/src/cvode/libsundials_cvode.a \ + ../libCML/libs/sundials-5.7.0/build-macos-clang/src/nvector/serial/libsundials_nvecserial.a \ + -lcurl -ljson-c" +``` + +If your build needs additional SUNDIALS components, append the exact generated `.a` paths from your local build. + +If link fails with `std::__1` vs `std::__cxx11` symbols: +- libCML was not fully rebuilt with clang, or stale objects were reused. +- Do full clean of libCML build dir and rebuild Step 4. + +If link fails with SUNDIALS symbols missing: +- wrong SUNDIALS lib set or wrong order. +- inspect undefined symbol names and add corresponding SUNDIALS modules from your build output. + +Rollback for Step 5: + +```bash +cd /Users/iganarendra/DrugSimulation +make clean || true +git restore --source=HEAD --staged --worktree -- Makefile +``` + +(If you only used command-line overrides and did not edit Makefile, rollback is just `make clean`.) + +--- + +## Step 6: Run the Simulation (Skip Python) + +From simulation folder: + +```bash +cd /Users/iganarendra/DrugSimulation/bin/simulation_cpu +``` + +Run executable directly or with MPI depending on your project expectation. + +Examples: + +```bash +../drugsim_CiPAORdv1.0 +``` + +or + +```bash +mpirun -np 10 ../drugsim_CiPAORdv1.0 +``` + +Use `param.txt` already present in this folder. + +If run fails immediately: +- Check binary exists and has execute permission. +- Confirm current working directory is `bin/simulation_cpu`. +- Validate input files referenced by `param.txt` exist. + +Rollback for Step 6: none required (runtime only). + +--- + +## Step 7: Validation Checklist + +Mark complete only when all are true: + +- `libCML/build/libcml.a` exists and was rebuilt in current attempt. +- DrugSimulation binary (for your target model) is produced. +- No `std::__1` vs `std::__cxx11` unresolved symbol errors. +- Simulation starts and progresses through pacing/concentrations. +- Output files appear in expected output directories. + +--- + +## Fast Rollback Recipes + +## A) Undo local edits only (keep history) + +```bash +cd /Users/iganarendra/libCML +git restore --source=HEAD --staged --worktree -- . +rm -rf build libs/sundials-5.7.0/build-macos-clang + +cd /Users/iganarendra/DrugSimulation +git restore --source=HEAD --staged --worktree -- . +make clean || true +``` + +## B) Return to stash snapshot + +```bash +cd /Users/iganarendra/libCML +git stash list +git stash apply stash@{0} + +cd /Users/iganarendra/DrugSimulation +git stash list +git stash apply stash@{0} +``` + +## C) Nuclear restore from tar backups + +```bash +cd /Users/iganarendra +mv DrugSimulation DrugSimulation.failed.$(date +%Y%m%d-%H%M%S) +mv libCML libCML.failed.$(date +%Y%m%d-%H%M%S) + +tar -xzf backup_solution2/DrugSimulation-pre-solution2.tar.gz +tar -xzf backup_solution2/libCML-pre-solution2.tar.gz +``` + +Use this only if git state is beyond easy repair. + +--- + +## Practical Execution Strategy (Recommended) + +1. Perform Steps 0-2 fully. +2. Commit checkpoint in libCML: + +```bash +cd /Users/iganarendra/libCML +git add -A +git commit -m "checkpoint: native sundials built" +``` + +3. Perform Step 3 patching and commit: + +```bash +git add -A +git commit -m "fix: rename tm enum identifiers for clang" +``` + +4. Perform Step 4 and commit if build succeeds. +5. Perform Step 5 in DrugSimulation, preferably with CLI overrides first (avoid permanent Makefile edits). +6. Run Step 6. + +This gives clean, reversible checkpoints at each major stage. + +--- + +## When to Stop and Ask for Help + +Stop immediately if: +- You see massive unintended diffs in libCML model files. +- You get a new class of compiler errors unrelated to `tm` or SUNDIALS. +- You accidentally mix Homebrew SUNDIALS 7.x headers with SUNDIALS 5.x libs. + +At that point, rollback to last checkpoint and re-run from the previous stable step.