Skip to content

Configuration API

PlotFilterConfig

PlotFilterConfig controls which plots are considered relevant for performance linking. Use the factory class methods to get pre-configured instances for your domain.

PlotFilterConfig

Bases: BaseModel

Configuration for filtering plots based on axis characteristics.

This allows domain-specific customization of what constitutes a "relevant" plot for performance data extraction.

Attributes:

Name Type Description
x_axis_labels list[str]

Labels that indicate a relevant x-axis (case-insensitive)

x_axis_units list[str]

Units that indicate a relevant x-axis (case-insensitive)

y_axis_keywords list[str]

Keywords in y-axis label indicating performance metrics

y_axis_units list[str]

Units that suggest performance data (e.g., "%")

require_y_keyword_with_percentage bool

If True, % unit alone is not enough; the label must also contain a y_axis_keyword

filter_x_axis bool

Whether to apply x-axis filtering

filter_y_axis bool

Whether to apply y-axis filtering

Methods:

is_relevant_x_axis(label, unit)

Check if x-axis indicates a relevant plot.

Parameters:

Name Type Description Default
label str | None

X-axis label (e.g., "Temperature")

required
unit str | None

X-axis unit (e.g., "°C")

required

Returns:

Type Description
bool

True if x-axis matches configured criteria

Source code in src/llm_synthesis/config/plot_filter_config.py
def is_relevant_x_axis(self, label: str | None, unit: str | None) -> bool:
    """Check if x-axis indicates a relevant plot.

    Args:
        label: X-axis label (e.g., "Temperature")
        unit: X-axis unit (e.g., "°C")

    Returns:
        True if x-axis matches configured criteria
    """
    if not self.filter_x_axis:
        return True

    label_lower = self._normalize_axis_text((label or "").lower().strip())
    unit_lower = self._normalize_axis_text((unit or "").lower().strip())

    # Check if label contains any configured labels (substring match)
    if any(t in label_lower for t in self.x_axis_labels):
        return True

    # Check if unit matches any configured units (exact match for units)
    if any(u == unit_lower for u in self.x_axis_units):
        return True

    return False

is_relevant_y_axis(label, unit)

Check if y-axis indicates a performance metric.

Parameters:

Name Type Description Default
label str | None

Y-axis label (e.g., "Conversion")

required
unit str | None

Y-axis unit (e.g., "%")

required

Returns:

Type Description
bool

True if y-axis matches configured criteria

Source code in src/llm_synthesis/config/plot_filter_config.py
def is_relevant_y_axis(self, label: str | None, unit: str | None) -> bool:
    """Check if y-axis indicates a performance metric.

    Args:
        label: Y-axis label (e.g., "Conversion")
        unit: Y-axis unit (e.g., "%")

    Returns:
        True if y-axis matches configured criteria
    """
    if not self.filter_y_axis:
        return True

    label_lower = self._normalize_axis_text((label or "").lower().strip())
    unit_lower = self._normalize_axis_text((unit or "").lower().strip())

    # If y-axis is completely empty, we can't verify — reject it
    if not label_lower and not unit_lower:
        return False

    # Check exclusion patterns first — reject derived/difference plots
    if any(pat in label_lower for pat in self.y_axis_exclude_patterns):
        return False

    # Check for conversion keywords in label
    has_keyword = any(kw in label_lower for kw in self.y_axis_keywords)

    # Check for conversion symbol (e.g., "X" for conversion)
    is_x_symbol = label_lower in self.x_symbol_exact or any(
        label_lower.startswith(p) for p in self.x_symbol_prefixes
    )

    # Check for percentage unit
    has_percentage_unit = unit_lower in self.y_axis_units

    # Main logic
    if has_percentage_unit:
        if self.require_y_keyword_with_percentage:
            # % unit requires keyword/symbol confirmation
            return has_keyword or is_x_symbol
        else:
            # % unit alone is sufficient
            return True

    # Keywords/symbols alone are sufficient
    return has_keyword or is_x_symbol

for_catalysis() classmethod

Factory method for catalysis domain (default configuration).

Source code in src/llm_synthesis/config/plot_filter_config.py
@classmethod
def for_catalysis(cls) -> "PlotFilterConfig":
    """Factory method for catalysis domain (default configuration)."""
    return cls()

for_electrochemistry() classmethod

Factory method for electrochemistry domain.

