Skip to content

Index

Job management utilities for simulation servers.

Provides sample generation, output handling, and run management utilities adapted from mada-job-management jobkit.

FolderOutputHandler

Bases: BaseOutputHandler

Output handler for writing samples to a folder.

Attributes:

Name Type Description
settings_class

The settings class for this output handler.

supported_kwargs

The full set of supported keyword arguments for the write method.

required_kwargs

The set of required keyword arguments for the write method.

Methods:

Name Description
write

Writes samples to a folder structure.

Source code in src/mada_tools/simulation/simutils/samples/output/folder_output_handler.py
class FolderOutputHandler(BaseOutputHandler):
    """
    Output handler for writing samples to a folder.

    Attributes:
        settings_class: The settings class for this output handler.
        supported_kwargs: The full set of supported keyword arguments for the `write` method.
        required_kwargs: The set of required keyword arguments for the `write` method.

    Methods:
        write: Writes samples to a folder structure.
    """

    def _get_settings_class(self) -> FolderOutputSettings:
        """
        Get the settings class for this output handler.

        Returns:
            The settings class for this output handler.
        """
        return FolderOutputSettings

    def _get_supported_kwargs(self) -> Set[str]:
        """
        Get the set of supported keyword arguments for the `write` method.

        Returns:
            A set of supported keyword argument names for output configuration.
        """
        return {"output_dir", "param_file"}

    def _get_required_kwargs(self) -> Set[str]:
        """
        Get the set of required keyword arguments for the `write` method.

        Returns:
            A set of required keyword argument names for output configuration.
        """
        return {}

    def write(
        self,
        samples: np.ndarray,
        parameter_names: List[str] | None = None,
        **kwargs: Dict[str, Any],
    ) -> SampleOutputResult:
        """
        Creates a folder structure for each sample, writes the parameters to individual files,
        and dumps all `RunInstances` to a JSON file.

        Args:
            samples: A list of samples, where each sample is a list of floats.
            parameter_names: List of parameter names.
            **kwargs: Additional keyword arguments for output configuration.

        Returns:
            A `SampleOutputResult` object containing the output path, output type, and run instances.
        """
        self.check_if_samples_are_empty(samples)

        output_settings = self.build_settings_from_kwargs(kwargs)

        print(f"Creating folder structure in '{output_settings.output_dir}'...")

        # Ensure the base output directory exists
        if not os.path.exists(output_settings.output_dir):
            os.makedirs(output_settings.output_dir)
            print(f"Created base output directory: {output_settings.output_dir}")
        else:
            print(f"Base output directory already exists: {output_settings.output_dir}")

        # Create subdirectories and RunInstance objects for each sample
        padding = len(str(len(samples)))  # Determine padding for folder names (e.g., run001)
        run_instances = []  # List to store all RunInstance objects

        if parameter_names is not None:
            # Build DataFrame
            run_ids = [str(i).zfill(padding) for i in range(len(samples))]
            df = pd.DataFrame(samples, columns=parameter_names)
            df.insert(0, "run#", run_ids)

            # Save CSV
            csv_path = os.path.join(output_settings.output_dir, "run_parameters.csv")
            df.to_csv(csv_path, index=False)
            print(f"Saved run parameter summary CSV: {csv_path}\n")

        for i, sample in enumerate(samples):
            # Create a subdirectory for the sample
            sample_dir = os.path.join(output_settings.output_dir, f"run{str(i).zfill(padding)}")
            os.makedirs(sample_dir, exist_ok=True)

            # Write the sample parameters to the specified file
            param_file_path = os.path.join(sample_dir, output_settings.param_file)
            with open(param_file_path, "w") as param_file:
                if parameter_names is not None:
                    param_file.write("\n".join(f"{name}: {value}" for name, value in zip(parameter_names, sample)))
                else:
                    param_file.write("\n".join(map(str, sample)))

            # Create a RunInstance object
            run_id = str(i).zfill(padding)
            run_instance = RunInstance(run_location=sample_dir, id=run_id)
            run_instances.append(run_instance)

            print(f"Created folder: {sample_dir}")
            print(f"\tWritten parameters to: {param_file_path}")
            print(f"\tRunInstance created: {run_instance}")

        # Dump all RunInstances to a JSON file in case they need to be loaded later
        run_instances_json_path = os.path.join(output_settings.output_dir, "run_instances.json")
        with open(run_instances_json_path, "w") as json_file:
            json.dump([run.to_dict() for run in run_instances], json_file, indent=4)

        print(f"RunInstances dumped to JSON file: {run_instances_json_path}")
        print("Folder structure created successfully.")

        return SampleOutputResult(
            output_path=output_settings.output_dir,
            output_type="folders",
            run_instances=run_instances,
        )

write(samples, parameter_names=None, **kwargs)

Creates a folder structure for each sample, writes the parameters to individual files, and dumps all RunInstances to a JSON file.

Parameters:

Name Type Description Default
samples ndarray

A list of samples, where each sample is a list of floats.

required
parameter_names List[str] | None

List of parameter names.

None
**kwargs Dict[str, Any]

Additional keyword arguments for output configuration.

{}

Returns:

Type Description
SampleOutputResult

A SampleOutputResult object containing the output path, output type, and run instances.

Source code in src/mada_tools/simulation/simutils/samples/output/folder_output_handler.py
def write(
    self,
    samples: np.ndarray,
    parameter_names: List[str] | None = None,
    **kwargs: Dict[str, Any],
) -> SampleOutputResult:
    """
    Creates a folder structure for each sample, writes the parameters to individual files,
    and dumps all `RunInstances` to a JSON file.

    Args:
        samples: A list of samples, where each sample is a list of floats.
        parameter_names: List of parameter names.
        **kwargs: Additional keyword arguments for output configuration.

    Returns:
        A `SampleOutputResult` object containing the output path, output type, and run instances.
    """
    self.check_if_samples_are_empty(samples)

    output_settings = self.build_settings_from_kwargs(kwargs)

    print(f"Creating folder structure in '{output_settings.output_dir}'...")

    # Ensure the base output directory exists
    if not os.path.exists(output_settings.output_dir):
        os.makedirs(output_settings.output_dir)
        print(f"Created base output directory: {output_settings.output_dir}")
    else:
        print(f"Base output directory already exists: {output_settings.output_dir}")

    # Create subdirectories and RunInstance objects for each sample
    padding = len(str(len(samples)))  # Determine padding for folder names (e.g., run001)
    run_instances = []  # List to store all RunInstance objects

    if parameter_names is not None:
        # Build DataFrame
        run_ids = [str(i).zfill(padding) for i in range(len(samples))]
        df = pd.DataFrame(samples, columns=parameter_names)
        df.insert(0, "run#", run_ids)

        # Save CSV
        csv_path = os.path.join(output_settings.output_dir, "run_parameters.csv")
        df.to_csv(csv_path, index=False)
        print(f"Saved run parameter summary CSV: {csv_path}\n")

    for i, sample in enumerate(samples):
        # Create a subdirectory for the sample
        sample_dir = os.path.join(output_settings.output_dir, f"run{str(i).zfill(padding)}")
        os.makedirs(sample_dir, exist_ok=True)

        # Write the sample parameters to the specified file
        param_file_path = os.path.join(sample_dir, output_settings.param_file)
        with open(param_file_path, "w") as param_file:
            if parameter_names is not None:
                param_file.write("\n".join(f"{name}: {value}" for name, value in zip(parameter_names, sample)))
            else:
                param_file.write("\n".join(map(str, sample)))

        # Create a RunInstance object
        run_id = str(i).zfill(padding)
        run_instance = RunInstance(run_location=sample_dir, id=run_id)
        run_instances.append(run_instance)

        print(f"Created folder: {sample_dir}")
        print(f"\tWritten parameters to: {param_file_path}")
        print(f"\tRunInstance created: {run_instance}")

    # Dump all RunInstances to a JSON file in case they need to be loaded later
    run_instances_json_path = os.path.join(output_settings.output_dir, "run_instances.json")
    with open(run_instances_json_path, "w") as json_file:
        json.dump([run.to_dict() for run in run_instances], json_file, indent=4)

    print(f"RunInstances dumped to JSON file: {run_instances_json_path}")
    print("Folder structure created successfully.")

    return SampleOutputResult(
        output_path=output_settings.output_dir,
        output_type="folders",
        run_instances=run_instances,
    )

