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.

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 with r-squared

    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:
        # Try both directions, return best fit
        return _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:
        # Fit with specified direction
        return _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,
        )

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 with r-squared

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