Source code in src/llm_synthesis/config/plot_filter_config.py
@classmethod
def for_electrochemistry(cls) -> "PlotFilterConfig":
    """Factory method for electrochemistry domain."""
    return cls(
        x_axis_labels=["potential", "voltage", "e", "v"],
        x_axis_units=["v", "mv", "v vs. rhe", "v vs rhe"],
        y_axis_keywords=["current", "capacitance", "capacity", "coulombic"],
        y_axis_units=["%", "percent", "ma", "a", "f/g", "mah/g"],
        require_y_keyword_with_percentage=False,
    )

for_superconductivity() classmethod

Factory method for superconductivity domain (R(T) plots).

Source code in src/llm_synthesis/config/plot_filter_config.py
@classmethod
def for_superconductivity(cls) -> "PlotFilterConfig":
    """Factory method for superconductivity domain (R(T) plots)."""
    return cls(
        x_axis_labels=[
            "temperature",
            "temp",
            "t (k)",
            "t(k)",
            "t [k]",
            "t[k]",
        ],
        x_axis_units=["k", "°k", "kelvin"],
        y_axis_keywords=[
            "resistance",
            "resistivity",
            "r(t)",
            "r/r",
            "ρ",
            "rho",
            "normalized resistance",
            "r (ω",
            "r (m",
            "r (μ",
            "r [ω",
            "r [m",
            "r [μ",
            "ρ (",
            "ρ [",
            "ρ/ρ",
        ],
        y_axis_units=[
            "ω",
            "ohm",
            "mω",
            "μω",
            "ω·cm",
            "μω·cm",
            "mω·cm",
            "ω cm",
            "μω cm",
            "mω cm",
            "ωcm",
            "μωcm",
            "mωcm",
            "ω⋅cm",
            "μω⋅cm",
            "mω⋅cm",
            "a.u.",
        ],
        y_axis_exclude_patterns=[
            # Difference / subtracted quantities
            "ρ-ρ",
            "r-r",
            "ρ−ρ",
            "r−r",  # minus sign variants
            "ρ - ρ",
            "r - r",  # spaced minus
            "δρ",
            "δr",
            "Δρ",
            "Δr",  # delta variants
            # Derivatives
            "dρ/dt",
            "dr/dt",
            "dρ/d",
            "dr/d",
            # Ratio to residual resistivity (but NOT normalized to room
            # temp)
            # "ρ/ρ₀", "r/r₀", "r/r0" are residual-ratio plots (not useful)
            # "ρ/ρ₃₀₀" or "r/r(300)" are room-temp-normalized R(T) (useful!)
            "ρ/ρ₀",
            "ρ/ρ0",
            "r/r₀",
            "r/r0",
        ],
        require_y_keyword_with_percentage=False,
    )

for_coverage() classmethod

Factory method for porous materials (adsorption isotherm plots).

Source code in src/llm_synthesis/config/plot_filter_config.py
@classmethod
def for_coverage(cls) -> "PlotFilterConfig":
    """Factory method for porous materials (adsorption isotherm plots)."""
    return cls(
        x_axis_labels=[
            "pressure",
            "p",
            "p/p0",
            "p/p₀",
            "relative pressure",
            "p (bar)",
            "p (kpa)",
            "p (mpa)",
            "p (atm)",
            "p (pa)",
            "p/p0 (atm)",
            "p [bar]",
            "p [kpa]",
            "p [atm]",
        ],
        x_axis_units=["bar", "kpa", "mpa", "atm", "pa", "p0", "p/p0"],
        y_axis_keywords=[
            "loading",
            "uptake",
            "adsorption",
            "coverage",
            "surface area",
            "amount adsorbed",
            "quantity adsorbed",
            "n",
            "q",
            "w",
            "v",
            "cm³/g",
            "cm3/g",
            "mmol/g",
            "mol/kg",
            "mg/g",
            "wt%",
            "cc/g",
        ],
        y_axis_units=[
            "mmol/g",
            "mol/kg",
            "cm³/g",
            "cm3/g",
            "cc/g",
            "mg/g",
            "wt%",
            "ml/g",
            "l/g",
            "g/g",
            "mmol g⁻¹",
            "mol kg⁻¹",
            "cm³ g⁻¹",
        ],
        y_axis_exclude_patterns=[
            "temperature",
            "time",
            "heat",
            "enthalpy",
            "selectivity",
            "permeability",
            "diffusivity",
        ],
        require_y_keyword_with_percentage=False,
    )

no_filter() classmethod

Factory method that disables all filtering (link all plots).