LHSampleGenerator

Bases: BaseSampleGenerator

Latin Hypercube Sample generator component.

Attributes:

Name Type Description
settings_class

The settings class for this output handler.

supported_kwargs

The full set of supported keyword arguments for the write method.

required_kwargs

The set of required keyword arguments for the write method.

Methods:

Name Description
find_missing_required_kwargs

Validates that all required keyword arguments are present.

build_settings_from_kwargs

Validates the keyword arguments and builds the settings object.

generate

Generate a list of samples from the provided data.

Source code in src/mada_tools/simulation/simutils/samples/generation/lhs_sample_generator.py
class LHSampleGenerator(BaseSampleGenerator):
    """
    Latin Hypercube Sample generator component.

    Attributes:
        settings_class: The settings class for this output handler.
        supported_kwargs: The full set of supported keyword arguments for the `write` method.
        required_kwargs: The set of required keyword arguments for the `write` method.

    Methods:
        find_missing_required_kwargs: Validates that all required keyword arguments are present.
        build_settings_from_kwargs: Validates the keyword arguments and builds the settings object.
        generate: Generate a list of samples from the provided data.
    """

    def _get_settings_class(self) -> LHSampleSettings:
        """
        Get the settings class for this sample generator.

        Returns:
            The settings class for this sample generator.
        """
        return LHSampleSettings

    def _get_supported_kwargs(self):
        """
        Get the full set of supported keyword arguments for the `generate` method.

        Returns:
            A set of supported keyword argument names.
        """
        return {"dims", "n_samples", "lower_bounds", "upper_bounds", "rng"}

    def _get_required_kwargs(self) -> Set[str]:
        """
        Get the set of required keyword arguments for the `generate` method.

        Returns:
            A set of required keyword argument names for sample generation.
        """
        return {"dims", "n_samples", "lower_bounds", "upper_bounds"}

    def generate(self, **kwargs: Dict[str, Any]) -> np.ndarray:
        """
        Generate a list of samples from the provided data.

        This method will convert the keyword arguments into a `LHSampleSettings` object
        for validation and logging purposes. It will then use these settings to generate
        samples using the Latin Hypercube Sampling method.

        Args:
            **kwargs: Keyword arguments for sample generation. For LHS sampling, this must include:
                - dims: int, number of dimensions
                - n_samples: int, number of samples to generate
                - lower_bounds: List[float], lower bounds for each dimension
                - upper_bounds: List[float], upper bounds for each dimension
                - rng: Optional[np.random.Generator], random number generator

        Returns:
            A numpy array of generated samples.
        """
        print("Generating samples...")

        sample_settings = self.build_settings_from_kwargs(kwargs)

        lower = np.asarray(sample_settings.lower_bounds, dtype=float)
        upper = np.asarray(sample_settings.upper_bounds, dtype=float)
        # Create an LHS sampler. Older SciPy versions accept a Generator but
        # do not reliably preserve reproducibility across fresh equivalent
        # generators, so derive SciPy's integer seed from the supplied stream.
        lhs_seed = None
        if sample_settings.rng is not None:
            lhs_seed = int(
                sample_settings.rng.integers(
                    0,
                    np.iinfo(np.uint32).max,
                    dtype=np.uint32,
                )
            )
        varying_mask = lower < upper
        fixed_mask = lower == upper

        result = np.empty((sample_settings.n_samples, sample_settings.dims), dtype=float)

        if np.any(varying_mask):
            lhs = qmc.LatinHypercube(d=int(np.sum(varying_mask)), seed=lhs_seed)
            samples = lhs.random(n=sample_settings.n_samples)

            scaled = qmc.scale(
                samples,
                l_bounds=lower[varying_mask],
                u_bounds=upper[varying_mask],
            )

            result[:, varying_mask] = scaled

        if np.any(fixed_mask):
            result[:, fixed_mask] = lower[fixed_mask]

        print("Samples generated successfully!\n")
        return result

generate(**kwargs)

Generate a list of samples from the provided data.

This method will convert the keyword arguments into a LHSampleSettings object for validation and logging purposes. It will then use these settings to generate samples using the Latin Hypercube Sampling method.

Parameters:

Name Type Description Default
**kwargs Dict[str, Any]

Keyword arguments for sample generation. For LHS sampling, this must include: - dims: int, number of dimensions - n_samples: int, number of samples to generate - lower_bounds: List[float], lower bounds for each dimension - upper_bounds: List[float], upper bounds for each dimension - rng: Optional[np.random.Generator], random number generator

{}

Returns:

Type Description
ndarray

A numpy array of generated samples.

Source code in src/mada_tools/simulation/simutils/samples/generation/lhs_sample_generator.py
def generate(self, **kwargs: Dict[str, Any]) -> np.ndarray:
    """
    Generate a list of samples from the provided data.

    This method will convert the keyword arguments into a `LHSampleSettings` object
    for validation and logging purposes. It will then use these settings to generate
    samples using the Latin Hypercube Sampling method.

    Args:
        **kwargs: Keyword arguments for sample generation. For LHS sampling, this must include:
            - dims: int, number of dimensions
            - n_samples: int, number of samples to generate
            - lower_bounds: List[float], lower bounds for each dimension
            - upper_bounds: List[float], upper bounds for each dimension
            - rng: Optional[np.random.Generator], random number generator

    Returns:
        A numpy array of generated samples.
    """
    print("Generating samples...")

    sample_settings = self.build_settings_from_kwargs(kwargs)

    lower = np.asarray(sample_settings.lower_bounds, dtype=float)
    upper = np.asarray(sample_settings.upper_bounds, dtype=float)
    # Create an LHS sampler. Older SciPy versions accept a Generator but
    # do not reliably preserve reproducibility across fresh equivalent
    # generators, so derive SciPy's integer seed from the supplied stream.
    lhs_seed = None
    if sample_settings.rng is not None:
        lhs_seed = int(
            sample_settings.rng.integers(
                0,
                np.iinfo(np.uint32).max,
                dtype=np.uint32,
            )
        )
    varying_mask = lower < upper
    fixed_mask = lower == upper

    result = np.empty((sample_settings.n_samples, sample_settings.dims), dtype=float)

    if np.any(varying_mask):
        lhs = qmc.LatinHypercube(d=int(np.sum(varying_mask)), seed=lhs_seed)
        samples = lhs.random(n=sample_settings.n_samples)

        scaled = qmc.scale(
            samples,
            l_bounds=lower[varying_mask],
            u_bounds=upper[varying_mask],
        )

        result[:, varying_mask] = scaled

    if np.any(fixed_mask):
        result[:, fixed_mask] = lower[fixed_mask]

    print("Samples generated successfully!\n")
    return result

