[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M6XF8PXSY18GDMV6Q8TYEQFE":3,"skill-same:01M6XF8PXSY18GDMV6Q8TYEQFE":89},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":47,"commitSha":48,"skillDirectory":49,"forksCount":14,"repositoryHtmlUrl":50,"repositoryHomepageUrl":6,"ownerHtmlUrl":51,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":52,"install":53,"resources":66},"01M6XF8PXSY18GDMV6Q8TYEQFE","immunology-assays",null,"Computational analysis of immunology experimental data. ATAC-seq differential accessibility, immune cell tracking from microscopy, ELISA data processing with 4-parameter logistic fitting, immunohistochemistry quantification, antibody titer analysis, and cell cycle phase duration estimation. For flow cytometry use flow-cytometry-analysis; for scRNA-seq use scanpy.","fadrienne\u002FMyGitHub","fadrienne","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F197874571?v=4","01M6X64V8VWCRSMFZ4EPS2BWXG","Francesca Adrienne","01M6X64VD5MW4F9PHDWQKS7ZAF",0,"MIT","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"immunology assays","immunology","computational","phase","for","seq","with","processing","biology","atac","accessibility","estimation","cycle","Immune","use","data processing","scanpy","analysis","data","flow","2026-11-10 10:12:30",false,"---\nname: immunology-assays\ndescription: Computational analysis of immunology experimental data. ATAC-seq differential accessibility, immune cell tracking from microscopy, ELISA data processing with 4-parameter logistic fitting, immunohistochemistry quantification, antibody titer analysis, and cell cycle phase duration estimation. For flow cytometry use flow-cytometry-analysis; for scRNA-seq use scanpy.\ncategory: biology\nlicense: MIT license\nmetadata:\n    skill-author: InkVell Inc.\n---\n\n# Immunology Assays: Experimental Data Analysis\n\n## Overview\n\nImmunology Assays provides computational tools for analyzing data from common immunology experiments. This skill covers ATAC-seq differential accessibility analysis (MACS2 peak calling, motif enrichment), ELISA data processing with 4-parameter logistic standard curve fitting, immune cell tracking from time-lapse microscopy, immunohistochemistry (IHC) quantification with H-score calculation, antibody titer determination from serial dilution ELISA, cell cycle phase duration estimation from dual-nucleoside labeling, and multiplex cytokine assay data processing.\n\n## When to Use This Skill\n\n- Processing ELISA plate data with standard curve fitting (4PL)\n- Quantifying IHC staining intensity (H-score, positive pixel percentage)\n- Analyzing ATAC-seq peaks and differential chromatin accessibility\n- Tracking immune cell migration from microscopy time-lapse data\n- Determining antibody titers from serial dilution experiments\n- Estimating cell cycle phase durations from pulse-labeling data\n- Processing multiplex cytokine\u002Fchemokine assay data (Luminex, MSD)\n\n**Related Skills:** For flow cytometry analysis use `flow-cytometry-analysis`. For single-cell RNA-seq use `scanpy`. For bioimage analysis use `bioimage-analysis`.\n\n## Installation\n\n```bash\nuv pip install scipy scikit-image opencv-python numpy pandas matplotlib\n```\n\nFor ATAC-seq (optional):\n```bash\n# conda install -c bioconda macs2 homer\n```\n\n## Quick Start\n\n```python\nimport numpy as np\nfrom scipy.optimize import curve_fit\n\n# 4-Parameter Logistic (4PL) for ELISA standard curve\ndef four_pl(x, a, b, c, d):\n    \"\"\"a=min, b=Hill slope, c=EC50, d=max\"\"\"\n    return d + (a - d) \u002F (1 + (x \u002F c) ** b)\n\n# Standard curve data\nconcentrations = np.array([0, 15.6, 31.25, 62.5, 125, 250, 500, 1000])\nod_values = np.array([0.05, 0.12, 0.22, 0.45, 0.82, 1.35, 1.85, 2.15])\n\npopt, pcov = curve_fit(four_pl, concentrations[1:], od_values[1:],\n                       p0=[0.05, 1.0, 200, 2.2], maxfev=10000)\nprint(f\"EC50: {popt[2]:.1f} pg\u002FmL\")\nprint(f\"Dynamic range: {popt[0]:.3f} - {popt[3]:.3f} OD\")\n```\n\n## Core Capabilities\n\n### 1. ATAC-seq Accessibility Analysis\n\nPeak calling and differential accessibility.\n\n```python\nimport subprocess\nimport pandas as pd\n\ndef run_macs2_atacseq(bam_path, output_prefix, genome_size='hs'):\n    \"\"\"Call ATAC-seq peaks with MACS2.\"\"\"\n    cmd = [\n        'macs2', 'callpeak',\n        '-t', bam_path,\n        '-f', 'BAMPE',           # Paired-end\n        '-g', genome_size,\n        '--nomodel',\n        '--shift', '-100',\n        '--extsize', '200',\n        '--broad',               # Broad peaks for open chromatin\n        '-n', output_prefix,\n        '--outdir', 'macs2_output'\n    ]\n    result = subprocess.run(cmd, capture_output=True, text=True)\n    if result.returncode != 0:\n        raise RuntimeError(f\"MACS2 failed: {result.stderr}\")\n    return f'macs2_output\u002F{output_prefix}_peaks.broadPeak'\n\ndef parse_broadpeak(peak_file):\n    \"\"\"Parse MACS2 broadPeak output.\"\"\"\n    cols = ['chrom', 'start', 'end', 'name', 'score', 'strand',\n            'signal', 'pvalue', 'qvalue']\n    df = pd.read_csv(peak_file, sep='\\t', header=None, names=cols)\n    print(f\"Total peaks: {len(df)}\")\n    print(f\"Mean peak width: {(df['end'] - df['start']).mean():.0f} bp\")\n    return df\n\ndef differential_accessibility(count_matrix, conditions, padj_threshold=0.05):\n    \"\"\"DESeq2-style differential accessibility from peak count matrix.\n\n    Args:\n        count_matrix: peaks x samples DataFrame (integer counts)\n        conditions: list of condition labels per sample\n    \"\"\"\n    from scipy.stats import mannwhitneyu\n    from statsmodels.stats.multitest import multipletests\n\n    results = []\n    groups = list(set(conditions))\n    g1_idx = [i for i, c in enumerate(conditions) if c == groups[0]]\n    g2_idx = [i for i, c in enumerate(conditions) if c == groups[1]]\n\n    for peak in count_matrix.index:\n        vals1 = count_matrix.loc[peak].iloc[g1_idx].values\n        vals2 = count_matrix.loc[peak].iloc[g2_idx].values\n\n        fc = (vals2.mean() + 1) \u002F (vals1.mean() + 1)\n        log2fc = np.log2(fc)\n\n        stat, pval = mannwhitneyu(vals1, vals2, alternative='two-sided')\n        results.append({'peak': peak, 'log2FC': log2fc, 'pvalue': pval})\n\n    df = pd.DataFrame(results)\n    _, df['padj'], _, _ = multipletests(df['pvalue'], method='fdr_bh')\n\n    sig = df[df['padj'] \u003C padj_threshold]\n    print(f\"Significant DA peaks: {len(sig)} \u002F {len(df)}\")\n    print(f\"  More accessible: {(sig['log2FC'] > 0).sum()}\")\n    print(f\"  Less accessible: {(sig['log2FC'] \u003C 0).sum()}\")\n    return df\n\ndef run_homer_motif(peak_bed, genome, output_dir):\n    \"\"\"Run HOMER motif enrichment on peaks.\"\"\"\n    cmd = [\n        'findMotifsGenome.pl', peak_bed, genome, output_dir,\n        '-size', '200', '-mask', '-p', '4'\n    ]\n    subprocess.run(cmd, capture_output=True, text=True, check=True)\n    return output_dir\n```\n\n### 2. ELISA Data Processing\n\n4-parameter logistic standard curve fitting and unknown interpolation.\n\n```python\nimport numpy as np\nfrom scipy.optimize import curve_fit\nimport pandas as pd\n\ndef four_pl(x, a, b, c, d):\n    \"\"\"4-Parameter Logistic curve.\n    a = minimum asymptote, b = Hill slope, c = EC50, d = maximum asymptote\"\"\"\n    return d + (a - d) \u002F (1 + (x \u002F c) ** b)\n\ndef inverse_four_pl(y, a, b, c, d):\n    \"\"\"Inverse 4PL to interpolate concentration from OD.\"\"\"\n    return c * ((a - d) \u002F (y - d) - 1) ** (1 \u002F b)\n\ndef process_elisa_plate(standards, unknowns, blank_od=None):\n    \"\"\"Process ELISA plate with 4PL standard curve.\n\n    Args:\n        standards: dict of concentration -> list of OD replicates\n        unknowns: dict of sample_name -> list of OD replicates\n        blank_od: blank well OD (subtracted from all values)\n    \"\"\"\n    # Prepare standard curve data\n    conc_list, od_list = [], []\n    for conc, ods in sorted(standards.items()):\n        for od in ods:\n            if conc > 0:  # Skip zero for fitting\n                conc_list.append(conc)\n                od_list.append(od - (blank_od or 0))\n\n    conc_arr = np.array(conc_list)\n    od_arr = np.array(od_list)\n\n    # Fit 4PL\n    p0 = [min(od_arr), 1.0, np.median(conc_arr), max(od_arr)]\n    popt, pcov = curve_fit(four_pl, conc_arr, od_arr, p0=p0, maxfev=10000)\n    a, b, c, d = popt\n\n    # R-squared\n    predicted = four_pl(conc_arr, *popt)\n    ss_res = np.sum((od_arr - predicted) ** 2)\n    ss_tot = np.sum((od_arr - np.mean(od_arr)) ** 2)\n    r_squared = 1 - ss_res \u002F ss_tot\n\n    # Detection limits\n    lod = inverse_four_pl(a + 2 * np.std(od_arr[:3]), *popt) if a + 2 * np.std(od_arr[:3]) \u003C d else None\n\n    print(f\"4PL fit: R² = {r_squared:.4f}\")\n    print(f\"EC50: {c:.2f}\")\n    print(f\"Dynamic range: {a:.3f} - {d:.3f} OD\")\n    if lod:\n        print(f\"LOD: {lod:.2f}\")\n\n    # Interpolate unknowns\n    results = []\n    for name, ods in unknowns.items():\n        od_corrected = [od - (blank_od or 0) for od in ods]\n        concentrations = []\n        for od in od_corrected:\n            if a \u003C od \u003C d:  # Within curve range\n                conc = inverse_four_pl(od, *popt)\n                concentrations.append(conc)\n            else:\n                concentrations.append(np.nan)\n\n        results.append({\n            'sample': name,\n            'mean_od': np.mean(od_corrected),\n            'mean_conc': np.nanmean(concentrations),\n            'std_conc': np.nanstd(concentrations),\n            'n': len(concentrations),\n            'in_range': sum(~np.isnan(c) for c in concentrations)\n        })\n\n    return pd.DataFrame(results), popt, r_squared\n\n# Example\nstandards = {\n    0: [0.05, 0.06], 15.6: [0.11, 0.13], 31.25: [0.21, 0.23],\n    62.5: [0.44, 0.46], 125: [0.80, 0.84], 250: [1.32, 1.38],\n    500: [1.82, 1.88], 1000: [2.12, 2.18]\n}\nunknowns = {\n    'Patient_1': [0.95, 0.98], 'Patient_2': [0.35, 0.38],\n    'Patient_3': [1.55, 1.60], 'Control': [0.08, 0.09]\n}\n\nresults, params, r2 = process_elisa_plate(standards, unknowns, blank_od=0.05)\nprint(results[['sample', 'mean_conc', 'std_conc']])\n```\n\n### 3. Immune Cell Tracking\n\nTrack immune cells in time-lapse microscopy.\n\n```python\nimport numpy as np\nimport pandas as pd\n\ndef track_immune_cells(tracks_df, pixel_size_um=0.65, frame_interval_min=1):\n    \"\"\"Analyze immune cell migration from tracking data.\n\n    Args:\n        tracks_df: DataFrame with columns [particle, frame, x, y]\n        pixel_size_um: microns per pixel\n        frame_interval_min: minutes between frames\n    \"\"\"\n    results = []\n\n    for pid, track in tracks_df.groupby('particle'):\n        track = track.sort_values('frame')\n        x = track['x'].values * pixel_size_um\n        y = track['y'].values * pixel_size_um\n        t = track['frame'].values * frame_interval_min\n\n        # Instantaneous velocity\n        dx = np.diff(x)\n        dy = np.diff(y)\n        dt = np.diff(t)\n        speeds = np.sqrt(dx**2 + dy**2) \u002F dt\n\n        # Displacement (start to end)\n        displacement = np.sqrt((x[-1] - x[0])**2 + (y[-1] - y[0])**2)\n\n        # Total path length\n        path_length = np.sum(np.sqrt(dx**2 + dy**2))\n\n        # Confinement ratio (displacement \u002F path_length)\n        confinement = displacement \u002F path_length if path_length > 0 else 0\n\n        # Mean squared displacement\n        msd_values = []\n        for lag in range(1, min(len(x), 20)):\n            displacements = (x[lag:] - x[:-lag])**2 + (y[lag:] - y[:-lag])**2\n            msd_values.append(np.mean(displacements))\n\n        results.append({\n            'particle': pid,\n            'mean_speed': np.mean(speeds),\n            'max_speed': np.max(speeds),\n            'displacement': displacement,\n            'path_length': path_length,\n            'confinement_ratio': confinement,\n            'duration_min': t[-1] - t[0],\n            'n_frames': len(track)\n        })\n\n    df = pd.DataFrame(results)\n\n    # Classify migratory phenotype\n    df['phenotype'] = 'confined'\n    df.loc[df['confinement_ratio'] > 0.5, 'phenotype'] = 'directed'\n    df.loc[(df['confinement_ratio'] > 0.2) & (df['confinement_ratio'] \u003C= 0.5), 'phenotype'] = 'random_walk'\n\n    print(f\"Tracked {len(df)} cells\")\n    print(f\"Mean speed: {df['mean_speed'].mean():.2f} um\u002Fmin\")\n    print(f\"Phenotypes:\\n{df['phenotype'].value_counts()}\")\n    return df\n```\n\n### 4. IHC Quantification\n\nQuantify immunohistochemistry staining intensity.\n\n```python\nimport numpy as np\nfrom skimage.color import rgb2hed\nimport skimage.io\n\ndef quantify_ihc(image_path, method='h_score'):\n    \"\"\"Quantify IHC staining using color deconvolution.\n\n    Separates DAB (brown) from hematoxylin (blue) using\n    Ruifrok & Johnston color deconvolution.\n    \"\"\"\n    image = skimage.io.imread(image_path)\n\n    # Color deconvolution: RGB → HED (Hematoxylin, Eosin, DAB)\n    hed = rgb2hed(image)\n    dab_channel = hed[:, :, 2]  # DAB is channel 2\n    hematoxylin = hed[:, :, 0]\n\n    # Threshold to identify tissue\n    tissue_mask = hematoxylin > 0.05\n\n    # DAB intensity classification for H-score\n    dab_tissue = dab_channel[tissue_mask]\n\n    # Classify: negative (0), weak (1+), moderate (2+), strong (3+)\n    negative = (dab_tissue \u003C 0.1).sum()\n    weak = ((dab_tissue >= 0.1) & (dab_tissue \u003C 0.2)).sum()\n    moderate = ((dab_tissue >= 0.2) & (dab_tissue \u003C 0.4)).sum()\n    strong = (dab_tissue >= 0.4).sum()\n    total = len(dab_tissue)\n\n    # H-score: 1*(% weak) + 2*(% moderate) + 3*(% strong)\n    h_score = (1 * weak + 2 * moderate + 3 * strong) \u002F total * 100\n\n    # Positive pixel percentage\n    positive_pct = 100 * (weak + moderate + strong) \u002F total\n\n    print(f\"H-score: {h_score:.1f} (range 0-300)\")\n    print(f\"Positive pixels: {positive_pct:.1f}%\")\n    print(f\"  Weak (1+): {100*weak\u002Ftotal:.1f}%\")\n    print(f\"  Moderate (2+): {100*moderate\u002Ftotal:.1f}%\")\n    print(f\"  Strong (3+): {100*strong\u002Ftotal:.1f}%\")\n\n    return {\n        'h_score': h_score,\n        'positive_pct': positive_pct,\n        'negative_pct': 100 * negative \u002F total,\n        'weak_pct': 100 * weak \u002F total,\n        'moderate_pct': 100 * moderate \u002F total,\n        'strong_pct': 100 * strong \u002F total\n    }\n```\n\n### 5. Antibody Titer Analysis\n\nDetermine endpoint titers from serial dilution ELISA.\n\n```python\nimport numpy as np\nfrom scipy import stats\n\ndef calculate_endpoint_titer(dilutions, od_values, cutoff_method='mean_plus_3sd',\n                              negative_ods=None):\n    \"\"\"Determine endpoint antibody titer from serial dilution ELISA.\n\n    Args:\n        dilutions: list of dilution factors (e.g., [100, 200, 400, ...])\n        od_values: list of OD values at each dilution\n        negative_ods: list of negative control OD values\n    \"\"\"\n    if negative_ods is None:\n        cutoff = 0.1  # Default cutoff\n    elif cutoff_method == 'mean_plus_3sd':\n        cutoff = np.mean(negative_ods) + 3 * np.std(negative_ods)\n    elif cutoff_method == 'mean_plus_2sd':\n        cutoff = np.mean(negative_ods) + 2 * np.std(negative_ods)\n    else:\n        cutoff = 0.1\n\n    # Find last dilution above cutoff\n    endpoint = None\n    for dil, od in zip(dilutions, od_values):\n        if od > cutoff:\n            endpoint = dil\n\n    if endpoint is None:\n        return {'titer': '\u003C' + str(min(dilutions)), 'cutoff': cutoff}\n\n    return {\n        'titer': endpoint,\n        'log2_titer': np.log2(endpoint),\n        'cutoff': cutoff\n    }\n\ndef geometric_mean_titer(titers):\n    \"\"\"Calculate geometric mean titer from multiple samples.\"\"\"\n    log_titers = np.log2([t for t in titers if t > 0])\n    gmt = 2 ** np.mean(log_titers)\n    ci = stats.t.interval(0.95, df=len(log_titers)-1,\n                          loc=np.mean(log_titers),\n                          scale=stats.sem(log_titers))\n    return {\n        'gmt': gmt,\n        'ci_lower': 2 ** ci[0],\n        'ci_upper': 2 ** ci[1],\n        'n': len(log_titers)\n    }\n\n# Example\ndilutions = [100, 200, 400, 800, 1600, 3200, 6400, 12800]\nsample_ods = [2.1, 1.8, 1.4, 0.9, 0.45, 0.18, 0.08, 0.05]\nneg_ods = [0.06, 0.05, 0.07, 0.04]\n\nresult = calculate_endpoint_titer(dilutions, sample_ods, negative_ods=neg_ods)\nprint(f\"Endpoint titer: 1:{result['titer']}\")\nprint(f\"Cutoff OD: {result['cutoff']:.3f}\")\n```\n\n### 6. Cell Cycle Phase Duration\n\nEstimate phase durations from dual-nucleoside pulse labeling.\n\n```python\nimport numpy as np\n\ndef estimate_phase_durations(labeled_fractions, pulse_interval_hours,\n                              total_cycle_time=None):\n    \"\"\"Estimate cell cycle phase durations from dual-nucleoside labeling.\n\n    Args:\n        labeled_fractions: dict of timepoints -> fraction labeled\n        pulse_interval_hours: time between pulses\n        total_cycle_time: if known, constrains the estimates\n    \"\"\"\n    times = sorted(labeled_fractions.keys())\n    fractions = [labeled_fractions[t] for t in times]\n\n    # S-phase duration estimate: fraction labeled at first timepoint * total cycle time\n    # If total cycle time unknown, estimate from growth rate\n    if total_cycle_time is None:\n        # Assume exponential growth, estimate from labeling kinetics\n        # Rate of increase in labeled fraction approximates 1\u002FTc\n        if len(fractions) > 1:\n            rate = (fractions[-1] - fractions[0]) \u002F (times[-1] - times[0])\n            total_cycle_time = 1 \u002F rate if rate > 0 else 24\n        else:\n            total_cycle_time = 24  # Default\n\n    s_phase = fractions[0] * total_cycle_time\n    g2m_phase = pulse_interval_hours  # Time for labeled cells to reach mitosis\n\n    # G1 = total - S - G2\u002FM\n    g1_phase = total_cycle_time - s_phase - g2m_phase\n    g1_phase = max(g1_phase, 0)\n\n    return {\n        'total_cycle': total_cycle_time,\n        'G1': g1_phase,\n        'S': s_phase,\n        'G2_M': g2m_phase\n    }\n```\n\n### 7. Cytokine Analysis\n\nProcess multiplex cytokine assay data.\n\n```python\nimport numpy as np\nimport pandas as pd\nfrom scipy.optimize import curve_fit\n\ndef process_multiplex_cytokines(plate_data, analytes, standard_curves):\n    \"\"\"Process multiplex cytokine assay (Luminex\u002FMSD) data.\n\n    Args:\n        plate_data: DataFrame with well, sample, analyte, MFI columns\n        analytes: list of analyte names\n        standard_curves: dict of analyte -> (concentrations, MFI_values)\n    \"\"\"\n    def five_pl(x, a, b, c, d, g):\n        return d + (a - d) \u002F (1 + (x \u002F c) ** b) ** g\n\n    results = []\n    for analyte in analytes:\n        # Fit standard curve\n        conc, mfi = standard_curves[analyte]\n        try:\n            popt, _ = curve_fit(five_pl, conc[conc > 0], mfi[conc > 0],\n                               p0=[min(mfi), 1, np.median(conc), max(mfi), 1],\n                               maxfev=10000)\n        except RuntimeError:\n            # Fall back to 4PL\n            popt = None\n\n        # Interpolate unknowns\n        analyte_data = plate_data[plate_data['analyte'] == analyte]\n        for _, row in analyte_data.iterrows():\n            if popt is not None:\n                # Inverse 5PL\n                try:\n                    from scipy.optimize import brentq\n                    conc_val = brentq(lambda x: five_pl(x, *popt) - row['MFI'],\n                                     0.01, 100000)\n                except ValueError:\n                    conc_val = np.nan\n            else:\n                conc_val = np.nan\n\n            results.append({\n                'sample': row['sample'],\n                'analyte': analyte,\n                'MFI': row['MFI'],\n                'concentration': conc_val\n            })\n\n    return pd.DataFrame(results)\n```\n\n## Typical Workflows\n\n### Workflow 1: Process ELISA Plate Data with 4PL Standard Curve\n\n```python\nstandards = {0: [0.05], 15.6: [0.12, 0.13], 31.25: [0.22, 0.24],\n             62.5: [0.45, 0.47], 125: [0.82, 0.85], 250: [1.35, 1.38],\n             500: [1.85, 1.88], 1000: [2.15, 2.18]}\nunknowns = {'Sample_A': [0.68, 0.72], 'Sample_B': [1.45, 1.50]}\nresults, params, r2 = process_elisa_plate(standards, unknowns, blank_od=0.05)\nprint(results)\n```\n\n### Workflow 2: Quantify IHC Staining Intensity (H-Score)\n\n```python\nresult = quantify_ihc('ihc_slide.tif')\nprint(f\"H-score: {result['h_score']:.1f}\")\nprint(f\"Positive: {result['positive_pct']:.1f}%\")\n```\n\n### Workflow 3: ATAC-seq Peak Calling and Differential Accessibility\n\n```python\n# Call peaks for each condition\npeaks_ctrl = run_macs2_atacseq('control.bam', 'control')\npeaks_treat = run_macs2_atacseq('treated.bam', 'treated')\n\n# Parse and analyze\nctrl_df = parse_broadpeak(peaks_ctrl)\ntreat_df = parse_broadpeak(peaks_treat)\nprint(f\"Control peaks: {len(ctrl_df)}, Treatment peaks: {len(treat_df)}\")\n```\n\n## Best Practices\n\n1. **ELISA standard curves** — always run standards in duplicate; verify R² > 0.99 for 4PL fit; samples outside curve range should be re-run at appropriate dilutions\n2. **IHC quantification** — use color deconvolution (not simple thresholding) for DAB separation; calibrate thresholds for weak\u002Fmoderate\u002Fstrong on control tissue\n3. **ATAC-seq** — use paired-end mode (`-f BAMPE`); shift reads to account for Tn5 insertion; call narrow peaks for TF footprinting, broad peaks for open chromatin\n4. **Antibody titers** — use geometric mean for group statistics (titers are log-distributed); calculate seroconversion as 4-fold rise\n5. **Cell tracking** — require minimum track length (>10 frames) to exclude artifacts; validate tracking by visual inspection of overlaid tracks\n6. **Multiplex assays** — check for cross-reactivity; flag analytes with CV >15% between replicates\n\n## Troubleshooting\n\n**Problem:** 4PL fit fails to converge\n**Solution:** Check that standard curve spans the expected range. Adjust initial parameters (p0). Ensure blank subtraction is correct. Try 5PL for asymmetric curves.\n\n**Problem:** IHC color deconvolution gives poor separation\n**Solution:** Verify image is RGB (not grayscale). Check that staining uses standard DAB\u002Fhematoxylin. Adjust HED matrix if using non-standard chromogens.\n\n**Problem:** ATAC-seq produces too many peaks\n**Solution:** Increase `-q` (q-value) threshold. Filter for peaks with fold enrichment >2. Check for high mitochondrial read fraction (indicates poor quality).\n\n**Problem:** Antibody titer is below lowest dilution\n**Solution:** Use lower starting dilution. Report as \"\u003C [lowest dilution]\". Check that positive control shows expected titer.\n\n## Resources\n\n- [ELISA Technical Guide (Thermo Fisher)](https:\u002F\u002Fwww.thermofisher.com\u002Fus\u002Fen\u002Fhome\u002Flife-science\u002Fprotein-biology\u002Fprotein-biology-learning-center\u002Fprotein-biology-resource-library\u002Fpierce-protein-methods\u002Felisa-technical-guide.html)\n- [MACS2 Documentation](https:\u002F\u002Fgithub.com\u002Fmacs3-project\u002FMACS)\n- [HOMER Motif Analysis](http:\u002F\u002Fhomer.ucsd.edu\u002Fhomer\u002F)\n- [H-score Calculation Guidelines](https:\u002F\u002Fdoi.org\u002F10.1007\u002F978-1-4939-1124-0_6)\n","# Immunology Assays: Experimental Data Analysis\n\n## Overview\n\nImmunology Assays provides computational tools for analyzing data from common immunology experiments. This skill covers ATAC-seq differential accessibility analysis (MACS2 peak calling, motif enrichment), ELISA data processing with 4-parameter logistic standard curve fitting, immune cell tracking from time-lapse microscopy, immunohistochemistry (IHC) quantification with H-score calculation, antibody titer determination from serial dilution ELISA, cell cycle phase duration estimation from dual-nucleoside labeling, and multiplex cytokine assay data processing.\n\n## When to Use This Skill\n\n- Processing ELISA plate data with standard curve fitting (4PL)\n- Quantifying IHC staining intensity (H-score, positive pixel percentage)\n- Analyzing ATAC-seq peaks and differential chromatin accessibility\n- Tracking immune cell migration from microscopy time-lapse data\n- Determining antibody titers from serial dilution experiments\n- Estimating cell cycle phase durations from pulse-labeling data\n- Processing multiplex cytokine\u002Fchemokine assay data (Luminex, MSD)\n\n**Related Skills:** For flow cytometry analysis use `flow-cytometry-analysis`. For single-cell RNA-seq use `scanpy`. For bioimage analysis use `bioimage-analysis`.\n\n## Installation\n\n```bash\nuv pip install scipy scikit-image opencv-python numpy pandas matplotlib\n```\n\nFor ATAC-seq (optional):\n```bash\n# conda install -c bioconda macs2 homer\n```\n\n## Quick Start\n\n```python\nimport numpy as np\nfrom scipy.optimize import curve_fit\n\n# 4-Parameter Logistic (4PL) for ELISA standard curve\ndef four_pl(x, a, b, c, d):\n    \"\"\"a=min, b=Hill slope, c=EC50, d=max\"\"\"\n    return d + (a - d) \u002F (1 + (x \u002F c) ** b)\n\n# Standard curve data\nconcentrations = np.array([0, 15.6, 31.25, 62.5, 125, 250, 500, 1000])\nod_values = np.array([0.05, 0.12, 0.22, 0.45, 0.82, 1.35, 1.85, 2.15])\n\npopt, pcov = curve_fit(four_pl, concentrations[1:], od_values[1:],\n                       p0=[0.05, 1.0, 200, 2.2], maxfev=10000)\nprint(f\"EC50: {popt[2]:.1f} pg\u002FmL\")\nprint(f\"Dynamic range: {popt[0]:.3f} - {popt[3]:.3f} OD\")\n```\n\n## Core Capabilities\n\n### 1. ATAC-seq Accessibility Analysis\n\nPeak calling and differential accessibility.\n\n```python\nimport subprocess\nimport pandas as pd\n\ndef run_macs2_atacseq(bam_path, output_prefix, genome_size='hs'):\n    \"\"\"Call ATAC-seq peaks with MACS2.\"\"\"\n    cmd = [\n        'macs2', 'callpeak',\n        '-t', bam_path,\n        '-f', 'BAMPE',           # Paired-end\n        '-g', genome_size,\n        '--nomodel',\n        '--shift', '-100',\n        '--extsize', '200',\n        '--broad',               # Broad peaks for open chromatin\n        '-n', output_prefix,\n        '--outdir', 'macs2_output'\n    ]\n    result = subprocess.run(cmd, capture_output=True, text=True)\n    if result.returncode != 0:\n        raise RuntimeError(f\"MACS2 failed: {result.stderr}\")\n    return f'macs2_output\u002F{output_prefix}_peaks.broadPeak'\n\ndef parse_broadpeak(peak_file):\n    \"\"\"Parse MACS2 broadPeak output.\"\"\"\n    cols = ['chrom', 'start', 'end', 'name', 'score', 'strand',\n            'signal', 'pvalue', 'qvalue']\n    df = pd.read_csv(peak_file, sep='\\t', header=None, names=cols)\n    print(f\"Total peaks: {len(df)}\")\n    print(f\"Mean peak width: {(df['end'] - df['start']).mean():.0f} bp\")\n    return df\n\ndef differential_accessibility(count_matrix, conditions, padj_threshold=0.05):\n    \"\"\"DESeq2-style differential accessibility from peak count matrix.\n\n    Args:\n        count_matrix: peaks x samples DataFrame (integer counts)\n        conditions: list of condition labels per sample\n    \"\"\"\n    from scipy.stats import mannwhitneyu\n    from statsmodels.stats.multitest import multipletests\n\n    results = []\n    groups = list(set(conditions))\n    g1_idx = [i for i, c in enumerate(conditions) if c == groups[0]]\n    g2_idx = [i for i, c in enumerate(conditions) if c == groups[1]]\n\n    for peak in count_matrix.index:\n        vals1 = count_matrix.loc[peak].iloc[g1_idx].values\n        vals2 = count_matrix.loc[peak].iloc[g2_idx].values\n\n        fc = (vals2.mean() + 1) \u002F (vals1.mean() + 1)\n        log2fc = np.log2(fc)\n\n        stat, pval = mannwhitneyu(vals1, vals2, alternative='two-sided')\n        results.append({'peak': peak, 'log2FC': log2fc, 'pvalue': pval})\n\n    df = pd.DataFrame(results)\n    _, df['padj'], _, _ = multipletests(df['pvalue'], method='fdr_bh')\n\n    sig = df[df['padj'] \u003C padj_threshold]\n    print(f\"Significant DA peaks: {len(sig)} \u002F {len(df)}\")\n    print(f\"  More accessible: {(sig['log2FC'] > 0).sum()}\")\n    print(f\"  Less accessible: {(sig['log2FC'] \u003C 0).sum()}\")\n    return df\n\ndef run_homer_motif(peak_bed, genome, output_dir):\n    \"\"\"Run HOMER motif enrichment on peaks.\"\"\"\n    cmd = [\n        'findMotifsGenome.pl', peak_bed, genome, output_dir,\n        '-size', '200', '-mask', '-p', '4'\n    ]\n    subprocess.run(cmd, capture_output=True, text=True, check=True)\n    return output_dir\n```\n\n### 2. ELISA Data Processing\n\n4-parameter logistic standard curve fitting and unknown interpolation.\n\n```python\nimport numpy as np\nfrom scipy.optimize import curve_fit\nimport pandas as pd\n\ndef four_pl(x, a, b, c, d):\n    \"\"\"4-Parameter Logistic curve.\n    a = minimum asymptote, b = Hill slope, c = EC50, d = maximum asymptote\"\"\"\n    return d + (a - d) \u002F (1 + (x \u002F c) ** b)\n\ndef inverse_four_pl(y, a, b, c, d):\n    \"\"\"Inverse 4PL to interpolate concentration from OD.\"\"\"\n    return c * ((a - d) \u002F (y - d) - 1) ** (1 \u002F b)\n\ndef process_elisa_plate(standards, unknowns, blank_od=None):\n    \"\"\"Process ELISA plate with 4PL standard curve.\n\n    Args:\n        standards: dict of concentration -> list of OD replicates\n        unknowns: dict of sample_name -> list of OD replicates\n        blank_od: blank well OD (subtracted from all values)\n    \"\"\"\n    # Prepare standard curve data\n    conc_list, od_list = [], []\n    for conc, ods in sorted(standards.items()):\n        for od in ods:\n            if conc > 0:  # Skip zero for fitting\n                conc_list.append(conc)\n                od_list.append(od - (blank_od or 0))\n\n    conc_arr = np.array(conc_list)\n    od_arr = np.array(od_list)\n\n    # Fit 4PL\n    p0 = [min(od_arr), 1.0, np.median(conc_arr), max(od_arr)]\n    popt, pcov = curve_fit(four_pl, conc_arr, od_arr, p0=p0, maxfev=10000)\n    a, b, c, d = popt\n\n    # R-squared\n    predicted = four_pl(conc_arr, *popt)\n    ss_res = np.sum((od_arr - predicted) ** 2)\n    ss_tot = np.sum((od_arr - np.mean(od_arr)) ** 2)\n    r_squared = 1 - ss_res \u002F ss_tot\n\n    # Detection limits\n    lod = inverse_four_pl(a + 2 * np.std(od_arr[:3]), *popt) if a + 2 * np.std(od_arr[:3]) \u003C d else None\n\n    print(f\"4PL fit: R² = {r_squared:.4f}\")\n    print(f\"EC50: {c:.2f}\")\n    print(f\"Dynamic range: {a:.3f} - {d:.3f} OD\")\n    if lod:\n        print(f\"LOD: {lod:.2f}\")\n\n    # Interpolate unknowns\n    results = []\n    for name, ods in unknowns.items():\n        od_corrected = [od - (blank_od or 0) for od in ods]\n        concentrations = []\n        for od in od_corrected:\n            if a \u003C od \u003C d:  # Within curve range\n                conc = inverse_four_pl(od, *popt)\n                concentrations.append(conc)\n            else:\n                concentrations.append(np.nan)\n\n        results.append({\n            'sample': name,\n            'mean_od': np.mean(od_corrected),\n            'mean_conc': np.nanmean(concentrations),\n            'std_conc': np.nanstd(concentrations),\n            'n': len(concentrations),\n            'in_range': sum(~np.isnan(c) for c in concentrations)\n        })\n\n    return pd.DataFrame(results), popt, r_squared\n\n# Example\nstandards = {\n    0: [0.05, 0.06], 15.6: [0.11, 0.13], 31.25: [0.21, 0.23],\n    62.5: [0.44, 0.46], 125: [0.80, 0.84], 250: [1.32, 1.38],\n    500: [1.82, 1.88], 1000: [2.12, 2.18]\n}\nunknowns = {\n    'Patient_1': [0.95, 0.98], 'Patient_2': [0.35, 0.38],\n    'Patient_3': [1.55, 1.60], 'Control': [0.08, 0.09]\n}\n\nresults, params, r2 = process_elisa_plate(standards, unknowns, blank_od=0.05)\nprint(results[['sample', 'mean_conc', 'std_conc']])\n```\n\n### 3. Immune Cell Tracking\n\nTrack immune cells in time-lapse microscopy.\n\n```python\nimport numpy as np\nimport pandas as pd\n\ndef track_immune_cells(tracks_df, pixel_size_um=0.65, frame_interval_min=1):\n    \"\"\"Analyze immune cell migration from tracking data.\n\n    Args:\n        tracks_df: DataFrame with columns [particle, frame, x, y]\n        pixel_size_um: microns per pixel\n        frame_interval_min: minutes between frames\n    \"\"\"\n    results = []\n\n    for pid, track in tracks_df.groupby('particle'):\n        track = track.sort_values('frame')\n        x = track['x'].values * pixel_size_um\n        y = track['y'].values * pixel_size_um\n        t = track['frame'].values * frame_interval_min\n\n        # Instantaneous velocity\n        dx = np.diff(x)\n        dy = np.diff(y)\n        dt = np.diff(t)\n        speeds = np.sqrt(dx**2 + dy**2) \u002F dt\n\n        # Displacement (start to end)\n        displacement = np.sqrt((x[-1] - x[0])**2 + (y[-1] - y[0])**2)\n\n        # Total path length\n        path_length = np.sum(np.sqrt(dx**2 + dy**2))\n\n        # Confinement ratio (displacement \u002F path_length)\n        confinement = displacement \u002F path_length if path_length > 0 else 0\n\n        # Mean squared displacement\n        msd_values = []\n        for lag in range(1, min(len(x), 20)):\n            displacements = (x[lag:] - x[:-lag])**2 + (y[lag:] - y[:-lag])**2\n            msd_values.append(np.mean(displacements))\n\n        results.append({\n            'particle': pid,\n            'mean_speed': np.mean(speeds),\n            'max_speed': np.max(speeds),\n            'displacement': displacement,\n            'path_length': path_length,\n            'confinement_ratio': confinement,\n            'duration_min': t[-1] - t[0],\n            'n_frames': len(track)\n        })\n\n    df = pd.DataFrame(results)\n\n    # Classify migratory phenotype\n    df['phenotype'] = 'confined'\n    df.loc[df['confinement_ratio'] > 0.5, 'phenotype'] = 'directed'\n    df.loc[(df['confinement_ratio'] > 0.2) & (df['confinement_ratio'] \u003C= 0.5), 'phenotype'] = 'random_walk'\n\n    print(f\"Tracked {len(df)} cells\")\n    print(f\"Mean speed: {df['mean_speed'].mean():.2f} um\u002Fmin\")\n    print(f\"Phenotypes:\\n{df['phenotype'].value_counts()}\")\n    return df\n```\n\n### 4. IHC Quantification\n\nQuantify immunohistochemistry staining intensity.\n\n```python\nimport numpy as np\nfrom skimage.color import rgb2hed\nimport skimage.io\n\ndef quantify_ihc(image_path, method='h_score'):\n    \"\"\"Quantify IHC staining using color deconvolution.\n\n    Separates DAB (brown) from hematoxylin (blue) using\n    Ruifrok & Johnston color deconvolution.\n    \"\"\"\n    image = skimage.io.imread(image_path)\n\n    # Color deconvolution: RGB → HED (Hematoxylin, Eosin, DAB)\n    hed = rgb2hed(image)\n    dab_channel = hed[:, :, 2]  # DAB is channel 2\n    hematoxylin = hed[:, :, 0]\n\n    # Threshold to identify tissue\n    tissue_mask = hematoxylin > 0.05\n\n    # DAB intensity classification for H-score\n    dab_tissue = dab_channel[tissue_mask]\n\n    # Classify: negative (0), weak (1+), moderate (2+), strong (3+)\n    negative = (dab_tissue \u003C 0.1).sum()\n    weak = ((dab_tissue >= 0.1) & (dab_tissue \u003C 0.2)).sum()\n    moderate = ((dab_tissue >= 0.2) & (dab_tissue \u003C 0.4)).sum()\n    strong = (dab_tissue >= 0.4).sum()\n    total = len(dab_tissue)\n\n    # H-score: 1*(% weak) + 2*(% moderate) + 3*(% strong)\n    h_score = (1 * weak + 2 * moderate + 3 * strong) \u002F total * 100\n\n    # Positive pixel percentage\n    positive_pct = 100 * (weak + moderate + strong) \u002F total\n\n    print(f\"H-score: {h_score:.1f} (range 0-300)\")\n    print(f\"Positive pixels: {positive_pct:.1f}%\")\n    print(f\"  Weak (1+): {100*weak\u002Ftotal:.1f}%\")\n    print(f\"  Moderate (2+): {100*moderate\u002Ftotal:.1f}%\")\n    print(f\"  Strong (3+): {100*strong\u002Ftotal:.1f}%\")\n\n    return {\n        'h_score': h_score,\n        'positive_pct': positive_pct,\n        'negative_pct': 100 * negative \u002F total,\n        'weak_pct': 100 * weak \u002F total,\n        'moderate_pct': 100 * moderate \u002F total,\n        'strong_pct': 100 * strong \u002F total\n    }\n```\n\n### 5. Antibody Titer Analysis\n\nDetermine endpoint titers from serial dilution ELISA.\n\n```python\nimport numpy as np\nfrom scipy import stats\n\ndef calculate_endpoint_titer(dilutions, od_values, cutoff_method='mean_plus_3sd',\n                              negative_ods=None):\n    \"\"\"Determine endpoint antibody titer from serial dilution ELISA.\n\n    Args:\n        dilutions: list of dilution factors (e.g., [100, 200, 400, ...])\n        od_values: list of OD values at each dilution\n        negative_ods: list of negative control OD values\n    \"\"\"\n    if negative_ods is None:\n        cutoff = 0.1  # Default cutoff\n    elif cutoff_method == 'mean_plus_3sd':\n        cutoff = np.mean(negative_ods) + 3 * np.std(negative_ods)\n    elif cutoff_method == 'mean_plus_2sd':\n        cutoff = np.mean(negative_ods) + 2 * np.std(negative_ods)\n    else:\n        cutoff = 0.1\n\n    # Find last dilution above cutoff\n    endpoint = None\n    for dil, od in zip(dilutions, od_values):\n        if od > cutoff:\n            endpoint = dil\n\n    if endpoint is None:\n        return {'titer': '\u003C' + str(min(dilutions)), 'cutoff': cutoff}\n\n    return {\n        'titer': endpoint,\n        'log2_titer': np.log2(endpoint),\n        'cutoff': cutoff\n    }\n\ndef geometric_mean_titer(titers):\n    \"\"\"Calculate geometric mean titer from multiple samples.\"\"\"\n    log_titers = np.log2([t for t in titers if t > 0])\n    gmt = 2 ** np.mean(log_titers)\n    ci = stats.t.interval(0.95, df=len(log_titers)-1,\n                          loc=np.mean(log_titers),\n                          scale=stats.sem(log_titers))\n    return {\n        'gmt': gmt,\n        'ci_lower': 2 ** ci[0],\n        'ci_upper': 2 ** ci[1],\n        'n': len(log_titers)\n    }\n\n# Example\ndilutions = [100, 200, 400, 800, 1600, 3200, 6400, 12800]\nsample_ods = [2.1, 1.8, 1.4, 0.9, 0.45, 0.18, 0.08, 0.05]\nneg_ods = [0.06, 0.05, 0.07, 0.04]\n\nresult = calculate_endpoint_titer(dilutions, sample_ods, negative_ods=neg_ods)\nprint(f\"Endpoint titer: 1:{result['titer']}\")\nprint(f\"Cutoff OD: {result['cutoff']:.3f}\")\n```\n\n### 6. Cell Cycle Phase Duration\n\nEstimate phase durations from dual-nucleoside pulse labeling.\n\n```python\nimport numpy as np\n\ndef estimate_phase_durations(labeled_fractions, pulse_interval_hours,\n                              total_cycle_time=None):\n    \"\"\"Estimate cell cycle phase durations from dual-nucleoside labeling.\n\n    Args:\n        labeled_fractions: dict of timepoints -> fraction labeled\n        pulse_interval_hours: time between pulses\n        total_cycle_time: if known, constrains the estimates\n    \"\"\"\n    times = sorted(labeled_fractions.keys())\n    fractions = [labeled_fractions[t] for t in times]\n\n    # S-phase duration estimate: fraction labeled at first timepoint * total cycle time\n    # If total cycle time unknown, estimate from growth rate\n    if total_cycle_time is None:\n        # Assume exponential growth, estimate from labeling kinetics\n        # Rate of increase in labeled fraction approximates 1\u002FTc\n        if len(fractions) > 1:\n            rate = (fractions[-1] - fractions[0]) \u002F (times[-1] - times[0])\n            total_cycle_time = 1 \u002F rate if rate > 0 else 24\n        else:\n            total_cycle_time = 24  # Default\n\n    s_phase = fractions[0] * total_cycle_time\n    g2m_phase = pulse_interval_hours  # Time for labeled cells to reach mitosis\n\n    # G1 = total - S - G2\u002FM\n    g1_phase = total_cycle_time - s_phase - g2m_phase\n    g1_phase = max(g1_phase, 0)\n\n    return {\n        'total_cycle': total_cycle_time,\n        'G1': g1_phase,\n        'S': s_phase,\n        'G2_M': g2m_phase\n    }\n```\n\n### 7. Cytokine Analysis\n\nProcess multiplex cytokine assay data.\n\n```python\nimport numpy as np\nimport pandas as pd\nfrom scipy.optimize import curve_fit\n\ndef process_multiplex_cytokines(plate_data, analytes, standard_curves):\n    \"\"\"Process multiplex cytokine assay (Luminex\u002FMSD) data.\n\n    Args:\n        plate_data: DataFrame with well, sample, analyte, MFI columns\n        analytes: list of analyte names\n        standard_curves: dict of analyte -> (concentrations, MFI_values)\n    \"\"\"\n    def five_pl(x, a, b, c, d, g):\n        return d + (a - d) \u002F (1 + (x \u002F c) ** b) ** g\n\n    results = []\n    for analyte in analytes:\n        # Fit standard curve\n        conc, mfi = standard_curves[analyte]\n        try:\n            popt, _ = curve_fit(five_pl, conc[conc > 0], mfi[conc > 0],\n                               p0=[min(mfi), 1, np.median(conc), max(mfi), 1],\n                               maxfev=10000)\n        except RuntimeError:\n            # Fall back to 4PL\n            popt = None\n\n        # Interpolate unknowns\n        analyte_data = plate_data[plate_data['analyte'] == analyte]\n        for _, row in analyte_data.iterrows():\n            if popt is not None:\n                # Inverse 5PL\n                try:\n                    from scipy.optimize import brentq\n                    conc_val = brentq(lambda x: five_pl(x, *popt) - row['MFI'],\n                                     0.01, 100000)\n                except ValueError:\n                    conc_val = np.nan\n            else:\n                conc_val = np.nan\n\n            results.append({\n                'sample': row['sample'],\n                'analyte': analyte,\n                'MFI': row['MFI'],\n                'concentration': conc_val\n            })\n\n    return pd.DataFrame(results)\n```\n\n## Typical Workflows\n\n### Workflow 1: Process ELISA Plate Data with 4PL Standard Curve\n\n```python\nstandards = {0: [0.05], 15.6: [0.12, 0.13], 31.25: [0.22, 0.24],\n             62.5: [0.45, 0.47], 125: [0.82, 0.85], 250: [1.35, 1.38],\n             500: [1.85, 1.88], 1000: [2.15, 2.18]}\nunknowns = {'Sample_A': [0.68, 0.72], 'Sample_B': [1.45, 1.50]}\nresults, params, r2 = process_elisa_plate(standards, unknowns, blank_od=0.05)\nprint(results)\n```\n\n### Workflow 2: Quantify IHC Staining Intensity (H-Score)\n\n```python\nresult = quantify_ihc('ihc_slide.tif')\nprint(f\"H-score: {result['h_score']:.1f}\")\nprint(f\"Positive: {result['positive_pct']:.1f}%\")\n```\n\n### Workflow 3: ATAC-seq Peak Calling and Differential Accessibility\n\n```python\n# Call peaks for each condition\npeaks_ctrl = run_macs2_atacseq('control.bam', 'control')\npeaks_treat = run_macs2_atacseq('treated.bam', 'treated')\n\n# Parse and analyze\nctrl_df = parse_broadpeak(peaks_ctrl)\ntreat_df = parse_broadpeak(peaks_treat)\nprint(f\"Control peaks: {len(ctrl_df)}, Treatment peaks: {len(treat_df)}\")\n```\n\n## Best Practices\n\n1. **ELISA standard curves** — always run standards in duplicate; verify R² > 0.99 for 4PL fit; samples outside curve range should be re-run at appropriate dilutions\n2. **IHC quantification** — use color deconvolution (not simple thresholding) for DAB separation; calibrate thresholds for weak\u002Fmoderate\u002Fstrong on control tissue\n3. **ATAC-seq** — use paired-end mode (`-f BAMPE`); shift reads to account for Tn5 insertion; call narrow peaks for TF footprinting, broad peaks for open chromatin\n4. **Antibody titers** — use geometric mean for group statistics (titers are log-distributed); calculate seroconversion as 4-fold rise\n5. **Cell tracking** — require minimum track length (>10 frames) to exclude artifacts; validate tracking by visual inspection of overlaid tracks\n6. **Multiplex assays** — check for cross-reactivity; flag analytes with CV >15% between replicates\n\n## Troubleshooting\n\n**Problem:** 4PL fit fails to converge\n**Solution:** Check that standard curve spans the expected range. Adjust initial parameters (p0). Ensure blank subtraction is correct. Try 5PL for asymmetric curves.\n\n**Problem:** IHC color deconvolution gives poor separation\n**Solution:** Verify image is RGB (not grayscale). Check that staining uses standard DAB\u002Fhematoxylin. Adjust HED matrix if using non-standard chromogens.\n\n**Problem:** ATAC-seq produces too many peaks\n**Solution:** Increase `-q` (q-value) threshold. Filter for peaks with fold enrichment >2. Check for high mitochondrial read fraction (indicates poor quality).\n\n**Problem:** Antibody titer is below lowest dilution\n**Solution:** Use lower starting dilution. Report as \"\u003C [lowest dilution]\". Check that positive control shows expected titer.\n\n## Resources\n\n- [ELISA Technical Guide (Thermo Fisher)](https:\u002F\u002Fwww.thermofisher.com\u002Fus\u002Fen\u002Fhome\u002Flife-science\u002Fprotein-biology\u002Fprotein-biology-learning-center\u002Fprotein-biology-resource-library\u002Fpierce-protein-methods\u002Felisa-technical-guide.html)\n- [MACS2 Documentation](https:\u002F\u002Fgithub.com\u002Fmacs3-project\u002FMACS)\n- [HOMER Motif Analysis](http:\u002F\u002Fhomer.ucsd.edu\u002Fhomer\u002F)\n- [H-score Calculation Guidelines](https:\u002F\u002Fdoi.org\u002F10.1007\u002F978-1-4939-1124-0_6)\n",{"name":5,"license":44,"category":27,"metadata":45,"description":7},"MIT license",{"skill-author":46},"InkVell Inc.","main","4aa39efd1cdab0b4657ecee16c92a92011a5b10f","openscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub","https:\u002F\u002Fgithub.com\u002Ffadrienne","2026-10-28 00:43:09",{"source":8,"skillName":5,"commands":54,"prompt":64,"zipAvailable":65},[55,58,61],{"tool":56,"command":57},"npx","npx skills add fadrienne\u002FMyGitHub --skill immunology-assays",{"tool":59,"command":60},"bunx","bunx skills add fadrienne\u002FMyGitHub --skill immunology-assays",{"tool":62,"command":63},"pnpm","pnpm dlx skills add fadrienne\u002FMyGitHub --skill immunology-assays","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays\u002FSKILL.md",true,[67,73,77,81,85],{"path":68,"resourceType":69,"resourceTypeDesc":70,"sizeBytes":71,"sourceHtmlUrl":72},"scripts\u002Fantibody_titer.py","SCRIPT","脚本",10728,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays\u002Fscripts\u002Fantibody_titer.py",{"path":74,"resourceType":69,"resourceTypeDesc":70,"sizeBytes":75,"sourceHtmlUrl":76},"scripts\u002Fatac_peaks.py",12799,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays\u002Fscripts\u002Fatac_peaks.py",{"path":78,"resourceType":69,"resourceTypeDesc":70,"sizeBytes":79,"sourceHtmlUrl":80},"scripts\u002Fprocess_elisa.py",12109,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays\u002Fscripts\u002Fprocess_elisa.py",{"path":82,"resourceType":69,"resourceTypeDesc":70,"sizeBytes":83,"sourceHtmlUrl":84},"scripts\u002Fquantify_ihc.py",13271,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays\u002Fscripts\u002Fquantify_ihc.py",{"path":86,"resourceType":69,"resourceTypeDesc":70,"sizeBytes":87,"sourceHtmlUrl":88},"scripts\u002Ftrack_immune_cells.py",17679,"https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fopenscience\u002Fbackend\u002Fcli\u002Fskills\u002Fbiology\u002Fimmunology-assays\u002Fscripts\u002Ftrack_immune_cells.py",[90,121,144,178,205,232],{"skillNo":91,"name":92,"nameZh":6,"description":93,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":94,"categories":95,"keywords":101,"publishedAt":39},"01M7167B28PK20HP7R7ZED2DY0","statistical-power","Sample size calculation, power analysis, and effect size conversion for scientific studies. Use BEFORE data collection (a priori) to determine required N, or after (post-hoc) to compute achieved power. Covers parametric tests (t-test, ANOVA, chi-square, proportions, correlation, regression), simulation-based power for complex\u002Fclustered\u002Fnested designs, and power curves. Use this skill when someone asks 'how many samples do I need', 'what is my power', 'what effect size can I detect', or any variant. Pairs with \u002Fexperimental-design (choose design first, then compute power) and \u002Fstatistical-analysis (analyze data after collection).","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fscientific-agent-skills\u002Fskills\u002Fstatistical-power\u002FSKILL.md",[96],{"categoryNo":97,"categoryName":98,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000023","Skill与智能体开发","SCENARIO","使用场景",[102,103,104,105,106,107,108,109,110,111,112,36,113,114,115,116,117,118,119,120],"power","statistical power","effect","scipy","regression","choose","design","uv","when","Bash","collection","calculation","ANOVA","conversion","analyze","t-test","chi-square","simulation","sample",{"skillNo":122,"name":123,"nameZh":6,"description":124,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":125,"categories":126,"keywords":130,"publishedAt":39},"01M71677EBCM8EPCE9BZCK8505","statistical-analysis","Guided statistical analysis with test selection and reporting. Use when you need help choosing appropriate tests for your data, assumption checking, power analysis, and APA-formatted results. Best for academic research reporting, test selection guidance. For implementing specific models programmatically use statsmodels.","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fscientific-agent-skills\u002Fskills\u002Fstatistical-analysis\u002FSKILL.md",[127],{"categoryNo":128,"categoryName":129,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000008","测试与质量",[36,131,102,132,133,134,110,135,136,37,23,137,33,25,138,139,140,141,142,143],"statistical analysis","academic research","implementing","selection","you","reporting","tests","test","research","results","statsmodels","models","help",{"skillNo":145,"name":146,"nameZh":6,"description":147,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":148,"categories":149,"keywords":159,"publishedAt":39},"01M71673R93XAHFH81DBNVNG7H","scientific-writing","Core skill for the deep research and writing tool. Write scientific manuscripts in full paragraphs (never bullet points). Use two-stage process with (1) section outlines with key points using research-lookup then (2) convert to flowing prose. IMRAD structure, citations (APA\u002FAMA\u002FVancouver), figures\u002Ftables, reporting guidelines (CONSORT\u002FSTROBE\u002FPRISMA), for research papers and journal submissions.","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fscientific-agent-skills\u002Fskills\u002Fscientific-writing\u002FSKILL.md",[150,153,154],{"categoryNo":151,"categoryName":152,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000018","写作与文案",{"categoryNo":97,"categoryName":98,"taxonomyType":99,"taxonomyTypeDesc":100},{"categoryNo":155,"categoryName":156,"taxonomyType":157,"taxonomyTypeDesc":158},"01JBSEEDCAT000000000000036","内容创作者","PROFESSION","适用职业",[160,161,162,163,164,165,166,167,168,169,111,170,171,172,173,174,175,176,136,177],"writing","scientific","scientific writing","Citations","deep","deep research","never","papers","journal","false","api","prisma","structure","openrouter","Outlines","core skill","read","features",{"skillNo":179,"name":180,"nameZh":6,"description":181,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":182,"categories":183,"keywords":188,"publishedAt":39},"01M7166ZPKXBF4EBPHVTJPQBB1","scientific-visualization","Meta-skill for publication-ready figures. Use when creating journal submission figures requiring multi-panel layouts, significance annotations, error bars, colorblind-safe palettes, and specific journal formatting (Nature, Science, Cell). Orchestrates matplotlib\u002Fseaborn\u002Fplotly with publication styles. For quick exploration use seaborn or plotly directly.","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fscientific-agent-skills\u002Fskills\u002Fscientific-visualization\u002FSKILL.md",[184,187],{"categoryNo":185,"categoryName":186,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000013","分析与可视化",{"categoryNo":97,"categoryName":98,"taxonomyType":99,"taxonomyTypeDesc":100},[189,161,190,191,192,193,194,195,196,168,110,197,198,199,200,201,202,203,23,204],"visualization","scientific visualization","error","ready","quick","exploration","meta-skill","Meta","safe","plotly","annotations","formatting","Publication","figures","panel","submission",{"skillNo":206,"name":207,"nameZh":6,"description":208,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":209,"categories":210,"keywords":217,"publishedAt":39},"01M7166VXJWQSSC3RQZP60WBGR","research-code","Debug, profile, review, and restructure Python research and analysis code. Use when analysis scripts crash, produce wrong results, run too slowly, or need a correctness review. Covers: reading tracebacks and diagnosing root causes, hypothesis-driven debugging, reviewing code for common statistical\u002Fnumerical errors, profiling data pipelines, improving reproducibility (seeds, environments, determinism), and structuring analysis scripts for sharing. Distinct from \u002Fexploratory-data-analysis (which understands data) and \u002Fstatistical-analysis (which chooses and runs tests) — this skill focuses on the code itself.","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fscientific-agent-skills\u002Fskills\u002Fresearch-code\u002FSKILL.md",[211,214],{"categoryNo":212,"categoryName":213,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000021","资料检索与研究",{"categoryNo":215,"categoryName":216,"taxonomyType":157,"taxonomyTypeDesc":158},"01JBSEEDCAT000000000000030","开发工程师",[218,219,139,220,221,222,223,109,224,110,111,36,225,226,227,228,229,230,176,231],"code","research code","which","memory","data-analysis","errors","debugging","common","review","data pipelines","run","scripts","line","write",{"skillNo":233,"name":234,"nameZh":6,"description":235,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":236,"licenseFileUrl":6,"sourceHtmlUrl":237,"categories":238,"keywords":245,"publishedAt":39},"01M7166CF5HSR7SVZYZFWTDX55","markdown-mermaid-writing","Comprehensive markdown and Mermaid diagram writing skill. Use when creating any scientific document, report, analysis, or visualization. Establishes text-based diagrams as the default documentation standard with full style guides (markdown + mermaid), 24 diagram type references, and 9 document templates.","Apache-2.0","https:\u002F\u002Fgithub.com\u002Ffadrienne\u002FMyGitHub\u002Fblob\u002Fmain\u002Fscientific-agent-skills\u002Fskills\u002Fmarkdown-mermaid-writing\u002FSKILL.md",[239,242,243,244],{"categoryNo":240,"categoryName":241,"taxonomyType":99,"taxonomyTypeDesc":100},"01JBSEEDCAT000000000000002","文档处理",{"categoryNo":151,"categoryName":152,"taxonomyType":99,"taxonomyTypeDesc":100},{"categoryNo":97,"categoryName":98,"taxonomyType":99,"taxonomyTypeDesc":100},{"categoryNo":155,"categoryName":156,"taxonomyType":157,"taxonomyTypeDesc":158},[160,246,247,248,249,250,189,251,252,110,111,36,253,254,255,256,257,258,161,176],"markdown mermaid writing","mermaid","markdown","community","diagrams","project","default","team","guides","text","documentation","references","diagram"]