Source code in src/llm_synthesis/config/plot_filter_config.py
@classmethod
def no_filter(cls) -> "PlotFilterConfig":
    """Factory method that disables all filtering (link all plots)."""
    return cls(
        filter_x_axis=False,
        filter_y_axis=False,
    )

LLM registry

LLMConfig(model, api_key=None, api_base=None, extra_kwargs=None) dataclass

A configuration for an LLM to instantiate with dspy. Includes the model name, and optional API key name in the environment (e.g. "OPENAI_API_KEY") and base URL. The latter is needed to call external providers with the OpenAI API. In DSPy, you can use dozens of LLM providers supported by LiteLLM. Simply follow their instructions for which {PROVIDER}_API_KEY to set and how to write pass the {provider_name}/{model_name} to the constructor.

Parameters:

Name Type Description Default
model str

The name of the model to instantiate.

required
api_key str | None

The name of the environment variable containing the API key.

None
api_base str | None

The base URL of the API.

None
extra_kwargs dict | None

addtl model-specific parameters (e.g., thinking mode).

None

LLMRegistry(configs) dataclass

A registry of LLMs to instantiate with dspy.

Parameters:

Name Type Description Default
configs Mapping[str, LLMConfig]

A mapping of model names to LLM configurations.

required

DSPy utilities

get_llm_from_name(llm_name, model_kwargs=None, system_prompt=None)

Get a dspy.LM from a given LLM name with cost tracking capabilities.

Parameters:

Name Type Description Default
llm_name str

The name of the LLM to get. cf. LLM_REGISTRY

required
model_kwargs dict | None

A dictionary of model kwargs to pass to the LLM.

None
system_prompt str | None

A system prompt to inject at the start of every call.

None

Returns:

Type Description
LM

A dspy.LM object with cost tracking capabilities.

Source code in src/llm_synthesis/utils/dspy_utils.py
def get_llm_from_name(
    llm_name: str,
    model_kwargs: dict | None = None,
    system_prompt: str | None = None,
) -> dspy.LM:
    """
    Get a dspy.LM from a given LLM name with cost tracking capabilities.

    Args:
        llm_name: The name of the LLM to get. cf. LLM_REGISTRY
        model_kwargs: A dictionary of model kwargs to pass to the LLM.
        system_prompt: A system prompt to inject at the start of every call.

    Returns:
        A dspy.LM object with cost tracking capabilities.
    """
    # Copy so we never mutate the caller's dict and so the default cannot
    # leak across invocations.
    model_kwargs = dict(model_kwargs) if model_kwargs else {}

    try:
        cfg: LLMConfig = LLM_REGISTRY.configs[llm_name]
    except KeyError:
        available_models = list(LLM_REGISTRY.configs.keys())
        raise ValueError(
            f"LLM name {llm_name!r} not supported. "
            f"Available: {available_models}"
        )

    if cfg.api_key:
        model_kwargs["api_key"] = cfg.api_key
        model_kwargs["api_base"] = cfg.api_base

    # Merge extra_kwargs from config
    if cfg.extra_kwargs:
        model_kwargs.update(cfg.extra_kwargs)

    system_prompt = system_prompt or ""
    return SystemPrefixedLM(system_prompt, cfg.model, **model_kwargs)

configure_dspy(lm, model_kwargs=None, system_prompt=None)

Configure dspy with a selected LLM with cost tracking.

Parameters:

Name Type Description Default
lm str

LLM key to configure (cf. LLM_REGISTRY).

required
model_kwargs dict | None

Additional model kwargs (e.g., {"temperature": 0.7}).

None
system_prompt str | None

A system prompt to inject at the start of every call.

None
Source code in src/llm_synthesis/utils/dspy_utils.py
def configure_dspy(
    lm: str,
    model_kwargs: dict | None = None,
    system_prompt: str | None = None,
) -> None:
    """
    Configure dspy with a selected LLM with cost tracking.

    Args:
        lm: LLM key to configure (cf. LLM_REGISTRY).
        model_kwargs: Additional model kwargs (e.g., {"temperature": 0.7}).
        system_prompt: A system prompt to inject at the start of every call.
    """
    model_kwargs = dict(model_kwargs) if model_kwargs else {}
    dspy.settings.configure(
        track_usage=True,
        lm=get_llm_from_name(lm, model_kwargs, system_prompt),
        adapter=dspy.adapters.JSONAdapter(),
    )
    logger.info("Configured dspy with %r and model_kwargs=%s", lm, model_kwargs)