ParameterSampleGenerator

Build concrete parameter rows from mixed parameter specifications.

ParameterSampleGenerator is simulation-agnostic shared infrastructure for parameterized run generation. It validates the common parameter schema and expands it into concrete sample rows, while server helpers remain responsible for translating those rows into simulation-specific runtime inputs such as command-line arguments, deck variables, environment variables, or staged files.

Attributes:

Name Type Description
allowed_types Optional[set[str]]

Optional set of server-supported parameter types. When provided, parameter specs using any other type are rejected during parsing.

continuous_allowed_types Optional[set[str]]

Optional set of parameter types allowed to use continuous sampling. This is typically used to restrict numeric continuous ranges to simulation-variable types rather than executable or CLI selectors.

max_exe_parameters Optional[int]

Optional maximum number of parameters whose type is exactly exe.

validate_cli_values bool

Whether parameters of type exactly cli should be validated with normalize_cli_value() during schema parsing.

Methods:

Name Description
__init__

Configure generator-level validation constraints.

generate

Validate parameter specifications and expand them into concrete sample rows plus reproducibility metadata.

parse_parameter_specs

Normalize and validate raw parameter specifications into structured ParameterSpec instances.

_generate_lhs_rows

Generate per-row values for continuous and discrete_lhs parameters.

_generate_grid_rows

Generate Cartesian-product rows for discrete and sampled discrete_random parameters.

_generate_zip_rows

Generate grouped rows for zip parameters by pairing values by index within each zip group and combining independent groups.

