Vaches sphériques

Cet exemple présente une implémentation simplifiée d’un job valjean pour la comparaison systématique de TRIPOLI-4® et MCNP sur des systèmes très simples : des sphères contenant un seul isotope et une source de neutrons monoénergétiques placée au centre. Nous calculons le flux neutron intégré sur la sphère et découpé à 616 groupes.

Note : cet exemple est censé être exécuté par l’exécutable valjean.

Fichier job.py

Imports

import os
from pathlib import Path

from valjean.cosette.run import RunTaskFactory
from valjean.cosette.task import TaskStatus
from valjean.cosette.pythontask import PythonTask
from valjean.eponine.tripoli4.parse import Parser
from valjean.eponine.tripoli4.data_convertor import convert_data
from valjean.gavroche.stat_tests.student import TestStudent
from valjean.gavroche.stat_tests.bonferroni import TestHolmBonferroni
from valjean.javert.test_report import TestReport
from valjean.javert.representation import Representation, FullRepresenter
from valjean.javert.rst import RstTestReportTask
import mcnp  # module auxiliaire de lecture de fichiers MCTAL, pas inclus dans valjean

Définitions des chemins et des paramètres

JOB_PATH = Path(__file__)

# paramètres pour TRIPOLI-4
T4_EXE = '/path/to/bin/tripoli4'
T4_PATH = '/path/to/t4path'

# paramètres pour MCNP
MCNP_EXE = '/path/to/bin/mcnp6'
MCNP_DATAPATH = '/path/to/MCNP_DATA'
MCNP_XSDIR = '/path/to/MCNP/xsdir'
os.environ['DATAPATH'] = MCNP_DATAPATH

TABPROB = True        # utiliser les tables de probabilités ?
N_HISTORIES = 100000  # nombre d'histoires à simuler

Systèmes à tester

On définit ici une liste de nucléides à étudier.

#             ┌────────────────────────────────────── noyau
#             │          ┌─────────────────────────── nom noyau TRIPOLI-4
#             │          │       ┌─────────────────── code noyau MCNP (ZZZAAA)
#             │          │       │     ┌───────────── énergie (MeV)
#             │          │       │     │    ┌──────── température (K)
#             │          │       │     │    │      ┌─ concentration en 1/(b cm²)
#             v          v       v     v    v      v
SYSTEMS = [('O16',     'O16',  8016, 14.0, 294, 1.527e-1),
           ('U235',   'U235', 92235, 14.0, 294, 4.549e-3),
           ('Pu239', 'PU239', 94239, 14.0, 294, 4.590e-3)]

Définition de la fonction job()

Cette fonction définit le travail à exécuter par valjean. On va déclarer les tâches et les dépendances entre elles, et valjean va s’occuper du lancement. Il existe des types de tâches différentes.

def job():
    # On crée une classe utilitaire pour générer des tâches qui
    # exécutent TRIPOLI-4. Les options de la ligne de commande T4
    # sont paramétrées par des mot-clés.
    t4_args = ['-s', '{fmt}', '-d', '{input_file}', '-c', '{t4path}',
               '-l', '{language}', '-a', '-u']
    t4_fac = RunTaskFactory.from_executable(T4_EXE, name='t4',
                                            default_args=t4_args,
                                            t4path=T4_PATH,
                                            language='english')

    # On fait la même chose pour MCNP.
    mcnp_args = ['i={input_file}', 'xs={xsdir}', 'o={output_file}']
    mcnp_fac = RunTaskFactory.from_executable(MCNP_EXE,
                                              name='mcnp',
                                              default_args=mcnp_args,
                                              xsdir=MCNP_XSDIR)

    # Voici la liste des tâches de comparaison TRIPOLI-4 vs. MCNP
    # à exécuter. La fonction make_comparison_task() apparaît
    # ci-dessous.
    tasks = [make_comparison_task(system, t4_fac, mcnp_fac)
             for system in SYSTEMS]

    # Les tâches de comparaison vont générer des résultat de tests statistiques.
    # On va les transformer en rapport HTML, via un format intermédiaire
    # (reStructuredText, une sorte de Markdown).
    my_repr = Representation(FullRepresenter())
    # La construction du rapport de test est aussi une tâche valjean ! La seule
    # ligne mystérieuse est...
    report_task = RstTestReportTask.from_tasks(name='report',
                                               make_report=make_report, # celle-ci
                                               eval_tasks=tasks,
                                               representation=my_repr,
                                               author='Davide Mancusi',
                                               version='0.0.1',
                                               kwargs={'title': 'Spherical cows'})
    # Ici make_report est une fonction qui décrit comment organiser les résultats
    # de test en sections. Dans notre cas, elle va être très simple.

    # On renvoie uniquement la tâche de construction du rapport. Elle dépend
    # implicitement des tâches de comparaison, qui dependent des tâches d'exécution
    # de TRIPOLI-4 et MCNP...
    return [report_task]

Comparaison TRIPOLI-4/MCNP pour un système donné

Cette fonction prépare les jeux de données TRIPOLI-4 et MCNP en remplissant les cases vides de fichiers templates (TRIPOLI-4, MCNP).

