Skip to content

Starter Example

Introduction

Purpose

This example introduces the StudPy core objects and demonstrates a complete workflow.

Use Case

In this example, we consider a binary simulation tool that:

  • takes a main.json file as input;
  • produces a results.txt file as output, containing a single numeric value.

The input and output files are assumed to have the following structures:

main.json

{
    "static_attributes": {
        "static_attribute_1": 1000
    },
    "thermodynamic": {
        "temperature": 273
    },
    "seed": 3305
}

results.txt

1234.5

Our goal is to generate a dataset of inputs/outputs by varying the temperature and seed parameters.

Design workflow

  1. Define the Files Builder which will create the case folder structure and case input file for the simulation
  2. Define the Case Builder and its parameters (1 linear and 1 random) which will create the cases and associated values
  3. Define the Study which associates Files Builder and Case Builder
  4. Define and execute the Batch which will execute and aggregate case results

Script

import shutil
from pathlib import Path

from studpy.inputs import FilesBuilder, FileProcessor
from studpy.cases import CaseBuilder
from studpy.study import Study
from studpy.batch import BatchExecCmdIter


# ##############################################################################
# 1. Creation of a FilesBuilder

files_builder = FilesBuilder(
    folder=Path(__file__).parent / "cases",
    processors=[
        FileProcessor(relative_path="main.json", source='{"static_attributes":{"static_attribute_1": 1000}}')
    ],
)

# ##############################################################################
# 2. Creation of a CaseBuilder

case_builder = CaseBuilder.load({
    "parameters": [
        {
            "input_path": "thermodynamic.temperature",
            "name": "Temperature",
            "config": {"type": "linear", "size": 3, "low": 273, "high": 700}
        },
        {
            "input_path": "seed",
            "name": "Seed",
            "config": {"type": "random_int", "size": 2, "low": 0, "high": 6000}
        },
    ]
})

# ##############################################################################
# 3. Creation of the Study 

study = Study(case_builder, files_builder)

# ##############################################################################
# 4. Creation of the Batch 

# ASSUMPTION: jq is installed

# Implement abstract Class
class ExampleBatch(BatchExecCmdIter):

    def __init__(self, study: Study):
        self.study = study
        self.total = 0

    # abstract implementation
    def get_nb_runs(self) -> int:
        return self.study.cases_count()

    # abstract implementation
    def start_callable(self) -> None:
        self.study.prepare()
        self.study.files_builder.prepare()

    # abstract implementation
    def pre_callable(self, index_run: int) -> None:
        self.study.case_build_inputs(index_run)

    # abstract implementation
    def cmd_and_cwd_callable(self, index_run: int) -> tuple[str, str|None]:
        case_folder = files_builder.get_case_folder(index_run)
        inputs_folder = self.study.files_builder.get_case_inputs_folder(index_run)
        # equivalent
        # return f"jq -r '.thermodynamic.temperature * .seed' {inputs_folder / 'main.json'} > {case_folder / 'results.txt'}", None
        return f"jq -r '.thermodynamic.temperature * .seed' {inputs_folder / 'main.json'} > results.txt", case_folder

    # abstract implementation
    def post_callable(self, index_run: int, returncode: int | None, stdout: str, stderr: str) -> str | None:
        if returncode == 0:
            case_folder = self.study.files_builder.get_case_folder(index_run)
            res = (case_folder / 'results.txt').read_text()
            self.total += float(res)

# Execute
batch = ExampleBatch(study)
batch.exec()

print(batch.total)

Explainations

1. Define the File Builder

# ##############################################################################
# 1. Creation of a FilesBuilder

files_builder = FilesBuilder(
    folder=Path(__file__).parent / "cases",
    processors=[
        FileProcessor(relative_path="main.json", source='{"static_attributes":{"static_attribute_1": 1000}}')
    ],
)

Purposes:

  • Define the main target folder that will contain all input and output files.
  • Specify how the input files are generated.

See API FilesBuilder

2. Define the Case Builder and its parameters

# ##############################################################################
# 2. Creation of a CaseBuilder

case_builder = CaseBuilder.load({
    "parameters": [
        {
            "input_path": "thermodynamic.temperature",
            "name": "Temperature",
            "config": {"type": "linear", "size": 3, "low": 273, "high": 700}
        },
        {
            "input_path": "seed",
            "name": "Seed",
            "config": {"type": "random_int", "size": 2, "low": 0, "high": 6000}
        },
    ]
})

Please note that there is no need to specify the main.json file in input_path, as there is only one file.

You can build cases with:

print(case_builder.build_cases())

Results:

Show the datasets of parameters values like :

   Temperature  Seed
0        273.0  3305
1        273.0  5404
2        486.5  3305
3        486.5  5404
4        700.0  3305
5        700.0  5404

3. Define the Study

study = Study(case_builder, files_builder)

You can manipulate the creation of cases

# FOR DEBUG : creates all cases folders and inputs files
files_builder.prepare()
study.case_build_inputs_all()

Results:

  • Create the cases folders and inputs files like :
cases/
├── 0/
│   └── main.json
├── 1/
│   └── main.json
├── 2/
│   └── main.json
├── 3/
│   └── main.json
├── 4/
│   └── main.json
└── 5/
    └── main.json

with the cases values applied like in cases/0/main.json:

{
    "static_attributes": {
        "static_attribute_1": 1000
    },
    "thermodynamic": {
        "temperature": 273.0
    },
    "seed": 5368.0
}

4. Define the Batch and Execute (Final script)

# ##############################################################################
# 4. Creation of the Batch 

# ASSUMPTION: jq is installed

# Implement abstract Class
class ExampleBatch(BatchExecCmdIter):

    def __init__(self, study: Study):
        self.study = study
        self.total = 0

    # abstract implementation
    def get_nb_runs(self) -> int:
        return self.study.cases_count()

    # abstract implementation
    def start_callable(self) -> None:
        self.study.files_builder.prepare()

    # abstract implementation
    def pre_callable(self, index_run: int) -> None:
        self.study.case_build_inputs(index_run)

    # abstract implementation
    def cmd_callable(self, index_run: int) -> str:
        case_folder = self.study.files_builder.get_case_inputs_folder(index_run)
        return f"jq -r '.thermodynamic.temperature * .seed' {case_folder / 'main.json'} > {case_folder / 'results.txt'}"

    # abstract implementation
    def post_callable(self, index_run: int, returncode: int | None, stdout: str, stderr: str) -> str | None:
        if returncode == 0:
            case_folder = self.study.files_builder.get_case_inputs_folder(index_run)
            res = (case_folder / 'results.txt').read_text()
            self.total += float(res)

# Execute
batch = ExampleBatch(study)
batch.exec()

print(batch.total)

Results:

  • Create the cases folders, inputs and results files like :
cases/
├── 0/
│   ├── main.json
│   └── results.txt
├── 1/
│   ├── main.json
│   └── results.txt
├── 2/
│   ├── main.json
│   └── results.txt
├── 3/
│   ├── main.json
│   └── results.txt
├── 4/
│   ├── main.json
│   └── results.txt
└── 5/
│   ├── main.json
│   └── results.txt
  • Displays output like
[INFO] [ExampleBatch] START — 6 runs
[INFO] [ExampleBatch] RUN 1/6
[INFO] [ExampleBatch] RUN 2/6
[INFO] [ExampleBatch] RUN 3/6
[INFO] [ExampleBatch] RUN 4/6
[INFO] [ExampleBatch] RUN 5/6
[INFO] [ExampleBatch] RUN 6/6
[INFO] [ExampleBatch] END — 0 errors / 6 runs
8352718.5