Source code in src/mada_tools/simulation/simutils/samples/generation/parameter_samples_generator.py
class ParameterSampleGenerator:
    """
    Build concrete parameter rows from mixed parameter specifications.

    `ParameterSampleGenerator` is simulation-agnostic shared infrastructure for
    parameterized run generation. It validates the common parameter schema and
    expands it into concrete sample rows, while server helpers remain responsible
    for translating those rows into simulation-specific runtime inputs such as
    command-line arguments, deck variables, environment variables, or staged files.

    Attributes:
        allowed_types (Optional[set[str]]): Optional set of server-supported
            parameter types. When provided, parameter specs using any other type are
            rejected during parsing.
        continuous_allowed_types (Optional[set[str]]): Optional set of parameter
            types allowed to use `continuous` sampling. This is typically used to
            restrict numeric continuous ranges to simulation-variable types rather
            than executable or CLI selectors.
        max_exe_parameters (Optional[int]): Optional maximum number of parameters
            whose type is exactly `exe`.
        validate_cli_values (bool): Whether parameters of type exactly `cli` should
            be validated with `normalize_cli_value()` during schema parsing.

    Methods:
        __init__: Configure generator-level validation constraints.
        generate: Validate parameter specifications and expand
            them into concrete sample rows plus reproducibility metadata.
        parse_parameter_specs: Normalize and validate raw parameter specifications into structured
            `ParameterSpec` instances.
        _generate_lhs_rows: Generate per-row values for `continuous` and `discrete_lhs` parameters.
        _generate_grid_rows: Generate Cartesian-product rows for `discrete`
            and sampled `discrete_random` parameters.
        _generate_zip_rows: Generate grouped rows for `zip` parameters by pairing values by index
            within each zip group and combining independent groups.
    """

    def __init__(
        self,
        allowed_types: Optional[set[str]] = None,
        continuous_allowed_types: Optional[set[str]] = None,
        max_exe_parameters: Optional[int] = None,
        validate_cli_values: bool = False,
    ):
        self.allowed_types = allowed_types
        self.continuous_allowed_types = continuous_allowed_types
        self.max_exe_parameters = max_exe_parameters
        self.validate_cli_values = validate_cli_values

    def generate(
        self,
        parameters: Dict[str, List[Any]],
        num_samples: Optional[int] = None,
        seed: Optional[int] = None,
        rng_bit_generator: Optional[str] = None,
    ) -> ParameterSampleResult:
        """
        Validate parameter specifications and generate all run rows.

        Continuous and discrete_lhs parameters produce `num_samples` LHS rows.
        Discrete and discrete_random parameters produce Cartesian grid rows. Zip
        parameters pair values by index within each zip group, and independent
        zip groups are combined as separate dimensions.

        The final run matrix is:

            grid rows * zip rows * LHS rows

        The returned ParameterSampleResult contains ordered sample rows,
        per-run dictionaries, validated specs, and reproducibility metadata.
        """
        rng_streams, sampling_metadata = create_sampling_rngs(seed, rng_bit_generator)
        parameter_specs = self.parse_parameter_specs(parameters)

        lhs_rows = self._generate_lhs_rows(parameter_specs, num_samples, rng_streams)
        grid_rows = self._generate_grid_rows(parameter_specs, rng_streams)
        zip_rows = self._generate_zip_rows(parameter_specs)

        parameter_names = [spec.name for spec in parameter_specs]
        samples = []
        row_values = []
        for grid_row in grid_rows:
            for zip_row in zip_rows:
                for lhs_row in lhs_rows:
                    run_values = {}
                    run_values.update(grid_row)
                    run_values.update(zip_row)
                    run_values.update(lhs_row)
                    row_values.append(run_values)
                    samples.append([run_values.get(name) for name in parameter_names])

        return ParameterSampleResult(
            parameter_names=parameter_names,
            samples=samples,
            row_values=row_values,
            specs=parameter_specs,
            sampling_metadata=sampling_metadata,
        )

    def parse_parameter_specs(self, parameters: Dict[str, List[Any]]) -> List[ParameterSpec]:
        """
        Validate and normalize structured parameter specifications.

        Each non-random, non-zip entry in `parameters` must use this list form:

            [param_type, param_selection, param_values]

        `discrete_random` and `zip` entries require a fourth value:

            [param_type, "discrete_random", param_values, param_num_selections]
            [param_type, "zip", param_values, param_zip_group_id]

        Parameter names must be non-empty strings. Parameter type and selection
        names are normalized to lowercase. `param_values` must normalize to a
        non-empty list. JSON-encoded list strings are accepted as a compatibility
        fallback, but callers should prefer passing native lists.

        Supported selections are `continuous`, `discrete`, `discrete_lhs`,
        `discrete_random`, and `zip`. Unsupported selections, invalid bounds,
        invalid random counts, unsupported parameter types, and mismatched zip
        groups raise built-in Python exceptions.
        """
        if not parameters:
            raise ValueError("parameters must contain at least one parameter")

        allowed_selections = {"continuous", "discrete", "discrete_lhs", "discrete_random", "zip"}
        unsupported_modes = {"discrete_random_zip"}
        specs = []
        exe_parameter_names = []

        for name, parameter_spec in parameters.items():
            if not isinstance(name, str) or not name:
                raise ValueError("parameter names must be non-empty strings")
            if not isinstance(parameter_spec, list) or len(parameter_spec) not in {3, 4}:
                raise ValueError(
                    f"parameters['{name}'] must be [param_type, param_selection, param_values],"
                    "[param_type, 'discrete_random', param_values, param_num_selections],"
                    "or [param_type, 'zip', param_values, param_zip_group_id]"
                )

            parameter_type = str(parameter_spec[0]).lower()
            selection = str(parameter_spec[1]).lower()
            values = normalize_values_list(name, parameter_spec[2])
            fourth_value = parameter_spec[3] if len(parameter_spec) == 4 else None
            num_selections = None
            zip_group = None

            if self.allowed_types is not None and parameter_type not in self.allowed_types:
                raise ValueError(f"parameters['{name}'] has unsupported parameter type: {parameter_spec[0]}")
            if selection in unsupported_modes or selection not in allowed_selections:
                raise ValueError(f"parameters['{name}'] has unsupported parameter selection: {parameter_spec[1]}")
            if (
                self.continuous_allowed_types is not None
                and selection == "continuous"
                and parameter_type not in self.continuous_allowed_types
            ):
                raise ValueError(f"parameters['{name}'] type '{parameter_type}' does not support continuous selection")
            if parameter_type == "exe" and self.max_exe_parameters is not None:
                exe_parameter_names.append(name)
                if not all(isinstance(value, str) and value for value in values):
                    raise ValueError(f"parameters['{name}'] exe values must be non-empty strings")
            if parameter_type == "cli" and self.validate_cli_values:
                for value in values:
                    normalize_cli_value(name, value)

            if selection == "continuous":
                if len(values) != 2 or not all(
                    isinstance(value, (int, float)) and not isinstance(value, bool) for value in values
                ):
                    raise ValueError(f"parameters['{name}'] continuous values must be exactly two numeric bounds")
            if selection == "discrete_random":
                num_selections = fourth_value
                if not isinstance(num_selections, int) or isinstance(num_selections, bool):
                    raise TypeError(f"parameters['{name}'] discrete_random requires an integer param_num_selections")
                if num_selections <= 0:
                    raise ValueError(f"parameters['{name}'] discrete_random param_num_selections must be positive")
                if num_selections > len(values):
                    raise ValueError(
                        f"parameters['{name}'] discrete_random param_num_selections cannot exceed param_values count"
                    )
            if selection == "zip":
                if len(parameter_spec) != 4:
                    raise ValueError(f"parameters['{name}'] zip requires a fourth param_zip_group_id")
                zip_group = fourth_value
                valid_int_group = isinstance(zip_group, int) and not isinstance(zip_group, bool) and zip_group >= 0
                valid_string_group = isinstance(zip_group, str) and zip_group != ""
                if not valid_int_group and not valid_string_group:
                    raise ValueError(
                        f"parameters['{name}'] param_zip_group_id must be a non-negative integer or non-empty string"
                    )

            specs.append(
                ParameterSpec(
                    name=name,
                    parameter_type=parameter_type,
                    selection=selection,
                    values=values,
                    num_selections=num_selections,
                    zip_group=zip_group,
                )
            )

        if self.max_exe_parameters is not None and len(exe_parameter_names) > self.max_exe_parameters:
            raise ValueError(
                f"At most {self.max_exe_parameters} exe parameter(s) are supported. "
                f"Found {len(exe_parameter_names)}: {exe_parameter_names}"
            )

        return specs

    def _generate_lhs_rows(
        self,
        parameter_specs: List[ParameterSpec],
        num_samples: int,
        rng_streams: Dict[str, np.random.Generator],
    ) -> List[Dict[str, Any]]:
        continuous_specs = [spec for spec in parameter_specs if spec.selection == "continuous"]
        discrete_lhs_specs = [spec for spec in parameter_specs if spec.selection == "discrete_lhs"]

        if not continuous_specs and not discrete_lhs_specs:
            return [{}]
        if num_samples is None:
            raise ValueError("num_samples must be provided when using continuous or discrete_lhs parameters")
        if not isinstance(num_samples, int) or isinstance(num_samples, bool) or num_samples <= 0:
            raise ValueError("num_samples must be a positive integer")

        lhs_rows: List[Dict[str, Any]] = [{} for _ in range(num_samples)]
        if continuous_specs:
            sample_settings: Dict[str, Any] = {
                "dims": len(continuous_specs),
                "n_samples": num_samples,
                "lower_bounds": [float(spec.values[0]) for spec in continuous_specs],
                "upper_bounds": [float(spec.values[1]) for spec in continuous_specs],
                "rng": rng_streams["lhs"],
            }

            sample_generator = LHSampleGenerator()
            continuous_samples = sample_generator.generate(**sample_settings).tolist()
            for row, continuous_sample in zip(lhs_rows, continuous_samples):
                for spec, value in zip(continuous_specs, continuous_sample):
                    row[spec.name] = value

        for spec in discrete_lhs_specs:
            for row in lhs_rows:
                value_index = int(rng_streams["discrete_lhs"].integers(len(spec.values)))
                row[spec.name] = spec.values[value_index]

        return lhs_rows

    def _generate_grid_rows(
        self,
        parameter_specs: List[ParameterSpec],
        rng_streams: Dict[str, np.random.Generator],
    ) -> List[Dict[str, Any]]:
        grid_specs = [spec for spec in parameter_specs if spec.selection in {"discrete", "discrete_random"}]
        if not grid_specs:
            return [{}]

        value_lists = []
        for spec in grid_specs:
            if spec.selection == "discrete_random":
                if spec.num_selections is None:
                    raise RuntimeError(f"parameters['{spec.name}'] discrete_random requires param_num_selections")
                value_indices = rng_streams["discrete_random"].choice(
                    len(spec.values),
                    size=spec.num_selections,
                    replace=False,
                )
                values = [spec.values[int(index)] for index in value_indices]
            else:
                values = spec.values
            value_lists.append([(spec.name, value) for value in values])

        return [dict(values) for values in itertools.product(*value_lists)]

    def _generate_zip_rows(self, parameter_specs: List[ParameterSpec]) -> List[Dict[str, Any]]:
        zip_specs = [spec for spec in parameter_specs if spec.selection == "zip"]
        if not zip_specs:
            return [{}]

        grouped_specs: Dict[int | str, List[ParameterSpec]] = {}
        for spec in zip_specs:
            if spec.zip_group is None:
                raise RuntimeError(f"parameters['{spec.name}'] zip requires a zip group")
            grouped_specs.setdefault(spec.zip_group, []).append(spec)

        grouped_rows = []
        for zip_group, group_specs in grouped_specs.items():
            value_counts = {len(spec.values) for spec in group_specs}
            if len(value_counts) != 1:
                raise ValueError(f"All zip parameter value lists in group {zip_group} must have the same length")

            grouped_rows.append(
                [{spec.name: spec.values[index] for spec in group_specs} for index in range(next(iter(value_counts)))]
            )

        rows = []
        for row_parts in itertools.product(*grouped_rows):
            row = {}
            for row_part in row_parts:
                row.update(row_part)
            rows.append(row)
        return rows