def make_comparison_task(system, t4_fac, mcnp_fac):

    # On déballe le tuple qui décrit le système
    nucleus, t4_nucleus, zaid, energy, temperature, conc = system
    nuc_id = str(zaid)

    input_path = JOB_PATH.with_name('input') / f'{nucleus}_{energy}'
    input_path.mkdir(parents=True, exist_ok=True)

    # On crée le jeu de données pour TRIPOLI-4 à partir des paramètres du
    # système et d'un jeu de données "template".
    t4_input_path = input_path / 't4'
    format_t4_input(t4_input_path, nucleus=t4_nucleus, concentration=conc,
                    n_histories=N_HISTORIES, temperature=temperature,
                    energy=energy)

    # On fait la même chose pour MCNP
    mcnp_input_path = input_path / 'mcnp'
    mcnp_output_path = 'mcnp.out'
    format_mcnp_input(mcnp_input_path, nucleus=nucleus, concentration=conc,
                      nuc_id=nuc_id, tabprob=TABPROB,
                      n_histories=N_HISTORIES, temperature=temperature,
                      energy=energy)

    # Les fonctions format_t4_input() et format_mcnp_input() apparaîssent
    # plus bas.

    # Voici les tâches qui vont exécuter TRIPOLI-4 et MCNP
    t4_fmt = 'TABPROB' if TABPROB else 'NJOY'
    t4_task = t4_fac.make(input_file=str(t4_input_path), fmt=t4_fmt,
                          name=f'{nucleus}@{energy}')
    mcnp_task = mcnp_fac.make(input_file=str(mcnp_input_path),
                              output_file=str(mcnp_output_path),
                              name=f'{nucleus}@{energy}')

    # Et voici la tâche de comparaison entre le résultat de TRIPOLI-4 et
    # celui de MCNP. La tâche consiste à appeler une fonction appelée
    # compare(), qui apparaitra dans la suite
    task_name = f'comparison {nucleus}_{energy}'
    compare_task = PythonTask(task_name,
                              compare,  # voici la fonction à appeler
                              env_kwarg='env',
                              # voici les arguments à passer à compare()
                              kwargs={'nucleus': nucleus,
                                      'energy': float(energy),
                                      't4_name': t4_task.name,
                                      'task_name': task_name,
                                      'mcnp_name': mcnp_task.name},
                              # on déclare que cette tâche dépend de t4_task et mcnp_task
                              deps=[t4_task, mcnp_task])
    return compare_task

Que fait la fonction de comparaison? Elle lit le listing de sortie TRIPOLI-4 et celui de MCNP, extrait les flux neutron multigroupe et fait un test statistique de compatibilité entre les deux.

def compare(*, t4_name, mcnp_name, task_name, nucleus, energy, env):
    mcnp_mctal = Path(env[mcnp_name]['result']).with_name('mctal')
    mcnp_ds = mcnp.MCTALResult(str(mcnp_mctal)).result(4, 1)
    mcnp_ds.name = mcnp_name

    t4_out = Path(env[t4_name]['result'])
    t4_br = Parser(t4_out).parse_from_index().to_browser()
    t4_res = t4_br.select_by(score_name='flux_score')['results']
    t4_ds = (convert_data(t4_res, 'spectrum', name=t4_name, what='flux')
             .squeeze())

    test = TestHolmBonferroni(test=TestStudent(t4_ds, mcnp_ds, name=nucleus),
                              name=f'Holm-Bonferroni test, {nucleus} '
                                   f'@ {energy} MeV',
                              labels={'nucleus': nucleus,
                                      'energy': energy})
    test_result = test.evaluate()
    return {task_name: {'result': [test_result]}}, TaskStatus.DONE

Fonctions auxiliaires

Le reste, c’est de la bureaucratie. La fonction qui génère le jeu de données TRIPOLI-4 ne fait que lire le template et remplir les cases vides.

