Module sprime.hill_fitting

Hill curve fitting module for sprime.

Implements a linear-x four-parameter logistic (linear-x 4PL) model: the independent variable is concentration x on a linear scale, entering as (x / EC50) in the denominator. This matches forms such as AAT Bioquest and generic (x/C)^B calculators.

Industry note: "Hill equation" and "4PL" are not standardized. Many tools (e.g. SigmaPlot, GraphPad-style log-dose forms) use a log-x 4PL where the dose axis is log10(concentration), which yields different Hill slope sign conventions for the same curve. Our slope values are numerically consistent with the linear-x parameterization; compare log-x vs linear-x in docs/background/README_4PL_Dose_Response.md#linear-x-vs-log-x-4pl-hill-slope.

Naming: The exponent n in (x/EC50)^n is exposed as steepness_coefficient (and initial_steepness_coefficient for guesses). That is the same role as the classical Hill coefficient n in linear-x Hill formulations; we avoid the name hill_coefficient here so it is not confused with log-x "Hill slope" outputs from other packages.

Sign convention: After fitting, parameters are always returned in canonical form using Kendall concordance over all dose-response pairs to detect the biological curve direction. For inhibitory curves (response decreases with dose), this guarantees zero_asymptote > inf_asymptote and therefore S' > 0. For disinhibitory curves, zero_asymptote < inf_asymptote and S' < 0. This prevents the degenerate negative-n parameterization that scipy can converge to for steep inhibitory curves, which would otherwise silently flip the sign of S'.

Adapted to work with sprime's domain entities.

Functions

def fit_hill_curve(concentrations: List[float],
responses: List[float],
*,
initial_zero_asymptote: float | None = None,
initial_inf_asymptote: float | None = None,
initial_ec50: float | None = None,
initial_steepness_coefficient: float | None = None,
curve_direction: str | None = None,
maxfev: int = 3000000,
zero_replacement: float = 1e-24,
bounds: Tuple[List[float], List[float]] | None = None,
**curve_fit_kwargs)
Expand source code
def fit_hill_curve(
    concentrations: List[float],
    responses: List[float],
    *,
    # Initial parameter guesses (all optional with defaults)
    initial_zero_asymptote: Optional[float] = None,
    initial_inf_asymptote: Optional[float] = None,
    initial_ec50: Optional[float] = None,
    initial_steepness_coefficient: Optional[float] = None,
    # Curve direction
    curve_direction: Optional[str] = None,  # "up", "down", or None for auto-detect
    # Optimization parameters
    maxfev: int = 3000000,
    # Zero concentration handling
    zero_replacement: float = 1e-24,
    # Parameter bounds (optional)
    bounds: Optional[Tuple[List[float], List[float]]] = None,
    # Additional scipy.optimize.curve_fit parameters
    **curve_fit_kwargs,
):
    """
    Fit four-parameter Hill equation to dose-response data.

    Fits a sigmoidal curve to concentration-response data and returns
    HillCurveParams with fitted parameters.

    Args:
        concentrations: List of concentration values
        responses: List of response values (must match length of concentrations)
        initial_zero_asymptote: Initial guess for zero asymptote (default: auto-estimated)
        initial_inf_asymptote: Initial guess for inf asymptote (default: auto-estimated)
        initial_ec50: Initial guess for EC50 (default: auto-estimated)
        initial_steepness_coefficient: Initial guess for steepness coefficient n (default: auto-estimated)
        curve_direction: Curve direction - "up" (increasing), "down" (decreasing),
                        or None for auto-detect (tries both, selects best r-squared)
        maxfev: Maximum function evaluations for optimization (default: 3,000,000)
        zero_replacement: Value to replace zero concentrations (default: 1e-24)
        bounds: Optional parameter bounds as (lower_bounds, upper_bounds) tuples
                Format: ([zero_asymptote_min, steepness_min, ec50_min, inf_asymptote_min],
                        [zero_asymptote_max, steepness_max, ec50_max, inf_asymptote_max])
        **curve_fit_kwargs: Additional arguments passed to scipy.optimize.curve_fit

    Returns:
        HillCurveParams: Fitted curve parameters. steepness_coefficient sign and thus S' direction
        are determined by Kendall concordance: inhibitory curves (positive S') or disinhibitory
        curves (negative S').

    Raises:
        ValueError: If inputs are invalid
        RuntimeError: If curve fitting fails
        ImportError: If numpy/scipy are not installed
    """
    if np is None or curve_fit is None:
        raise ImportError("Hill curve fitting requires scipy. " "Install with: pip install scipy")

    # Validate inputs
    if len(concentrations) != len(responses):
        raise ValueError("Concentrations and responses must have same length")

    if len(concentrations) < 4:
        raise ValueError("Need at least 4 data points to fit 4-parameter Hill equation")

    # Convert to numpy arrays (make copies to avoid modifying originals)
    concentrations = list(concentrations)
    responses = list(responses)

    # Sort data if needed (ascending concentrations)
    if concentrations[0] > concentrations[-1]:
        concentrations.reverse()
        responses.reverse()

    # Handle zero concentrations
    if concentrations[0] == 0:
        concentrations[0] = zero_replacement

    x_data = np.array(concentrations)
    y_data = np.array(responses)

    # Auto-detect or use specified curve direction
    if curve_direction is None:
        result = _fit_with_auto_direction(
            x_data,
            y_data,
            initial_zero_asymptote,
            initial_inf_asymptote,
            initial_ec50,
            initial_steepness_coefficient,
            maxfev,
            bounds,
            **curve_fit_kwargs,
        )
    else:
        result = _fit_single_direction(
            x_data,
            y_data,
            curve_direction,
            initial_zero_asymptote,
            initial_inf_asymptote,
            initial_ec50,
            initial_steepness_coefficient,
            maxfev,
            bounds,
            **curve_fit_kwargs,
        )

    # Enforce canonical sign convention regardless of which scipy basin was found.
    # Inhibitory (data goes down) -> zero > inf -> S' positive.
    # Activatory (data goes up)   -> zero < inf -> S' negative.
    return _normalize_direction(result, y_data)