generate(parameters, num_samples=None, seed=None, rng_bit_generator=None)

Validate parameter specifications and generate all run rows.

Continuous and discrete_lhs parameters produce num_samples LHS rows. Discrete and discrete_random parameters produce Cartesian grid rows. Zip parameters pair values by index within each zip group, and independent zip groups are combined as separate dimensions.

The final run matrix is:

grid rows * zip rows * LHS rows

The returned ParameterSampleResult contains ordered sample rows, per-run dictionaries, validated specs, and reproducibility metadata.

Source code in src/mada_tools/simulation/simutils/samples/generation/parameter_samples_generator.py
def generate(
    self,
    parameters: Dict[str, List[Any]],
    num_samples: Optional[int] = None,
    seed: Optional[int] = None,
    rng_bit_generator: Optional[str] = None,
) -> ParameterSampleResult:
    """
    Validate parameter specifications and generate all run rows.

    Continuous and discrete_lhs parameters produce `num_samples` LHS rows.
    Discrete and discrete_random parameters produce Cartesian grid rows. Zip
    parameters pair values by index within each zip group, and independent
    zip groups are combined as separate dimensions.

    The final run matrix is:

        grid rows * zip rows * LHS rows

    The returned ParameterSampleResult contains ordered sample rows,
    per-run dictionaries, validated specs, and reproducibility metadata.
    """
    rng_streams, sampling_metadata = create_sampling_rngs(seed, rng_bit_generator)
    parameter_specs = self.parse_parameter_specs(parameters)

    lhs_rows = self._generate_lhs_rows(parameter_specs, num_samples, rng_streams)
    grid_rows = self._generate_grid_rows(parameter_specs, rng_streams)
    zip_rows = self._generate_zip_rows(parameter_specs)

    parameter_names = [spec.name for spec in parameter_specs]
    samples = []
    row_values = []
    for grid_row in grid_rows:
        for zip_row in zip_rows:
            for lhs_row in lhs_rows:
                run_values = {}
                run_values.update(grid_row)
                run_values.update(zip_row)
                run_values.update(lhs_row)
                row_values.append(run_values)
                samples.append([run_values.get(name) for name in parameter_names])

    return ParameterSampleResult(
        parameter_names=parameter_names,
        samples=samples,
        row_values=row_values,
        specs=parameter_specs,
        sampling_metadata=sampling_metadata,
    )

parse_parameter_specs(parameters)

Validate and normalize structured parameter specifications.

Each non-random, non-zip entry in parameters must use this list form:

[param_type, param_selection, param_values]

discrete_random and zip entries require a fourth value:

[param_type, "discrete_random", param_values, param_num_selections]
[param_type, "zip", param_values, param_zip_group_id]

Parameter names must be non-empty strings. Parameter type and selection names are normalized to lowercase. param_values must normalize to a non-empty list. JSON-encoded list strings are accepted as a compatibility fallback, but callers should prefer passing native lists.

Supported selections are continuous, discrete, discrete_lhs, discrete_random, and zip. Unsupported selections, invalid bounds, invalid random counts, unsupported parameter types, and mismatched zip groups raise built-in Python exceptions.

Source code in src/mada_tools/simulation/simutils/samples/generation/parameter_samples_generator.py
def parse_parameter_specs(self, parameters: Dict[str, List[Any]]) -> List[ParameterSpec]:
    """
    Validate and normalize structured parameter specifications.

    Each non-random, non-zip entry in `parameters` must use this list form:

        [param_type, param_selection, param_values]

    `discrete_random` and `zip` entries require a fourth value:

        [param_type, "discrete_random", param_values, param_num_selections]
        [param_type, "zip", param_values, param_zip_group_id]

    Parameter names must be non-empty strings. Parameter type and selection
    names are normalized to lowercase. `param_values` must normalize to a
    non-empty list. JSON-encoded list strings are accepted as a compatibility
    fallback, but callers should prefer passing native lists.

    Supported selections are `continuous`, `discrete`, `discrete_lhs`,
    `discrete_random`, and `zip`. Unsupported selections, invalid bounds,
    invalid random counts, unsupported parameter types, and mismatched zip
    groups raise built-in Python exceptions.
    """
    if not parameters:
        raise ValueError("parameters must contain at least one parameter")

    allowed_selections = {"continuous", "discrete", "discrete_lhs", "discrete_random", "zip"}
    unsupported_modes = {"discrete_random_zip"}
    specs = []
    exe_parameter_names = []

    for name, parameter_spec in parameters.items():
        if not isinstance(name, str) or not name:
            raise ValueError("parameter names must be non-empty strings")
        if not isinstance(parameter_spec, list) or len(parameter_spec) not in {3, 4}:
            raise ValueError(
                f"parameters['{name}'] must be [param_type, param_selection, param_values],"
                "[param_type, 'discrete_random', param_values, param_num_selections],"
                "or [param_type, 'zip', param_values, param_zip_group_id]"
            )

        parameter_type = str(parameter_spec[0]).lower()
        selection = str(parameter_spec[1]).lower()
        values = normalize_values_list(name, parameter_spec[2])
        fourth_value = parameter_spec[3] if len(parameter_spec) == 4 else None
        num_selections = None
        zip_group = None

        if self.allowed_types is not None and parameter_type not in self.allowed_types:
            raise ValueError(f"parameters['{name}'] has unsupported parameter type: {parameter_spec[0]}")
        if selection in unsupported_modes or selection not in allowed_selections:
            raise ValueError(f"parameters['{name}'] has unsupported parameter selection: {parameter_spec[1]}")
        if (
            self.continuous_allowed_types is not None
            and selection == "continuous"
            and parameter_type not in self.continuous_allowed_types
        ):
            raise ValueError(f"parameters['{name}'] type '{parameter_type}' does not support continuous selection")
        if parameter_type == "exe" and self.max_exe_parameters is not None:
            exe_parameter_names.append(name)
            if not all(isinstance(value, str) and value for value in values):
                raise ValueError(f"parameters['{name}'] exe values must be non-empty strings")
        if parameter_type == "cli" and self.validate_cli_values:
            for value in values:
                normalize_cli_value(name, value)

        if selection == "continuous":
            if len(values) != 2 or not all(
                isinstance(value, (int, float)) and not isinstance(value, bool) for value in values
            ):
                raise ValueError(f"parameters['{name}'] continuous values must be exactly two numeric bounds")
        if selection == "discrete_random":
            num_selections = fourth_value
            if not isinstance(num_selections, int) or isinstance(num_selections, bool):
                raise TypeError(f"parameters['{name}'] discrete_random requires an integer param_num_selections")
            if num_selections <= 0:
                raise ValueError(f"parameters['{name}'] discrete_random param_num_selections must be positive")
            if num_selections > len(values):
                raise ValueError(
                    f"parameters['{name}'] discrete_random param_num_selections cannot exceed param_values count"
                )
        if selection == "zip":
            if len(parameter_spec) != 4:
                raise ValueError(f"parameters['{name}'] zip requires a fourth param_zip_group_id")
            zip_group = fourth_value
            valid_int_group = isinstance(zip_group, int) and not isinstance(zip_group, bool) and zip_group >= 0
            valid_string_group = isinstance(zip_group, str) and zip_group != ""
            if not valid_int_group and not valid_string_group:
                raise ValueError(
                    f"parameters['{name}'] param_zip_group_id must be a non-negative integer or non-empty string"
                )

        specs.append(
            ParameterSpec(
                name=name,
                parameter_type=parameter_type,
                selection=selection,
                values=values,
                num_selections=num_selections,
                zip_group=zip_group,
            )
        )

    if self.max_exe_parameters is not None and len(exe_parameter_names) > self.max_exe_parameters:
        raise ValueError(
            f"At most {self.max_exe_parameters} exe parameter(s) are supported. "
            f"Found {len(exe_parameter_names)}: {exe_parameter_names}"
        )

    return specs