def format_t4_input(output_path, *, n_batch=200, temperature, n_histories,
                    **kwargs):
    template_path = JOB_PATH.with_name('templates') / 't4'
    template = template_path.read_text()
    batch_size = max(1, n_histories // n_batch)
    edition = max(1, n_batch // 10)
    content = template.format(batch_size=batch_size, n_batch=n_batch,
                              temperature=int(temperature), edition=edition,
                              **kwargs)
    Path(output_path).write_text(content)

La fonction MCNP est très similaire.

def format_mcnp_input(output_path, *, tabprob, temperature, **kwargs):
    boltzmann = 8.617341e-11  # MeV/K
    temperature_MeV = temperature*boltzmann
    template_path = JOB_PATH.with_name('templates') / 'mcnp'
    template = template_path.read_text()
    tabprob_int = 0 if tabprob else 1
    content = template.format(tabprob=tabprob_int, temperature=temperature_MeV,
                              **kwargs)
    Path(output_path).write_text(content)

Voici enfin la fonction make_report(), qui organise les résultats de test en sections.

def make_report(all_test_results, *, title):
    # Ici all_test_results est un dictionnaire qui associe les nom des tâches
    # à des listes de résultats de tests.
    sections = []
    for task_name, test_results in sorted(all_test_results.items()):
        # On récupère le nom du noyau et l'énergie du test pour construire
        # le titre de la page.
        test = test_results[0].test
        nucleus = test.labels['nucleus']
        energy = test.labels['energy']
        section = TestReport(title=f'{nucleus}, {energy} MeV',
                             content=test_results)
        sections.append(section)

    report = TestReport(title=title,
                        content=[TestReport(title='Test results',
                                            text='Here is the good stuff.',
                                            content=sections)])
    return report

Exécution

Voici ce qui se passe quand on exécute le job avec valjean dans un terminal :

$ valjean run job.py
* graphs built in 0.6359915770590305 seconds
* hard_graph contains 11 tasks
* soft_graph contains 11 tasks
* will schedule up to 4 tasks in parallel
* deserializing pickle environment from 'valjean.env' files in spherical_cows/output
* 0 environment files found and deserialized
* graph completed in 1.5154480934143066e-05 seconds
* graph completed in 1.0513700544834137e-05 seconds
* hard graph copied in 0.00017551984637975693 seconds
* hard graph flattened in 1.2297183275222778e-05 seconds
* graph completed in 9.783543646335602e-06 seconds
* full graph computed in 0.00023496989160776138 seconds
* full graph flattened in 9.047798812389374e-06 seconds
* scheduling tasks
* full graph sorted in 6.301887333393097e-05 seconds
* master: 11 tasks left
* master: 5 tasks left
* task 'U235@14.0.t4' starts
* task 'U235@14.0.mcnp' starts
* task 'O16@14.0.mcnp' starts
* task 'O16@14.0.t4' starts
* task 'U235@14.0.mcnp' completed with status TaskStatus.DONE
* task 'Pu239@14.0.t4' starts
* master: 5 tasks left
* task 'O16@14.0.mcnp' completed with status TaskStatus.DONE
* task 'Pu239@14.0.mcnp' starts
* master: 5 tasks left
* task 'U235@14.0.t4' completed with status TaskStatus.DONE
* master: 5 tasks left
* master: 4 tasks left
* task 'comparison U235_14.0' starts
* Parsing spherical_cows/output/U235@14.0.t4/stdout
* Successful scan in 0.017888 s
* Successful parsing in 0.130465 s
* task 'Pu239@14.0.mcnp' completed with status TaskStatus.DONE
* master: 4 tasks left
* task 'comparison U235_14.0' completed with status TaskStatus.DONE
* master: 4 tasks left
* task 'Pu239@14.0.t4' completed with status TaskStatus.DONE
* master: 4 tasks left
* master: 3 tasks left
* task 'comparison Pu239_14.0' starts
* Parsing spherical_cows/output/Pu239@14.0.t4/stdout
* Successful scan in 0.029409 s
* Successful parsing in 0.129308 s
* task 'comparison Pu239_14.0' completed with status TaskStatus.DONE
* master: 3 tasks left
* task 'O16@14.0.t4' completed with status TaskStatus.DONE
* master: 3 tasks left
* master: 2 tasks left
* task 'comparison O16_14.0' starts
* Parsing spherical_cows/output/O16@14.0.t4/stdout
* Successful scan in 0.027760 s
* Successful parsing in 0.093801 s
* task 'comparison O16_14.0' completed with status TaskStatus.DONE
* master: 2 tasks left
* master: 1 tasks left
* task 'report-report' starts
* task 'report-report' completed with status TaskStatus.DONE
* master: 1 tasks left
* task 'report' starts
* writing tree_path: spherical_cows/report/report/index
* writing tree_path: spherical_cows/report/report/Test results
* writing tree_path: spherical_cows/report/report/Test results/O16, 14.0 MeV
* writing tree_path: spherical_cows/report/report/Test results/Pu239, 14.0 MeV
* writing tree_path: spherical_cows/report/report/Test results/U235, 14.0 MeV
* writing 3 plots using 4 subprocesses
valjean/valjean/gavroche/stat_tests/student.py:478: RuntimeWarning: invalid value encountered in true_divide
  studentt = diff.value / diff.error
* drawing figure spherical_cows/report/report/figures/plot_cc240cbb3a9ae844616da16989931f4a21578e75aa8b9b3d895d36b2247ac7fd.png
* drawing figure spherical_cows/report/report/figures/plot_9589d583b0b1af6b1ff2ec2aefcc88deda3ee5cec98d1a25c487e247c5aeaf52.png
* drawing figure spherical_cows/report/report/figures/plot_1f8ae35814f479a1b25ba26f0e810126875b2a1cfb5ce3d2bb23bcd4f2de6f9a.png
* task 'report' completed with status TaskStatus.DONE
* final environment statistics:
     DONE: 11/11 (100.0%)
* serializing pickle environment to 'valjean.env' files
* 7 environment files written

Un rapport au format RST a été généré dans report/report. Je le convertis en HTML avec sphinx :

$ sphinx-build -j30 -a -bhtml report/report report/html