Fit four-parameter Hill equation to dose-response data.

Fits a sigmoidal curve to concentration-response data and returns HillCurveParams with fitted parameters.

Args

concentrations
List of concentration values
responses
List of response values (must match length of concentrations)
initial_zero_asymptote
Initial guess for zero asymptote (default: auto-estimated)
initial_inf_asymptote
Initial guess for inf asymptote (default: auto-estimated)
initial_ec50
Initial guess for EC50 (default: auto-estimated)
initial_steepness_coefficient
Initial guess for steepness coefficient n (default: auto-estimated)
curve_direction
Curve direction - "up" (increasing), "down" (decreasing), or None for auto-detect (tries both, selects best r-squared)
maxfev
Maximum function evaluations for optimization (default: 3,000,000)
zero_replacement
Value to replace zero concentrations (default: 1e-24)
bounds
Optional parameter bounds as (lower_bounds, upper_bounds) tuples Format: ([zero_asymptote_min, steepness_min, ec50_min, inf_asymptote_min], [zero_asymptote_max, steepness_max, ec50_max, inf_asymptote_max])
**curve_fit_kwargs
Additional arguments passed to scipy.optimize.curve_fit

Returns

HillCurveParams
Fitted curve parameters. steepness_coefficient sign and thus S' direction
are determined by Kendall concordance
inhibitory curves (positive S') or disinhibitory

curves (negative S').

Raises

ValueError
If inputs are invalid
RuntimeError
If curve fitting fails
ImportError
If numpy/scipy are not installed
def hill_equation(x,
zero_asymptote: float,
steepness_coefficient: float,
ec50: float,
inf_asymptote: float)
Expand source code
def hill_equation(
    x, zero_asymptote: float, steepness_coefficient: float, ec50: float, inf_asymptote: float
):
    """
    Linear-x four-parameter logistic (linear-x 4PL) Hill form.

    Response vs concentration x (linear scale, same units as EC50):

        y = inf_asymptote + (zero_asymptote - inf_asymptote) / (1 + (x / C)^n)

    At concentration approaching zero, y approaches zero_asymptote; at saturating concentration,
    y approaches inf_asymptote. C = ec50, n = steepness_coefficient. Signed n encodes curve direction
    together with asymptote ordering; this is not interchangeable with log-x 4PL slope reporting.

    Args:
        x: Concentration values (linear scale)
        zero_asymptote: Response as concentration -> 0 (left side of dose axis)
        steepness_coefficient: Exponent n in (x/C)^n. Conceptually the same quantity often called
            the **Hill coefficient** in linear-x dose-response formulations (cooperativity exponent);
            we use ``steepness_coefficient`` here to avoid confusion with log-x tools that report a
            different "Hill slope" (see module docstring).
        ec50: Half-maximal concentration C
        inf_asymptote: Response at saturating concentration (right of dose axis)

    Returns:
        Response values
    """
    return inf_asymptote + (zero_asymptote - inf_asymptote) / (
        1 + (x / ec50) ** steepness_coefficient
    )

Linear-x four-parameter logistic (linear-x 4PL) Hill form.

Response vs concentration x (linear scale, same units as EC50):

y = inf_asymptote + (zero_asymptote - inf_asymptote) / (1 + (x / C)^n)

At concentration approaching zero, y approaches zero_asymptote; at saturating concentration, y approaches inf_asymptote. C = ec50, n = steepness_coefficient. Signed n encodes curve direction together with asymptote ordering; this is not interchangeable with log-x 4PL slope reporting.

Args

x
Concentration values (linear scale)
zero_asymptote
Response as concentration -> 0 (left side of dose axis)
steepness_coefficient
Exponent n in (x/C)^n. Conceptually the same quantity often called the Hill coefficient in linear-x dose-response formulations (cooperativity exponent); we use steepness_coefficient here to avoid confusion with log-x tools that report a different "Hill slope" (see module docstring).
ec50
Half-maximal concentration C
inf_asymptote
Response at saturating concentration (right of dose axis)

Returns

Response values