ParameterSampleResult dataclass

Generated parameter samples and reproducibility metadata.

This dataclass is the main output of ParameterSampleGenerator.generate(). It contains the generated sample table, row-oriented dictionaries for server helpers, the validated schema used to produce them, and metadata required to reproduce randomized sampling.

Attributes:

Name Type Description
parameter_names List[str]

Ordered parameter names defining the column order for samples.

samples List[List[Any]]

Sample table in row-major form. Each inner list contains values ordered to match parameter_names.

row_values List[Dict[str, Any]]

One dictionary per generated run keyed by parameter name. This is typically the most convenient structure for translating sampled values into command lines, deck edits, manifests, or staged input files.

specs List[ParameterSpec]

Validated parameter specifications used to generate the sample rows.

sampling_metadata Dict[str, Any]

Reproducibility metadata returned from create_sampling_rngs(), including the effective seed, requested seed, seed source, and RNG bit generator name.

Source code in src/mada_tools/simulation/simutils/samples/generation/parameter_samples_generator.py
@dataclass(frozen=True)
class ParameterSampleResult:
    """
    Generated parameter samples and reproducibility metadata.

    This dataclass is the main output of `ParameterSampleGenerator.generate()`.
    It contains the generated sample table, row-oriented dictionaries for server
    helpers, the validated schema used to produce them, and metadata required to
    reproduce randomized sampling.

    Attributes:
        parameter_names (List[str]): Ordered parameter names defining the column
            order for `samples`.
        samples (List[List[Any]]): Sample table in row-major form. Each inner list
            contains values ordered to match `parameter_names`.
        row_values (List[Dict[str, Any]]): One dictionary per generated run keyed
            by parameter name. This is typically the most convenient structure for
            translating sampled values into command lines, deck edits, manifests,
            or staged input files.
        specs (List[ParameterSpec]): Validated parameter specifications used to
            generate the sample rows.
        sampling_metadata (Dict[str, Any]): Reproducibility metadata returned from
            `create_sampling_rngs()`, including the effective seed, requested seed,
            seed source, and RNG bit generator name.
    """

    parameter_names: List[str]
    samples: List[List[Any]]
    row_values: List[Dict[str, Any]]
    specs: List[ParameterSpec]
    sampling_metadata: Dict[str, Any]

ParameterSpec dataclass

Validated parameter generation specification.

Instances of this dataclass represent one normalized parameter definition after schema validation. The fields correspond to the shared prompt-facing tuple shape consumed by ParameterSampleGenerator.parse_parameter_specs().

Attributes:

Name Type Description
name str

Parameter name used in generated rows and output tables.

parameter_type str

Server-defined parameter type such as def, cli, or exe, normalized to lowercase.

selection str

Shared sampling mode, normalized to lowercase. Supported modes are continuous, discrete, discrete_lhs, discrete_random, and zip.

values List[Any]

Normalized non-empty list of candidate values or numeric bounds, depending on selection.

num_selections Optional[int]

Number of values to choose without replacement for discrete_random parameters. None for other selection types.

zip_group Optional[int | str]

Zip-group identifier used to pair values by index across related zip parameters. None for non-zip parameters.

Source code in src/mada_tools/simulation/simutils/samples/generation/parameter_samples_generator.py
@dataclass(frozen=True)
class ParameterSpec:
    """
    Validated parameter generation specification.

    Instances of this dataclass represent one normalized parameter definition after
    schema validation. The fields correspond to the shared prompt-facing tuple
    shape consumed by `ParameterSampleGenerator.parse_parameter_specs()`.

    Attributes:
        name (str): Parameter name used in generated rows and output tables.
        parameter_type (str): Server-defined parameter type such as `def`, `cli`,
            or `exe`, normalized to lowercase.
        selection (str): Shared sampling mode, normalized to lowercase. Supported
            modes are `continuous`, `discrete`, `discrete_lhs`,
            `discrete_random`, and `zip`.
        values (List[Any]): Normalized non-empty list of candidate values or
            numeric bounds, depending on `selection`.
        num_selections (Optional[int]): Number of values to choose without
            replacement for `discrete_random` parameters. `None` for other
            selection types.
        zip_group (Optional[int | str]): Zip-group identifier used to pair values
            by index across related `zip` parameters. `None` for non-zip
            parameters.
    """

    name: str
    parameter_type: str
    selection: str
    values: List[Any]
    num_selections: Optional[int] = None
    zip_group: Optional[int | str] = None

RunInstance dataclass

Represents a single instance of a computational run, storing relevant data and metadata associated with the run.

Attributes:

Name Type Description
run_location str

The directory where the run will be executed.

id str

The identifier for a run instance.

command str

The executable command to run (optional).

args List[str]

The arguments to pass to the command (optional).

Methods:

Name Description
to_dict

Converts the RunInstance object into a JSON-serializable dictionary.

get_command_with_args

Returns the command and arguments for execution.

Source code in src/mada_tools/simulation/simutils/models.py
@dataclass
class RunInstance:
    """
    Represents a single instance of a computational run, storing relevant data
    and metadata associated with the run.

    Attributes:
        run_location (str): The directory where the run will be executed.
        id (str): The identifier for a run instance.
        command (str): The executable command to run (optional).
        args (List[str]): The arguments to pass to the command (optional).

    Methods:
        to_dict: Converts the RunInstance object into a JSON-serializable dictionary.
        get_command_with_args: Returns the command and arguments for execution.
    """

    run_location: str
    id: str
    command: str = None
    args: List[str] = None

    def to_dict(self) -> Dict:
        """
        Converts the RunInstance object into a JSON-serializable dictionary.

        Returns:
            A dictionary representation of the RunInstance object.
        """
        result = {"id": self.id, "run_location": self.run_location}

        # Include command and args if they are set
        if self.command:
            result["command"] = self.command
        if self.args:
            result["args"] = self.args

        return result

    @classmethod
    def from_dict(cls, data: Dict) -> "RunInstance":
        """
        Creates a RunInstance object from a dictionary.

        Args:
            data: Dictionary containing run instance data

        Returns:
            RunInstance object with data from the dictionary
        """
        return cls(
            run_location=data.get("run_location"),
            id=data.get("id"),
            command=data.get("command"),
            args=data.get("args"),
        )

    def __str__(self) -> str:
        """
        Returns a human-readable string representation of the RunInstance object.

        Returns:
            str: A string describing the run instance.
        """
        result = f"RunInstance '{self.id}':\n\tLocation: {self.run_location}"
        if self.command:
            result += f"\n\tCommand: {self.command}"
        if self.args:
            result += f"\n\tArgs: {self.args}"
        return result

    def __repr__(self) -> str:
        """
        Returns a detailed string representation of the RunInstance object for debugging.

        Returns:
            str: A detailed string representation of the object.
        """
        return f"RunInstance(id={self.id}, run_location={self.run_location}, command={self.command}, args={self.args})"

    def get_command_with_args(self) -> Tuple[str, List[str]]:
        """
        Returns the command and arguments for execution.

        Returns:
            Tuple[str, List[str]]: The command and its arguments.
        """
        if self.command and self.args:
            return self.command, self.args
        else:
            raise ValueError("Command and args not set for this RunInstance")

    def get_run_command(self) -> str:
        """
        Constructs the shell command to execute this run instance.

        Returns:
            str: The shell command to execute this run.
        """
        if self.command and self.args:
            return f"{self.command} {' '.join(self.args)}"
        else:
            raise ValueError("No command specified for this RunInstance")

__repr__()

Returns a detailed string representation of the RunInstance object for debugging.

Returns:

Name Type Description
str str

A detailed string representation of the object.

Source code in src/mada_tools/simulation/simutils/models.py
def __repr__(self) -> str:
    """
    Returns a detailed string representation of the RunInstance object for debugging.

    Returns:
        str: A detailed string representation of the object.
    """
    return f"RunInstance(id={self.id}, run_location={self.run_location}, command={self.command}, args={self.args})"

__str__()

Returns a human-readable string representation of the RunInstance object.

Returns:

Name Type Description
str str

A string describing the run instance.

Source code in src/mada_tools/simulation/simutils/models.py
def __str__(self) -> str:
    """
    Returns a human-readable string representation of the RunInstance object.

    Returns:
        str: A string describing the run instance.
    """
    result = f"RunInstance '{self.id}':\n\tLocation: {self.run_location}"
    if self.command:
        result += f"\n\tCommand: {self.command}"
    if self.args:
        result += f"\n\tArgs: {self.args}"
    return result

from_dict(data) classmethod

Creates a RunInstance object from a dictionary.

Parameters:

Name Type Description Default
data Dict

Dictionary containing run instance data

required

Returns:

Type Description
RunInstance

RunInstance object with data from the dictionary

Source code in src/mada_tools/simulation/simutils/models.py
@classmethod
def from_dict(cls, data: Dict) -> "RunInstance":
    """
    Creates a RunInstance object from a dictionary.

    Args:
        data: Dictionary containing run instance data

    Returns:
        RunInstance object with data from the dictionary
    """
    return cls(
        run_location=data.get("run_location"),
        id=data.get("id"),
        command=data.get("command"),
        args=data.get("args"),
    )

get_command_with_args()

Returns the command and arguments for execution.

Returns:

Type Description
Tuple[str, List[str]]

Tuple[str, List[str]]: The command and its arguments.

Source code in src/mada_tools/simulation/simutils/models.py
def get_command_with_args(self) -> Tuple[str, List[str]]:
    """
    Returns the command and arguments for execution.

    Returns:
        Tuple[str, List[str]]: The command and its arguments.
    """
    if self.command and self.args:
        return self.command, self.args
    else:
        raise ValueError("Command and args not set for this RunInstance")

get_run_command()

Constructs the shell command to execute this run instance.

Returns:

Name Type Description
str str

The shell command to execute this run.

Source code in src/mada_tools/simulation/simutils/models.py
def get_run_command(self) -> str:
    """
    Constructs the shell command to execute this run instance.

    Returns:
        str: The shell command to execute this run.
    """
    if self.command and self.args:
        return f"{self.command} {' '.join(self.args)}"
    else:
        raise ValueError("No command specified for this RunInstance")

to_dict()

Converts the RunInstance object into a JSON-serializable dictionary.

Returns:

Type Description
Dict

A dictionary representation of the RunInstance object.

Source code in src/mada_tools/simulation/simutils/models.py
def to_dict(self) -> Dict:
    """
    Converts the RunInstance object into a JSON-serializable dictionary.

    Returns:
        A dictionary representation of the RunInstance object.
    """
    result = {"id": self.id, "run_location": self.run_location}

    # Include command and args if they are set
    if self.command:
        result["command"] = self.command
    if self.args:
        result["args"] = self.args

    return result

SampleOutputResult dataclass

Represents the result of sample output generation, storing the output path, output type, and optionally the run instances (if necessary).

Attributes:

Name Type Description
output_path str

The path where the samples are written. For "csv" output, this is the path to the CSV file. For "folder" output, this is the path to the directory.

output_type str

The type of output generated (e.g., "csv", "folder").

run_instances List[RunInstance]

A list of RunInstance objects if the output type is "folder".

Source code in src/mada_tools/simulation/simutils/models.py
@dataclass
class SampleOutputResult:
    """
    Represents the result of sample output generation, storing the output path,
    output type, and optionally the run instances (if necessary).

    Attributes:
        output_path (str): The path where the samples are written. For "csv" output, this
            is the path to the CSV file. For "folder" output, this is the path to the directory.
        output_type (str): The type of output generated (e.g., "csv", "folder").
        run_instances (List[RunInstance]): A list of RunInstance objects if the output type is "folder".
    """

    output_path: str
    output_type: str  # e.g., "csv", "folder"
    run_instances: List[RunInstance] = None

    def __str__(self) -> str:
        """
        Returns a human-readable string representation of the SampleOutputResult object.

        Returns:
            str: A string describing the sample output result.
        """
        return (
            f"SampleOutputResult:\n"
            f"  Output path: {self.output_path}\n"
            f"  Output type: {self.output_type}\n"
            f"  Run instances: {self.run_instances}"
        )

    def __repr__(self) -> str:
        """
        Returns a detailed string representation of the SampleOutputResult object for debugging.

        Returns:
            str: A detailed string representation of the object.
        """
        return (
            f"SampleOutputResult(output_path={self.output_path}, "
            f"output_type={self.output_type}, run_instances={self.run_instances})"
        )

__repr__()

Returns a detailed string representation of the SampleOutputResult object for debugging.

Returns:

Name Type Description
str str

A detailed string representation of the object.

Source code in src/mada_tools/simulation/simutils/models.py
def __repr__(self) -> str:
    """
    Returns a detailed string representation of the SampleOutputResult object for debugging.

    Returns:
        str: A detailed string representation of the object.
    """
    return (
        f"SampleOutputResult(output_path={self.output_path}, "
        f"output_type={self.output_type}, run_instances={self.run_instances})"
    )

__str__()

Returns a human-readable string representation of the SampleOutputResult object.

Returns:

Name Type Description
str str

A string describing the sample output result.

Source code in src/mada_tools/simulation/simutils/models.py
def __str__(self) -> str:
    """
    Returns a human-readable string representation of the SampleOutputResult object.

    Returns:
        str: A string describing the sample output result.
    """
    return (
        f"SampleOutputResult:\n"
        f"  Output path: {self.output_path}\n"
        f"  Output type: {self.output_type}\n"
        f"  Run instances: {self.run_instances}"
    )

create_sampling_rngs(seed=None, rng_bit_generator='MT19937')

Create independent RNG streams and metadata for parameter sampling.

The returned streams are keyed by sampling mode: lhs, discrete_lhs, and discrete_random. They are derived from jumped versions of one root bit generator so each mode is reproducible without sharing mutable RNG state.

seed must be None or a non-negative integer. When seed is None, NumPy initializes from system entropy and the effective entropy value is recorded in the metadata. rng_bit_generator may be MT19937, PCG64, PCG64DXSM, or PHILOX. If omitted, MT19937 is used.

Source code in src/mada_tools/simulation/simutils/samples/generation/parameter_samples_generator.py
def create_sampling_rngs(
    seed: Optional[int] = None,
    rng_bit_generator: Optional[str] = "MT19937",
) -> tuple[Dict[str, np.random.Generator], Dict[str, Any]]:
    """
    Create independent RNG streams and metadata for parameter sampling.

    The returned streams are keyed by sampling mode: `lhs`, `discrete_lhs`, and
    `discrete_random`. They are derived from jumped versions of one root bit
    generator so each mode is reproducible without sharing mutable RNG state.

    `seed` must be None or a non-negative integer. When `seed` is None, NumPy
    initializes from system entropy and the effective entropy value is recorded
    in the metadata. `rng_bit_generator` may be MT19937, PCG64, PCG64DXSM, or
    PHILOX. If omitted, MT19937 is used.
    """
    if seed is not None and (not isinstance(seed, int) or isinstance(seed, bool) or seed < 0):
        raise ValueError("seed must be a non-negative integer or null")

    normalized_bit_generator = str(rng_bit_generator or "MT19937").upper()

    bit_generator_classes = {
        "MT19937": np.random.MT19937,
        "PCG64": np.random.PCG64,
        "PCG64DXSM": np.random.PCG64DXSM,
        "PHILOX": np.random.Philox,
    }
    bit_generator_class = bit_generator_classes.get(normalized_bit_generator)
    if bit_generator_class is None:
        raise ValueError("rng_bit_generator must be one of: MT19937, PCG64, PCG64DXSM, PHILOX")

    seed_sequence = np.random.SeedSequence(seed)
    effective_seed = int(cast(int, seed_sequence.entropy))
    root_bit_generator = bit_generator_class(seed_sequence)
    rng_streams = {
        "lhs": np.random.Generator(root_bit_generator.jumped(1)),
        "discrete_lhs": np.random.Generator(root_bit_generator.jumped(2)),
        "discrete_random": np.random.Generator(root_bit_generator.jumped(3)),
    }
    return rng_streams, {
        "seed": effective_seed,
        "requested_seed": seed,
        "seed_source": "user" if seed is not None else "system_entropy",
        "rng_bit_generator": normalized_bit_generator,
    }

get_run_instances(run_instances_json_path)

Reads the run_instances.json file from the specified study directory and converts it into a list of RunInstance objects.

Parameters:

Name Type Description Default
run_instances_json_path str

The path to the run_instances.json file.

required

Returns:

Type Description
List[RunInstance]

A list of RunInstance objects.

Source code in src/mada_tools/simulation/simutils/utils.py
def get_run_instances(run_instances_json_path: str) -> List[RunInstance]:
    """
    Reads the run_instances.json file from the specified study directory and
    converts it into a list of RunInstance objects.

    Args:
        run_instances_json_path: The path to the run_instances.json file.

    Returns:
        A list of RunInstance objects.
    """
    print(f"Attempting to retrieve run instances from '{run_instances_json_path}'...")

    # Check if the file exists
    if not os.path.exists(run_instances_json_path):
        print(f"ERROR: The file containing run instances does not exist: '{run_instances_json_path}'.")
        print("Did you run the sample generation script yet?")
        return []

    # Read the JSON file and convert it into RunInstance objects
    with open(run_instances_json_path, "r") as json_file:
        try:
            run_info = json.load(json_file)
            if isinstance(run_info, dict):
                run_instances_data = run_info.get("runs", [])
            else:
                run_instances_data = run_info
            if not isinstance(run_instances_data, list):
                print("ERROR: Run instances JSON must be a list or an object with a 'runs' list.")
                return []
            run_instances = []
            for data in run_instances_data:
                if not isinstance(data, dict):
                    print("ERROR: Run instance entries must be JSON objects.")
                    return []
                run_instance = RunInstance(run_location=data["run_location"], id=data["id"])
                # Set command and args if present
                if "command" in data:
                    run_instance.command = data["command"]
                if "args" in data:
                    run_instance.args = data["args"]
                run_instances.append(run_instance)
            print("Successfully retrieved run instances.")
            return run_instances
        except json.JSONDecodeError as e:
            print(f"ERROR: Failed to parse JSON file: {run_instances_json_path}.")
            print(f"Details: {e}")
            return []
        except KeyError as e:
            print(f"ERROR: Missing expected key in JSON data: {e}")
            return []

normalize_cli_value(parameter_name, value)

Normalize a CLI parameter value into argv tokens.

A non-empty string is split with shlex.split so quoted groups are preserved. A non-empty list of non-empty strings is treated as an explicit argv token list. Shell expansion is not performed; variables, globs, redirects, pipes, and command substitutions remain ordinary argv text after splitting.

Source code in src/mada_tools/simulation/simutils/samples/generation/parameter_samples_generator.py
def normalize_cli_value(parameter_name: str, value: Any) -> List[str]:
    """
    Normalize a CLI parameter value into argv tokens.

    A non-empty string is split with shlex.split so quoted groups are preserved.
    A non-empty list of non-empty strings is treated as an explicit argv token
    list. Shell expansion is not performed; variables, globs, redirects, pipes,
    and command substitutions remain ordinary argv text after splitting.
    """
    if isinstance(value, str):
        if not value:
            raise ValueError(f"parameters['{parameter_name}'] cli values must be non-empty strings or lists of strings")
        try:
            cli_args = shlex.split(value)
        except ValueError as exc:
            raise ValueError(f"parameters['{parameter_name}'] cli value could not be parsed: {exc}") from exc
        if not cli_args:
            raise ValueError(f"parameters['{parameter_name}'] cli values must produce at least one argv token")
        return cli_args

    if isinstance(value, list):
        if not value or not all(isinstance(arg, str) and arg for arg in value):
            raise ValueError(f"parameters['{parameter_name}'] cli argv lists must contain non-empty strings")
        return value

    raise TypeError(f"parameters['{parameter_name}'] cli values must be non-empty strings or lists of strings")