From 0ac18f80e90235412a7ff249b7fc5a99ac623c1e Mon Sep 17 00:00:00 2001 From: Robert Carson Date: Mon, 29 Jun 2026 15:09:30 -0700 Subject: [PATCH] [claude / codex] Refactor of calibration / optimization workflows The older workflow/optimization workflow was starting to show its age and was no longer really useable with the v0.9 refactor of ExaConstit. Additionally, it wasn't super flexible for newer features one might be interested in or making backends easier to use. So, I threw Claude Opus at the issue because why not... In reality, my bandwidth is way more limited than I would like to re-engineer everything from scratch. It in many ways way over engineered things. However, I would say the new framework should make be more extensive to even codes outside of ExaConstit. So, one should be able to sub in a diffferent code and its input and output without too much extra effort in theory... Part of this in the super extensive test suite... I think there's something like 300 or 400 tests total... New abstractions, a much nicer backend aspect of things so that it's easier to work with like mpi or flux. The flux backend is now much nicer than previously in that it provides async status updates and makes it easier to see how jobs are progressing. Additionally, we can track whether or not each job has finished or not so during restarts we can either start at the beginning of gene or only run the ones that didn't finish. A number of different smoothers are also available to make it easier to interp from experimental data to simulation data in a sane manner. Next, the problem, case setup, objective aspect have all been abstracted out into their own thing to make it easier to compose things together / build something up. Outside of that, we also have the ability to do like SQL databases to archive the results and then query those. Finally, all of the above were common workflow aspects and don't in any way tie into the actual optimizer being used. So, the NSGA-III aspect of things lives in a completely seperate set of directories. It is now its own thing with more abstracted out set of functions that should hopefully make it ultimately easier to navigate and try to parse out what everything is doing. The final portion of code is in many ways easier to reason but further clean-up is likely needed to make things even simpler... Additionally, a few nicety files have been added for the NSGA-III stuff which can regenerate a logbook if things were wiped out but the main SQL database still lives. Also, a simple script can let you do CLI views of how the job is progressing for the different objectives. There are also a number of markdown files that should make it easier to navigate all of this / also keep what ever agentic coding tool happy / able to understand what's going on. Lastly, I've got a very simple example that largely replicates what the old optimization script was doing but this time leverages the newer framework and also shows off how to build things up for your own needs. It has a lot of comments in it so hopefully it should be not too hard to understand what's going on. The test cases are based on just the ExaConstit/test/voce_full.toml test cases but I had one case at 1e-3 1/s and the other at 1e-1 1/s. --- .../exaconstit-calibrate/ARCHITECTURE.md | 262 ++ workflows/exaconstit-calibrate/AUDIT_PLAN.md | 1643 +++++++++++++ workflows/exaconstit-calibrate/LICENSE | 29 + workflows/exaconstit-calibrate/MANIFEST.in | 14 + workflows/exaconstit-calibrate/README.md | 399 +++ workflows/exaconstit-calibrate/RELEASING.md | 337 +++ .../exaconstit-calibrate/examples/README.md | 132 + .../examples/SLURM_RUN_GUIDE.md | 379 +++ .../experiments/expt_strain_rate_1m1.txt | 62 + .../experiments/expt_strain_rate_1m3.txt | 60 + .../examples/master_options.toml | 138 ++ .../examples/nsga3_calibration.py | 1616 +++++++++++++ .../examples/nsga3_slurm_helpers.sh | 260 ++ .../examples/options.toml | 815 +++++++ .../examples/parameter_creation.py | 70 + .../examples/plot_solutions.py | 1623 +++++++++++++ .../examples/run_nsga3_slurm.sh | 178 ++ .../examples/template_options.toml | 170 ++ .../voce_ea_1m1/avg_def_grad_global.txt | 70 + .../avg_def_grad_region_default_1.txt | 70 + .../voce_ea_1m1/avg_elastic_strain_global.txt | 70 + .../avg_elastic_strain_region_default_1.txt | 70 + .../voce_ea_1m1/avg_eq_pl_strain_global.txt | 70 + .../avg_eq_pl_strain_region_default_1.txt | 70 + .../avg_euler_strain_global copy.txt | 63 + .../voce_ea_1m1/avg_euler_strain_global.txt | 70 + .../avg_euler_strain_region_default_1.txt | 70 + .../voce_ea_1m1/avg_pl_work_global.txt | 70 + .../avg_pl_work_region_default_1.txt | 70 + .../voce_ea_1m1/avg_stress_global copy.txt | 63 + .../voce_ea_1m1/avg_stress_global.txt | 70 + .../avg_stress_region_default_1.txt | 70 + .../voce_ea_1m3/avg_def_grad_global.txt | 61 + .../avg_def_grad_region_default_1.txt | 61 + .../voce_ea_1m3/avg_elastic_strain_global.txt | 61 + .../avg_elastic_strain_region_default_1.txt | 61 + .../voce_ea_1m3/avg_eq_pl_strain_global.txt | 61 + .../avg_eq_pl_strain_region_default_1.txt | 61 + .../voce_ea_1m3/avg_euler_strain_global.txt | 61 + .../avg_euler_strain_region_default_1.txt | 61 + .../voce_ea_1m3/avg_pl_work_global.txt | 61 + .../avg_pl_work_region_default_1.txt | 61 + .../voce_ea_1m3/avg_stress_global.txt | 61 + .../avg_stress_region_default_1.txt | 61 + workflows/exaconstit-calibrate/pyproject.toml | 134 ++ workflows/exaconstit-calibrate/test_script.sh | 6 + .../exaconstit-calibrate/tests/conftest.py | 272 +++ .../tests/demo_workflow.py | 536 +++++ .../tests/test_archive.py | 1215 ++++++++++ .../tests/test_case_setup.py | 442 ++++ .../tests/test_example_imports.py | 69 + .../exaconstit-calibrate/tests/test_fs.py | 112 + .../tests/test_inspect_archive.py | 1202 ++++++++++ .../tests/test_integration.py | 513 ++++ .../tests/test_integration_restart.py | 292 +++ .../tests/test_integration_smoothing.py | 410 ++++ .../tests/test_local_backend.py | 289 +++ .../tests/test_manifest.py | 241 ++ .../tests/test_nsga3_driver.py | 2130 +++++++++++++++++ .../tests/test_objectives.py | 718 ++++++ .../exaconstit-calibrate/tests/test_paths.py | 201 ++ .../tests/test_plot_solutions.py | 1957 +++++++++++++++ .../tests/test_postprocess.py | 506 ++++ .../tests/test_problem.py | 1131 +++++++++ .../tests/test_progress.py | 168 ++ .../tests/test_regenerate_logbook_files.py | 171 ++ .../tests/test_results.py | 393 +++ .../tests/test_smoothing.py | 334 +++ .../tests/test_templates.py | 102 + .../workflow_common/ARCHITECTURE.md | 361 +++ .../workflow_common/MIGRATION.md | 1010 ++++++++ .../workflow_common/__init__.py | 312 +++ .../workflow_common/_fs.py | 253 ++ .../workflow_common/archive.py | 1489 ++++++++++++ .../workflow_common/backends/__init__.py | 82 + .../workflow_common/backends/base.py | 495 ++++ .../workflow_common/backends/flux_backend.py | 645 +++++ .../workflow_common/backends/local.py | 424 ++++ .../workflow_common/case_setup.py | 666 ++++++ .../workflow_common/logging_utils.py | 230 ++ .../workflow_common/manifest.py | 664 +++++ .../workflow_common/objectives.py | 868 +++++++ .../workflow_common/paths.py | 427 ++++ .../workflow_common/platform_detect.py | 79 + .../workflow_common/postprocess.py | 788 ++++++ .../workflow_common/problem.py | 1377 +++++++++++ .../workflow_common/progress.py | 251 ++ .../workflow_common/results.py | 941 ++++++++ .../workflow_common/sentinel.py | 333 +++ .../workflow_common/smoothing.py | 705 ++++++ .../workflow_common/templates.py | 261 ++ .../workflows/__init__.py | 7 + .../workflows/optimization/__init__.py | 54 + .../workflows/optimization/inspect_archive.py | 1317 ++++++++++ .../workflows/optimization/nsga3_driver.py | 1531 ++++++++++++ .../optimization/regenerate_logbook_files.py | 142 ++ 96 files changed, 39141 insertions(+) create mode 100644 workflows/exaconstit-calibrate/ARCHITECTURE.md create mode 100644 workflows/exaconstit-calibrate/AUDIT_PLAN.md create mode 100644 workflows/exaconstit-calibrate/LICENSE create mode 100644 workflows/exaconstit-calibrate/MANIFEST.in create mode 100644 workflows/exaconstit-calibrate/README.md create mode 100644 workflows/exaconstit-calibrate/RELEASING.md create mode 100644 workflows/exaconstit-calibrate/examples/README.md create mode 100644 workflows/exaconstit-calibrate/examples/SLURM_RUN_GUIDE.md create mode 100644 workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m1.txt create mode 100644 workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m3.txt create mode 100644 workflows/exaconstit-calibrate/examples/master_options.toml create mode 100644 workflows/exaconstit-calibrate/examples/nsga3_calibration.py create mode 100755 workflows/exaconstit-calibrate/examples/nsga3_slurm_helpers.sh create mode 100644 workflows/exaconstit-calibrate/examples/options.toml create mode 100644 workflows/exaconstit-calibrate/examples/parameter_creation.py create mode 100644 workflows/exaconstit-calibrate/examples/plot_solutions.py create mode 100755 workflows/exaconstit-calibrate/examples/run_nsga3_slurm.sh create mode 100644 workflows/exaconstit-calibrate/examples/template_options.toml create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global copy.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global copy.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_global.txt create mode 100644 workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_region_default_1.txt create mode 100644 workflows/exaconstit-calibrate/pyproject.toml create mode 100644 workflows/exaconstit-calibrate/test_script.sh create mode 100644 workflows/exaconstit-calibrate/tests/conftest.py create mode 100644 workflows/exaconstit-calibrate/tests/demo_workflow.py create mode 100644 workflows/exaconstit-calibrate/tests/test_archive.py create mode 100644 workflows/exaconstit-calibrate/tests/test_case_setup.py create mode 100644 workflows/exaconstit-calibrate/tests/test_example_imports.py create mode 100644 workflows/exaconstit-calibrate/tests/test_fs.py create mode 100644 workflows/exaconstit-calibrate/tests/test_inspect_archive.py create mode 100644 workflows/exaconstit-calibrate/tests/test_integration.py create mode 100644 workflows/exaconstit-calibrate/tests/test_integration_restart.py create mode 100644 workflows/exaconstit-calibrate/tests/test_integration_smoothing.py create mode 100644 workflows/exaconstit-calibrate/tests/test_local_backend.py create mode 100644 workflows/exaconstit-calibrate/tests/test_manifest.py create mode 100644 workflows/exaconstit-calibrate/tests/test_nsga3_driver.py create mode 100644 workflows/exaconstit-calibrate/tests/test_objectives.py create mode 100644 workflows/exaconstit-calibrate/tests/test_paths.py create mode 100644 workflows/exaconstit-calibrate/tests/test_plot_solutions.py create mode 100644 workflows/exaconstit-calibrate/tests/test_postprocess.py create mode 100644 workflows/exaconstit-calibrate/tests/test_problem.py create mode 100644 workflows/exaconstit-calibrate/tests/test_progress.py create mode 100644 workflows/exaconstit-calibrate/tests/test_regenerate_logbook_files.py create mode 100644 workflows/exaconstit-calibrate/tests/test_results.py create mode 100644 workflows/exaconstit-calibrate/tests/test_smoothing.py create mode 100644 workflows/exaconstit-calibrate/tests/test_templates.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/ARCHITECTURE.md create mode 100644 workflows/exaconstit-calibrate/workflow_common/MIGRATION.md create mode 100644 workflows/exaconstit-calibrate/workflow_common/__init__.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/_fs.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/archive.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/backends/__init__.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/backends/base.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/backends/flux_backend.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/backends/local.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/case_setup.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/logging_utils.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/manifest.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/objectives.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/paths.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/platform_detect.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/postprocess.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/problem.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/progress.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/results.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/sentinel.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/smoothing.py create mode 100644 workflows/exaconstit-calibrate/workflow_common/templates.py create mode 100644 workflows/exaconstit-calibrate/workflows/__init__.py create mode 100644 workflows/exaconstit-calibrate/workflows/optimization/__init__.py create mode 100644 workflows/exaconstit-calibrate/workflows/optimization/inspect_archive.py create mode 100644 workflows/exaconstit-calibrate/workflows/optimization/nsga3_driver.py create mode 100644 workflows/exaconstit-calibrate/workflows/optimization/regenerate_logbook_files.py diff --git a/workflows/exaconstit-calibrate/ARCHITECTURE.md b/workflows/exaconstit-calibrate/ARCHITECTURE.md new file mode 100644 index 0000000..5d733fc --- /dev/null +++ b/workflows/exaconstit-calibrate/ARCHITECTURE.md @@ -0,0 +1,262 @@ +# workflow_common — Architecture + +Read this if you are new to the codebase and want a one-document +overview before diving into individual modules. The goal here is not +to replace the in-source docstrings (those are the source of truth) +but to show how the pieces fit together. + +## What problem this package solves + +A scientific workflow — a parameter sweep, an optimization, a UQ +campaign — ends up doing the same handful of things no matter what +simulation code sits underneath: + +1. Render per-case input files from a master template. +2. Lay out working directories in a predictable way. +3. Launch simulations on some hardware (laptop, cluster, HPC). +4. Collect results, deal with failures, survive an allocation timeout. + +Historically these concerns were tangled together inside a single +driver script. `workflow_common` separates them so that each one is +independently reusable and independently testable — and so the same +driver can swap in a different simulation code by supplying a +different template and a different path pattern, rather than being +rewritten. + +## One-picture overview + +``` + +-------------------------+ + | Driver / user code | + | (optimizer, sweep, ...)| + +-------------------------+ + | | | | + v v v v + +------------+ +------+ +------+ +--------+ + | templates | |paths | |manif.| |sentinel| + +------------+ +------+ +------+ +--------+ + | + | submits SimJobSpec + v + +------------------------+ + | JobBackend Protocol | + +------------------------+ + / \ + / \ + +------------------+ +------------------+ + | LocalBackend | | FluxBackend | + | (subprocess + | | (flux.job. | + | ThreadPool) | | FluxExecutor) | + +------------------+ +------------------+ + | | + v v + local subprocesses Flux-submitted jobs +``` + +The driver is the only code that knows the user's optimization +algorithm. Everything below the driver is problem-agnostic. + +## Module responsibilities, one line each + +| Module | Responsibility | +| --- | --- | +| `_fs` | Directory context manager, atomic text writes | +| `logging_utils` | Stdlib `logging` setup + shims for old call sites | +| `templates` | `%%key%%` substitution for input-file rendering | +| `paths` | Case → working-directory and logical → output-file resolution | +| `manifest` | JSONL event log for crash-safe case state tracking | +| `sentinel` | Per-case `.done` file as authoritative completion marker | +| `platform_detect` | Hostname-based detection of Spectrum-MPI machines | +| `backends.base` | `SimJobSpec` / `JobResult` / `JobBackend` Protocol | +| `backends.local` | Run sims as local subprocesses | +| `backends.flux_backend` | Run sims via `flux.job.FluxExecutor` | + +## The two most important interfaces + +Almost everything in the package hangs off two small abstractions. +If you internalize these, the rest is mechanical. + +### `PathResolver` + +```python +class PathResolver(Protocol): + def working_dir(self, ctx: CaseContext) -> Path: ... + def output_file(self, logical_name: str, ctx: CaseContext) -> Path: ... +``` + +Given a `CaseContext` (a (generation, gene, obj) triple plus extras), +a resolver answers: + +- "Where does this case live on disk?" +- "Where will its `avg_stress.txt` appear after the sim runs?" + +`TemplatePathResolver` is the supplied implementation: layouts are +described by Python format-string patterns. Users with exotic needs +can write their own class satisfying the Protocol. + +### `JobBackend` + +```python +class JobBackend(Protocol): + def stream_batch(self, specs: Sequence[SimJobSpec]) -> Iterator[JobResult]: ... + def submit_batch(self, specs: Sequence[SimJobSpec]) -> List[JobResult]: ... + def submit_one(self, spec: SimJobSpec) -> JobResult: ... +``` + +Given a batch of `SimJobSpec`s, a backend runs them and produces +`JobResult`s. Callers do not need to know whether the backend uses +subprocesses, Flux, SLURM, or something not yet written. Concrete +backends inherit from `BaseBackend` and only need to implement +`stream_batch`; the other two methods fall out for free. + +## How a case flows through the system + +The case lifecycle is the clearest way to see how the modules +cooperate. For each case, in order: + +1. **Path resolution.** Driver builds a `CaseContext` and asks the + `PathResolver` for the working directory. It creates the directory + and renders `options.toml` (or whatever) from the master template + using `render_template_file`. +2. **Manifest: SUBMITTED.** Driver records a `ManifestEntry(state= + SUBMITTED)` with the case key and working dir. If the driver is + killed between this write and a terminal state, restart logic + will later promote this entry to `INTERRUPTED`. +3. **Backend handoff.** Driver builds a `SimJobSpec` pointing at the + simulation binary and hands it to the backend's `stream_batch`. + The backend launches the job (subprocess or Flux) and, later, + yields a `JobResult`. +4. **Output validation.** Driver calls `validate_outputs` to confirm + the required files are present and non-empty. A clean `rc=0` with + missing files is still a workflow-level failure. +5. **Sentinel write.** Driver calls `write_sentinel` with a `Sentinel` + object describing what happened. The write is atomic + (tempfile + rename). After this point, the case is considered + done regardless of what happens next. +6. **Manifest: COMPLETED / FAILED.** Driver records the terminal + `ManifestEntry`. This is the last step on purpose: a crash between + steps 5 and 6 is safely recoverable (sentinel tells the truth), + but a crash in the opposite order would leave the manifest + claiming work was done that was never validated. + +``` + CaseContext build SimJobSpec JobResult + | | | + | render template | stream_batch | validate outputs + v v v + options.toml backend launches +-----------+ + flux / subprocess | Sentinel | step 5 + | write | (FIRST) + +-----------+ + | + v + +-----------+ + | Manifest | step 6 + | record | (SECOND) + +-----------+ +``` + +## Why two state files (manifest and sentinel)? + +A common question. Both record case state; why not unify them? + +They answer different questions: + +- **The manifest** answers "what is the global state of this run?" + It is append-only (JSONL), holds every transition for every case, + and is the source of truth for restart planning. +- **The sentinel** answers "is the work in this specific directory + finished?" It is a single file in the case directory, written + atomically, and is the source of truth for per-case skip logic. + +The sentinel is locally inspectable. An analyst looking at one +output directory knows at a glance whether it is safe to post-process +the files inside. They do not need to parse the manifest, they do +not need to know which run this directory came from — the `.done` +file tells them. + +The manifest is globally inspectable. The driver needs to know, on +restart, which cases are done so it can skip them, and which cases +were submitted but never finished so it can retry them. A pile of +sentinels cannot tell you about cases that were planned but never +launched. + +Keeping both means each layer is small, simple, and can be reasoned +about in isolation. + +## Restart model + +Optimization runs routinely outlive their HPC allocations. The +restart story is: + +1. On startup, driver calls `Manifest.load()`. This replays the + snapshot + JSONL log to rebuild the in-memory state. +2. Driver calls `Manifest.mark_submitted_as_interrupted()`. Anything + stuck in SUBMITTED is assumed to have been killed alongside the + old allocation. +3. For each case in the new plan, driver checks + `is_case_complete(working_dir)`. A present sentinel means the + case is done (regardless of what the manifest says) and can be + skipped. No sentinel means the case either never ran, was + interrupted, or failed without producing a sentinel, and should + be submitted. + +There is deliberately no Flux-state recovery. When the allocation +dies, the Flux instance dies with it, and its jobids are +unrecoverable. The restart machinery is entirely filesystem-driven. + +## Non-goals + +Things `workflow_common` deliberately does not do: + +- **Parse simulation input files.** Master templates are text; we + substitute `%%key%%` placeholders and nothing else. The framework + never knows whether the file is TOML, XML, INI, or a shell script. +- **Know about physical quantities.** Strain rates, yield stresses, + stress-strain curves — all of that is user code. The framework + only carries opaque parameter dicts from the driver into the + template renderer. +- **Optimization.** No GA, no nearest-neighbor sampling, no + surrogate models. Those live in the driver. The framework's job + is to make driving the simulation reproducible and restartable. +- **Auto-starting Flux.** You start a Flux instance yourself (via + `flux start` inside an allocation); the backend connects to + whatever is already running. + +## Adding a new backend + +If you need to support a new execution environment (e.g. a SLURM +step backend, or a remote SSH backend for tiny shared clusters): + +1. Subclass `BaseBackend` (from `backends.base`). +2. Implement `stream_batch(specs) -> Iterator[JobResult]`. The + `submit_batch` and `submit_one` methods come for free. +3. Use `SimJobSpec` fields for all inputs; do not introduce new + fields unless absolutely necessary. If you do need new fields, + put them inside `SimJobSpec.extra` so core callers remain + backend-agnostic. +4. Return `JobResult` with a meaningful `JobOutcome`. + +No Protocol inheritance declaration required — Python's structural +typing means any class with the right method signatures is accepted +wherever `JobBackend` is expected. + +## Adding a new path layout + +If `TemplatePathResolver`'s format-string approach does not fit your +needs (e.g. you want paths looked up from a database, or computed +from some hash of the parameter values): + +1. Write a class with `working_dir(self, ctx)` and + `output_file(self, logical_name, ctx)` methods. +2. Hand an instance to the driver where it expects a `PathResolver`. + +That's it. Again, no inheritance declaration needed. + +## Where to look next + +- `tests/demo_workflow.py` — runnable end-to-end example, no HPC + required. Read it top to bottom and you will see every module in + use. +- Each module's header docstring — the "why" for that module. +- The function-level docstrings — the "what" and the "how". diff --git a/workflows/exaconstit-calibrate/AUDIT_PLAN.md b/workflows/exaconstit-calibrate/AUDIT_PLAN.md new file mode 100644 index 0000000..5163c4b --- /dev/null +++ b/workflows/exaconstit-calibrate/AUDIT_PLAN.md @@ -0,0 +1,1643 @@ +# Audit Findings & Action Plan — `exaconstit-calibrate` + +Date: 2026-04-23 +Scope: systematic review of path handling, subprocess launches, default +values, and validation coverage across `workflow_common`, `workflows`, +and the backends. + +## Bug-class meta-pattern + +Three families of bugs surfaced in recent debugging, all variants of the +same meta-pattern: **a signature that accepts values it cannot honor, or +a handoff where two sides each think they own the same step.** + +1. **Silent-default-disables-feature.** A default value makes an + adjacent feature quietly inert. Example: `LocalBackend` defaulted + `mpi_launcher=None`, so `SimCase.num_tasks=4` silently produced + single-rank runs. The user never sees a warning. + +2. **Redundant handoff / double-compose.** Two layers each do the same + composition (path join, validation, substitution). Idempotent on + absolute inputs, so tests on tmp-dir absolute paths never see it; + surfaces only in production on relative user paths. Example: + `problem.py::_handle_backend_result` pre-joined `working_dir / p` + before calling `validate_outputs` which ALSO joined against + `working_dir`, producing `//...` on relative + workspaces. + +3. **API-drift / invented kwarg.** Example code against a docstring + mental model of the API instead of the real signature. Example: + `TemplateTarget(substitute=False)` was invented before that field + existed; `configure_logging(restart=...)` used the old ExaConstit + logger's kwarg name. + +All three share one root cause: **tests exercise happy-path absolute +tmp-dir inputs only.** Production users have relative `WORKSPACE` +paths, unusual kwarg combinations, and shapes the tests never cover. + +## Findings + +### Finding 1 — Silent escape via `TemplateTarget.dest` (FIXED) + +**Severity:** Medium — data can land outside the case dir. + +**Mechanism:** `CaseTemplater.render` joined `layout.working_dir / +target.dest` without validating `dest`. On POSIX, `Path("/abs") / +"/absolute"` returns `/absolute`, silently dropping the working-dir +prefix. An unvalidated `dest="../escape/foo.toml"` escapes the case +dir. An unvalidated `dest="{gene}.toml"` produces a literal `{gene}.toml` +filename. + +**Fix:** `_validate_case_relative_dest(dest, field_name)` helper in +`case_setup.py`, called from `TemplateTarget.__post_init__`. Rejects +absolute paths, `..` parent refs, `{...}` placeholder syntax, and +non-string / empty values. + +**Status:** Fixed. + +### Finding 2 — Same silent escape via `TemplatePropertyWriter.dest` and `DelimitedPropertyWriter.dest` (FIXED) + +**Severity:** Medium — same mechanism as #1, different classes. + +**Mechanism:** `TemplatePropertyWriter` and `DelimitedPropertyWriter` +both have `dest: str` fields that feed directly into `layout.working_dir +/ self.dest` inside `write()`. Same validation gap as #1. + +**Fix:** Same `_validate_case_relative_dest` helper, wired in via +`__post_init__` on both classes. `CallablePropertyWriter.dest_hint` is +cosmetic (logging only) and intentionally left unvalidated — the user's +callable does the real writing. + +**Status:** Fixed. + +### Finding 3 — `skip_completed` reuses stale results after config change (NOT FIXED) + +**Severity:** **High** — silent wrong-answer on restart. + +**Mechanism:** `Problem.skip_completed=True` (the default) uses +`is_case_complete(layout.working_dir)` to decide whether to re-run a +case. If a sentinel is present, the cached result is loaded. The +sentinel file does NOT fingerprint: + +- The gene vector that produced the result +- The `param_names` ordering +- The `ProblemConfig.binary` or `binary_args` +- `SimCase.case_data` (boundary conditions, strain rate, ...) + +So a user who: + +1. Runs generation 0 to completion +2. Realizes their `param_names` order was wrong, edits the driver +3. Resumes with `--resume-from checkpoint_gen_0.pkl` + +will get their new gene-vector interpretation evaluated against the +OLD sentinel-cached results. Objective values are silently wrong for +every case that didn't need re-running. No error, no warning. + +**Design questions for the fix:** + +1. **What counts as "the config changed"?** At minimum: the gene + vector itself (hash the numpy array); `param_names` (hash the + tuple); `ProblemConfig.binary` path + `binary_args` tuple; the + full `SimCase.case_data` dict. Omit `num_tasks`, `duration_s`, + and other resource fields — they don't affect the *physics* of the + simulation, only its execution. + +2. **Storage:** add `config_fingerprint: Optional[str]` to + `Sentinel`. Missing (old sentinel) → treat as untrusted, force + re-run with a one-line log message. Mismatched → force re-run with + a LOUD warning that includes the diff. Matched → skip as today. + +3. **Fingerprint canonicalization:** `hashlib.sha256(json.dumps({...}, + sort_keys=True, default=_canonicalize_value).encode()).hexdigest()`. + The `_canonicalize_value` helper handles numpy arrays (convert to + lists), Path objects (str), and tuples (list, so JSON stable). + +4. **Backward compatibility:** existing on-disk sentinels don't have + the fingerprint. Treat missing-fingerprint as "force re-run" — the + "safe" direction. Log once per restart: "N sentinels lack a config + fingerprint; re-running those cases for safety. This is expected + after a framework upgrade." + +5. **Opt-out:** some users will legitimately want to skip the + fingerprint check (they know their edits were cosmetic). Add + `ProblemConfig.fingerprint_policy: Literal["check", "force", + "skip"] = "check"`. `"force"` ignores the fingerprint (current + behavior). `"skip"` — cases without a fingerprint still skip + rather than re-run. Default `"check"` is the safe behavior. + +**Test plan:** at least five new tests: + +- Matching fingerprint → skip as expected +- Missing fingerprint (old sentinel) → re-run with log +- Mismatched fingerprint → re-run with warning +- `fingerprint_policy="force"` → skip regardless +- `fingerprint_policy="skip"` → skip when missing, warn-and-re-run + when mismatched + +**Estimated effort:** 200 lines of code, 5 test cases, plus an update +to `MIGRATION.md` explaining the new sentinel field and its +backward-compatibility path. + +**Status:** Not fixed. Highest-priority follow-up item. + +### Finding 4 — `clear_outputs` could delete files outside the case dir (FIXED) + +**Severity:** Medium — data-destruction hazard if a user misconfigures +`output_file_patterns` with an absolute path. + +**Mechanism:** `CaseLayout.clear_outputs()` iterates known outputs and +`unlink()`s each. If a user's `TemplatePathResolver` has an +`output_file_patterns` entry whose rendered path is absolute (or +escapes via `..`), `clear_outputs_on_rerun=True` would delete that +file on every restart. + +**Fix:** Clamp-to-workdir safety gate in `results.py::CaseLayout.clear_outputs`. +Resolve both the target path and the working dir (with symlinks) and +refuse to delete anything whose resolved path isn't `relative_to(wd)`. +Log the refusal at WARNING level with the offending path so the +misconfiguration is visible. + +**Status:** Fixed. + +### Finding 5 — `pickle.load` on `resume_from` path (DOCSTRING WARNING) + +**Severity:** Low — requires an attacker with write access to the +checkpoint workspace. + +**Mechanism:** `_load_checkpoint` calls `pickle.load` on a user-supplied +path. A malicious pickle executes arbitrary Python during unpickling. +On shared HPC scratch filesystems this is a real risk vector. + +**Fix:** Docstring warning on `_load_checkpoint` explaining the risk +and suggesting JSON+npz as an alternative exchange format. Did not +switch the serialization format — that's a bigger change and the risk +profile is low for the typical single-user workspace. + +**Status:** Warned. Possible follow-up: optional JSON+npz checkpoint +format for cross-user / cross-host exchange. + +### Finding 6 — FluxBackend passed raw (possibly relative) paths to Flux (FIXED) + +**Severity:** Low — version-dependent Flux behavior, would manifest +as "job runs in unexpected cwd" or "stdout lands nowhere obvious" on +relative-workspace runs. + +**Mechanism:** `FluxBackend._build_jobspec` set `js.cwd = str(spec.working_dir)` +and `js.stdout = spec.stdout` using the RAW user strings. Flux's +interpretation of a relative `cwd` depends on the broker version; +`LocalBackend` uses `resolved_stdout()` (absolute) consistently. + +**Fix:** Both `js.cwd` and `js.stdout/stderr` now use resolved +absolute paths at the boundary. Matches LocalBackend's behavior. + +**Status:** Fixed. + +### Finding 7 — Zero test coverage of relative-workspace roots (ACTION ITEM) + +**Severity:** Meta — this is the reason findings 3 and the prior +double-join bug reached production. + +**Mechanism:** Every existing integration test uses `tmp_path` (an +absolute pytest-fixture path) as the workspace. Users naturally write +`WORKSPACE = Path("./calibration_run")` in their drivers. The double-join +bug only surfaced when a join chain produced a relative path that was +then re-joined. + +**Action item:** parameterize the main end-to-end integration test +over `workspace_style` in `{"absolute", "relative"}`. The relative +variant uses `monkeypatch.chdir(tmp_path)` then `Path("calibration_run")` +as the workspace. Expected to shake out any remaining double-join or +relative-vs-absolute confusion. + +**Estimated effort:** 30-50 lines of test code, applied to +`test_integration.py` and `test_nsga3_driver.py`. No framework +changes. + +**Status:** Not done. Medium-priority follow-up. + +## Bugs confirmed NOT present after audit + +Documented for future maintainers so the same ground isn't re-covered: + +- No duplicate template-substitution passes. `{working_dir}` format + key is injected only during `output_file()` rendering; the + `working_dir_pattern` render raises cleanly on self-reference. +- No SQL concatenation in `archive.py`. All queries use parameterized + bindings. +- `LocalBackend._run_one` is the only subprocess-launch site. Already + hardened with the `mpi_launcher` check and the running-jobs lock. +- Backends don't duplicate `validate_outputs` / `write_sentinel`. + `Problem` is the single authority for both. +- `StressStrainExtractor.strain_rate=None` fails loudly when combined + with `strain_source="time_rate"`. Safe default. +- `FluxBackend.spectrum_mpi=None` triggers `is_spectrum_machine()` + auto-detection. Safe default. +- Path joins in `paths.py:355`, `sentinel.py:325`, + `backends/base.py:257/267` all gate on `.is_absolute()` correctly. +- `CaseTemplater` with `targets=[]` is a safe no-op (already tested). +- `_fs.atomic_write_text` uses tempfile+rename, crash-safe. + +## Action-item summary + +| # | Item | Priority | Effort | Status | +|---|------|----------|--------|--------| +| 3 | Sentinel config fingerprint for `skip_completed` safety | **High** | ~200 lines code + 5 tests + MIGRATION.md | **Open** | +| 7 | Parameterize integration tests over absolute/relative workspace | Medium | ~50 lines | **Done** | +| 5 | JSON+npz optional checkpoint format for cross-user exchange | Low | ~100 lines + 2 tests | Open | +| 8 | `output_file_patterns` bare-relative auto-prepend symmetry | Medium | ~10 lines + 2 tests | **Done** | +| 9 | Restore `logbook1_stats.log` / `logbook2_solutions.log` writes | Medium | ~100 lines + 3 tests | **Done** | +| — | `inspect_archive` CLI for SQL-free archive viewing | Medium | ~400 lines + 10 tests | **Done** | +| — | Cross-gen `--pareto-only` + `--top` | Medium | ~150 lines + 5 tests | **Done** | +| — | Scale work: vectorized ranking + `load_all_genes` batch | Medium | ~80 lines + bench | **Done** | +| — | `--gens-best` convergence view + `--limit` cap | Medium | ~180 lines + 6 tests | **Done** | +| — | `l2_norm` column + `--pareto-only` implies `--genes` | Low | ~60 lines + 6 tests | **Done** | +| — | `--resume-latest` / integer `--resume-from N` | Medium | ~80 lines in example | **Done** | +| — | Relax `Problem.__init__` for resume orphan case | Medium | ~15 lines + 2 tests | **Done** | +| — | Diagnostic resume logging (`resume path:`, before/after discard) | Low | ~25 lines | **Done** | +| — | `delete_run` + `prune_empty_runs` API + CLI | Medium | ~150 lines + 12 tests | **Done** | +| — | `regenerate_logbook_files` CLI for lost `.log` recovery | Low | ~120 lines + 6 tests | **Done** | +| — | Binary-BLOB gene-vector storage for >100k records | Low | Schema migration | Future | +| — | Run full regression after fixes; add 6-8 tests | Medium | ~100 lines of test code | **Done** | + +## Things for future consideration + +- **Linter or runtime check for API-drift in examples.** The + `test_example_imports.py` guard catches module-level import breakage + but not kwarg drift inside function bodies. Consider a + `main(dry_run=True)` path that constructs every framework object + without actually running simulations, and have the guard call it. +- **`ExaConstit_Problems.py` "partially successful" simulation + behavior.** Old code zero-padded the result tail when + `error_strain > 0.01`. `ZeroPadPartialHandler` was discussed but + never implemented. Flagged in MIGRATION troubleshooting. +- **Migration from `required_outputs` toward reader-owned validation.** + The two pre-flight checks overlap. Consider deprecating + `ProblemConfig.required_outputs` once every user has moved to + `TextTableSpec(required=True)`. +- **`FluxBackend.poll_stats` coverage.** Two error-handling branches + marked `# pragma: no cover`. Not a correctness issue; CI coverage + gap only. + +### Finding 8 — `output_file_patterns` bare-relative asymmetry (FIXED) + +**Severity:** Medium — silent-wrong-path bug that manifested as +"reader found no files" at runtime. + +**Mechanism:** `TemplatePathResolver.output_file()` rendered patterns +via `str.format_map` and returned `Path(rendered)` directly. Patterns +that included `{working_dir}/` got the correct per-case path; +patterns written as "obviously relative to the case dir" without +that prefix (e.g. `"results/avg_stress.txt"`) got rendered as bare +relative paths that `path.exists()` then resolved against the +driver's cwd — missing the case dir entirely. The asymmetry vs +`working_dir_pattern` (which auto-prepends `root`) was not +documented clearly enough, and a user writing what seemed like the +obvious config got silently wrong behavior. + +**Fix:** `output_file()` now auto-prepends `working_dir` when the +rendered pattern is (a) not absolute AND (b) doesn't contain the +`{working_dir}` marker. Patterns that explicitly use the marker +are unchanged; absolute patterns are honored exactly as before. +Symmetry with `working_dir_pattern`'s root-prepend behavior +restored. + +**Test coverage:** two new tests in `test_paths.py` +(`test_output_file_auto_prepends_working_dir_for_bare_relative_pattern` +and `test_output_file_bare_relative_matches_working_dir_regardless_of_root_type`). + +**Status:** Fixed. + +## Session 2 deltas + +Between the initial audit and this second pass, the following +additional work was completed: + +- **Column-name convention aligned to ExaConstit.** All framework + defaults, docstring examples, fixtures, and the fake binary now + use the ``# Time Volume Sxx/Syy/Szz/Sxy/Sxz/Syz`` header + format from + ``ExaConstit/src/postprocessing/postprocessing_file_manager.hpp`` + ``GetVolumeAverageHeader``. `StressStrainExtractor` defaults are + `stress_column="Szz"`, `def_grad_column="F33"`, `time_column="Time"`; + `common_time_range` and `interpolate_to` default `time_column` + is `"Time"`. Non-ExaConstit users override explicitly. + +- **Pandas engine auto-switch for commented files.** The C engine + was mishandling ExaConstit's "indented `#` header + indented + data rows" shape, raising a misleading + `EmptyDataError: No columns to parse from file`. `TextTableReader` + now switches to the python engine whenever `spec.comment` is + set, and wraps `EmptyDataError` in a `ValueError` that includes + the first 512 bytes of the file so users can see what pandas + actually saw. + +- **`strain_source` renamed.** `"f11_minus_one"` → `"axial_minus_one"`, + `"log_f11"` → `"log_axial"`. The old labels stay accepted via a + normalization shim in `extract()` that emits a `DeprecationWarning` + naming the new label. The internal local variable `F11` was also + renamed to `axial_stretch` so the code reads correctly when + `def_grad_column="F33"` (ExaConstit z-axis default). + +- **Finding 7 closed.** `test_integration.py` now has a + `@pytest.mark.parametrize("workspace_style", ["absolute", "relative"])` + variant on the happy-path pipeline. The relative branch uses + `monkeypatch.chdir(tmp_path)` + `Path("wf_rel")` as the + workspace root, exactly matching how users in production + wrote `WORKSPACE = Path("./calibration_run")`. + +- **Finding 3 still not addressed.** The sentinel config + fingerprint for safe `skip_completed` restart is the remaining + silent-wrong-answer hazard. Detailed design is in this document; + needs a dedicated session. + +Test count progression across the two audit passes: +- Start of audit: 287 +- After audit fixes (Findings 1, 2, 4, 6): 290 +- After Finding 8 (bare-relative auto-prepend): 290 +- After column-name sweep + ExaConstit-first defaults: 291 +- After reader C-engine fix: 291 +- After `strain_source` rename: 292 +- After Finding 7 parameterization: 294 + +## Session 3 deltas + +### Finding 9 — Logbook text files dropped during refactor (FIXED) + +**Severity:** Medium — observability regression vs pre-refactor +driver. Not a correctness bug, but "the run is producing zero +user-visible output about its own progress" is a serious quality +regression that hid the health of multi-day runs. + +**Mechanism:** The pre-refactor `ExaConstit_NSGA3.py` wrote two +tab-delimited text files every generation: +- `logbook1_stats.log` — avg/std/min/max fitness per generation, + plus ND / GD / HV for multi-objective runs +- `logbook2_solutions.log` — per-individual gene vector + fitness + +The refactor preserved the in-memory DEAP `Logbook` objects and +even pickled them into the checkpoint, but the file writes were +dropped. Users had no way to watch the optimization's progress +mid-run short of inspecting the pickle or the SQLite archive. + +**Fix:** Added `_LogbookWriter` helper that writes both files using +DEAP's `logbook.stream` delta mechanic — each call emits only +records added since the previous access. Calls are inserted after +every `logbook.record(...)` site in `run_nsga3`. Resume-from- +checkpoint truncates both files and replays the loaded logbooks +from scratch (using DEAP's `buffindex` cursor field, verified by +reading `deap.tools.Logbook.stream`'s source) so no gaps or +duplicates result. + +Two new `RunConfig` fields: +- `log_dir: Optional[Path] = None` — default behavior picks + checkpoint-adjacent or cwd +- `write_logbook_files: bool = True` — opt-out for tests / + silent workflows; in-memory logbooks still populated regardless + +Three new tests in `test_nsga3_driver.py` cover the happy path, +the opt-out, and the resume rewrite. + +**Status:** Fixed. + +### New deliverable — `inspect_archive` CLI + +Not a bug — a usability gap flagged alongside Finding 9. The +SQLite archive at `archive.db` is the authoritative per-run record +but users couldn't inspect it without a SQLite client. Added +`workflows/optimization/inspect_archive.py` — a standalone CLI +with three views (`--runs`, `--gens`, `--genes`), three formats +(`table` / `csv` / `json`), and `--pareto-only` / `--run` / `--gen` +filters. Expands gene vectors into named columns using the run's +stored `param_names` + `objective_labels`. + +Ten tests in `test_inspect_archive.py` cover every view × every +format × error paths. + +Invocation examples: +``` +python -m workflows.optimization.inspect_archive ./wf --runs +python -m workflows.optimization.inspect_archive ./wf --gens +python -m workflows.optimization.inspect_archive ./wf --genes \ + --gen 10 --pareto-only --format csv > front.csv +``` + +### Test count through session 3 + +- End of session 2: 294 +- +3 logbook writer tests: 297 +- +10 inspect CLI tests: 307 + +## Session 4 deltas — Scale work + +User flagged that real calibrations run at 40k–50k total simulations +(100 gens × 60–80 pop × ~8 objectives per gene, or 500 gens × 100 pop). +Two categories of work addressed. + +### Performance on large archives + +Baseline profile of `inspect_archive --genes --pareto-only` on a +50k-record archive was 2.1 seconds. Two changes dropped this to +~1.2 s without adding any new dependencies: + +1. **`ArchiveDB.load_all_genes(run_id)`** — one SQL query instead of + one per generation. Eliminates ~500 round-trips on a 500-gen + archive. Saved ~150 ms of pure query overhead. +2. **Vectorized top-N ranking** — the three separate Python loops + (`_top_n_by_l2`, `_top_n_by_objective`, `_finite_fitness`) were + replaced with one `(N, M)` numpy fitness matrix and `np.argsort` + with non-finite values masked to `+inf`. Saved ~700 ms on 50k + rows; dropped the 450,000 `math.isfinite` calls in the profile. + +Remaining cost is dominated by JSON decode in +`sqlite3 → GeneRecord` hydration (~0.67s of the remaining 1.2s). +Further reduction would require either a faster JSON library +(orjson — adds a dependency) or a schema migration to store +`gene_vector` / `fitness` as a binary BLOB. Neither fits under the +"tuning" bucket, so the JSON cost was left alone. Filed for a +future dedicated turn if scaling above ~100k records becomes +common. + +Benchmark table (8 objectives, 6 params, Python 3.12): + +| Scale | --runs | --gens | --gens-best | --pareto-only | +|----------------------|--------|--------|-------------|---------------| +| 8000 records | 4 ms | 6 ms | 156 ms | 228 ms | +| 50000 records | 3 ms | 5 ms | 1043 ms | 1316 ms | + +### New views + +- **`--gens-best`** (convergence view). Per-generation row with + running-best fitness on each objective, current L2 champion's + birth generation, and cumulative `n_seen`. Designed to answer + "is the GA still improving?" at a glance — if + `champion_birth_gen` holds the same value for many rows, the + run has plateaued. +- **`--limit N`** cap on the raw `--genes` dump (default 200). + Prevents accidentally flooding the terminal when a generation + has a huge population. `--limit 0` disables. Ignored for + `--pareto-only` (already bounded). Stderr notice when + truncation happens so the row count isn't mysteriously round. + +Six new tests bring total to 327. + +## Session 5 deltas — Resume UX, observability, housekeeping + +Four distinct threads of work in this session. All driven by +Robert hitting real usability issues during his first multi-day +calibration run. Each thread ended up teaching us something +worth keeping documented. + +### UX thread: the `--resume-latest` / `--resume-from N` flow + +Robert typed `python nsga3_calibration.py --resume-from 16` to pick +up from generation 16 and got a terse `FileNotFoundError: '16'`. +The example's argparse had `type=Path`, which dutifully wrapped the +string "16" as a Path object that didn't exist on disk. + +Shipped a three-behavior `--resume-from` accepting any of: +- a plain integer like `15` → looks up + `checkpoint_dir/checkpoint_gen_15.pkl` +- a full path to a pickle → used verbatim +- nothing (absent) → fresh run + +Plus a separate `--resume-latest` flag that scans for the +highest-numbered `checkpoint_gen_*.pkl` in the configured +directory — the "just keep going from wherever we left off" +ergonomic default that shouldn't require a number. + +A cosmetic `Path.relative_to()` call in the display line blew up +when the checkpoint path was relative and cwd was absolute. Wrapped +in try/except with a fallback to the unresolved path. Minor but +would have bounced users with perfectly valid invocations. + +### Usability thread: archive_run_id mismatch on resume + +Robert's first successful `--resume-from ` immediately died +with `ValueError: checkpoint archive_run_id='3155cb92-...' differs +from Problem.archive_run_id='679fde72-...'`. Root cause: the +example unconditionally called `archive.start_run(...)` on every +invocation, generating a fresh UUID on resume that conflicted with +the one baked into the pickle. + +Two fixes, one in the library and one in the example: + +1. **Library `Problem.__init__` relaxed** to accept + `archive=..., archive_run_id=None` — the orphan case that + lets resume work cleanly. The deferred check moved to + `_archive_case`, which raises `RuntimeError` at first write + if `archive_run_id` is still None by then. Same invariant, + enforced at the real failure point. +2. **Example now gates** `start_run` on `resume_pickle_path is None`. + On resume it leaves `archive_run_id=None`; the library reads + the pickled UUID and assigns it to `problem.archive_run_id` + before any writes happen. The existing `discard_from_generation` + call in the resume path then cleans stale post-crash rows. + +The mismatch ValueError itself got upgraded to name both UUIDs +and quote the correct driver pattern verbatim: + +``` +archive_run_id mismatch on resume: + checkpoint's run_id: '3155cb92-...' + Problem's run_id: '679fde72-...' + +Fix: in your driver, only call start_run() when NOT resuming: + if args.resume_from is None: + problem.archive_run_id = archive.start_run(...) +``` + +### Observability thread: diagnostic resume logging + +Robert reported "resume starts from gen 0, logs wiped, DB wiped" +and we spent time guessing where the bug was before realizing +the symptoms all described the library taking the fresh path +instead of the resume path. The example driver was passing +`resume_from=args.resume_from` to `RunConfig` when it should have +been passing `resume_pickle_path` (which is what the resolver +block populated). `--resume-latest` only sets `args.resume_latest`, +not `args.resume_from`, so the library saw `None` and took fresh. + +Fixed the line. Ship diagnostic logging so the **next** time a +resume misbehaves it's one-glance diagnosable: + +``` +resume path: loading checkpoint from ... +resume: last completed gen=15, pop_library has 16 entries, + logbook1 has 16 records, logbook2 has 320 records, + pickled archive_run_id=3155cb92-... +resume: next generation to run is 16 +archive resume: run=3155cb92-... has gens [0..15] before discard; + keeping gens <= 15, dropping >= 16 +archive resume: after discard, run=3155cb92-... has gens [0..15] +``` + +The two-line before/after discard lines specifically address the +"my archive got wiped" class of report — confirm at a glance that +the framework is only touching the current run's post-crash rows. + +### Housekeeping thread: archive cleanup tooling + +Multiple debug sessions left Robert with 4+ empty aborted run +rows cluttering his archive. He deleted them manually because no +API existed. That's wrong. + +Two new `ArchiveDB` methods: + +- **`delete_run(run_id) -> int`** — single-run deletion. Schema + CASCADE handles dependent rows in `generations` / `genes` / + `case_outputs`. Raises `KeyError` on unknown IDs so typos + surface rather than silently no-op. Returns the parent-row + delete count (1 on success). +- **`prune_empty_runs(*, min_age_minutes=60.0, dry_run=False)`** — + bulk cleanup. "Empty" means no `generations` AND no + `case_outputs` rows; a partial crash mid-gen-0 that wrote some + case outputs doesn't qualify because those are real simulation + artifacts. Two safety gates protect in-progress runs: completed + runs (those with `end_run` called) are always eligible; still- + running runs must be older than `min_age_minutes` (default 60) + before being touched. Pass 0 to override. + +CLI surface on `inspect_archive`: + +- `--clean-empty-runs` — wraps `prune_empty_runs` +- `--dry-run` — preview without deleting; prints candidates on + stderr +- `--age-minutes N` — override the age gate + +Zero-delete path prints a helper line pointing at +`--age-minutes 0` so users don't have to `--help` to find the +escape hatch. + +### Logbook recovery tool + +Separate deliverable: `workflows/optimization/regenerate_logbook_files` +CLI. When the `.log` files on disk have been lost (truncated by +a misconfigured run, accidentally deleted), the pickle still +carries the full DEAP `Logbook` history. This tool reads the +pickle and reuses the driver's own `_LogbookWriter.rewrite_from` +to regenerate the `.log` files byte-identically to a live run. +Defaults output dir to the pickle's parent directory (where the +live driver would have put them); `--output-dir` overrides. + +### `l2_norm` column on the cross-gen view + +Smaller cosmetic change flagged by Robert mid-session. The cross-gen +`--pareto-only` view ranks genes by L2 norm but didn't display the +value. Added `l2_norm` column at the end of every row — L2-block +rows read monotonically, per-objective rows let users spot narrow +specialists vs balanced-and-specialist winners at a glance. + +Also: `--pareto-only` alone (without `--genes`) silently fell +through to the default `--gens` view. Fixed — it now implies +`--genes`. Combining `--pareto-only` with `--runs` / `--gens` / +`--gens-best` / `--clean-empty-runs` is now an explicit error +rather than a silent flag-drop. + +### Test count through session 5 + +- End of session 4: 327 +- Cross-gen view + L2 column + pareto-only implies genes: 333 +- Resume UX fixes + mismatch error + orphan archive case: 336 +- `delete_run`: 339 +- `regenerate_logbook_files`: 345 +- `prune_empty_runs` + CLI: 357 + +## Session 6 deltas — SimCase API consolidation + +One focused thread: collapse `SimCase.template_values` and +`SimCase.context_extra` into a single `SimCase.case_data` field. + +### Why the split was a usability rough edge + +Robert asked how to provide per-case constants to a property +writer (e.g. temperature-dependent elastic constants). The answer +was conceptually simple — "put them in `template_values` and read +them off `sim_case.template_values` inside the writer" — but only +because `template_values` happened to be visible to the writer +through SimCase passthrough. Meanwhile, `context_extra` was the +field for "things the path resolver needs," visible to the writer +in a different way (`layout.ctx.extra`). The semantic was: + +- `template_values` — read by the templater; ALSO visible to the + writer via `sim_case.template_values` +- `context_extra` — read by the path resolver; ALSO visible to + the writer via `layout.ctx.extra` + +Two fields, both functionally per-case constants, distinguished +only by which downstream consumer reads them. This forced the +user to remember the decision tree: + +1. Will it appear as `%%key%%` in a template? → `template_values` +2. Will it appear as `{key}` in a path pattern? → `context_extra` +3. Both? → `template_values` works, but path resolver doesn't + see it… + +The decision tree itself is the smell. Same data, two boxes, +arbitrary partition. + +### What changed + +`SimCase.case_data` replaces both fields. The single mapping +flows to all three consumers: + +- the **templater** for `%%key%%` substitution +- the **path resolver** for `{key}` substitution +- the **property writer** via `sim_case.case_data` + +One dict, three readers. No decision tree. + +The path resolver is still strict about missing keys (it raises +`KeyError` with a helpful "available: [...]" message), so typos +still surface — the merge doesn't sacrifice that safety net. + +### Files touched + +- `workflow_common/problem.py` — field rename, docstring rewrite + (Fields section, Example block, Problem class docstring example + using `case_data=`). Two `extra=dict(sim_case.context_extra)` + call sites changed to `extra=dict(sim_case.case_data)`. The + templater-values build site simplifies because `ctx.extra` is + now populated from the same source — kept the existing + `setdefault` loop for defensive clarity but it's a no-op in + practice now. +- `workflow_common/case_setup.py` — `CallablePropertyWriter` + docstring example updated to read from `sim_case.case_data`. +- `workflow_common/MIGRATION.md` — Step 3 ("DEP_UNOPT") rewritten + to introduce `case_data` as the single home with a brief + historical note about the previous two-field design. +- `workflow_common/ARCHITECTURE.md` — SimCase prose updated. +- `examples/nsga3_calibration.py` — large per-case-constants + comment block rewritten (was a 3-row table, now a unified + description). Two prose comments fixed. Two SimCase definitions + updated. write_properties docstring updated. +- `workflows/optimization/nsga3_driver.py` — two docstring + examples updated. +- `tests/test_problem.py` — appended new behavioral test + `test_case_data_visible_to_templater_path_resolver_and_writer` + that pins the merged-field design end-to-end. The test runs a + real Problem with a working_dir_pattern that includes + `{rve_name}` and a templater target that references + `%%temperature_k%%`, then asserts: (1) the case dir lands at + the expected path (proves resolver consumed case_data), (2) + the rendered file content carries the substituted value + (proves templater consumed case_data), (3) the writer's + capture-dict shows both fields read off `sim_case.case_data` + (proves writer consumed case_data). If anyone ever re-splits + the field, this test fails because all three flow paths + exercise from one dict. +- `tests/test_case_setup.py` — + `test_callable_property_writer_receives_sim_case` updated to + use `case_data` and assert on it. +- `AUDIT_PLAN.md` — Finding 3's design references updated to + `case_data`. + +No backward-compat alias was added. Robert is the only user; +hard rename keeps the API surface clean for incoming users +who'll never have seen the old names. + +### Test count through session 6 + +- End of session 5: 357 +- + new behavioral test pinning case_data flow: 358 + +## Session 6 deltas — addendum: case_data design clarifications + +After the rename landed, two follow-on improvements based on +Robert's feedback that the example was steering users wrong: + +### Per-case constants live IN case_data, not in the writer + +The illustrative example in `examples/nsga3_calibration.py` had +shown a Python `ELASTIC_TABLE = {temp: {c11, c12, c44}}` lookup +table inside `write_properties`, with the writer indexing by +`sim_case.case_data["temperature_k"]`. That's the wrong split: +it splits per-case data between two places (the SimCase +definition and the writer's source code), forcing users who +add a new case to edit two files. + +The corrected guidance: **anything that varies per case goes +directly into that SimCase's `case_data` dict**. The writer +just reads the keys. No Python lookup tables, no logic that +maps cases to values. + +```python +sim_cases = [ + SimCase(case_data={ + "temperature_k": 298.0, + "c11": 168.4, "c12": 121.4, "c44": 75.4, + # ... loading + path-pattern fields ... + }, label="cold"), + SimCase(case_data={ + "temperature_k": 600.0, + "c11": 156.0, "c12": 117.0, "c44": 72.0, + }, label="hot"), +] +``` + +The example's illustrative comment block, the SimCase docstring +in `problem.py`, and the matching MIGRATION.md walkthrough all +got rewritten to show this pattern. Every reference to the +old "lookup table inside the writer" pattern was removed. + +### Templater is template-driven, not data-driven + +Robert flagged that case_data shouldn't fail when it carries +keys the master template doesn't reference. Verified the +existing behavior — `render_template`'s `strict=True` mode +already only complains about template `%%key%%` placeholders +that have NO matching value in the supplied dict; extra keys +in the dict are silently ignored. So Ask 2 was already met +at the mechanism level. + +What WASN'T great: the error message from +`UnresolvedPlaceholderError` says "available keys: [...]" but +doesn't tell the user that the missing value should be added +to `case_data` specifically. From the user's point of view +they see "%%c11%% has no value" and have to figure out where +to add c11. The right place is the SimCase's `case_data` dict +— the framework knows this; the template machinery doesn't. + +Fix: `Problem._dispatch_one_case` now wraps `templater.render` +in a try/except that catches `UnresolvedPlaceholderError`, +preserves the original message (key name + available-keys +list), and appends a one-line hint pointing the user at +`SimCase(label=..., case_data=...)` for the specific case. + +This puts the "what to fix" hint exactly where the +case-specific context lives, without coupling the generic +`render_template` machinery to the Problem layer's +abstractions. + +### Test count through session 6 addendum + +- Start of addendum: 358 (after the rename) +- + extra-keys-in-case_data-don't-fail test: 359 +- + missing-template-key-helpful-error test: 360 + +## Session 7 deltas — Plotting example, public selection API + +Two threads in this session, both driven by Robert's request for +"a way to plot the optimized cases vs the experimental data": + +### Promoting inspect_archive's selection logic to a public API + +The `inspect_archive` CLI had several internal helpers +(`_pareto_history_rows`, `_dedup_on_gene_vector`, `_pick_run`, +`_collect_all_genes`, `_resolve_archive_path`) that contained the +exact logic any post-run analysis tool would want: locate the +archive, pick a run, top-N rank by L2 / per-objective. Robert's +plotting ask would have meant re-implementing that, which is the +worst kind of duplication — same math in two places that drift +apart over time. + +Fix: promote five helpers to public API on +`workflows.optimization.inspect_archive`: + +- `RankedGene` — new public dataclass carrying a `GeneRecord` plus + rank/category/score metadata. The `GeneRecord` keeps the archive + lookup keys (birth_gen/birth_gene); the metadata lets a consumer + know why this gene was selected and color/label accordingly. +- `select_top_genes(genes, *, objective_labels, top_n, + categories=("l2","per_objective"), dedup=True)` — the headline + selector. Returns `Dict[str, List[RankedGene]]` keyed by category + name. The CLI's `_pareto_history_rows` was rewritten to call this + and just format the row dicts on top. +- `pick_run(archive, run_id=None, *, latest_if_none=True)` — picks + a run; raises `ValueError` rather than the CLI's `SystemExit(1)`, + so library callers can catch it cleanly. +- `dedup_on_gene_vector(genes)` — drops duplicate gene-vectors + (caused by NSGA-III elitism carrying winners across generations). + First-sighting-wins ordering preserved. +- `collect_all_genes(archive, run_id)` — public name for + `archive.load_all_genes`. +- `resolve_archive_path(path, default_name)` — public alias of + the existing `_resolve_archive_path` (which already had a clean + injectable signature for testability). + +The CLI now calls into these. Tests pin the new public contracts +independently of the CLI behavior tests. + +### `examples/plot_solutions.py` — interactive plotter + +Single-file example: ~600 lines including docstrings, importable +from notebooks AND runnable as `python examples/plot_solutions.py +calibration_run --top 10 ...`. Composes the public selection API +with `ArchiveDB.load_case_outputs` and `StressStrainExtractor` to +produce: + +1. **Headline figure** — one subplot per SimCase, top-N simulated + stress-strain curves overlaid on the experimental reference. + Curves are colored by rank using a red→blue colormap so rank 0 + is visually obvious. +2. **Slider** below the subplots — sets the visible rank threshold + K. Ranks < K are full opacity; ranks ≥ K fade to 7% opacity. + Lets users interactively narrow focus from "show me the top 10" + to "show me the top 3" without re-running. +3. **Click-to-show parameter panel** — clicking any simulated curve + prints that gene's parameter values + fitness + birth coordinates + in a monospace text strip below the slider. Uses matplotlib's + `pick_event` with a 5-pixel tolerance. +4. **Pareto-front side plot** (`--pareto i,j`) — 2-D scatter for the + chosen objective pair with the L2-closest gene highlighted in + red. Reuses `workflow_common.postprocess.plot_pareto_front`. + +Three ranking modes: `l2` (default — closest to utopian origin), +`objective` (best on a single named objective), `last-gen` (every +individual in the final generation, no ranking). The objective +specifier accepts both indices and label strings. + +The plotter reads exclusively from the SQLite archive — gene +records via `load_all_genes`, simulation outputs via +`load_case_outputs`. The on-disk case directories don't need to +still exist; the archive carries everything needed to reconstruct +stress-strain curves. This was specifically by design — the +archive's `case_outputs` table was added in an earlier session +precisely so post-run analysis would survive workspace cleanup. + +### Test count through session 7 + +- End of session 6: 360 +- Promotion: select_top_genes / pick_run / dedup / collect / resolve + contract tests: 372 +- Plot solutions integration tests (mode selection, sim_case probe, + CLI error handling): 382 + +## Session 8 — Plotter polish + Pareto interactivity + +Robert hit several bugs in the post-run plotter and asked for the +Pareto plot to gain the same interactive affordances as the headline +overlay. + +### Bug fixes in `examples/plot_solutions.py` + +* **`--objective N` no longer silently runs L2 ranking.** + Previously `--mode l2` was the default and `--objective` had no + effect unless the user also explicitly passed `--mode objective`. + This was the root cause of "changing --objective shows the same + plot." `main()` now auto-promotes `--mode l2` → `"objective"` when + `--objective` is supplied. `--objective` with `--mode last-gen` is + reported as an incompatible combination (last-gen mode has no + ranking step, so the flag would have nothing to apply to). + +* **`--no-overlay` flag added.** Previously the headline overlay was + unconditional; passing `--pareto` produced two figures whether the + user wanted both or just the Pareto. Now `--no-overlay --pareto i,j` + produces only the Pareto plot. `--no-overlay` alone reports an + error rather than silently exiting with no figures. + +* **`top_n` honored on the Pareto plot.** The previous implementation + scattered every finite-fitness gene regardless of `--top`. Now + `top_n > 0` restricts the scatter to the N lowest-L2 genes (using + `select_top_genes` for consistency with the inspect-archive + CLI's `--pareto-only` table). `top_n=0` (default) plots every + rank-0 gene — the actual Pareto front — with gene-vector dedup so + elitism-carried duplicates don't pile on. + +* **Slider skip when `n_total == 1`.** A single-solution overlay used + to trigger a matplotlib "identical low/high xlim" warning when + building the slider with `valmin=valmax=1`. The slider would also + be functionless. Now the slider is skipped and its axes hidden + when there's nothing to toggle. + +### Pareto interactivity + +The Pareto figure is now a 2-column layout: scatter on the left, +response inset on the right. + +* **Clickable points.** Each scatter point has `picker=5`. Click + fires a `pick_event` that updates a parameter panel below the + plots (gene vector, fitness, birth coordinates, L2 norm) AND + redraws the response inset. +* **Response inset.** The right axes shows the clicked gene's + simulated stress-strain curves (one line per SimCase, colored by + `tab10`) overlaid with the experimental references (dashed, + same color per SimCase). Initially populated with the L2 + winner's response. +* **L2-winner ring** stays — same red ring on the L2-closest point, + with the colorbar of L2 norms making "balance vs specialization" + legible at a glance. +* **Archive-first experimental data** — the inset reuses + `_resolve_experimental_for_case` so users get the right + reference curve per SimCase without supplying CSV paths. + +### Slope plotting (already in session 7) + +The overlay plot has both stress-strain and slope-strain rows. +Slope is computed via `np.gradient(stress, strain)` for both +simulated and experimental curves so the values match what the +slope objective scored against. Toggleable via `show_slopes=False`. + +### Test count through session 8 + +* Start of session: 390 +* Pareto top_n / inset / picker / experimental tests: +5 → 395 +* CLI auto-promote / no-overlay / l2-vs-objective title tests: +5 → 400 +* Archive-first experimental + slope helper + overlay no-CSV: +3 → 403 + +Total: **403 passing** (33 driver + 370 non-driver). + +## Session 8 addendum — Backward compat for archives without `experiments` + +Robert hit `sqlite3.OperationalError: no such table: experiments` +running the plotter against an archive created before the +`experiments` table was added (older code, real .db file on his +machine). He had supplied `--experimental` CSV paths as the fallback, +but the load_experiment call crashed before the fallback was reached. + +Root cause: `ArchiveDB.open()` only runs the schema-creation script +on writable opens (a read-only SQLite connection can't modify the +schema, by design). So a read-only open of a pre-experiments-table +archive never gets the `IF NOT EXISTS` table-creation statement +executed; the table is genuinely absent, and the first SELECT +against it raises `OperationalError`. + +Fix: `load_experiment` and `list_experiments` now check for the +table's existence via a small `_has_table(name)` helper before +querying. Missing → return `None` / `[]` respectively, matching the +contract those methods already had for "no row found." The plotter's +`_resolve_experimental_for_case` therefore reaches its CSV-fallback +branch cleanly when the archive is too old to have stored +experimental data. + +Writable opens are unaffected: they run the full schema script on +every open, so missing tables get created on first write. A user +who opens a pre-experiments archive in write mode and calls +`record_experiment` gets the table created on the fly with no +manual migration step required. + +### Test count through session 8 addendum + +- Start: 403 +- Archive-level missing-table tests (load returns None, + list returns [], writable open creates table): +3 → 406 +- Plot-level tests reproducing Robert's command line + (resolve falls back to CSV, returns None without fallback, + full main() flow): +3 → 409 + +Total: **409 passing** (33 driver + 376 non-driver). + +## Session 9 — Optimization windowing, generic strain, plotter polish + +Robert's feedback covered four threads: + +1. Slope plots needed semi-log y because elastic-vs-plastic slopes + span orders of magnitude and the optimized curves were + visually invisible compressed against the elastic spike. +2. Optimized curves weren't appearing on slope axes (turned out to + be the same compression issue — the lines were drawn but + indistinguishable from the x-axis at linear scale). +3. The old framework's `minmax_strain` field was documented in + `case_data` but unwired anywhere — optimizer was scoring against + the full curve, often dominated by elastic regime instead of + the plastic regime users actually care about. +4. `StressStrainExtractor`'s field names baked def-grad-specific + assumptions that mismatched users with sims that already write + strain measures (Lagrange/Euler) directly. + +### Extractor refactor (workflow_common/objectives.py) + +* `def_grad_output` → `strain_source_output` +* `def_grad_column` → `strain_source_column` +* `axial_minus_one` → `biot` (it IS Biot strain in 1D; the old + name was inscrutable) +* `log_axial` → `log` +* New `direct` strain source: reads the column verbatim, no + transformation. Use case: sim already wrote + `avg_lagrangian_strain.txt` and the user wants `E33` straight. +* New `window: Optional[Tuple[float, float]]` field — crops + ``(strain, stress)`` to a strain interval. Compares against + ``|strain|`` so the same value works for tension and compression + without sign-handling at the call site. +* Legacy aliases (`f11_minus_one`, `log_f11`) and their + `DeprecationWarning` machinery removed entirely. No deprecation + period — Robert is the only user. + +### Optimization windowing wired through + +`StressStrainExtractor.window` is the single point of windowing. +Both `StressStrainObjective` and any custom evaluator that uses an +extractor get cropping for free, since they consume the +extractor's output. The example `nsga3_calibration.py` reads +`sim_case.case_data["minmax_strain"]` and passes it to the +extractor as `window=`. The custom `_StdNormalizedStress/Slope` +evaluators dropped their `desired_strain` parameter — that was +half-windowing (upper bound only) and is now subsumed by the +extractor's `window`. + +### Archive: minmax_strain column on experiments + +Added `minmax_strain TEXT` column storing JSON `[lo, hi]` so +plotter can shade the optimized region against the full curve. +Both sides may be null for unbounded. + +* `record_experiment(..., minmax_strain=)` — new optional kwarg. +* `load_experiment_window(run_id, sim_case_idx)` — new method + returning the stored `(lo, hi)` tuple. Separate from + `load_experiment` so callers that only need the window + (e.g. plotter shading) skip the DataFrame deserialize. +* Backward compat for archives missing the column: + `load_experiment_window` returns None gracefully via a new + `_has_column` helper. +* Forward compat for archives missing the column on writable + open: a new `_migrate_add_missing_columns` runs after the + schema script and `ALTER TABLE`s the column in. Idempotent + (guarded by `_has_column`) so reopens of fresh archives + don't raise "duplicate column." + +### Driver wiring + +`_record_experiments_from_problem` now reads +`SimCase.case_data["minmax_strain"]`, defensively unpacks it as a +2-tuple of floats/Nones, and passes it to `record_experiment`. +Bad input (e.g. a string) logs a warning and drops the window — +the experiment still gets recorded so plotting keeps working. + +### Plotter (examples/plot_solutions.py) + +* Slope axes now use `set_yscale("symlog", linthresh=...)` with + `linthresh = 0.001 * max(|slope|)`. Resolves both the "elastic + spike compresses everything" issue and noise-induced sign-flip + brittleness in one shot. +* Window shading: `axvspan` on both stress and slope axes for + each SimCase whose archive entry has a non-None window. + Translucent green (alpha=0.08), zorder=0 so it sits behind + curves. Mirrors to negative-strain region for compression + loadings (sign detected from experimental or first sim curve). +* `per_case_window` collected alongside `per_case_curves` and + `per_case_exp` inside the same `with ArchiveDB(...)` block so + there's only one connection lifecycle to manage. + +### Tests added + +* Extractor: `direct` strain, window cropping (tension), window + cropping (compression via abs), exclude-all error, inverted + bounds error. (5) +* Archive: window persistence, unbounded sides, missing-record + returns None, missing-column returns None, writable open + migrates, migration idempotent on fresh archive. (6) +* Driver: case_data minmax_strain plumbed to archive (with + multiple SimCase variants), malformed minmax_strain handled + gracefully. (2) +* Plot: optimized curves present on slope axes, slope yscale is + symlog, window shading drawn when archive has it, no shading + when window is None. (4) + +Existing tests updated where API names changed: `test_objectives` +log/biot renames + drop legacy-alias test, `test_example_imports` +drop `desired_strain` arg, `_make_result_set` helper accepts a +custom output name for direct-strain tests. + +### Test count through session 9 + +- Start: 409 +- Extractor (direct strain + window): +5 → 414 +- Archive (minmax_strain column + migration): +6 → 420 +- Driver (case_data plumbing): +2 → 422 +- Plot (slope curves + symlog + shading): +4 → 426 + +Note: also dropped the legacy-aliases test on the old field +names, so net delta to test_objectives was +5 minus 1 = +4. Net +total **425 passing** (35 driver + 390 non-driver). + +## Session 10 — Pareto interactivity fixes, archived extractor configs + +Robert reported the new Pareto figure was clean but had three real +issues plus one usability nit: + +1. Bug — clicking a Pareto point only plotted "1 of 2 SimCases" in + the inset response. The same SimCase failed regardless of which + Pareto pair was selected, so the bug wasn't in pair-selection + logic. +2. The headline overlay still showed alongside `--pareto`. Robert + wanted bare `--pareto` to mean "only the Pareto." +3. The L2 winner ring used full-objective L2, not the projected + pair's L2 — surprising when the user picks 2 of N objectives + and expects the "balanced winner ON THIS PROJECTION." +4. Coloring by full L2 made it hard to see whether a Pareto front + was a true tradeoff curve. Different metrics highlight + different aspects (proximity to Y=0 plane, X=0 plane, + asymmetry between specialists and compromises). + +### Root cause for the SimCase-disappearing bug + +The plotter's click handler built a default +`StressStrainExtractor()` for every SimCase. Robert's calibration +used `strain_source="time_rate"` with case-specific `strain_rate`; +the default uses `strain_source="biot"` against `F33`. For ANY +SimCase whose archived `case_outputs` shape didn't fit the default +Biot-strain expectations (or whose curve was indistinguishable +from noise after default extraction), `_extract_curve` returned +None silently — and the SimCase vanished from the inset. + +The architectural fix is to archive the extractor config the +optimizer actually used. The plotter consults the archive first, +so a user is guaranteed to see plots that match what the optimizer +scored against — not whatever the plotter's default would produce. + +### Archive: extractor_config column on experiments + +* New `extractor_config TEXT` column on the `experiments` table. + JSON-encoded dict from `StressStrainExtractor.to_dict()`. +* `record_experiment(..., extractor_config=)` accepts the dict. +* `load_extractor_config(run_id, sim_case_idx)` returns the dict + or None (missing column / row / null value all fold to None). +* Migration entry added to `_migrate_add_missing_columns` so old + archives gain the column on writable open. + +### Extractor JSON contract + +* `StressStrainExtractor.to_dict()` — serializes all 8 fields, + converting `window: tuple` to `[lo, hi]` for JSON safety. +* `StressStrainExtractor.from_dict(d)` — tolerant of missing keys + (defaults fill in) and extra keys (silently ignored), so + forward-compat works in both directions: an older archive on + newer code OR a newer archive on older code both load. + +### Driver wiring + +`_record_experiments_from_problem` now also reads +`evaluator.extractor.to_dict()` if the evaluator exposes +`.extractor`. Custom evaluators without that attribute (or whose +extractor doesn't have `to_dict`) get a warning and the experiment +is recorded with extractor_config=NULL. Plot-time fallback to a +default extractor still works in that case. + +### Plotter resolver + +New `_resolve_extractor_for_case(archive, run_id, sc_idx, factory=)`: + + archive's stored config → user-supplied factory → default + +The window field is **stripped** from any archived extractor +before plot-time use: the plotter shows full curves with the +window shaded as an overlay, so cropping at extraction would hide +exactly the context users want to see. + +Both `plot_top_solutions_overlay` and the Pareto plotter use the +resolver. The Pareto plotter also hoists resolution outside the +gene loop (extractors are per-SimCase, not per-gene, so building +them per-gene was wasted work). + +### Pareto subset L2 + color modes + +The L2 winner ring now lands on the gene with minimum +**subset L2** (sqrt(x² + y²) on the projected pair) — answering +"best balanced compromise on what I'm looking at" rather than +"globally best across all objectives." + +New `--color-by` flag with five options: + +* `subset_l2` (default) — distance from utopia in the projected + plane. Iso-curves are circles. Uniform color along the front + signals a true tradeoff curve. +* `full_l2` — distance across all objectives. Catches points + that look balanced on the projection but are bad on the + hidden objectives. +* `x` / `y` — single axis. Iso-curves are vertical/horizontal + lines. Reveals proximity to Y=0 / X=0 plane. +* `asymmetry` — `|x - y| / (x + y)`. Zero = perfectly balanced + on the pair. Highlights specialists vs compromises. + +### CLI inversion + +* `--no-overlay` removed. +* `--overlay` added — explicit opt-in to keep the overlay alongside + `--pareto`. +* Bare `--pareto i,j` → only Pareto figure produced. +* Bare command (no `--pareto`) → still shows overlay (backward + compat with the "user just wants to look at results" workflow). + +### Tests added + +* Archive (5): extractor_config persists, missing record returns + None, null record returns None, old archive without column + returns None, writable open auto-migrates. +* Objectives (4): JSON round-trip with tuple→tuple, missing-keys + tolerance, extra-keys tolerance, window=None round-trip. +* Driver (2): extractor config from evaluator flows through to + archive, evaluators without `.extractor` skip cleanly. +* Plot (12): subset-L2 winner ring (not full-L2), color_by exact + values for subset_l2 and full_l2, x/y/asymmetry render OK, + invalid color_by raises, Pareto inset shows all SimCases on + initial draw, archived extractor used when present (with + custom column names that would fail the default), bare + `--pareto` skips overlay, `--pareto --overlay` shows both, + no-pareto-no-overlay still shows overlay. + +The autouse `_agg_backend` fixture now also runs `plt.close("all")` +on test teardown — without that, the figure-leak warning escalated +once test count exceeded matplotlib's 20-figure threshold. + +### Test count through session 10 + +- Start: 425 +- Archive (extractor_config column + migration): +5 → 430 +- Objectives (to_dict / from_dict round-trip): +4 → 434 +- Driver (extractor config plumbing): +2 → 436 +- Plot (subset L2 + color modes + click-handler + extractor + resolver + CLI inversion): +12, minus 2 dropped `--no-overlay` + tests → **net +10** → **446** + +Total: **446 passing** (37 driver + 409 non-driver). + +## Session 11 — Save the simulation curve too + +Robert's response to session 10 had two parts: (1) the diagnosis +of "only 1 of 2 SimCases plotted" was incomplete (the same +default-extractor logic worked for the overlay so something else +was going on, and the architectural fix is still right but my +narrative was wrong), and (2) more substantively, the framework +should save the SIMULATION-side (independent, dependent) curve +alongside the experimental data. Storing only the experimental +side and reconstructing the sim side after the fact loses +information and makes re-analysis harder than it needs to be. + +Robert called out three motivations: + +* Direct retrieval beats reconstruction. With the sim curve + persisted, post-run analyses pull it back as data, not as + something rebuilt by re-running the extraction pipeline (which + could drift if extractor logic ever changes). +* Re-running analyses with different metrics (RMSE → MAE, + different windowing, different weighting) becomes a query, not + a re-extraction. Especially valuable when deciding to evaluate + parameter sets against new criteria after the fact without + re-running the whole optimization. +* Bayesian / surrogate model training. Mapping + ``gene_vector → response_curve`` lets surrogate-driven search + speed up future optimizations; that requires the curves to be + available and stable. + +Critical detail: store the FULL extraction range, not the +windowed range. The optimizer's window crops which region the +metric was computed over, but downstream analyses might want +data outside that region. Strip the window before extraction +when archiving. + +### Schema: case_curves table + +New table keyed on ``(run_id, birth_gen, birth_gene, sim_case_idx)`` +matching the case_outputs key shape. Columns: + +* ``independent_blob`` — pickled np.ndarray (typically strain). +* ``dependent_blob`` — pickled np.ndarray (typically stress). +* ``independent_label`` / ``dependent_label`` — TEXT hints; the + framework defaults to "strain" / "stress" but the columns let + future extractor types (thermal, creep) carry their own labels. +* FK cascade on ``runs(run_id)``. + +Why a separate table rather than a column on case_outputs: +case_outputs holds raw simulator output (avg_stress.txt etc.), +which is an INPUT to extraction. The extracted curve is the +comparison target the optimizer scored against. Different +conceptual layer; different table. + +### Archive API + +* ``record_case_curve(run_id, *, birth_gen, birth_gene, sim_case_idx, independent, dependent, independent_label=None, dependent_label=None)``. + Validates 1-D shapes and matching length. +* ``load_case_curve(run_id, *, birth_gen, birth_gene, sim_case_idx)`` + returns ``(ind, dep, ind_label, dep_label)`` or None. Tolerates + missing table on read-only opens of older archives. +* ``discard_from_generation`` extended to also delete case_curves + rows past the resume point. +* ``delete_run`` cascades automatically via FK. + +### Problem-level wiring + +``Problem._archive_case`` now calls a new +``_archive_case_curve(ctx, rs)`` after writing case_outputs. +Logic: + +* Cache the extractor per ``sim_case_idx`` on the Problem + instance (lazy, populated on first hit). The extractor is + found by walking ``objective_specs`` for the first one with + ``spec.sim_case == sim_case_idx`` AND a usable + ``.extractor`` attribute. +* Strip the window from the cached extractor via the + ``to_dict`` / ``from_dict`` round-trip so the archived curve + covers the full range. Falls back to using the original + extractor if to_dict isn't available — better windowed than + nothing. +* Run the extractor, archive the result. Failures log at debug + (not warning) since the evaluator's own failure handler is + the primary path for reporting extraction problems; this is + a bonus archive write. + +Custom evaluators without an ``.extractor`` attribute leave +case_curves empty for that SimCase. The plotter handles that +by falling back to ``case_outputs`` + extraction. + +### Plotter + +New ``_load_or_extract_curve`` helper: + + archived case_curve → re-extract from case_outputs → None + +Both ``plot_top_solutions_overlay`` and the Pareto plotter +route through this helper. Old archives without case_curves +hit path 2 and continue working unchanged. + +### Tests added + +* Archive (7): record/load round-trip, shape validation, missing + record returns None, missing table returns None on read-only, + collision-replace, FK cascade on delete, discard_from_generation. +* Driver (2): full-range curves archived per (gene, sim_case) + with window stripped, custom evaluators without extractor + skip cleanly. +* Plot (1): plotter prefers archived curves over re-extraction. + +### Test count through session 11 + +- Start: 446 +- Archive (case_curves CRUD + cascade + discard): +7 → 453 +- Driver (curves archived from Problem hook): +2 → 455 +- Plot (archived-curve preference): +1 → 456 + +Total: **456 passing** (39 driver + 417 non-driver). + +## Session 12 — Sign correction for compressive loading + +Robert reported the plotter rendering a compressive case with +positive strain alongside negative stress — strain axis flipped +relative to the stress axis. Root cause: the example calibration +script applied ``abs()`` to ``case_data["strain_rate"]`` before +constructing the extractor, so the extractor's +``strain = strain_rate * time`` produced positive strain even on +runs the user had configured as compression (signed strain rate). +Stress (Szz) was naturally negative. The two axes ended up +mirrored relative to each other. + +### Three layers of fix + +**1. Example fix (preventive).** Drop ``abs()`` on the strain +rate so it carries sign through to the extractor unchanged. +Evaluators that score on magnitudes (the example's +``_StdNormalized*Evaluator`` family) already apply ``np.abs`` +themselves before computing RMSE, so the sign change doesn't +affect optimization scores. The fix only changes what gets +passed to the extractor for archiving and plotting. + +**2. Post-processing fix (Robert's specific ask).** This is +where users notice the bug — plots, not run logs. The plotter's +``_load_or_extract_curve`` helper now accepts an +``experimental_reference`` kwarg ``(exp_ind, exp_dep)``; when +supplied, both the loaded simulated arrays are sign-matched +against the experimental columns via +``match_sign_to_reference``. Both ``plot_top_solutions_overlay`` +and the Pareto plotter resolve experimental data BEFORE the +gene-curve loop and thread it through, so the sign-correction +sits at the single curve-loading point. Old archives written +before the save-off fix get corrected at plot time without any +upgrade path needed. + +**3. Save-off fix (defensive, archive-side).** ``Problem. +_archive_case_curve`` now also runs the same correction before +calling ``record_case_curve``. The same evaluator that supplied +the extractor must also supply ``.experimental`` and the +matching column-name attributes (``experimental_strain_col`` / +``experimental_stress_col``, defaulting to ``"strain"`` / +``"stress"``). Resolved together via a new +``_resolve_case_curve_state(sim_case_idx) -> +Tuple[extractor, exp_df]`` so both come from the same evaluator +choice — keeps them consistent. Result: archived ``case_curves`` +rows are correctly-signed for any downstream consumer +(surrogate-model trainers, alternative post-processing scripts), +not just the bundled plotter. + +### The helper itself + +Originally drafted with a "compute dominant signs of both arrays, +multiply by -1 if they disagree" approach. Robert pointed out +``np.copysign`` is the right primitive — simpler and conveys +intent more clearly. Final form: + +```python +def match_sign_to_reference(sim, reference): + if sim.size == 0 or reference.size == 0: + return sim + ref_dominant = float(reference[int(np.argmax(np.abs(reference)))]) + if ref_dominant == 0.0: + return sim + return np.copysign(sim, ref_dominant) +``` + +Picking a single representative sign from the reference (its +max-magnitude value) sidesteps numerical noise around zero — +a Voce stress curve's max-magnitude point is at peak load, +unambiguous in sign. Then ``np.copysign(sim, scalar)`` forces +every element of ``sim`` to that sign while preserving +magnitude. For monotonic loading (the framework's target) this +is correct; for cyclic loading where the curve genuinely +crosses zero this would over-correct, but the framework +doesn't currently target cyclic fits. + +The helper is exported from ``workflow_common`` so external +code can apply the same correction. + +### Tests added + +* Objectives unit (7): compression flip, already-aligned no-op, + unaligned lengths handled, empty sim, empty ref, all-zero ref, + noise-near-zero robustness. +* Driver (2): sign-correction at save-off given a compression + experimental reference, no correction when experimental data + is absent. +* Plot (2): sign-correction at plot time given wrong-signed + archived curves, no correction when experimental data is + absent. + +### Test count through session 12 + +- Start: 456 +- Objectives helper: +7 → 463 +- Driver save-off: +2 → 465 +- Plot post-processing: +2 → 467 + +Total: **467 passing** (41 driver + 426 non-driver). + +## Session 13 — Use the smoothing module, in the right direction + +Robert called out two related issues with the example evaluators +``_StdNormalizedStressEvaluator`` / ``_StdNormalizedSlopeEvaluator`` +in ``nsga3_calibration.py``: + +1. **Wrong direction.** They were interpolating sim onto exp's + strain grid (``np.interp(target_strain=exp, sim_strain, + sim_stress)``). The original ExaConstit code interpolated + experimental data onto the simulation's grid — which is the + right way: the optimizer is computing error at sim's own + sample points. + +2. **Bypassing the framework.** Used raw ``np.interp`` + instead of the framework's smoothing module + (``PchipSmoother``, ``ArcLengthSmoother``, + ``auto_smoother``). The smoothing module exists exactly for + this — picks PCHIP for monotonic data, ``ArcLengthSmoother`` + for non-monotonic, and the ``sample_at`` method gives + "evaluate at these target x's" semantics with PCHIP's + no-overshoot guarantee. + +### Evaluator rewrite + +Both ``_StdNormalized{Stress,Slope}Evaluator.evaluate`` now: + +* Mask SIM (not exp) to the common strain range — sim's sample + points become the comparison grid. +* ``PchipSmoother(strict_monotonic=False).sample_at(exp_strain, + exp_stress, sim_strain_in)`` brings exp onto sim's grid via + PCHIP. ``strict_monotonic=False`` accepts mild noise in + exp_strain (auto-sort); it doesn't handle genuinely + non-monotonic data, which the framework's ``ArcLengthSmoother`` + is for. Mechanical-test exp data is cleaned and clipped before + calibration anyway, per Robert. +* RMSE / std computed on the sim-grid-aligned arrays. + +### Sign-correction sites updated + +The post-processing plotter and the archive-time +``Problem._archive_case_curve`` previously used a +``match_sign_to_reference`` helper that wrapped ``np.copysign`` +with a parametric ``np.interp`` resample. Robert pointed out +that's the wrong tool — the framework already has +``PchipSmoother`` for the same job. Both sites updated to: + +```python +smoother = PchipSmoother(strict_monotonic=False) +exp_dep_at_sim = smoother.sample_at( + np.abs(exp_ind), exp_dep, np.abs(sim_ind), +).y +sim_dep = np.copysign(sim_dep, exp_dep_at_sim) +sim_ind = np.copysign(sim_ind, exp_ind[-1]) # scalar from last point +``` + +Element-wise ``np.copysign`` on the dependent axis preserves +cyclic structure (each sim point inherits the sign of the +corresponding exp point at the same strain magnitude). The +independent axis uses a scalar sign drawn from exp's last +strain value — unambiguous for monotonic loading; cyclic data +ending back at zero would degenerate, but the framework +targets monotonic loading near-term. + +The thin ``match_sign_to_reference`` helper has been removed — +``np.copysign`` doesn't need a wrapper. + +### Tests changed + +* Removed: 7 ``match_sign_to_reference`` unit tests (helper gone). +* Added: 2 evaluator-direction tests pinning the + PchipSmoother-based score against a hand-computed + expected value, ensuring the rewrite uses exp→sim alignment + and not sim→exp. +* The 2 existing driver tests (sign-corrected curves at + save-off) and 2 plot tests (sign-corrected curves at plot + time) continue to exercise the new PCHIP-aligned path — + they pass unchanged. + +### Test count through session 13 + +- Start: 467 +- Removed match_sign helper tests: -7 → 460 +- Added evaluator-direction tests: +2 → 462 + +Total: **462 passing** (41 driver + 421 non-driver). + +## Session 14 — Slope plot truncation + +Robert's report: with slope data like ``[strain=-1e-6..-0.12, slope=-128, -40, +210, +128, ..., 0.7]`` the slope subplot only shows "the very start" — the rest of the curve appears unplotted. Confirmed with and without symlog. Only ``abs(slope)`` made the rest visible, which Robert explicitly didn't want. + +### Diagnosis + +Matplotlib was actually plotting all 133 points. Verified with standalone repro on Robert's exact arrays — every point made it onto the canvas. The problem was **visualization**, not data: an elastic spike (~+210) and a brief start-of-test numerical-noise spike (~-128) dominated the auto-scaled y-axis. The plastic-regime bulk (slope ≈ 0.7-3, the actual response of interest) got compressed into a hairline near the chart edge. To Robert's eye, that hairline read as "only the start was plotted." ``abs(slope)`` happened to fix it because the negative spike's contribution to ylim disappeared, doubling the bulk's relative real estate — but at the cost of losing sign information. + +Old code (``examples/plot_solutions.py``): + +```python +max_abs = float(np.max(np.abs(concat))) +linthresh = max(max_abs * 0.001, 1e-9) +slope_ax.set_yscale("symlog", linthresh=linthresh) +``` + +Two failure modes baked in: ``max(|slope|)`` lets a single outlier dictate ``linthresh``, and there's no explicit ``set_ylim`` so matplotlib's auto-scale also sizes against the spike. + +### Fix + +Drive both the y-axis range AND ``linthresh`` from robust percentiles of ``|slope|``, not extremes: + +```python +finite = concat[np.isfinite(concat)] +abs_finite = np.abs(finite) +nonzero = abs_finite[abs_finite > 0] +# 90th percentile: captures the bulk Voce response, excludes +# spikes (typically ≤5% of points). 1.5x for headroom. +p90 = float(np.percentile(abs_finite, 90)) +bulk_extent = max(p90 * 1.5, 1.0) +slope_ax.set_ylim(-bulk_extent, bulk_extent) +# Linthresh from 5th percentile of nonzero |slope| — keeps the +# bulk in the LOG region, not the linear band. +linthresh = max(float(np.percentile(nonzero, 5)) * 0.5, 1e-9) +slope_ax.set_yscale("symlog", linthresh=linthresh) +``` + +For Robert's data: +- ``p5(|slope|)`` ≈ 0.77, ``p50`` ≈ 1.43, ``p90`` ≈ 2.54, ``max`` ≈ 210.9 +- Old: ``linthresh = 0.21``, ylim auto ≈ ``(-323, +530)`` — bulk visually compressed. +- New: ``linthresh = 0.38``, ylim = ``(-3.81, +3.81)`` — bulk fills the chart; spikes extend off the top and bottom edges, still visible as line segments leaving the visible region. + +Crucially the spike data is **not** clipped from the line — matplotlib draws the line in full and just renders the portions outside ``ylim`` past the chart edge. Tested explicitly via ``line.get_ydata().max() > 100`` and ``< -50`` after plotting. + +### Tests + +Added ``test_plot_overlay_slope_ylim_robust_against_outliers``: +- Builds a sim curve with the failure pattern (elastic + noise spikes plus smooth bulk) +- Asserts symmetric ylim with ``abs_extent < 50`` (bulk-scale, not spike-scale) +- Asserts plotted y-data still includes the spike values (range, not data, was the fix) + +### Test count through session 14 + +- Start: 462 +- Added robust-ylim slope test: +1 → 463 + +Total: **463 passing**. diff --git a/workflows/exaconstit-calibrate/LICENSE b/workflows/exaconstit-calibrate/LICENSE new file mode 100644 index 0000000..e1db481 --- /dev/null +++ b/workflows/exaconstit-calibrate/LICENSE @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2024, Lawrence Livermore National Security, LLC. +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/workflows/exaconstit-calibrate/MANIFEST.in b/workflows/exaconstit-calibrate/MANIFEST.in new file mode 100644 index 0000000..50b490d --- /dev/null +++ b/workflows/exaconstit-calibrate/MANIFEST.in @@ -0,0 +1,14 @@ +# Extra files to include in the sdist. The wheel is unaffected +# (wheel contents come from setuptools.packages.find + package_data). +# +# These files matter to people installing from source but not to +# people installing from the wheel: +# - examples/ is the runnable side-by-side port of the pre-refactor +# script. Users pulling from source want it; wheel users will +# typically read it on GitHub instead. +# - RELEASING.md is only useful to maintainers cutting new releases. + +recursive-include examples *.py *.md +include RELEASING.md +include README.md +include LICENSE diff --git a/workflows/exaconstit-calibrate/README.md b/workflows/exaconstit-calibrate/README.md new file mode 100644 index 0000000..55232bc --- /dev/null +++ b/workflows/exaconstit-calibrate/README.md @@ -0,0 +1,399 @@ +# exaconstit-calibrate + +Calibration infrastructure for [ExaConstit](https://github.com/LLNL/ExaConstit): +material-parameter fitting via (U-)NSGA-III on the +[rcarson3/deap](https://github.com/rcarson3/deap) fork, plus a +code-agnostic framework for driving external simulation codes from +Python workflows. + +**Current scope:** NSGA-III-based crystal-plasticity parameter +calibration (replaces `ExaConstit_NSGA3.py` + friends). + +**Planned scope:** Macroscale yield-surface data generation and Barlat +surface fitting. The framework layer is code-agnostic and already +supports arbitrary drivers; only the yld-specific driver is pending. + +**Out of scope:** Full additive-manufacturing challenge-problem +workflows (those live under `workflows/Stage3/` in ExaConstit and +are a separate pipeline). + +The package ships two importable names: + +- `workflow_common` — the code-agnostic framework (Problem + orchestrator, ArchiveDB, result readers, backends). No DEAP + dependency. +- `workflows.optimization` — the NSGA-III driver. Requires the + DEAP fork. Named `workflows` to preserve import paths from + migrated pre-refactor drivers. + +## Installation + +Requires **Python 3.12 or newer**. Most HPC sites are standardizing +on 3.12+; older versions are not supported because the codebase +uses post-3.10 typing idioms (`X | Y`, PEP 604 unions, etc.). + +### Core framework only + +```bash +pip install exaconstit-calibrate +``` + +This installs `workflow_common` and `workflows` with `numpy`, +`pandas`, and `scipy` only. No DEAP, no matplotlib, no flux-core. +Suitable for users who want the Problem orchestrator, archive, +and result readers but are plugging in their own optimizer or +using a different plotting stack. `import workflows.optimization` +still works but driver functions raise `ModuleNotFoundError` on +first use (they are lazily imported). + +### With the NSGA-III driver (typical user setup) + +```bash +pip install "exaconstit-calibrate[nsga3,plot]" +``` + +Adds the DEAP fork and matplotlib. This is what you want for +material-parameter-calibration workflows and for running the +`run_nsga3` driver. + +### Development install + +```bash +git clone https://github.com/LLNL/ExaConstit.git +cd ExaConstit/workflows # or wherever this package lives +pip install -e ".[test]" +python -m pytest tests/ -q +``` + +`-e` gives you an editable install so your checkout is what gets +imported. `.[test]` pulls in pytest, matplotlib, and the DEAP fork. +Expected output: `254 passed` in 30-45 seconds. + +### HPC install (LLNL, ...) + +```bash +pip install "exaconstit-calibrate[nsga3,plot,flux]" +``` + +The `flux` extra pulls `flux-python` for the `FluxBackend`. This +will only succeed on systems where `flux-core` is already +installed (that is, inside a Flux allocation on an HPC system). +On a developer laptop you almost certainly do not want this +extra. + +## Quick start + +```python +from pathlib import Path +import numpy as np +import pandas as pd +from workflow_common import ( + CaseTemplater, TemplateTarget, + TemplatePathResolver, TemplatePropertyWriter, + TextTableReader, TextTableSpec, + Problem, ProblemConfig, SimCase, ObjectiveSpec, + StressStrainExtractor, StressStrainObjective, + LocalBackend, Manifest, ArchiveDB, + load_experimental_csv, +) +from workflows.optimization import Bounds, RunConfig, run_nsga3 + +# ... build templater / writer / resolver / reader / evaluator ... + +archive = ArchiveDB("opt.db") +archive.open() +run_id = archive.start_run(seed=42, param_names=["yield", "hardening"]) + +problem = Problem( + config=ProblemConfig(binary=Path("/path/to/mechanics"), ...), + param_names=["yield", "hardening"], + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(evaluator, sim_case=0)], + templater=templater, + property_writer=writer, + resolver=resolver, + backend=LocalBackend(max_workers=4), + reader=reader, + manifest=Manifest("wf/manifest.jsonl"), + archive=archive, + archive_run_id=run_id, +) + +result = run_nsga3( + problem, + bounds=Bounds(lower=np.array([100., 1000.]), + upper=np.array([500., 3000.])), + config=RunConfig( + n_generations=50, population_size=60, + unsga3=True, seed=42, + checkpoint_dir=Path("ck"), + cleanup_keep_generations=2, # keep inode count bounded + ), +) + +archive.close() +print("Pareto size:", len(result.pareto_front)) +``` + +See `workflow_common/MIGRATION.md` for a full walk-through of +porting an existing `ExaConstit_NSGA3.py` driver to this package, +and `workflow_common/ARCHITECTURE.md` for the design-rationale +overview, layering diagram, and decision table. + +For a runnable side-by-side example that mirrors the old script +top-to-bottom — same parameter bounds, same two-experiment +topology, same GA defaults — with inline comments quoting the +pre-refactor code next to each new-framework equivalent, see +`examples/nsga3_calibration.py`. It's the "show, don't tell" +counterpart to the migration guide. + +## Watching a run in progress + +During a run, the driver writes two human-readable text files +(tab-delimited, matches the pre-refactor driver's format exactly): + +- `logbook1_stats.log` — per-generation avg / std / min / max + fitness, plus ND / GD / HV for multi-objective runs +- `logbook2_solutions.log` — one row per individual per + generation with gene vector and fitness + +These land in `RunConfig.log_dir`, which defaults to the +`checkpoint_dir` if set, otherwise `cwd`. `less`, `tail -f`, and +`grep` all work. Turn them off with `write_logbook_files=False`. + +If you enabled archiving (`ArchiveDB`), a `archive.db` SQLite +file sits alongside. You don't need a SQLite client to read it — +there's a CLI: + +``` +python -m workflows.optimization.inspect_archive ./run_dir --runs +python -m workflows.optimization.inspect_archive ./run_dir --gens +python -m workflows.optimization.inspect_archive ./run_dir --gens-best +python -m workflows.optimization.inspect_archive ./run_dir \ + --genes --pareto-only --top 5 --format csv > best.csv +python -m workflows.optimization.inspect_archive ./run_dir \ + --clean-empty-runs --dry-run +``` + +Views: + +- `--runs` — every run in the archive +- `--gens` — per-generation summary stats (default) +- `--gens-best` — convergence view. Running best fitness per + objective, plus the generation the current L2 champion was born + in. Plateau detection: if `champion_birth_gen` holds steady for + many rows, the GA is stuck +- `--genes` — individual gene records; capped at 200 rows by + default (`--limit N` to override, `--limit 0` for no cap). Pair + with `--pareto-only` for top-N-across-history (top 3 by L2 norm + plus top 3 per objective, with an explicit `l2_norm` column so + every row is self-verifying), or with `--gen N --pareto-only` + for the rank-0 set at one specific generation. Passing + `--pareto-only` by itself implies `--genes` +- `--clean-empty-runs` — prune runs that have no generations and + no case outputs (typical residue of aborted debugging sessions). + Pair with `--dry-run` to preview; `--age-minutes 0` overrides + the default 60-minute safety gate that protects in-progress + runs + +Formats: `table` (default), `csv`, `json`. Filters: `--run RUN_ID`, +`--gen GEN_IDX`, `--pareto-only`, `--top N`, `--limit N`. Gene +vectors and fitness tuples are expanded into named columns using +the run's stored `param_names` and `objective_labels`, so the +output has meaningful headers (`yield_stress`, `hardening`, +`rmse`) rather than cryptic positional indices. + +Scale: on a 50 000-record archive (500 gens × 100 pop × 8 +objectives), `--runs` / `--gens` are near-instant; `--gens-best` +and `--pareto-only` run in about a second, dominated by SQLite +JSON decode. + +## Resuming a run + +Two ways to pick up from the last checkpoint: + +``` +python nsga3_calibration.py --resume-latest +python nsga3_calibration.py --resume-from 15 # gen number +python nsga3_calibration.py --resume-from path/to/checkpoint_gen_15.pkl +``` + +`--resume-latest` picks the highest-numbered pickle in the +checkpoint directory — the common case after a crash. +`--resume-from N` looks up `checkpoint_gen_N.pkl` in the +configured `checkpoint_dir`; giving a path uses it verbatim. + +On resume the example driver deliberately does NOT call +`archive.start_run()`, leaving `Problem.archive_run_id=None` so +the library adopts the UUID stored in the pickle. Calling +`start_run` on resume generates a fresh UUID that doesn't match +the pickle, which the library rejects with a diagnostic error +pointing at the fix. Stale archive rows from a pre-crash partial +generation are cleaned automatically via +`discard_from_generation`. + +## Recovering blown-away logs + +If the `.log` files have been lost or truncated but the pickle is +still around: + +``` +python -m workflows.optimization.regenerate_logbook_files \ + calibration_run/checkpoint_files/checkpoint_gen_15.pkl +``` + +Writes fresh `logbook1_stats.log` + `logbook2_solutions.log` next +to the pickle (or elsewhere with `--output-dir`). Output is +byte-identical to what a live run would have produced, because +it reuses the driver's own `_LogbookWriter`. + +## Plotting solutions vs experimental data + +Once a calibration finishes, point the example plotter at the +workspace: + +``` +python examples/plot_solutions.py calibration_run --top 10 +``` + +The plotter reads everything from the SQLite archive — gene +records, simulation outputs, AND the experimental reference +DataFrames (the driver records these at run start so you don't +have to keep CSVs around or worry about CSV-to-SimCase ordering +post-run). The on-disk case directories are not required. + +The headline figure is a grid of subplots, one column per SimCase, +with two rows: stress-strain on top, slope-strain on the bottom +(slope = `np.gradient(stress, strain)`, computed identically for +sim and exp so it matches what the slope objective scored). +Below the subplots: + +- a slider that fades curves below a chosen rank — narrow focus + from N to K without re-running; +- a click panel that prints the gene's parameters and fitness + when you click a curve. + +Selection modes: + +- `--objective 0` (or `--objective stress_rmse_0`) ranks by a single + objective. Pass `--objective` and the plotter picks `--mode + objective` automatically. +- `--mode last-gen` plots every individual in the final generation + with no ranking. +- L2 norm is the default — closest-to-utopian-origin across all + objectives. + +Pareto front: + +- `--pareto 0,1` produces a 2-D scatter of two objectives. Points + are colored by L2 norm; the L2 winner is ringed in red. **Every + point is clickable** — click reveals the gene's parameters AND + draws its stress-strain response in an inset axes overlaid against + the experimental references. With `--top N` the scatter is + restricted to the N lowest-L2 genes; without it, every rank-0 + gene from the run is shown. +- `--no-overlay` skips the headline overlay so you can request only + the Pareto. + +Headless / batch: + +``` +python examples/plot_solutions.py calibration_run \ + --top 10 --pareto 0,1 \ + --save overlay.png --save-pareto pareto.png --no-show +``` + +The plotter reuses the selection logic from `inspect_archive` +(the public `select_top_genes` API) so the L2 / per-objective / +dedup behavior matches what `inspect_archive --pareto-only` would +show. + +If for any reason the archive doesn't carry experimental data +(older archives written before the experiments table existed), +fall back to passing CSVs explicitly: + +``` +python examples/plot_solutions.py calibration_run \ + --top 10 --experimental experiments/exp1.txt experiments/exp2.txt +``` + +## Package layout + +``` +exaconstit-calibrate/ +├── pyproject.toml +├── README.md +├── LICENSE +├── workflow_common/ Framework package +│ ├── __init__.py Public API +│ ├── ARCHITECTURE.md Layering, decision table, FAQ +│ ├── MIGRATION.md Step-by-step port guide +│ ├── _fs.py Atomic writes, cd context manager +│ ├── logging_utils.py Stdlib-logging wrapper +│ ├── templates.py %%key%% substitution +│ ├── paths.py PathResolver, TemplatePathResolver +│ ├── manifest.py JSONL run manifest +│ ├── sentinel.py Per-case atomic .done markers +│ ├── platform_detect.py HPC-specific quirks +│ ├── results.py CaseLayout, ResultReader, TextTableReader +│ ├── smoothing.py PCHIP / arc-length / legacy smoothers +│ ├── case_setup.py CaseTemplater + PropertyWriter +│ ├── objectives.py Extractor, evaluator, failure handlers +│ ├── problem.py Problem orchestrator +│ ├── postprocess.py BestSol, plotting, checkpoint loader +│ ├── archive.py SQLite archive (ArchiveDB) +│ └── backends/ LocalBackend + FluxBackend +│ +├── workflows/ +│ └── optimization/ (U-)NSGA-III driver on DEAP fork +│ +├── examples/ +│ ├── README.md How to adapt and run the example +│ └── nsga3_calibration.py Side-by-side port of ExaConstit_NSGA3.py +│ +└── tests/ 255 tests +``` + +## Testing + +After an editable install: + +```bash +python -m pytest tests/ -q # full suite (~30-45s) +python -m pytest tests/test_archive.py -v # one module +python -m pytest tests/ -k "determin" # keyword filter +``` + +The test suite uses a fake Python "simulation binary" defined in +`tests/conftest.py`; no ExaConstit build is required. + +## A note on the package name + +`exaconstit-calibrate` captures the current and planned scope — +crystal-plasticity parameter calibration today, macroscale +yield-surface fitting tomorrow — without overreaching into other +ExaConstit workflow territory (AM challenge-problem orchestration, +etc.). The importable names (`workflow_common`, `workflows`) are +deliberately kept generic and unchanged from the pre-refactor +code so migrated drivers don't need import-path rewrites. + +## License + +BSD-3-Clause (matches ExaConstit). See `LICENSE`. + +## For maintainers + +Cutting a new release? See `RELEASING.md` for the build-and-publish +workflow. Most of the time the whole sequence is: + +```bash +# edit version in pyproject.toml, then: +python -m pytest tests/ -q +rm -rf dist build *.egg-info +python -m build +git tag -a v -m "Release v" +``` + +but there are failure modes (wheel sanity check, stale build dirs, +PyPI immutability) worth being aware of before publishing. The +guide covers all of them. diff --git a/workflows/exaconstit-calibrate/RELEASING.md b/workflows/exaconstit-calibrate/RELEASING.md new file mode 100644 index 0000000..149a4c5 --- /dev/null +++ b/workflows/exaconstit-calibrate/RELEASING.md @@ -0,0 +1,337 @@ +# Releasing exaconstit-calibrate + +This is a maintainer guide for cutting new versions of the package. +End users installing the package do not need to read this. + +## TL;DR + +```bash +# 0. One-time: install the build front-end (see Prerequisites) +pip install --upgrade build + +# 1. Bump the version in pyproject.toml +# 2. Run the full test suite +python -m pytest tests/ -q + +# 3. Build the artifacts +rm -rf dist build *.egg-info +python -m build + +# 4. Sanity-check the wheel in a clean venv +python -m venv /tmp/releasecheck +/tmp/releasecheck/bin/pip install "dist/exaconstit_calibrate--py3-none-any.whl[test]" +cp -r tests /tmp/releasecheck/ +/tmp/releasecheck/bin/python -m pytest /tmp/releasecheck/tests -q + +# 5. (Optional) tag and publish +git tag -a v -m "Release v" +git push origin v +# For PyPI: python -m twine upload dist/* +``` + +If any step fails, do not publish. Fix the failure and start over. + +## Prerequisites + +A one-time setup on your development machine: + +```bash +pip install --upgrade build # PEP 517 front-end +pip install --upgrade twine # only if you publish to a package index +``` + +Both are stdlib-external but they are the standard Python Packaging +Authority tools and should be considered mandatory for any Python +release work. `build` replaces the old `python setup.py bdist_wheel` +invocation that PEP 517 deprecated. + +You also need Python 3.12 or newer (same floor as the package itself +— building a wheel that requires `>=3.12` on an older Python would +silently produce metadata that's wrong for the interpreter running +the tests). + +## Step 1 — Decide on a version number + +The version lives in exactly one place: the `version = "X.Y.Z"` line +near the top of `pyproject.toml`. Everything else (wheel filename, +sdist filename, metadata, `importlib.metadata.version(...)`) derives +from that single string. + +We follow [semantic versioning](https://semver.org/): + +- **PATCH (0.1.0 → 0.1.1)** — bug fixes, no API changes. Users + can pip-upgrade without reading release notes. +- **MINOR (0.1.0 → 0.2.0)** — new features, backward-compatible. + Users can pip-upgrade but might want to read release notes. +- **MAJOR (0.1.0 → 1.0.0)** — breaking changes. Users need to + update their code. Reserve this for genuine API rewrites. + +Pre-1.0 versions have a softer contract: 0.X minor bumps can break +API if you call it out in the release notes. After 1.0, minor bumps +must not break API. + +Example edit: + +```toml +# pyproject.toml +[project] +name = "exaconstit-calibrate" +version = "0.2.0" # was 0.1.0 +``` + +Do NOT hardcode the version anywhere else. No `__version__` string +in `workflow_common/__init__.py`, no VERSION file. If you need +the version at runtime, use `importlib.metadata`: + +```python +from importlib.metadata import version +v = version("exaconstit-calibrate") +``` + +## Step 2 — Run the tests + +Before building anything, prove the code works. From the project +root: + +```bash +python -m pytest tests/ -q +``` + +Expected output: `254 passed` (as of v0.1.0; the number grows as +tests are added). If any test fails, do not build a release — +fix the failure and re-run. + +The test suite uses the fake simulation binary in +`tests/conftest.py`; you do not need ExaConstit or any HPC tool +installed to validate a release candidate. + +## Step 3 — Clean the build directory + +Stale artifacts from a previous build can cause subtle wrong-file +inclusions in the new build. Always wipe before building: + +```bash +rm -rf dist build *.egg-info +``` + +`dist/` holds the final artifacts (wheel and sdist). `build/` is +setuptools' scratch area. `*.egg-info` is editable-install metadata +that should regenerate cleanly each time. + +Also confirm the `build` front-end is installed in the environment +you plan to invoke it from: + +```bash +python -c "import build; print('build available')" || \ + pip install --upgrade build +``` + +`build` is a maintainer tool, not a runtime dependency of the +package, so it is intentionally absent from +`[project.optional-dependencies]`. If you see "No module named +'build'", install it explicitly — it will not come in via +`pip install .[test]`. + +## Step 4 — Build the wheel and sdist + +```bash +python -m build +``` + +This produces two files in `dist/`: + +- **`exaconstit_calibrate--py3-none-any.whl`** — the wheel. + Pure-Python, universal (py3, no platform tag). This is what + `pip install` uses by default. +- **`exaconstit_calibrate-.tar.gz`** — the sdist (source + distribution). Pip falls back to this if no wheel is available; + PyPI requires both. + +The names use underscores (`exaconstit_calibrate`) not hyphens +even though the distribution name in `pyproject.toml` uses hyphens +(`exaconstit-calibrate`). That's a packaging convention, not a +mistake: distribution names are normalized to underscores for +filenames. + +Expect to see a lot of "adding 'workflow_common/...'" lines scroll +past. The build ends with: + +``` +Successfully built exaconstit_calibrate-.tar.gz and +exaconstit_calibrate--py3-none-any.whl +``` + +If the build fails, the most common causes (in order of how often +you will hit them): + +- **setuptools too old.** We require `>=77.0` for PEP 639 license + expressions. Upgrade: `pip install --upgrade setuptools`. +- **Python too old.** `>=3.12` per the `requires-python` field. +- **Syntax error in pyproject.toml.** TOML is picky; a misplaced + comma or missing quote will produce a cryptic error from + setuptools. Validate with `python -c "import tomllib; tomllib.load(open('pyproject.toml','rb'))"`. + +## Step 5 — Sanity-check the wheel + +**Never publish a wheel you have not test-installed.** The build +succeeding only proves the build tool was happy; it doesn't prove +the wheel is importable, has the right files, or passes tests. + +Do this in a fresh venv, not your development environment: + +```bash +python -m venv /tmp/releasecheck +/tmp/releasecheck/bin/pip install \ + "dist/exaconstit_calibrate--py3-none-any.whl[test]" + +# The wheel doesn't include the tests directory, so copy it over: +cp -r tests /tmp/releasecheck/ + +# Run the test suite against the installed package: +cd /tmp/releasecheck +./bin/python -m pytest tests -q +``` + +Expected: `254 passed` (or however many tests exist in this +version). If any test fails, the wheel is broken — do not publish. + +Common things this test catches that the source-tree test did not: + +- A Markdown file was forgotten in `[tool.setuptools.package-data]` + and doesn't ship in the wheel. +- A helper module was added but its import path is wrong for an + installed layout. +- The lazy `__getattr__` in `workflows/optimization/__init__.py` + was accidentally replaced with an eager import, breaking the + base-install path. + +While you're here, also verify the documentation files made it +into the wheel: + +```bash +./bin/python -c " +import workflow_common, pathlib +d = pathlib.Path(workflow_common.__file__).parent +for name in ('ARCHITECTURE.md', 'MIGRATION.md'): + p = d / name + print(name, 'present:', p.is_file(), 'size:', p.stat().st_size if p.is_file() else '(missing!)') +" +``` + +Both files should report sizes > 0. + +## Step 6 — (Optional) Git tag the release + +If the project is under version control (it is — this is in the +ExaConstit repo), mark the release with an annotated tag: + +```bash +git add pyproject.toml +git commit -m "Release v" +git tag -a v -m "Release v" +git push origin main +git push origin v +``` + +The `-a` makes it an annotated tag (has its own commit-like object +and a message) rather than a lightweight tag. Annotated tags are +what GitHub uses to create Releases. + +## Step 7 — (Optional) Publish to a package index + +### To a GitHub Release + +The low-friction option. Go to +https://github.com/LLNL/ExaConstit/releases, click "Draft a new +release", pick the tag from step 6, and attach both artifacts from +`dist/` as release assets. Users install with: + +```bash +pip install https://github.com/LLNL/ExaConstit/releases/download/v/exaconstit_calibrate--py3-none-any.whl +``` + +### To PyPI (public) + +If LLNL decides to publish on PyPI: + +```bash +python -m twine check dist/* # metadata sanity check +python -m twine upload dist/* # prompts for credentials +``` + +You will need a PyPI account with ownership of the +`exaconstit-calibrate` name. The first upload reserves the name; +subsequent uploads append new versions. PyPI does not allow +deleting or overwriting a published version — if you upload a +broken 0.2.0, you must release 0.2.1 to fix it. + +### To an internal / HPC index + +LLNL and other HPC sites often run an internal devpi or Nexus index. +`twine upload` accepts a `--repository-url` to target a non-PyPI +index; credentials go in `~/.pypirc`. Ask your site admin for the +URL and auth method. + +## Step 8 — Announce + +If there's a changelog (`CHANGELOG.md`, release notes section in +README, etc.), update it. Post-release is also a good moment to +bump the version in `pyproject.toml` to `0.X.Y+dev` or `0.X.(Y+1).dev0` +so main-branch installs are obviously not release builds. That's a +convention, not a requirement. + +## Troubleshooting + +### "No module named 'deap'" during `pytest` in the release check + +The `[test]` extra should pull DEAP from the rcarson3 fork. If it +didn't, check `pyproject.toml` under `[project.optional-dependencies]` +— the `test` list must include +`"deap @ git+https://github.com/rcarson3/deap.git"`. + +### "error in egg-info command" during `python -m build` + +Setuptools is too old. Upgrade it: `pip install --upgrade setuptools`. +Version `>=77.0` is needed for the PEP 639 SPDX license expression. + +### Wheel contains unexpected files / missing expected files + +`[tool.setuptools.packages.find]` controls which packages ship. +`[tool.setuptools.package-data]` controls non-`.py` files (Markdown +docs, config files, etc.). Edit both and rebuild. + +To inspect the wheel contents without installing: + +```bash +python -c " +import zipfile +with zipfile.ZipFile('dist/exaconstit_calibrate--py3-none-any.whl') as z: + for n in z.namelist(): + print(n) +" +``` + +### `pip install` falls back to building from sdist instead of using the wheel + +Your Python version or platform doesn't match the wheel's tags. +Since our wheel is `py3-none-any` (pure Python, any platform), this +usually means the user's Python is older than `requires-python`. +Check the `Requires-Python: >=3.12` line in the wheel's METADATA. + +### A published version has a critical bug + +You cannot overwrite or delete a published version on PyPI. Release +a patch version (`0.2.0` → `0.2.1`) with the fix. Document the known +bug in the GitHub Release notes for `0.2.0` so users know to skip +that version. + +## Reference + +- [Python Packaging User Guide](https://packaging.python.org/) + — the canonical docs. Start here for anything not covered above. +- [PEP 517](https://peps.python.org/pep-0517/) — the build-system + standard that `python -m build` implements. +- [PEP 639](https://peps.python.org/pep-0639/) — SPDX license + expressions in pyproject.toml. +- [Semantic Versioning](https://semver.org/) — the version-number + contract. diff --git a/workflows/exaconstit-calibrate/examples/README.md b/workflows/exaconstit-calibrate/examples/README.md new file mode 100644 index 0000000..1105d88 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/README.md @@ -0,0 +1,132 @@ +# Examples + +## `nsga3_calibration.py` + +Drop-in replacement for the pre-refactor +`ExaConstit_NSGA3.py` + `ExaConstit_Problems.py` + `normal_map.py` ++ `ExaConstit_Logger.py` set. Runs the real ExaConstit `mechanics` +binary against the same two-experiment topology, the same parameter +bounds, and the same GA defaults as the original. + +Every section in the file has a commented-out block quoting the +relevant slice of the old script, followed by its new equivalent. +Useful for mapping old code mentally onto new code in one sitting. + +### Running it + +For Slurm users, start with `run_nsga3_slurm.sh`, +`nsga3_slurm_helpers.sh`, and `SLURM_RUN_GUIDE.md`. The Slurm script +is the small file users edit. The helper contains the longer shell +functions and should stay in the same directory unless +`HELPER_SCRIPT` points to its full path. + +1. Install the package with the NSGA-III extra: + + ```bash + pip install "exaconstit-calibrate[nsga3,plot]" + ``` + +2. Make sure the example lives inside an ExaConstit checkout. The + script auto-discovers: + - the enclosing ExaConstit repo root + - `test/data` inputs such as `voce_quats.ori`, `grains.txt`, + and `state_cp_voce.txt` + - a built `mechanics` binary in common locations such as + `build_cpu/bin/mechanics` + + Override discovery only if your layout differs: + - `EXACONSTIT_ROOT=/path/to/ExaConstit` + - `EXACONSTIT_MECHANICS=/path/to/mechanics` + +3. Pick a backend: + - `--backend flux` is the default and is the intended HPC path. + Flux reads the per-case `SimCase` resource fields directly + (`num_nodes`, `num_tasks`, `cores_per_task`, `gpus_per_task`, + `duration_s`). + - `--backend local` is mainly for workstation/debug use. The + shipped example cases request `num_tasks=4`, so local mode is + not a drop-in replacement unless you either: + - reduce the cases to `num_tasks=1` for serial debugging, or + - adapt the backend construction in the example to provide an + MPI launcher such as `mpirun`/`srun` for multi-rank local runs + +4. Run: + + ```bash + python nsga3_calibration.py --backend flux + ``` + + For a Flux allocation started from Slurm, a typical launch looks like: + + ```bash + srun -n 1 --pty --mpi=none --mpibind=off flux start \ + python nsga3_calibration.py --backend flux + ``` + + For a serial local debug run, first lower the example's + `SimCase.num_tasks` values to `1`, then run: + + ```bash + python nsga3_calibration.py --backend local + ``` + + Optional flags: + - `--no-archive` — disable the SQLite archive + rolling cleanup + (most literal reproduction of the old script's behavior) + - `--resume-from PATH/checkpoint_gen_N.pkl` — resume a previous run + - `--resume-latest` — resume from the highest checkpoint in the run dir + - `--seed N` — override the RNG seed + +### Paths And Templates + +- `examples/template_options.toml` is repo-portable: the example + fills in `%%properties_file%%`, `%%state_vars_file%%`, + `%%grain_file%%`, and the other case-specific placeholders at render + time. +- The example script's local package root is + `ExaConstit/workflows/exaconstit-calibrate`, while the ExaConstit + repo root is the higher-level `ExaConstit` directory. The code uses + separate names for those on purpose: `PACKAGE_ROOT` versus + `EXACONSTIT_ROOT`. + +### Resource Configuration + +- Backend choice is separate from resource description. The resource + shape lives on each `SimCase`. +- Typical CPU-only Flux case: + `num_nodes=1, num_tasks=4, cores_per_task=1, gpus_per_task=0` +- Typical GPU Flux case with one rank per GPU: + `num_nodes=1, num_tasks=4, cores_per_task=1, gpus_per_task=1` +- Hybrid MPI+threads case: + `num_nodes=1, num_tasks=4, cores_per_task=7, gpus_per_task=1` + which requests 28 CPU cores plus 4 GPUs for that one simulation. + +### Things worth knowing before you port your own driver + +- **Normalization.** The built-in `StressStrainObjective` returns + plain RMSE. The old script divided by `np.std(exp)`. The example + includes two small custom evaluator classes + (`_StdNormalizedStressEvaluator`, `_StdNormalizedSlopeEvaluator`) + that reproduce the old normalization exactly. Copy those into + your own driver or replace with whatever metric you prefer. + +- **Population size.** The old script derived `NPOP` from the + reference-point count. The new `RunConfig` does the same when + `population_size=None` (default). You get `NPOP = 288` for + `NOBJ=4, p=10` either way. + +- **Case-directory naming.** The example uses + `gen_{generation}/gene_{gene}_obj_{obj}` to exactly match the + pre-refactor layout, so a mix of old + new runs in the same + workspace doesn't collide. + +- **SimCase vs ObjectiveSpec.** One SimCase per experiment (two + in this example). Two ObjectiveSpecs per SimCase (stress and + slope). That's the `NOBJ = NEXP * 2` pattern, preserved exactly + but with the sim-vs-evaluator split that the framework makes + explicit. + +For the conceptual walkthrough that this file is the +"show, don't tell" counterpart to, read +`workflow_common/MIGRATION.md` — especially the step-by-step +section 4-8 for the Problem construction. diff --git a/workflows/exaconstit-calibrate/examples/SLURM_RUN_GUIDE.md b/workflows/exaconstit-calibrate/examples/SLURM_RUN_GUIDE.md new file mode 100644 index 0000000..fc49b4e --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/SLURM_RUN_GUIDE.md @@ -0,0 +1,379 @@ +# Slurm Run Guide For `nsga3_calibration.py` + +This guide is for running the NSGA-III calibration example on a Slurm +cluster that starts a Flux instance inside the Slurm job. + +The short version: + +```bash +cd ExaConstit/workflows/exaconstit-calibrate/examples +sbatch run_nsga3_slurm.sh +``` + +Before you submit, edit the top of `run_nsga3_slurm.sh`. + +Keep these two files together in the same directory: + +- `run_nsga3_slurm.sh` +- `nsga3_slurm_helpers.sh` + +Most users should edit only `run_nsga3_slurm.sh`. The helper file holds +the longer shell functions so the job script stays short and harder to +break by accident. + +If you move `nsga3_slurm_helpers.sh` somewhere else, set +`HELPER_SCRIPT` in `run_nsga3_slurm.sh` to the full path: + +```sh +HELPER_SCRIPT="/usr/workspace/myname/scripts/nsga3_slurm_helpers.sh" +``` + +## What To Edit First + +Open `run_nsga3_slurm.sh` and edit the `#SBATCH` block: + +```sh +#SBATCH -A wbronze +#SBATCH -N 2 +#SBATCH -n 224 +#SBATCH -t 01:00:00 +#SBATCH -p pdebug +#SBATCH -J exaconstit_nsga3 +#SBATCH -o exaconstit_nsga3.%j.out +``` + +Meaning: + +- `-A` is your bank, project, or allocation account. +- `-N` is the number of nodes. +- `-n` is the total Slurm task count. A common choice is nodes times + CPU cores per node. +- `-t` is the wall-clock time limit. +- `-p` is the queue or partition. +- `-J` is the job name shown by Slurm. +- `-o` is the output log file. `%j` expands to the Slurm job id. + +Concrete examples: + +```sh +# Short debug job on 2 nodes: +#SBATCH -A wbronze +#SBATCH -N 2 +#SBATCH -n 224 +#SBATCH -t 01:00:00 +#SBATCH -p pdebug +#SBATCH -J exaconstit_debug +#SBATCH -o exaconstit_debug.%j.out +``` + +```sh +# Longer production-style job on 4 nodes: +#SBATCH -A your_bank_name +#SBATCH -N 4 +#SBATCH -n 448 +#SBATCH -t 08:00:00 +#SBATCH -p pbatch +#SBATCH -J exaconstit_calibration +#SBATCH -o exaconstit_calibration.%j.out +``` + +Then check the `User settings` block: + +```sh +PYTHON="/usr/tce/packages/python/python-3.12.2/bin/python" +FLUX_PYTHONPATH="/usr/lib64/flux/python3.12" +ACTION="all" +INSTALL_PACKAGE="0" +TOP_N="10" +``` + +Most users only need to change `PYTHON`, `ACTION`, and maybe +`INSTALL_PACKAGE`. + +Important shell syntax rule: do not add spaces around `=`. + +Correct: + +```sh +ACTION="all" +``` + +Incorrect: + +```sh +ACTION = "all" +``` + +More examples: + +```sh +# Use python3 from your loaded environment: +PYTHON="python3" +``` + +```sh +# Use a different Flux Python module directory: +FLUX_PYTHONPATH="/usr/lib64/flux/python3.11" +``` + +```sh +# Show 25 best solutions instead of 10: +TOP_N="25" +``` + +## Mechanics Binary + +The example tries to find the ExaConstit checkout and the `mechanics` +binary automatically. It looks for common paths like: + +```text +ExaConstit/build_cpu/bin/mechanics +ExaConstit/build/bin/mechanics +ExaConstit/build_hip/bin/mechanics +ExaConstit/build_cuda/bin/mechanics +``` + +If your build is somewhere else, set this in `run_nsga3_slurm.sh`: + +```sh +EXACONSTIT_MECHANICS="/full/path/to/mechanics" +``` + +If the script cannot find the ExaConstit checkout, set: + +```sh +EXACONSTIT_ROOT="/full/path/to/ExaConstit" +``` + +## Running A New Calibration + +Set: + +```sh +ACTION="run" +``` + +Then submit: + +```bash +sbatch run_nsga3_slurm.sh +``` + +This runs: + +```bash +srun -n 1 --mpi=none --mpibind=off flux start \ + python nsga3_calibration.py --backend flux +``` + +The driver writes its run directory under: + +```text +examples/calibration_run +``` + +Important files: + +- `calibration_run/calibration.db` is the SQLite archive. +- `calibration_run/checkpoint_files/checkpoint_gen_N.pkl` are restart files. +- `calibration_run/checkpoint_files/logbook1_stats.log` has generation stats. +- `calibration_run/checkpoint_files/logbook2_solutions.log` has individuals. + +## Running And Post-Processing In One Job + +Set: + +```sh +ACTION="all" +``` + +This runs the calibration first. If the calibration finishes, the script +then prints archive summaries and writes plots under: + +```text +examples/postprocess +``` + +This is the recommended first mode because it gives you useful outputs +without needing to remember separate commands. + +## Resuming A Run + +The calibration driver writes a checkpoint every generation. + +To resume from the newest checkpoint: + +```sh +ACTION="resume-latest" +``` + +To resume from a generation number: + +```sh +ACTION="resume-from" +CHECKPOINT_GEN="15" +CHECKPOINT_PATH="" +``` + +To resume from an explicit checkpoint path: + +```sh +ACTION="resume-from" +CHECKPOINT_GEN="" +CHECKPOINT_PATH="calibration_run/checkpoint_files/checkpoint_gen_15.pkl" +``` + +Only set one of `CHECKPOINT_GEN` or `CHECKPOINT_PATH`. + +## Inspecting Results Without Running More Simulations + +Set: + +```sh +ACTION="inspect" +``` + +This runs archive inspection commands only: + +```bash +python -m workflows.optimization.inspect_archive calibration_run --runs +python -m workflows.optimization.inspect_archive calibration_run --gens-best +python -m workflows.optimization.inspect_archive calibration_run \ + --genes --pareto-only --top 10 +``` + +What these mean: + +- `--runs` lists runs stored in the archive. +- `--gens-best` shows convergence by generation. +- `--genes --pareto-only --top 10` shows the best solutions across the + whole run, not just the last generation. + +The script also writes: + +```text +postprocess/top_solutions.csv +``` + +## Making Plots Without Running More Simulations + +Set: + +```sh +ACTION="plots" +``` + +This writes several figures under: + +```text +examples/postprocess +``` + +Useful plotting commands shown by the script: + +```bash +python plot_solutions.py calibration_run \ + --top 10 \ + --save postprocess/top_10_balanced_l2_overlay.png \ + --no-show +``` + +This plots the top 10 most balanced solutions across all objectives. + +```bash +python plot_solutions.py calibration_run \ + --top 10 --objective stress_1 \ + --save postprocess/top_10_stress_1_overlay.png \ + --no-show +``` + +This ranks by one objective only. Available objective labels in this +example are: + +- `stress_1` +- `slope_1` +- `stress_2` +- `slope_2` + +You can also use integer objective indices: + +- `0` means `stress_1` +- `1` means `slope_1` +- `2` means `stress_2` +- `3` means `slope_2` + +## Pareto Plots + +Pareto plots show tradeoffs between two objectives. + +Stress-vs-slope for experiment 1: + +```bash +python plot_solutions.py calibration_run \ + --top 10 --pareto 0,1 \ + --save-pareto postprocess/pareto_stress_1_vs_slope_1.png \ + --no-show +``` + +Stress objective from experiment 1 vs stress objective from experiment 2: + +```bash +python plot_solutions.py calibration_run \ + --top 10 --pareto 0,2 \ + --save-pareto postprocess/pareto_stress_1_vs_stress_2.png \ + --no-show +``` + +To make both a Pareto plot and the stress-strain overlay in one command, +add `--overlay`: + +```bash +python plot_solutions.py calibration_run \ + --top 10 --pareto 0,1 --overlay \ + --save postprocess/top_10_overlay.png \ + --save-pareto postprocess/pareto_0_1.png \ + --no-show +``` + +## Common Problems + +### The job says it cannot import `flux` + +The Flux Python module is not visible to your Python interpreter. Check: + +```sh +FLUX_PYTHONPATH="/usr/lib64/flux/python3.12" +``` + +That path must match your site and Python version. + +### The job says it cannot find `mechanics` + +Set: + +```sh +EXACONSTIT_MECHANICS="/full/path/to/mechanics" +``` + +### I want a quick test, not a full production run + +Use the `pdebug` partition, a short time limit, and `ACTION="run"` or +`ACTION="all"`. For a very small local test, edit `nsga3_calibration.py` +so each `SimCase` has `num_tasks=1`, then run with `--backend local`. + +### The run stopped before all generations + +That can be normal. The NSGA-III driver has early stopping criteria. Look +at: + +```bash +python -m workflows.optimization.inspect_archive calibration_run --gens-best +``` + +and: + +```text +calibration_run/checkpoint_files/logbook1_stats.log +``` + +to see whether it converged or stopped due to a failure. diff --git a/workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m1.txt b/workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m1.txt new file mode 100644 index 0000000..357812d --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m1.txt @@ -0,0 +1,62 @@ +-5.00E-06 -6.43E-04 +-2.06E-05 -2.65E-03 +-6.95E-05 -8.93E-03 +-2.22E-04 -2.85E-02 +-4.61E-04 -4.03E-02 +-6.47E-04 -4.23E-02 +-8.42E-04 -4.35E-02 +-1.04E-03 -4.44E-02 +-1.26E-03 -4.53E-02 +-1.48E-03 -4.61E-02 +-1.82E-03 -4.73E-02 +-2.18E-03 -4.85E-02 +-2.55E-03 -4.97E-02 +-2.94E-03 -5.09E-02 +-3.35E-03 -5.21E-02 +-3.78E-03 -5.34E-02 +-4.22E-03 -5.47E-02 +-4.68E-03 -5.60E-02 +-5.16E-03 -5.74E-02 +-5.67E-03 -5.88E-02 +-6.19E-03 -6.03E-02 +-7.01E-03 -6.26E-02 +-7.87E-03 -6.50E-02 +-8.77E-03 -6.74E-02 +-9.70E-03 -6.99E-02 +-1.07E-02 -7.25E-02 +-1.17E-02 -7.51E-02 +-1.28E-02 -7.79E-02 +-1.39E-02 -8.07E-02 +-1.50E-02 -8.36E-02 +-1.63E-02 -8.66E-02 +-1.75E-02 -8.97E-02 +-1.89E-02 -9.29E-02 +-2.03E-02 -9.62E-02 +-2.24E-02 -1.01E-01 +-2.47E-02 -1.06E-01 +-2.71E-02 -1.12E-01 +-2.97E-02 -1.17E-01 +-3.23E-02 -1.22E-01 +-3.51E-02 -1.28E-01 +-3.80E-02 -1.33E-01 +-4.11E-02 -1.39E-01 +-4.43E-02 -1.45E-01 +-4.77E-02 -1.51E-01 +-5.12E-02 -1.57E-01 +-5.50E-02 -1.63E-01 +-5.89E-02 -1.69E-01 +-6.31E-02 -1.75E-01 +-6.74E-02 -1.81E-01 +-7.21E-02 -1.87E-01 +-7.69E-02 -1.93E-01 +-8.20E-02 -1.99E-01 +-8.75E-02 -2.06E-01 +-9.32E-02 -2.12E-01 +-9.92E-02 -2.18E-01 +-1.06E-01 -2.24E-01 +-1.12E-01 -2.30E-01 +-1.20E-01 -2.36E-01 +-1.27E-01 -2.42E-01 +-1.35E-01 -2.48E-01 +-1.44E-01 -2.53E-01 +-1.46E-01 -2.55E-01 \ No newline at end of file diff --git a/workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m3.txt b/workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m3.txt new file mode 100644 index 0000000..7bd137e --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/experiments/expt_strain_rate_1m3.txt @@ -0,0 +1,60 @@ +-5.00E-05 -6.43E-03 +-2.06E-04 -2.64E-02 +-4.51E-04 -3.71E-02 +-6.42E-04 -3.88E-02 +-8.41E-04 -3.99E-02 +-1.05E-03 -4.07E-02 +-1.26E-03 -4.15E-02 +-1.49E-03 -4.23E-02 +-1.73E-03 -4.30E-02 +-2.09E-03 -4.41E-02 +-2.48E-03 -4.53E-02 +-2.88E-03 -4.64E-02 +-3.29E-03 -4.76E-02 +-3.73E-03 -4.87E-02 +-4.18E-03 -5.00E-02 +-4.65E-03 -5.12E-02 +-5.15E-03 -5.25E-02 +-5.66E-03 -5.39E-02 +-6.20E-03 -5.53E-02 +-6.76E-03 -5.67E-02 +-7.35E-03 -5.82E-02 +-7.96E-03 -5.97E-02 +-8.59E-03 -6.13E-02 +-9.26E-03 -6.29E-02 +-9.95E-03 -6.46E-02 +-1.07E-02 -6.64E-02 +-1.18E-02 -6.91E-02 +-1.30E-02 -7.19E-02 +-1.42E-02 -7.47E-02 +-1.55E-02 -7.77E-02 +-1.69E-02 -8.07E-02 +-1.83E-02 -8.39E-02 +-1.98E-02 -8.71E-02 +-2.13E-02 -9.04E-02 +-2.38E-02 -9.54E-02 +-2.63E-02 -1.01E-01 +-2.90E-02 -1.06E-01 +-3.18E-02 -1.11E-01 +-3.48E-02 -1.16E-01 +-3.79E-02 -1.22E-01 +-4.12E-02 -1.28E-01 +-4.46E-02 -1.33E-01 +-4.82E-02 -1.39E-01 +-5.20E-02 -1.45E-01 +-5.60E-02 -1.50E-01 +-6.03E-02 -1.56E-01 +-6.47E-02 -1.62E-01 +-6.94E-02 -1.68E-01 +-7.43E-02 -1.74E-01 +-7.95E-02 -1.80E-01 +-8.51E-02 -1.85E-01 +-9.09E-02 -1.91E-01 +-9.70E-02 -1.97E-01 +-1.04E-01 -2.03E-01 +-1.10E-01 -2.09E-01 +-1.18E-01 -2.14E-01 +-1.26E-01 -2.20E-01 +-1.34E-01 -2.25E-01 +-1.43E-01 -2.31E-01 +-1.46E-01 -2.33E-01 \ No newline at end of file diff --git a/workflows/exaconstit-calibrate/examples/master_options.toml b/workflows/exaconstit-calibrate/examples/master_options.toml new file mode 100644 index 0000000..d4af28e --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/master_options.toml @@ -0,0 +1,138 @@ +#The below show all of the options available and their default values +#Although, it should be noted that the BCs options have no default values +#and require you to input ones that are appropriate for your problem. +#Also while the below is indented to make things easier to read the parser doesn't care. +#More information on TOML files can be found at: https://en.wikipedia.org/wiki/TOML +#and https://github.com/toml-lang/toml/blob/master/README.md +Version = "0.6.0" +[Properties] + # A base temperature that all models will initially run at + temperature = %%temperature_k%% + #The below informs us about the material properties to use + [Properties.Matl_Props] + floc = "%%properties_file%%" + num_props = 17 + #These options tell inform the program about the state variables + [Properties.State_Vars] + floc = "%%state_vars_file%%" + num_vars = 24 + #These options are only used in xtal plasticity problems + [Properties.Grain] + # Tells us where the orientations are located for either a UMAT or + # ExaCMech problem. -1 indicates that it goes at the end of the state + # variable file. + # If ExaCMech is used the loc value will be overriden with values that are + # consistent with the library's expected location + ori_state_var_loc = 9 + ori_stride = 4 + #The following options are available for orientation type: euler, quat/quaternion, or custom. + #If one of these options is not provided the program will exit early. + ori_type = "quat" + num_grains = 500 + ori_floc = "%%ori_file%%" + # If auto generating a mesh a grain file is needed that associates a given + # element to a grain. If you are using a mesh file this information should + # already be embedded in the mesh using something akin to the MFEM v1.0 mesh + # file element attributes, and therefore this option is ignored. + grain_floc = "%%grain_file%%" +[BCs] + # Required - essential BC ids for the whole boundary + essential_ids = [1, 4] + # Required = component combo (free = 0, x = 1, y = 2, z = 3, xy = 4, yz = 5, xz = 6, xyz = 7) + # Note: ExaConstit v0.5.0 and earlier had xyz set to -1. This change was broken in v0.6.0 + # These numbers tell us which degrees of freedom are constrained for the given + # list of attributes provided within essential_ids + # Negative values of the below signify that for a given essential BC id that + # we want to use a constant velocity gradient rather than directly supplying the + # velocity values. + essential_comps = [3, 3] + #Vector of vals to be applied for each attribute + #The length of this should be #ids * dim of problem + essential_vals = %%essential_vals%% +[Model] + #This option tells us to run using a UMAT or exacmech + mech_type = "exacmech" + #This tells us that our model is a crystal plasticity problem + cp = true + [Model.ExaCMech] + #Need to specify the xtal type + #currently only FCC is supported + xtal_type = "fcc" + # Required - the slip kinetics and hardening form that we're going to be using + # The choices are either PowerVoce, PowerVoceNL, or MTSDD + # HCP is only available with MTSDD + slip_type = "powervoce" + +# Options related to our time steps +# For the time options if all three or some combination of the following tables +# [Auto, Fixed, and Custom] are provided the priority of which one goes +# 1. Custom +# 2. Auto +# 3. Fixed +# +# Note: For fixed and auto time steppings the final simulation step is satified if +# abs(t_final - t_current) < abs(1e-3 * dt_current) +# Generally, the simulation driver will try to satisfy this to even tighter bounds +# but that is not always possible. +[Time] + [Time.Auto] + dt_start = %%dt_min%% + dt_min = %%dt_min%% + dt_max = %%dt_max%% + dt_scale = %%dt_scale%% + t_final = %%t_final%% +#Our visualizations options +[Visualizations] + #The stride that we want to use for when to take save off data for visualizations + steps = 1 + visit = false + conduit = false + paraview = false + additional_avgs = true +[Solvers] + # Option for how our assembly operation is conducted. Possible choices are + # FULL, PA, EA + # Full assembly fully assembles the stiffness matrix + # Partial assembly is completely matrix free and only performs the action of + # the stiffness matrix. + # Element assembly only assembles the elemental contributions to the stiffness + # matrix in order to perform the actions of the overall matrix. + assembly = "EA" + #Option for what our runtime is set to. Possible choices are CPU, OPENMP, or CUDA + rtmodel = "CPU" + #Options for our nonlinear solver + #The number of iterations should probably be low + #Some problems might have difficulty converging so you might need to relax + #the default tolerances + [Solvers.NR] + iter = 25 + rel_tol = 5e-5 + abs_tol = 5e-10 + #Options for our iterative linear solver + #A lot of times the iterative solver converges fairly quickly to a solved value + #However, the solvers could at worst take DOFs iterations to converge. In most of these + #solid mechanics problems that almost never occcurs unless the mesh is incredibly coarse. + [Solvers.Krylov] + iter = 1000 + rel_tol = 1e-7 + abs_tol = 1e-27 + #The following Krylov solvers are available GMRES, PCG, and MINRES + #If one of these options is not used the program will exit early. + solver = "CG" + preconditioner = "JACOBI" +[Mesh] + #Serial refinement level + ref_ser = 1 + #Parallel refinement level + ref_par = 0 + #The polynomial refinement/order of our shape functions + p_refinement = %%p_refinement%% + #Possible values here are cubit, auto, or other + #If one of these is not provided the program will exit early + type = "auto" + #The below shows the necessary options needed to automatically generate a mesh + [Mesh.Auto] + #The mesh length is needed + length = %%mesh_lengths%% + #The number of cuts along an edge of the mesh are also needed + ncuts = %%mesh_cuts%% diff --git a/workflows/exaconstit-calibrate/examples/nsga3_calibration.py b/workflows/exaconstit-calibrate/examples/nsga3_calibration.py new file mode 100644 index 0000000..746875b --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/nsga3_calibration.py @@ -0,0 +1,1616 @@ +""" +ExaConstit NSGA-III calibration — side-by-side with the pre-refactor script. + +This file is a drop-in replacement for the pre-refactor +``ExaConstit_NSGA3.py`` + ``ExaConstit_Problems.py`` + ``normal_map.py`` + +``ExaConstit_Logger.py`` set. It runs the real ``mechanics`` binary +against the same two-experiment topology, the same parameter bounds, +the same per-case resource allocation, and the same GA knobs as the +original. + +How to read this file +--------------------- +The whole thing is a single ``main()``. Each top-level section starts +with a commented block showing the relevant slice of the OLD script, +followed by the NEW equivalent. The OLD blocks are quoted verbatim +from the pre-refactor sources so you can grep for them and verify +the line-for-line mapping. + +Numerical defaults match the old script exactly. Running both against +the same seed, same experimental data, and same binary should produce +trajectories that agree to within float roundoff. + +How to run this file +-------------------- +1. Ensure the example lives inside an ExaConstit checkout. By default + it auto-discovers: + + * the repo root by walking upward from this file, + * ``test/data`` inputs like ``voce_quats.ori`` and ``grains.txt``, + * a built ``mechanics`` binary in common locations such as + ``build_cpu/bin/mechanics``. + + Override discovery with ``EXACONSTIT_ROOT=/path/to/ExaConstit`` or + ``EXACONSTIT_MECHANICS=/path/to/mechanics`` if your layout differs. + +2. Install the package with the NSGA-III extra: + + pip install "exaconstit-calibrate[nsga3,plot]" + +3. Run: + + python nsga3_calibration.py + + Or, to run without filesystem cleanup and without the archive + (closer to the old script's default behavior): + + python nsga3_calibration.py --no-archive + +For the conceptual overview of each framework component this script +uses, see ``workflow_common/MIGRATION.md``. This file is the +"show me working code" counterpart to that prose walkthrough. +""" +from __future__ import annotations + +import argparse +from dataclasses import dataclass +import os +from pathlib import Path +import sys +from typing import Any, Sequence + +import numpy as np +import pandas as pd + +EXAMPLE_DIR = Path(__file__).resolve().parent +PACKAGE_ROOT = EXAMPLE_DIR.parent +if str(PACKAGE_ROOT) not in sys.path: + # Make the example runnable directly from a source checkout + # without requiring an editable install first. + sys.path.insert(0, str(PACKAGE_ROOT)) + + +def _find_exaconstit_root(start: Path) -> Path: + """Find the surrounding ExaConstit checkout. + + Preference order: + 1. ``EXACONSTIT_ROOT`` environment variable. + 2. Walk upward from this example until we find ``test/data`` and + ``workflows`` together, which identifies the repo root in a + normal checkout. + 3. Fall back to the expected ``.../ExaConstit`` ancestor relative + to the example file. + """ + env_root = os.environ.get("EXACONSTIT_ROOT") + if env_root: + return Path(env_root).expanduser().resolve() + + for candidate in (start, *start.parents): + if (candidate / "test" / "data").is_dir() and ( + candidate / "workflows" + ).is_dir(): + return candidate.resolve() + + return start.parents[2].resolve() + + +def _first_existing(paths: Sequence[Path]) -> Path | None: + for path in paths: + if path.is_file(): + return path + return None + + +def _mechanics_candidates(exaconstit_root: Path) -> list[Path]: + candidates: list[Path] = [] + env_mechanics = os.environ.get("EXACONSTIT_MECHANICS") + if env_mechanics: + candidates.append(Path(env_mechanics).expanduser()) + + for build_dir in ( + "build_cpu", + "build", + "build_hip", + "build_cuda", + "build_gpu", + "build_debug", + "build_release", + ): + candidates.append(exaconstit_root / build_dir / "bin" / "mechanics") + return candidates + + +@dataclass(frozen=True) +class ParameterSpec: + """One optimized material parameter. + + Edit the PARAMS table in main(), not three separate lists. Bounds, + names, log output, checkpoint metadata, and write_properties() all + derive from this single table. + """ + + name: str + lower: float + upper: float + units: str = "" + description: str = "" + + +from workflow_common import ( + ArchiveDB, + CallablePropertyWriter, + CaseTemplater, + HAS_FLUX, + InfinityFailureHandler, + LocalBackend, + Manifest, + ObjectiveSpec, + PchipSmoother, + Problem, + ProblemConfig, + SimCase, + StressStrainExtractor, + TemplatePathResolver, + TemplateTarget, + TextTableReader, + TextTableSpec, + configure_logging, +) + + +# ================================================================ +# CONFIGURATION — repo-relative defaults for the shipped example. +# Everything below this block should work in a normal ExaConstit checkout. +# ================================================================ +# +# USER EDIT MAP +# ------------- +# If you are new to this workflow, start here. Most users only need +# to edit the items in this block and the matching sections called +# out below. +# +# 1. New experiment data: +# * Put your stress-strain text/CSV files under examples/experiments +# or point EXPERIMENT_FILES at their full paths. +# * Add one matching SimCase in the ``sim_cases = [...]`` section +# for each experiment. One SimCase means one simulation setup: +# strain rate, final strain, time stepping, mesh, orientation, +# and CPU/GPU resource request. +# * In the OBJECTIVES section, set ``stress_column`` to the stress +# component matching your loading direction: Sxx for x, Syy for y, +# Szz for z. +# +# 2. New orientations, grains, or state variables: +# * Point ORIENTATION_FILES, GRAIN_FILE, and STATE_VARS_FILE at the +# files you want copied/referenced by each case. +# * If different experiments use different orientations, keep one +# ORIENTATION_FILES entry per experiment and set each SimCase's +# ``ori_file`` to the filename that should appear inside the +# case directory. +# +# 3. New material model or new ExaConstit options: +# * Edit examples/template_options.toml. That is the options.toml +# template rendered into every case directory. +# * Edit ``write_properties()`` if your material model expects a +# different properties-file order or extra material constants. +# The property order must match the active ExaCMech material +# shortcut/model selected in template_options.toml. +# +# 4. New fitted parameters: +# * Edit the PARAMS table in main(). Bounds and parameter names are +# derived from that one table. +# * Then update ``write_properties()`` so each fitted parameter is +# written into the correct material-model slot. The order still +# matters when ExaConstit reads properties.txt. +# +# 5. Backend and hardware: +# * Pick ``--backend flux`` for HPC Flux runs or ``--backend local`` +# for small/debug runs. +# * CPU/GPU shape is controlled on each SimCase with num_nodes, +# num_tasks, cores_per_task, gpus_per_task, and duration_s. +# Changing only the Slurm header does not change what each +# individual simulation requests from Flux. + +EXACONSTIT_ROOT = _find_exaconstit_root(EXAMPLE_DIR) +TEST_DATA_DIR = EXACONSTIT_ROOT / "test" / "data" +MECHANICS_BINARY = ( + _first_existing(_mechanics_candidates(EXACONSTIT_ROOT)) + or (EXACONSTIT_ROOT / "build_cpu" / "bin" / "mechanics") +) +STATE_VARS_FILE = TEST_DATA_DIR / "state_cp_voce.txt" +GRAIN_FILE = TEST_DATA_DIR / "grains.txt" + +# Template options TOML with ``%%key%%`` placeholders. The keys +# substituted in per-case come from SimCase.template_values +# (see the ``sim_cases = [...]`` section). A minimal viable master +# file references: +# +# temperature_k = %%temperature_k%% +# orientation_file = "%%ori_file%%" +# t_final = %%t_final%% +# dt_min = %%dt_min%% +# dt_max = %%dt_max%% +# dt_scale = %%dt_scale%% +# essential_vals = %%essential_vals%% +# mesh_lengths = %%mesh_lengths%% +# mesh_cuts = %%mesh_cuts%% +# p_refinement = %%p_refinement%% +# properties_file = "%%properties_file%%" +# state_vars_file = "%%state_vars_file%%" +# grain_file = "%%grain_file%%" +# +# Note: ``strain_rate`` remains in SimCase.case_data because the +# objective extractor uses it to convert time to strain. The mechanics +# input currently receives direct velocity values via +# ``essential_vals``; if you switch to velocity-gradient BCs, map +# ``%%strain_rate%%`` into that block in template_options.toml. +MASTER_TOML = EXAMPLE_DIR / "template_options.toml" + +# One stress-strain CSV per experiment. Two-column format: +# strain, stress. Whitespace or comma-separated both work. +EXPERIMENT_FILES = [ + EXAMPLE_DIR / "experiments" / "expt_strain_rate_1m3.txt", + EXAMPLE_DIR / "experiments" / "expt_strain_rate_1m1.txt", +] + +# Orientation files (one per experiment) copied into each case dir. +# Matches the old ``ori_file=["voce_quats.ori", "voce_quats.ori"]``. +ORIENTATION_FILES = [ + TEST_DATA_DIR / "voce_quats.ori", + TEST_DATA_DIR / "voce_quats.ori", +] + +WORKSPACE = EXAMPLE_DIR / "calibration_run" + +# ================================================================ + + +def main(argv: Sequence[str] | None = None) -> None: + from workflows.optimization import ( + Bounds, + RunConfig, + build_reference_points, + derive_population_size, + run_nsga3, + ) + + parser = argparse.ArgumentParser( + description="NSGA-III calibration of ExaConstit parameters", + ) + parser.add_argument( + "--backend", + choices=("flux", "local"), + default="flux", + help=( + "Execution backend. 'flux' submits each case into the " + "current Flux instance. 'local' runs cases as ordinary " + "subprocesses on the current host; for this example, " + "local mode is mainly for serial debugging unless you also " + "adapt the resource settings and/or provide an MPI launcher." + ), + ) + parser.add_argument( + "--resume-from", default=None, + help=( + "Resume from an existing checkpoint. Accepts either: " + "(a) a path to a checkpoint pickle, e.g. " + "'calibration_run/checkpoint_files/checkpoint_gen_15.pkl'; " + "or (b) a plain integer like '15' meaning " + "'checkpoint_gen_15.pkl inside the run's checkpoint dir'. " + "Mutually exclusive with --resume-latest." + ), + ) + parser.add_argument( + "--resume-latest", action="store_true", + help=( + "Resume from the highest-numbered checkpoint pickle in " + "the run's checkpoint directory. Convenient after a " + "crash when you just want to pick back up wherever " + "the last good generation landed." + ), + ) + parser.add_argument( + "--no-archive", action="store_true", + help="Disable the SQLite archive + rolling cleanup. " + "Closest match to the pre-refactor script's disk behavior.", + ) + parser.add_argument( + "--seed", type=int, default=42, + help="Random seed for reproducibility (default: 42).", + ) + args = parser.parse_args(argv) + + required_files = [ + ("mechanics binary", MECHANICS_BINARY), + ("master options template", MASTER_TOML), + ("state vars file", STATE_VARS_FILE), + ("grain file", GRAIN_FILE), + ] + required_files.extend( + (f"experiment file #{i + 1}", path) + for i, path in enumerate(EXPERIMENT_FILES) + ) + required_files.extend( + (f"orientation file #{i + 1}", path) + for i, path in enumerate(ORIENTATION_FILES) + ) + missing = [(label, path) for label, path in required_files if not path.is_file()] + if missing: + details = "\n".join( + f" - {label}: {path}" for label, path in missing + ) + parser.error( + "required example inputs were not found:\n" + f"{details}\n" + "Set EXACONSTIT_ROOT to your checkout root or " + "EXACONSTIT_MECHANICS to an explicit mechanics binary path " + "if your build lives elsewhere." + ) + + # ============================================================ + # RESOLVE --resume-from / --resume-latest + # ============================================================ + # + # We let --resume-from accept either a checkpoint path or a + # generation number, because the user's mental model is "pick + # up from gen N" — they shouldn't have to remember the exact + # checkpoint filename convention. --resume-latest is a separate + # flag for the very common "just keep going from wherever we + # left off" case. + # + # The end product is ``resume_pickle_path``: either a Path + # pointing at the pickle to load, or None for "fresh run". + # This is what the library's RunConfig.resume_from wants. + + if args.resume_from is not None and args.resume_latest: + parser.error( + "--resume-from and --resume-latest are mutually exclusive" + ) + + CHECKPOINT_DIR = WORKSPACE / "checkpoint_files" + + resume_pickle_path: Path | None = None + if args.resume_latest: + # Find the highest-numbered pickle in the checkpoint dir. + # Pattern is fixed by the library as "checkpoint_gen_{N}.pkl". + if not CHECKPOINT_DIR.is_dir(): + parser.error( + f"--resume-latest: checkpoint directory not found at " + f"{CHECKPOINT_DIR}. Is this a fresh workspace?" + ) + candidates = list(CHECKPOINT_DIR.glob("checkpoint_gen_*.pkl")) + if not candidates: + parser.error( + f"--resume-latest: no checkpoint files under " + f"{CHECKPOINT_DIR}. Nothing to resume from." + ) + # Extract the integer part and pick the max. Robust against + # non-matching filenames — ``int(...)`` on a bad stem raises + # and we skip that candidate, preserving the behavior for + # any user's stray logs or backups in the same dir. + def _gen_idx_of(p: Path) -> int: + try: + return int(p.stem.split("_")[-1]) + except ValueError: + return -1 + resume_pickle_path = max(candidates, key=_gen_idx_of) + # Display-only line. Try to shorten to a cwd-relative path + # since that's nicer to read, but fall back to the full + # string if the checkpoint dir and cwd have no ancestor + # relationship (happens when the user runs from elsewhere, + # or when CHECKPOINT_DIR is itself a relative path that + # argparse hasn't resolved). + try: + display_path = resume_pickle_path.resolve().relative_to( + Path.cwd().resolve() + ) + except ValueError: + display_path = resume_pickle_path + print( + f"--resume-latest: picked {display_path} " + f"(gen {_gen_idx_of(resume_pickle_path)})" + ) + elif args.resume_from is not None: + # Try integer first; if it parses, it's a generation number. + # Otherwise treat as a path. + raw = str(args.resume_from) + try: + gen_idx = int(raw) + except ValueError: + # Not an integer → treat as path. + resume_pickle_path = Path(raw) + else: + # Integer → look up the canonical filename. We do this + # eagerly so the user gets a clear "no such checkpoint" + # error right here, not deep inside run_nsga3's pickle + # load. + resume_pickle_path = ( + CHECKPOINT_DIR / f"checkpoint_gen_{gen_idx}.pkl" + ) + if not resume_pickle_path.is_file(): + parser.error( + f"--resume-from: no checkpoint at {resume_pickle_path}" + ) + + # ============================================================ + # LOGGING + # ============================================================ + # + # OLD (ExaConstit_NSGA3.py): + # + # initialize_ExaProb_log( + # glob_loglvl="info", + # filename="logbook3_ExaProb.log", + # restart=restart, + # ) + # + # NEW: configure_logging wraps stdlib logging. Inside framework + # modules, loggers come from get_logger(__name__). + + WORKSPACE.mkdir(parents=True, exist_ok=True) + configure_logging( + level="info", + logfile=WORKSPACE / "logbook3_ExaProb.log", + append=(resume_pickle_path is not None), + ) + + # ============================================================ + # PARAMETER BOUNDS — Bounds replaces BOUND_LOW / BOUND_UP / NDIM + # ============================================================ + # + # OLD (ExaConstit_NSGA3.py): + # + # IND_LOW = [150, 100, 50, 1500, 1e-5, 1e-3, 1e-4, 1e-5, 1e-6] + # IND_UP = [200, 150, 100, 2500, 1e-3, 1e-1, 1e-2, 1e-3, 1e-4] + # BOUND_LOW = IND_LOW; BOUND_UP = IND_UP + # NDIM = len(BOUND_LOW) + # + # NEW: edit one PARAMS table. Bounds, names, archive metadata, + # log output, and write_properties() all derive from this table. + # + # IMPORTANT USER CONTRACT: + # PARAMS defines the optimized-parameter vector. The order here is + # the gene order: + # + # gene[0] -> PARAMS[0].name + # gene[1] -> PARAMS[1].name + # ... + # + # To add a fitted parameter, add one ParameterSpec below and then + # use that name in write_properties(). Example: to fit the Voce + # saturation strength, add + # + # ParameterSpec("crss_sat", 80.0e-3, 180.0e-3, "GPa", + # "CRSS saturation strength") + # + # and replace the fixed crss_sat value in write_properties() with + # gene_dict["crss_sat"]. + + PARAMS = [ + ParameterSpec( + name="mprime", + lower=0.01e0, + upper=0.05e0, + description="rate sensitivity exponent for power-law slip", + ), + ParameterSpec( + name="h0", + lower=200.0e-3, + upper=500.0e-3, + units="GPa", + description="Voce hardening coefficient", + ), + ParameterSpec( + name="crss0", + lower=10.0e-3, + upper=20.0e-3, + units="GPa", + description="initial critical resolved shear stress", + ), + ] + + param_names = [spec.name for spec in PARAMS] + bounds = Bounds( + lower=np.array([spec.lower for spec in PARAMS]), + upper=np.array([spec.upper for spec in PARAMS]), + ) + assert len(param_names) == bounds.n_params + + # ============================================================ + # SIMCASES — one per experiment, with FULL per-experiment dict + # ============================================================ + # + # OLD (ExaConstit_NSGA3.py, all the per-experiment parallel arrays): + # + # ncpus = [4, 4] + # ngpus = [0, 0] + # nnodes = [1, 1] + # temperature_k = [DEP_UNOPT[0][0], DEP_UNOPT[1][0]] + # ori_file = ["voce_quats.ori", "voce_quats.ori"] + # strain_rate = [-1.0e-3, -1.0e-1] + # desired_strain = [-0.1301, -0.1001] + # timeout = [6 * 60, 6 * 60] + # t_final = [120.0, 1.0] + # dt_min = [0.001, 0.00001] + # dt_max = [1.0, 0.01] + # dt_scale = [0.125, 0.125] + # minmax_strain = [None, None] + # essential_vals = [[0.0, 0.0, 0.0, 0.0, 0.0, -0.001], + # [0.0, 0.0, 0.0, 0.0, 0.0, -0.001]] + # mesh_lengths = [[1.0, 1.0, 1.0], [1.0, 1.0, 1.0]] + # mesh_cuts = [[1, 1, 1], [1, 1, 1]] + # p_refinement = [1, 1] + # + # test_dataframe = { + # "experiments": exper_input_files, + # "temp_k": temperature_k, "strain_rate": strain_rate, + # "desired_strain": desired_strain, "ori_file": ori_file, + # "t_final": t_final, "dt_min": dt_min, "dt_max": dt_max, + # "dt_scale": dt_scale, "essential_vals": essential_vals, + # "mesh_lengths": mesh_lengths, "mesh_cuts": mesh_cuts, + # "p_refinement": p_refinement, "timeout": timeout, + # "minmax_strain": minmax_strain, + # } + # test_dataframe = pd.DataFrame(data=test_dataframe) + # + # NEW: one SimCase per row of the old DataFrame. Per-experiment + # overlay (the old `test_dataframe` columns minus the experiment + # file itself) goes in SimCase.case_data and gets substituted + # into template_options.toml at render time. Per-experiment resource + # requests (ncpus/ngpus/nnodes/timeout) go in the SimCase's + # resource-override fields and flow through to the backend's + # SimJobSpec. + # + # PLAIN-LANGUAGE VERSION: + # One SimCase is one experiment you are trying to match. If you + # measured the same material at two strain rates, use two SimCases. + # If you measured tension and compression, use two SimCases. If + # you measured three temperatures, use three SimCases. + # + # To add an experiment, copy one SimCase block and change: + # * label: a short name that will appear in logs + # * EXPERIMENT_FILES above: add the measured curve file + # * strain_rate: signed strain rate for the experiment + # * desired_strain and minmax_strain: comparison strain range + # * t_final/dt_min/dt_max/dt_scale: simulation time stepping + # * essential_vals: imposed boundary values in options.toml + # * ori_file / ORIENTATION_FILES if orientation changes + # * mesh_lengths, mesh_cuts, p_refinement if the RVE changes + # * num_tasks, cores_per_task, gpus_per_task if resource needs + # change for that case + + # ============================================================ + # PER-CASE CONSTANTS — what to put on each SimCase + # ============================================================ + # + # SimCase.case_data is the single mapping for everything that's + # specific to one experiment but NOT part of the gene vector + # being optimized. Whatever you put here flows to all three + # framework components that need per-case context: + # + # * the TEMPLATER substitutes %%key%% references in + # template_options.toml (and any other rendered file) + # * the PATH RESOLVER substitutes {key} references in + # working_dir_pattern and output_file_patterns + # * the PROPERTY WRITER reads sim_case.case_data inside the + # CallablePropertyWriter callable + # + # One dict, three readers. No need to remember which field a + # particular variable belongs in. + # + # Common cases: + # * "different elastic constants per temperature" — put the + # constants directly in case_data as "c11", "c12", and + # "c44". The first shipped SimCase below does this explicitly; + # the second omits them and therefore uses write_properties() + # defaults. + # * "case-specific orientation file" — put the filename in + # case_data["ori_file"] so it lands in options.toml AND + # the property writer can read it. + # * "case-specific RVE name in the path" — put it in + # case_data["rve_name"] and use {rve_name} in + # working_dir_pattern. + # + # Per-case RESOURCE overrides (num_nodes / num_tasks / + # gpus_per_task / duration_s / binary / binary_args) are + # separate top-level fields on SimCase — see step 4a below. + # Resources are consumed by the backend, not by templates or + # paths, which is why they have dedicated fields rather than + # living inside case_data. + # + # NOTE: TemplatePropertyWriter ALSO has an `extra_values=` field, + # but those are constants applied to EVERY case (not per-case); + # for per-case property-writer constants, read them out of + # sim_case.case_data inside your CallablePropertyWriter callable. + # + # ILLUSTRATIVE EXAMPLE — not used in this run, but the pattern + # to imitate when constants need to vary per simulation case + # (elastic constants, lattice parameters, anisotropy ratios, + # whatever else is gene-independent but case-dependent): + # + # PUT EVERY VARYING CONSTANT DIRECTLY IN case_data. No Python + # lookup tables, no logic in the writer to map cases to values. + # The data lives next to the SimCase that uses it; modifying + # it later is one place to edit: + # + # sim_cases = [ + # SimCase( + # label="exp_298K", + # case_data={ + # # Loading + path-pattern fields: + # "strain_rate": -1.0e-3, + # "temperature_k": 298.0, + # "ori_file": "voce_298k.ori", + # "rve_name": "grain_32", + # # Per-case material constants — elastic moduli + # # at this case's temperature, lattice params, + # # whatever else varies. Just dictionary entries: + # "c11": 168.4, + # "c12": 121.4, + # "c44": 75.4, + # }, + # ), + # SimCase( + # label="exp_600K", + # case_data={ + # "strain_rate": -1.0e-3, + # "temperature_k": 600.0, + # "ori_file": "voce_600k.ori", + # "rve_name": "grain_32", + # # Different elastic constants for this temperature. + # # Same KEYS, different VALUES — that's all: + # "c11": 156.0, + # "c12": 117.0, + # "c44": 72.0, + # }, + # ), + # ] + # + # CONSUMING THIS DATA — three options, pick whichever fits your + # sim code's input format. All three see the same case_data; + # the framework wires it to all three readers automatically: + # + # OPTION 1 — let the templater put the constants in options.toml. + # If your template_options.toml has %%c11%%, %%c12%%, %%c44%% + # placeholders, the templater fills them from case_data. Zero + # writer code; nothing to change in the writer at all. + # + # OPTION 2 — let the writer pull from case_data and write a + # separate properties.txt. Useful if your sim code reads + # material constants from a dedicated properties file rather + # than options.toml. The writer reads case_data by key: + # + # def write_properties(case_dir, gene, names, sim_case): + # path = case_dir / "properties.txt" + # data = sim_case.case_data + # lines = [ + # f"c11 = {data['c11']}", + # f"c12 = {data['c12']}", + # f"c44 = {data['c44']}", + # # ... gene-derived values follow ... + # ] + # path.write_text("\n".join(lines) + "\n") + # return path + # + # OPTION 3 — split: some constants in options.toml via + # templater (Option 1), some in properties.txt via writer + # (Option 2). Same case_data dict, different consumers. The + # framework doesn't impose a particular layout. + # + # ABOUT EXTRA KEYS: case_data may contain keys not referenced + # by any template or path pattern. Those are silently ignored + # by the templater (validated at template-load time, not at + # data-load time) — the templater only complains when its + # template has a %%key%% with no matching case_data entry, + # which is exactly the typo-detection users want. + + # RESOURCE SHAPE PER CASE + # ----------------------- + # These four fields tell the backend how much hardware ONE + # simulation case should consume: + # + # num_nodes - nodes requested for this case + # num_tasks - MPI ranks for this case + # cores_per_task - CPU cores per MPI rank + # gpus_per_task - GPUs per MPI rank + # + # Examples: + # + # CPU-only, 4 MPI ranks on 1 node: + # num_nodes=1, num_tasks=4, cores_per_task=1, gpus_per_task=0 + # + # GPU run, 1 rank per GPU on a 4-GPU node: + # num_nodes=1, num_tasks=4, cores_per_task=1, gpus_per_task=1 + # + # Hybrid MPI+threads, 4 ranks each with 7 CPU cores and 1 GPU: + # num_nodes=1, num_tasks=4, cores_per_task=7, gpus_per_task=1 + # + # FluxBackend consumes these fields directly when building the + # jobspec. LocalBackend only launches local subprocesses, so + # ``num_tasks > 1`` requires wiring in an MPI launcher there. + # The shipped example keeps the old 4-rank CPU-only shape. + sim_cases = [ + SimCase( + label="exp1_quasi_static", + case_data={ + # Loading condition / boundary conditions. + "strain_rate": -1.0e-3, + "desired_strain": -0.1301, + "temperature_k": 298.0, + "ori_file": "voce_quats.ori", # filename IN case dir + # Example of per-case material constants. These are + # the same cubic elastic constants used as defaults + # in write_properties(), so adding them here verifies + # the case_data path without changing the shipped + # numerical behavior. To calibrate or compare + # experiments at different temperatures, put the + # temperature-specific elastic constants on each + # SimCase. + "c11": 168.4e0, + "c12": 121.4e0, + "c44": 75.2e0, + # Time stepping. + "t_final": 120.0, + "dt_min": 0.001, + "dt_max": 1.0, + "dt_scale": 0.125, + # Essential (Dirichlet) BC values for each dof. + # For this template, the six values are ordered like + # the six velocity/strain-rate components expected by + # the ExaConstit options file. The last entry controls + # the z-direction loading used by this example, so it + # matches strain_rate for uniaxial z compression. + # Negative means compression; positive means tension. + "essential_vals": [0.0, 0.0, 0.0, 0.0, 0.0, -0.001], + # Mesh controls. + "mesh_lengths": [1.0, 1.0, 1.0], + "mesh_cuts": [5, 5, 5], + "p_refinement": 1, + # Reference to the per-case properties file written + # by CallablePropertyWriter below. + "properties_file": "properties.txt", + # Repo-local support files resolved from this example's + # location so users can run from any working directory. + "state_vars_file": str(STATE_VARS_FILE), + "grain_file": str(GRAIN_FILE), + # Optimization window. (lo, hi) absolute-value strain + # bounds; the extractor crops both stress and slope + # comparisons to this interval. The elastic regime + # in metals is typically below ~0.002 strain — set + # lo above that to skip elastic, leaving the optimizer + # focused on the plastic portion the gene parameters + # actually control. Use None on either side for "no + # bound." None for the whole field disables windowing. + "minmax_strain": (-0.01, -0.13), + }, + # Per-case resource request. This example keeps the old + # CPU-only 4-rank shape: + # 1 node + # 4 MPI ranks + # 1 CPU core per rank + # 0 GPUs per rank + # + # To move to GPUs under Flux, typically change only + # ``gpus_per_task`` (and, if needed, ``cores_per_task``). + num_nodes=1, + num_tasks=4, # ncpus[0] + cores_per_task=1, + gpus_per_task=0, # ngpus[0] + duration_s=6 * 60, # timeout[0] (seconds) + # Per-SimCase args for the binary's CLI. + binary_args=("-opt", "options.toml"), + ), + SimCase( + label="exp2_high_rate", + case_data={ + "strain_rate": -1.0e-1, + "desired_strain": -0.1001, + "temperature_k": 298.0, + "ori_file": "voce_quats.ori", + "t_final": 1.0, + "dt_min": 0.00001, + "dt_max": 0.01, + "dt_scale": 0.125, + # Same convention as exp1: the last entry imposes + # z-direction compression at the faster rate. + "essential_vals": [0.0, 0.0, 0.0, 0.0, 0.0, -0.1], + "mesh_lengths": [1.0, 1.0, 1.0], + "mesh_cuts": [5, 5, 5], + "p_refinement": 1, + "properties_file": "properties.txt", + "state_vars_file": str(STATE_VARS_FILE), + "grain_file": str(GRAIN_FILE), + "minmax_strain": (-0.01, -0.1), + }, + num_nodes=1, + num_tasks=4, + cores_per_task=1, + gpus_per_task=0, + duration_s=6 * 60, + binary_args=("-opt", "options.toml"), + ), + ] + + # ============================================================ + # OBJECTIVES — stress + slope per experiment (2 * NEXP) + # ============================================================ + # + # OLD (ExaConstit_Problems.py::evaluate): + # + # f[iobj * 2] = RMSE(sim_stress, exp_stress) / np.std(exp_stress) + # f[iobj * 2 + 1] = RMSE(sim_slope, exp_slope) / np.std(exp_slope) + # + # NEW: one ObjectiveSpec per objective value, each pointing at + # its SimCase by index. Custom evaluator classes defined at the + # bottom of this file reproduce the OLD std-normalized RMSE + # exactly. The framework's built-in StressStrainObjective returns + # plain RMSE without normalization — that's a judgment call the + # framework doesn't make silently. + # + # Choosing the stress/strain comparison: + # * stress_column must match the loading direction in the + # simulation output: Sxx for x loading, Syy for y loading, + # Szz for z loading. + # * strain_rate should keep the experimental sign convention. + # This example uses negative strain for compression. + # * minmax_strain in each SimCase crops the comparison window. + # Use it to ignore elastic transients, machine seating, noisy + # tails, or strain ranges where the experiment is unreliable. + # * Some evaluators score using absolute strain/stress magnitudes + # so tension/compression signs do not dominate RMSE. Plots and + # physical interpretation still need the correct sign. + + objective_specs = [] + for i, (exp_file, sim_case) in enumerate(zip(EXPERIMENT_FILES, sim_cases)): + exp_df = _load_experimental_csv(exp_file) + # Pass strain_rate signed so the extractor's + # strain = strain_rate * time produces signed strain + # matching the loading direction (negative for compression, + # positive for tension). Stripping the sign here would + # flip the strain axis relative to the stress axis on + # compression cases, leaving plotted curves looking like + # tension responses with negative stress — wrong sign on + # the strain side, mismatched against experimental data. + # Evaluators that score on magnitudes still work because + # they apply np.abs themselves before computing RMSE. + strain_rate = float(sim_case.case_data["strain_rate"]) + + # Optional optimization window. case_data["minmax_strain"] is + # a (lo, hi) pair or None: lo=None or hi=None means "no + # bound on that side." If both bounds are missing the + # extractor sees window=None and returns the full curve. + # Anything real, including upper bounds derived from + # "desired_strain" if that's all the user wants, can go + # here. + minmax = sim_case.case_data.get("minmax_strain") + if minmax is not None and (minmax[0] is not None or minmax[1] is not None): + # Substitute 0 for missing lo and a huge number for + # missing hi — both are absolute-value bounds, so 0 means + # "include from origin" and 1e9 means "no upper limit". + lo = 0.0 if minmax[0] is None else float(abs(minmax[0])) + hi = 1e9 if minmax[1] is None else float(abs(minmax[1])) + window = (lo, hi) + else: + # Fall back to "desired_strain" as the upper bound when + # minmax_strain isn't set — this preserves the old + # convention where users only declared the maximum strain + # they cared about. + window = (0.0, abs(sim_case.case_data["desired_strain"])) + + # Build one extractor per SimCase. The window crops the + # extracted (strain, stress) arrays before any objective + # sees them, so both stress and slope evaluators below + # automatically respect the user's chosen interval. + extractor = StressStrainExtractor( + stress_output="avg_stress", + stress_column="Szz", # z-axis load — override for x/y loading + strain_source="time_rate", + strain_rate=strain_rate, + time_column="Time", + window=window, + ) + + stress_eval = _StdNormalizedStressEvaluator( + experimental=exp_df, extractor=extractor, + ) + slope_eval = _StdNormalizedSlopeEvaluator( + experimental=exp_df, extractor=extractor, + ) + objective_specs.extend([ + ObjectiveSpec(stress_eval, sim_case=i, label=f"stress_{i + 1}"), + ObjectiveSpec(slope_eval, sim_case=i, label=f"slope_{i + 1}"), + ]) + + n_obj = len(objective_specs) + + # ============================================================ + # PROPERTY WRITER — CallablePropertyWriter matching the old pattern + # ============================================================ + # + # OLD: a user-supplied Python function wrote a per-case properties + # file. The ExaProb class invoked that callable during each + # evaluation, right before running mechanics. + # + # NEW: CallablePropertyWriter is the direct analogue. Define your + # write function with signature + # ``(case_dir, gene, param_names, sim_case) -> Path`` and hand it + # to the writer. Everything else about the framework is unchanged. + # + # The writer's callable receives the SimCase for this evaluation, + # so per-experiment logic (different orientation files, temp- + # dependent derived values, etc.) works naturally. + # + # This example writer is intentionally minimal. It is enough to + # exercise the workflow and reproduce the old quick-start behavior, + # but it is not a complete material-model authoring interface. + # For production calibration work, port or write a material + # generator that names each physical parameter explicitly, similar + # to the older Matgen-style scripts. That keeps units, model + # assumptions, and parameter ordering visible to the mechanics + # user instead of hiding them in a list of numbers. + + def write_properties(case_dir: Path, gene: Sequence[float], + names: Sequence[str], sim_case: SimCase) -> Path: + """Write material-model properties for one case. + + Produces a `properties.txt` file whose format matches what + the old `ExaConstit_Problems.py` wrote. Adapt the body to + whatever format your mechanics binary actually reads. The + framework does not care about the file's internal format, + only that the file exists and is referenced correctly in + options.toml. ExaConstit does care: the numeric order here + must match the active material model selected in + template_options.toml. + + ``sim_case`` lets this callable behave differently per + experiment. Read per-case constants out of + ``sim_case.case_data`` (set up the SimCase) — typical + examples include ``temperature_k`` for a temp-dependent + elastic-constant lookup, ``ori_file`` for case-specific + orientation files, or any other field you put in the + SimCase's case_data dict. See the SimCase definitions + above for the conventions; the comment block before + ``sim_cases = [...]`` explains how case_data flows to the + templater, path resolver, and property writer. + """ + path = case_dir / "properties.txt" + # Zip gene values with their names so the output is both + # machine-readable AND human-auditable for debugging. + gene_dict = dict(zip(names, gene)) + + # ------------------------------------------------------------------ + # Voce material properties for this example + # ------------------------------------------------------------------ + # + # This section mirrors the older Matgen-style scripts: assign + # each physical quantity to a named Python variable first, then + # write the final numeric list in the exact order ExaConstit + # expects for the selected material model. + # + # Per-simulation constants: + # SimCase.case_data is available here as ``case_data``. To make + # a constant vary by experiment, add the key to each SimCase and + # read it with ``case_data.get("name", default)`` below. + # + # Example for temperature-dependent elastic constants: + # + # In each SimCase.case_data, add: + # "c11": 168.4, "c12": 121.4, "c44": 75.2 + # + # Then the c11/c12/c44 assignments below automatically use + # the case-specific values. If the keys are absent, the + # defaults shown here are used. + # + # Example for a temperature-driven reference energy: + # + # simulation_temperature_k = case_data["temperature_k"] + # reference_temperature_k = simulation_temperature_k + # + # This example keeps the historical 300 K reference energy so + # it reproduces the old quick-start behavior. + case_data = sim_case.case_data + + # Basic thermo-physical constants. + density = 8.920e-6 + heat_capacity_cv = 0.003435984 + tolerance = 1.0e-10 + + # Cubic elastic constants. Put c11/c12/c44 in SimCase.case_data + # to vary these by experiment, temperature, orientation set, or + # any other case-specific condition. + c11 = float(case_data.get("c11", 168.4e0)) + c12 = float(case_data.get("c12", 121.4e0)) + c44 = float(case_data.get("c44", 75.2e0)) + + # Voce hardening and power-law slip parameters. + # Values pulled from gene_dict are fitted by NSGA-III. The + # remaining values are fixed model constants for this example. + mu = (c11 - c12) / 2.0 + nu = c44 + voigt_shear = 0.2 * (2.0 * mu + 3.0 * nu) + reuss_shear = (mu * nu) / (nu + 3.0 * (mu - nu) * 0.2) + # Shear modulus calculation if not available in literature + shear_modulus = (voigt_shear + reuss_shear) / 2.0 + mprime = float(gene_dict["mprime"]) + gdot0 = 1.0e0 + h0 = float(gene_dict["h0"]) + crss0 = float(gene_dict["crss0"]) + crss_sat = 122.4e-3 + crss_sat_scaling_exponent = 0.0 + crss_sat_scaling_coefficient = 5.0e9 + hardening_initial = crss0 + + # Equation-of-state style constants used by the current model + # input. reference_internal_energy is tied to a reference + # temperature, not necessarily the SimCase's loading temperature. + gruneisen_parameter = 0.0 + reference_temperature_k = 300.0 + reference_internal_energy = -heat_capacity_cv * reference_temperature_k + + # IMPORTANT: this order is the file format. Keep it aligned + # with the active ExaCMech material shortcut/model in + # template_options.toml. The names above are for humans; the + # file written below remains one numeric value per line. + properties = [ + density, + heat_capacity_cv, + tolerance, + c11, + c12, + c44, + shear_modulus, + mprime, + gdot0, + h0, + crss0, + crss_sat, + crss_sat_scaling_exponent, + crss_sat_scaling_coefficient, + hardening_initial, + gruneisen_parameter, + reference_internal_energy, + ] + path.write_text( + "\n".join(f"{value:.12g}" for value in properties) + "\n" + ) + return path + + property_writer = CallablePropertyWriter( + func=write_properties, dest_hint="properties.txt", + ) + + # ============================================================ + # TEMPLATER — renders options.toml per case + copies ori file + # ============================================================ + # + # The CaseTemplater renders text files with %%key%% substitution. + # TWO targets here: + # + # 1. template_options.toml -> options.toml, with ALL the + # SimCase.case_data substituted. + # 2. voce_quats.ori -> voce_quats.ori (plain copy, no + # substitution needed since .ori files are static). + # + # The orientation file's source path is per-SimCase, so we build + # the target list dynamically per case. The ``TemplateTarget`` + # per_case_source mapping lets a single TemplateTarget resolve + # differently per SimCase's index. See the framework's + # CaseTemplater docstring for alternative patterns. + + # For maximum clarity in a short example, we use a single + # templater with one global target (the options.toml render) + # and copy the ori files in as part of each SimCase's + # case_data by passing the source path — the framework + # resolves it naturally because %%ori_file%% is a string in + # template_options.toml. + # + # In production you typically have orientation files that + # differ per SimCase; the simplest way is to place them in a + # known location OUTSIDE the case dir and reference by absolute + # path in template_options.toml, so no copy is needed. The + # example uses that pattern: each SimCase sets + # ``ori_file = "voce_quats.ori"`` as a filename IN the case dir, + # and a second TemplateTarget copies it in. + templater_targets = [ + TemplateTarget(source=MASTER_TOML, dest="options.toml"), + ] + # Only include the ori-file copy if the user actually pointed + # at a real file — matches how the old code handled optional + # orientation inputs. + for i, ori_path in enumerate(ORIENTATION_FILES): + if ori_path.is_file(): + templater_targets.append( + TemplateTarget( + source=ori_path, + dest="voce_quats.ori", + substitute=False, # binary-safe copy, no %%key%% pass + ) + ) + break # single global copy is fine if both experiments share it + templater = CaseTemplater(templater_targets) + + # ============================================================ + # PATH RESOLVER — where each case's working dir and outputs live + # ============================================================ + # + # The PathResolver maps a (generation, gene, obj) CaseContext onto + # a directory and a set of output filenames. Two concerns: + # + # 1. ``working_dir_pattern`` — template for the case's directory. + # Placeholders available: {generation}, {gene}, {obj}, plus + # anything in SimCase.case_data. The old code used + # "gen_{gen}/gene_{gene}_obj_{obj}", which we preserve so a + # mixed old+new workspace doesn't collide. + # + # 2. ``output_file_patterns`` — logical names -> template paths. + # These are the files you want to read BACK after the sim. + # Each key becomes a valid argument to reader.read() later, + # and each {working_dir} inside the pattern is expanded via + # the working_dir_pattern above. + # + # Think of output_file_patterns as "which output files does the + # reader care about, and where does the binary write them?" The + # filenames can differ from the binary's hard-coded output names + # via a simple rename step; here we match the old names directly. + + resolver = TemplatePathResolver( + working_dir_pattern="gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={ + "avg_stress": "{working_dir}/results/options/avg_stress_global.txt", + "avg_def_grad": "{working_dir}/results/options/avg_def_grad_global.txt", + }, + root=WORKSPACE, + ) + + # ============================================================ + # RESULT READER — which output columns matter, how to parse them + # ============================================================ + # + # TextTableReader is built for whitespace-separated numeric output + # like ExaConstit's avg_stress.txt. One TextTableSpec per logical + # output name (the keys match PathResolver.output_file_patterns). + # Each spec declares: + # + # columns - name each column in disk order; the resulting + # DataFrame uses these as .columns. This is where you + # say "column Szz" vs the old hardcoded [:, 2] + # indexing in ExaConstit_Problems.py. + # required - if True (default) a missing or empty file fails + # the case; if False the reader silently skips it + # and the evaluator handles the missing DataFrame. + # + # The extractor above picks from these DataFrames by column name. + # Adjust column order to match your binary's output format exactly, + # or use `comment="#"` / `delimiter=","` / etc. for non-default + # formats (see TextTableSpec docstring for the full option set). + + reader = TextTableReader({ + # avg_stress_global.txt: Time + Volume + 6 Cauchy-stress + # components. ExaConstit writes its volume-averaged output + # files with a ``# Time Volume ...`` header prefix followed + # by calc-type-specific columns. For stress the columns are + # Sxx/Syy/Szz/Sxy/Sxz/Syz (capital S, x/y/z axis indexing — + # Load direction here is z so stress_column="Szz" above. + # See ExaConstit/src/postprocessing/postprocessing_file_manager.hpp + # GetVolumeAverageHeader for the authoritative column lists. + "avg_stress": TextTableSpec(columns=[ + "Time", "Volume", + "Sxx", "Syy", "Szz", "Sxy", "Sxz", "Syz", + ]), + # avg_def_grad_global.txt: Time + Volume + 9 F-components. + # Written by mechanics but optional for calibration; we keep + # it around for post-processing plots. + "avg_def_grad": TextTableSpec( + columns=[ + "Time", "Volume", + "F11", "F12", "F13", + "F21", "F22", "F23", + "F31", "F32", "F33", + ], + required=False, + ), + }) + + # ============================================================ + # ARCHIVE + CLEANUP (opt-in, new feature) + # ============================================================ + + archive = None + archive_run_id = None + if not args.no_archive: + archive = ArchiveDB(WORKSPACE / "calibration.db") + archive.open() + if resume_pickle_path is None: + # Fresh run — create a new archive row and get back its + # UUID. That UUID is threaded into the Problem below so + # gene records write to the right row. + archive_run_id = archive.start_run( + seed=args.seed, + param_names=param_names, + objective_labels=[spec.label for spec in objective_specs], + config={ + "mechanics_binary": str(MECHANICS_BINARY), + "master_toml": str(MASTER_TOML), + "experiment_files": [str(p) for p in EXPERIMENT_FILES], + }, + ) + else: + # Resume — do NOT call start_run(). A fresh UUID here + # would conflict with the one baked into the pickle, + # and the library would refuse the mismatch with a + # ValueError. Leave archive_run_id = None; the library + # reads the pickled UUID out of the checkpoint and + # assigns it to Problem.archive_run_id on our behalf. + # The existing archive row keeps receiving gene records + # under its original UUID, uninterrupted. + pass + + # ============================================================ + # BACKEND + # ============================================================ + # + # OLD: normal_map.map_custom ran cases sequentially via subprocess. + # + # NEW: pluggable backend. LocalBackend for workstation use; + # FluxBackend for HPC. The per-SimCase resource fields above + # (num_nodes, num_tasks, cores_per_task, gpus_per_task, duration_s) + # flow through to whichever backend you pick. + # + # FluxBackend: + # * turns each SimCase into a Flux jobspec + # * requests the CPU/GPU shape encoded on that SimCase + # * handles MPI launch internally via Flux's job shell + # + # LocalBackend: + # * starts local subprocesses on the current host + # * is useful for serial debugging or small workstation runs + # * does NOT magically create MPI ranks by itself; multi-rank + # cases need an explicit MPI launcher in the backend config + # + # NOTE on mpi_launcher: ExaConstit's ``mechanics`` binary uses + # MPI internally, so any SimCase with ``num_tasks > 1`` must run + # under an MPI launcher. LocalBackend will refuse at submit time + # if ``num_tasks > 1`` and ``mpi_launcher`` is not set — prevents + # the failure mode where a four-rank request silently ran + # single-rank. Set this to "mpirun", "srun", "jsrun", or + # whichever launcher is on your PATH. If your launcher uses a + # non-standard ntasks flag (jsrun wants ``--nrs``, lrun wants + # ``-T``) pass ``mpi_launcher_ntasks_flag`` too. + + if args.backend == "flux": + if not HAS_FLUX: + parser.error( + "--backend=flux requested, but this Python interpreter " + "cannot import Flux's bindings. Use the same interpreter " + "for both install and run, and ensure the Flux module is " + "visible (for example via " + "PYTHONPATH=/usr/lib64/flux/python3.12 on this system)." + ) + from workflow_common.backends.flux_backend import FluxBackend + + backend = FluxBackend() + else: + # Debug-oriented default. The SimCases in this example request + # 4 MPI ranks, so local mode assumes ``mpirun`` is available + # on PATH and uses it to launch those ranks. For a simpler + # serial debug run, change each SimCase to ``num_tasks=1`` and + # set mpi_launcher=None here. If your system uses ``srun`` or + # another launcher instead of ``mpirun``, replace the string + # below with that command. + backend = LocalBackend(max_workers=4, mpi_launcher="mpirun") + + # On a Flux-managed HPC allocation: + # + # from workflow_common import FluxBackend + # backend = FluxBackend() + # + # (The per-case resource fields on each SimCase drive Flux's + # job submission; no global resource config is needed. For + # example, a SimCase with ``num_tasks=4, cores_per_task=7, + # gpus_per_task=1`` requests 4 MPI ranks, 28 CPU cores total, + # and 4 GPUs for that one simulation. Flux handles MPI rank + # launching internally; no mpi_launcher argument is involved + # for FluxBackend.) + + # The driver automatically attaches a ProgressReporter per + # generation, rendering a line like: + # + # gen 3/100 | 47/288 (16.3%) | running 4/4 | cores 16/16 | elapsed 2m14s | ETA 11m42s + # + # Interactive runs get in-place updates; captured-stdout runs + # (HPC job logs) get one line per update. Turn off with + # ``RunConfig(show_progress=False)`` if you want silent mode. + + # ============================================================ + # ASSEMBLE THE PROBLEM + # ============================================================ + # + # ProblemConfig now holds only the FALLBACK resource settings. + # If any SimCase leaves a resource field as None, the value + # here is used. In this example all fields are per-case, so + # ProblemConfig's resource numbers don't actually govern + # anything — they're shown for completeness. + + problem = Problem( + config=ProblemConfig( + binary=MECHANICS_BINARY, + binary_args=("-opt", "options.toml"), + num_nodes=1, num_tasks=4, + cores_per_task=1, gpus_per_task=0, + duration_s=3600, + stdout="stdout.log", stderr="stderr.log", + required_outputs=("results/options/avg_stress_global.txt",), + ), + param_names=param_names, + sim_cases=sim_cases, + objective_specs=objective_specs, + templater=templater, + property_writer=property_writer, + resolver=resolver, + backend=backend, + reader=reader, + failure_handler=InfinityFailureHandler(), + manifest=Manifest(WORKSPACE / "manifest.jsonl"), + archive=archive, + archive_run_id=archive_run_id, + ) + + # ============================================================ + # RUN CONFIG — every knob is 1:1 with the old globals + # ============================================================ + # + # OLD -> NEW mapping: + # + # NGEN = 100 -> n_generations=100 + # UNSGA3 = True -> unsga3=True + # p = [10, 0] -> ref_dirs_partitions=(10, 0) + # scaling = [1, 0] -> ref_dirs_scaling=(1.0, 0.0) + # seed = -> seed=args.seed + # mat_eta = 30.0 -> mate_eta=30.0 + # mut_eta = 20.0 -> mut_eta=20.0 + # indpb = 1.0 / NDIM -> mut_indpb=None (default = 1/n_params) + # fail_limit = 10 -> fail_limit=10 + # Imin = round(NGEN / 2) -> imin_fraction=0.5 + # stop_limit = 5 -> stop_limit=5 + # checkpoint_freq = 1 -> checkpoint_freq=1 + # + # cleanup_keep_generations=2 is NEW and requires the archive. + + ref_dirs_partitions = (3, 0) + ref_dirs_scaling = (1.0, 0.0) + + # Compute NPOP the same way the driver will, using the framework's + # public helpers. This replaces a hand-rolled P/H/NPOP calculation + # that's easy to get wrong for the two-hyperplane case (p_inner > 0). + _ref_points, h_count = build_reference_points( + n_obj, ref_dirs_partitions, ref_dirs_scaling, + ) + npop = derive_population_size(n_obj, h_count) + + # Pull n_generations out up front so the summary print and the + # RunConfig below share one source of truth. Same story for any + # other knob you want to surface at the top of the log. + n_generations = 30 + + print(f"\nNumber of experiments = {len(sim_cases)}") + print(f"Number of objectives = {n_obj}") + print(f"Number of parameters = {bounds.n_params}") + print(f"Number of generations = {n_generations}") + print(f"Number of reference points H = {h_count}") + print(f"Population size NPOP = {npop}") + print(f"Total simulation runs = " + f"{npop * len(sim_cases) * n_generations} " + f"(pop x n_sim_cases x gens)\n") + + config = RunConfig( + n_generations=n_generations, + unsga3=True, + ref_dirs_partitions=ref_dirs_partitions, + ref_dirs_scaling=ref_dirs_scaling, + seed=args.seed, + mate_eta=30.0, + mut_eta=20.0, + mut_indpb=None, # = 1/n_params, old default + fail_limit=10, + imin_fraction=0.5, # Imin = NGEN / 2 + stop_limit=5, + checkpoint_dir=WORKSPACE / "checkpoint_files", + checkpoint_freq=1, + resume_from=resume_pickle_path, + track_hypervolume=True, + cleanup_keep_generations=2 if not args.no_archive else None, + # log_dir defaults to checkpoint_dir if set (here, the + # checkpoint_files dir above); override if you want logs + # somewhere else. Two files are written per generation: + # * logbook1_stats.log — aggregate fitness stats (avg / + # std / min / max) plus ND / GD / HV for multi-objective + # * logbook2_solutions.log — every individual's gene + # vector + fitness + # Both are DEAP-style tab-delimited text — greppable, + # less-able, and parseable by the pre-refactor driver's + # downstream tooling. Set write_logbook_files=False to + # skip them. + ) + + # ============================================================ + # GO + # ============================================================ + # + # TIP: for a CLI peek at the SQLite archive during or after + # the run (if --archive is on), try: + # + # python -m workflows.optimization.inspect_archive \ + # ./calibration_run --gens + # + # Add --genes --gen N --pareto-only to see just the Pareto + # front at a specific generation. --format csv pipes into + # Excel / LibreOffice Calc. --format json is friendly to + # notebook post-processing. + + try: + result = run_nsga3(problem, bounds, config) + finally: + if archive is not None: + archive.close() + + # ============================================================ + # QUICK POST-RUN SUMMARY + # ============================================================ + + print(f"\nRun complete. Generations run: {result.generations_run}") + print(f"Final pop size: {len(result.final_pop)}") + print(f"Pareto front size: {len(result.pareto_front)}") + if result.stopped_early: + print("Stopped early via ND == NPOP criterion.") + + from workflow_common.postprocess import best_solution_eudist + fits = np.array([ind.fitness.values for ind in result.final_pop]) + best_idx = best_solution_eudist(fits, nsmallest=1)[0] + best_gene = result.final_pop[best_idx] + print(f"\nBest gene fitness: {best_gene.fitness.values}") + print("Best gene parameters:") + for name, val in zip(param_names, list(best_gene)): + print(f" {name:10s} = {val:.6g}") + + +# ============================================================================ +# Custom evaluators that reproduce the OLD std-normalized RMSE +# ============================================================================ +# +# The pre-refactor ExaProb computed: +# +# f[iobj * 2] = RMSE(sim_stress, exp_stress) / np.std(exp_stress) +# f[iobj * 2 + 1] = RMSE(sim_slope, exp_slope) / np.std(exp_slope) +# +# where slope = np.diff(stress) / np.diff(strain). The framework's +# built-in StressStrainObjective does plain RMSE without normalization; +# these two classes reproduce the old normalization exactly. If you +# prefer a different normalization (the old ExaConstit_Problems.py has +# commented alternatives with std / IQR / min-max / mean denominators), +# change the `denom = ...` line below and leave everything else alone. +# +# An evaluator is any object with an ``evaluate(results, ctx) -> float`` +# method. No base class to inherit from; duck typing suffices. + + +class _StdNormalizedStressEvaluator: + """RMSE(sim_stress, exp_stress) / std(exp_stress). + + Brings the experimental curve onto the simulation's strain + grid via the framework's PCHIP smoother (the right tool: it + can't overshoot, so a smoothed exp curve will not introduce + stress values that never appeared in the source data). This + is the direction the original ExaConstit calibration code + used and it's the one that matters for scoring: the + optimizer is computing sim error at sim's own sample points, + not at noisy raw exp points. + + The extractor is responsible for cropping the simulated + curve to the user's optimization window (via its ``window`` + field). This evaluator just compares whatever the extractor + returns against the experimental reference, restricted to + the common strain range. + """ + + def __init__( + self, + experimental: pd.DataFrame, + extractor: StressStrainExtractor, + ): + self.experimental = experimental + self.extractor = extractor + + def evaluate(self, results: Any, ctx: Any) -> float: + sim_strain, sim_stress = self.extractor.extract(results) + sim_strain = np.abs(sim_strain) + sim_stress = np.abs(sim_stress) + exp_strain = np.abs(self.experimental.iloc[:, 0].to_numpy()) + exp_stress = np.abs(self.experimental.iloc[:, 1].to_numpy()) + + # Mask SIM (not exp) to the common strain range — we want + # sim's sample points as the comparison grid since the + # optimizer is implicitly fitting at sim's resolution. + lo = max(float(sim_strain.min()), float(exp_strain.min())) + hi = min(float(sim_strain.max()), float(exp_strain.max())) + sim_mask = (sim_strain >= lo) & (sim_strain <= hi) + if sim_mask.sum() < 2: + return float("inf") + sim_strain_in = sim_strain[sim_mask] + sim_stress_in = sim_stress[sim_mask] + + # Bring exp onto sim's grid using PCHIP. ``strict_monotonic + # =False`` accepts mild noise in exp_strain (a tiny backward + # step from sampling jitter) by sorting; it does NOT handle + # genuinely non-monotonic data. Mechanical-test exp data is + # cleaned and clipped before reaching this code, per the + # standard calibration workflow. + smoother = PchipSmoother(strict_monotonic=False) + exp_at_sim = smoother.sample_at( + exp_strain, exp_stress, sim_strain_in, + ).y + + denom = float(np.std(exp_at_sim)) + if denom <= 0: + return float("inf") + residual = sim_stress_in - exp_at_sim + return float(np.sqrt(np.mean(residual ** 2)) / denom) + + +class _StdNormalizedSlopeEvaluator: + """RMSE(sim_slope, exp_slope) / std(exp_slope). + + Same architecture as the stress evaluator: brings exp onto + sim's strain grid via PCHIP smoothing, then compares + finite-difference slopes on that common grid. + """ + + def __init__( + self, + experimental: pd.DataFrame, + extractor: StressStrainExtractor, + ): + self.experimental = experimental + self.extractor = extractor + + def evaluate(self, results: Any, ctx: Any) -> float: + sim_strain, sim_stress = self.extractor.extract(results) + sim_strain = np.abs(sim_strain) + sim_stress = np.abs(sim_stress) + exp_strain = np.abs(self.experimental.iloc[:, 0].to_numpy()) + exp_stress = np.abs(self.experimental.iloc[:, 1].to_numpy()) + + lo = max(float(sim_strain.min()), float(exp_strain.min())) + hi = min(float(sim_strain.max()), float(exp_strain.max())) + sim_mask = (sim_strain >= lo) & (sim_strain <= hi) + if sim_mask.sum() < 3: + return float("inf") + sim_strain_in = sim_strain[sim_mask] + sim_stress_in = sim_stress[sim_mask] + + smoother = PchipSmoother(strict_monotonic=False) + exp_at_sim = smoother.sample_at( + exp_strain, exp_stress, sim_strain_in, + ).y + + # Slopes via finite difference on the common grid. + # ``np.diff`` shrinks length by one; using sim's strain + # spacing is correct because exp has now been sampled at + # exactly those points. + diff_strain = np.diff(sim_strain_in) + sim_slope = np.diff(sim_stress_in) / diff_strain + exp_slope = np.diff(exp_at_sim) / diff_strain + + denom = float(np.std(exp_slope)) + if denom <= 0: + return float("inf") + residual = sim_slope - exp_slope + return float(np.sqrt(np.mean(residual ** 2)) / denom) + +def _load_experimental_csv(path: Path) -> pd.DataFrame: + """Load a two-column (strain, stress) file. Any whitespace separator.""" + arr = np.loadtxt(path, dtype=float, ndmin=2) + return pd.DataFrame({"strain": arr[:, 0], "stress": arr[:, 1]}) + + +if __name__ == "__main__": + main() diff --git a/workflows/exaconstit-calibrate/examples/nsga3_slurm_helpers.sh b/workflows/exaconstit-calibrate/examples/nsga3_slurm_helpers.sh new file mode 100755 index 0000000..2d4dff9 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/nsga3_slurm_helpers.sh @@ -0,0 +1,260 @@ +#!/bin/sh +# +# Helper functions for run_nsga3_slurm.sh. +# +# Most users should not edit this file. It exists so the Slurm job +# script can stay short and easy to edit. +# +# Required location: +# Keep this file in the same directory as run_nsga3_slurm.sh. +# +# Alternative location: +# If you store this helper somewhere else, set HELPER_SCRIPT in +# run_nsga3_slurm.sh to this file's full path. + +# exa_nsga3_slurm_tasks_text +# -------------------------- +# Returns a human-readable Slurm task count. Some interactive/exclusive +# allocations are created by node count and do not export SLURM_NTASKS; +# that is not an error for this workflow because the launch command +# below explicitly starts Flux with ``srun -n 1``. +exa_nsga3_slurm_tasks_text() +{ + if [ -n "${SLURM_NTASKS:-}" ]; then + printf "%s" "${SLURM_NTASKS}" + else + printf "not set (common in interactive/exclusive allocations)" + fi +} + +# exa_nsga3_slurm_cpu_text +# ------------------------ +# Reports whichever Slurm CPU-count variable is available. These are +# useful sanity checks in interactive jobs where SLURM_NTASKS is unset. +exa_nsga3_slurm_cpu_text() +{ + if [ -n "${SLURM_CPUS_ON_NODE:-}" ]; then + printf "%s (SLURM_CPUS_ON_NODE)" "${SLURM_CPUS_ON_NODE}" + elif [ -n "${SLURM_JOB_CPUS_PER_NODE:-}" ]; then + printf "%s (SLURM_JOB_CPUS_PER_NODE)" "${SLURM_JOB_CPUS_PER_NODE}" + else + printf "unknown" + fi +} + +# exa_nsga3_setup +# ---------------- +# Sets paths, exports PYTHONPATH so Python can import this package and +# Flux, moves into the package directory, and optionally runs pip install. +exa_nsga3_setup() +{ + PACKAGE_ROOT=$(CDPATH= cd "${SCRIPT_DIR}/.." && pwd) + + export PYTHONPATH="${FLUX_PYTHONPATH}:${PACKAGE_ROOT}${PYTHONPATH:+:${PYTHONPATH}}" + export MPLBACKEND=Agg + + if [ -n "${EXACONSTIT_ROOT}" ]; then + export EXACONSTIT_ROOT + fi + + if [ -n "${EXACONSTIT_MECHANICS}" ]; then + export EXACONSTIT_MECHANICS + fi + + echo "=== ExaConstit NSGA-III Slurm job ===" + echo "date: $(date)" + echo "host: $(hostname)" + echo "job id: ${SLURM_JOB_ID:-not-running-under-slurm}" + echo "nodes: ${SLURM_JOB_NUM_NODES:-unknown}" + echo "slurm tasks: $(exa_nsga3_slurm_tasks_text)" + echo "slurm cpus/node: $(exa_nsga3_slurm_cpu_text)" + echo "flux launcher: srun -n 1 starts one Flux broker inside this allocation" + echo "python: ${PYTHON}" + echo "package root: ${PACKAGE_ROOT}" + echo "examples dir: ${SCRIPT_DIR}" + echo "workspace: ${SCRIPT_DIR}/${WORKSPACE}" + echo "postprocess dir: ${SCRIPT_DIR}/${POST_DIR}" + echo "action: ${ACTION}" + echo + + cd "${PACKAGE_ROOT}" + + if [ "${INSTALL_PACKAGE}" = "1" ]; then + echo "=== Installing editable package ===" + "${PYTHON}" -m pip install -e ".[test,nsga3,plot]" + echo + fi + + cd "${SCRIPT_DIR}" +} + +# exa_nsga3_calibration_args +# -------------------------- +# Converts ACTION and checkpoint settings into command-line arguments +# understood by nsga3_calibration.py. +exa_nsga3_calibration_args() +{ + CALIBRATION_ARGS="--backend flux" + + case "${ACTION}" in + run|all) + ;; + resume-latest) + CALIBRATION_ARGS="${CALIBRATION_ARGS} --resume-latest" + ;; + resume-from) + if [ -n "${CHECKPOINT_PATH}" ]; then + CALIBRATION_ARGS="${CALIBRATION_ARGS} --resume-from ${CHECKPOINT_PATH}" + elif [ -n "${CHECKPOINT_GEN}" ]; then + CALIBRATION_ARGS="${CALIBRATION_ARGS} --resume-from ${CHECKPOINT_GEN}" + else + echo "ERROR: ACTION=\"resume-from\" requires CHECKPOINT_GEN or CHECKPOINT_PATH." + exit 2 + fi + ;; + inspect|plots|postprocess) + ;; + *) + echo "ERROR: unknown ACTION=\"${ACTION}\"" + echo "Allowed actions:" + echo " run" + echo " resume-latest" + echo " resume-from" + echo " inspect" + echo " plots" + echo " postprocess" + echo " all" + exit 2 + ;; + esac +} + +# exa_nsga3_run_calibration +# ------------------------- +# Starts one Flux instance inside the Slurm job and runs the Python +# NSGA-III calibration driver inside that Flux instance. +exa_nsga3_run_calibration() +{ + echo "=== Starting calibration ===" + echo "command:" + echo " srun -n 1 --mpi=none --mpibind=off flux start ${PYTHON} nsga3_calibration.py ${CALIBRATION_ARGS}" + echo + + srun -n 1 --mpi=none --mpibind=off \ + flux start "${PYTHON}" nsga3_calibration.py ${CALIBRATION_ARGS} + + echo +} + +# exa_nsga3_inspect_results +# ------------------------- +# Prints easy-to-read archive summaries and writes a CSV table of the +# best solutions across the whole run. +exa_nsga3_inspect_results() +{ + echo "=== Inspecting archive tables ===" + mkdir -p "${POST_DIR}" + + echo + echo "--- Runs in the archive ---" + "${PYTHON}" -m workflows.optimization.inspect_archive "${WORKSPACE}" --runs + + echo + echo "--- Per-generation convergence summary ---" + "${PYTHON}" -m workflows.optimization.inspect_archive "${WORKSPACE}" --gens-best + + echo + echo "--- Best solutions across the whole run ---" + "${PYTHON}" -m workflows.optimization.inspect_archive \ + "${WORKSPACE}" --genes --pareto-only --top "${TOP_N}" + + echo + echo "--- Writing CSV table of best solutions ---" + "${PYTHON}" -m workflows.optimization.inspect_archive \ + "${WORKSPACE}" --genes --pareto-only --top "${TOP_N}" --format csv \ + > "${POST_DIR}/top_solutions.csv" + echo "wrote: ${SCRIPT_DIR}/${POST_DIR}/top_solutions.csv" + echo +} + +# exa_nsga3_make_plots +# -------------------- +# Writes PNG plots for balanced best solutions, per-objective best +# solutions, and a few common Pareto tradeoff views. +exa_nsga3_make_plots() +{ + echo "=== Making solution plots ===" + mkdir -p "${POST_DIR}" + + "${PYTHON}" plot_solutions.py "${WORKSPACE}" \ + --top "${TOP_N}" \ + --save "${POST_DIR}/top_${TOP_N}_balanced_l2_overlay.png" \ + --no-show + + "${PYTHON}" plot_solutions.py "${WORKSPACE}" \ + --top "${TOP_N}" --objective stress_1 \ + --save "${POST_DIR}/top_${TOP_N}_stress_1_overlay.png" \ + --no-show + + "${PYTHON}" plot_solutions.py "${WORKSPACE}" \ + --top "${TOP_N}" --objective slope_1 \ + --save "${POST_DIR}/top_${TOP_N}_slope_1_overlay.png" \ + --no-show + + "${PYTHON}" plot_solutions.py "${WORKSPACE}" \ + --top "${TOP_N}" --objective stress_2 \ + --save "${POST_DIR}/top_${TOP_N}_stress_2_overlay.png" \ + --no-show + + "${PYTHON}" plot_solutions.py "${WORKSPACE}" \ + --top "${TOP_N}" --objective slope_2 \ + --save "${POST_DIR}/top_${TOP_N}_slope_2_overlay.png" \ + --no-show + + "${PYTHON}" plot_solutions.py "${WORKSPACE}" \ + --top "${TOP_N}" --pareto 0,1 \ + --save-pareto "${POST_DIR}/pareto_stress_1_vs_slope_1.png" \ + --no-show + + "${PYTHON}" plot_solutions.py "${WORKSPACE}" \ + --top "${TOP_N}" --pareto 0,2 \ + --save-pareto "${POST_DIR}/pareto_stress_1_vs_stress_2.png" \ + --no-show + + echo "wrote plots under: ${SCRIPT_DIR}/${POST_DIR}" + echo +} + +# exa_nsga3_main +# -------------- +# Main dispatcher called by run_nsga3_slurm.sh after it loads this +# helper. It runs the action selected in the user-edited Slurm script. +exa_nsga3_main() +{ + exa_nsga3_setup + exa_nsga3_calibration_args + + case "${ACTION}" in + run|resume-latest|resume-from) + exa_nsga3_run_calibration + ;; + all) + exa_nsga3_run_calibration + exa_nsga3_inspect_results + exa_nsga3_make_plots + ;; + inspect) + exa_nsga3_inspect_results + ;; + plots) + exa_nsga3_make_plots + ;; + postprocess) + exa_nsga3_inspect_results + exa_nsga3_make_plots + ;; + esac + + echo "=== Done ===" + echo "date: $(date)" +} diff --git a/workflows/exaconstit-calibrate/examples/options.toml b/workflows/exaconstit-calibrate/examples/options.toml new file mode 100644 index 0000000..462dcd3 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/options.toml @@ -0,0 +1,815 @@ +# ExaConstit Configuration File +# This file controls all aspects of your solid mechanics simulation. +# Lines starting with '#' are comments and will be ignored by the program. +# The file uses TOML format where options are organized in sections [Section] and subsections [[Section.Subsection]] + +# Version of ExaConstit this configuration is designed for +# This helps ensure compatibility - use the version shown when you run 'exaconstit --version' +Version = "0.9.0" + +# Base name for the simulation output directory +# This creates a subdirectory to organize all outputs from this simulation +# For example, if basename = "tensile_test", all outputs go in a "tensile_test" folder +# If not specified, defaults to the name of this configuration file (without .toml extension) +basename = "multi_material_test" + +# ===================================== +# MULTI-MATERIAL SETUP (Optional) +# ===================================== +# For simulations with multiple materials, you need to tell the code how to assign +# materials to different parts of your mesh + +# Maps grain IDs / existing mesh element attributes to material region numbers +# This file should have two columns: element attributes / Grain IDs -> material_region_number +# Example content: +# 1 1 +# 2 1 +# 3 2 +# This would assign element attributes 1-2 to material 1, element 3 to material 2 +region_mapping_file = "region_mapping.txt" + +# For automatically generated meshes with multiple materials +# This file assigns each element to a grain (crystal orientation) +# Format: one integer per line, each representing a grain ID +# Line 1 = grain ID for element 1, Line 2 = grain ID for element 2, etc. +grain_file = "grains.txt" + +# ===================================== +# MATERIAL DEFINITIONS +# ===================================== +# Define properties for each material in your simulation +# Use [[Materials]] to define multiple materials (note the double brackets) +# Each material must have a unique region_id starting from 0 + +[[Materials]] + # User-friendly name for this material (for output files and identification) + material_name = "aluminum_alloy" + + # Which region of the mesh uses this material (starts at 1, must be sequential: 1, 2, 3...) + # This corresponds to the material numbers in your region_mapping_file + region_id = 1 + + # Initial temperature in Kelvin (affects material properties) + # Room temperature ≈ 298K (25°C or 77°F) + temperature = 298.0 + + # Material model type - tells the code which physics engine to use: + # - "umat" = User Material subroutine (custom material behavior) + # - "exacmech" = ExaCMech crystal plasticity library (for metals) + # - "" = Empty string if defined in Model section below + mech_type = "exacmech" + + # ===== Material Properties ===== + [Materials.Properties] + # Option 1: Read properties from a file + floc = "props.txt" # Path to properties file + num_props = 20 # Number of property values to read + + # Option 2: Define properties directly here (choose either floc OR values) + # values = [210000.0, 0.3, 450.0, ...] # [Young's modulus, Poisson's ratio, yield stress, ...] + + # ===== Internal State Variables ===== + # These track the material's history (plastic strain, damage, etc.) + [Materials.State_Vars] + # Option 1: Read initial values from file + floc = "state.txt" # Path to state variables file + num_vars = 30 # Number of state variables + + # Option 2: Define initial values directly (usually zeros for virgin material) + # values = [0.0, 0.0, 0.0, ...] # Initial values (often all zeros) + + # ===== Crystal/Grain Information (for crystal plasticity only) ===== + [Materials.Grain] + # Where orientations are stored in the state variable array + # Use -1 to append at the end (recommended for UMATs) + # Positive number = specific location in state variable array + # Value ignored for any ExaCMech model + ori_state_var_loc = -1 + + # If using custom orientation types we need to know the stride length values + # Value ignored for any ExaCMech model as always quaternions + ori_stride = 4 + + # How crystal orientations are represented: + # - "quat" or "quaternion" = 4 values per orientation (recommended for accuracy) + # - "euler" = 3 Euler angles in degrees (Bunge convention: Z-X-Z) + # - "custom" = User-defined format + # Value ignored for any ExaCMech model as always quaternions + ori_type = "quat" + + # Total number of unique crystal orientations in your simulation + # For single crystal: num_grains = 1 + # For polycrystal: num_grains = number of crystals + num_grains = 100 + + # File containing crystal orientations + # Format depends on ori_type: + # - Quaternions: 4 values per line (q0 q1 q2 q3) + # - Euler angles: 3 values per line in degrees (phi1 Phi phi2) + orientation_file = "orientations.txt" + + # Maps elements to grains (which orientation each element uses) + # Only needed for auto-generated meshes + # For mesh files, this info should be in element attributes + grain_file = "grain_map.txt" + + # ===== Material Model Configuration ===== + [Materials.Model] + # Must match the mech_type above (or define it here if not set above) + mech_type = "exacmech" + + # Is this a crystal plasticity model? + # - true = considers crystal orientation effects (required for ExaCMech) + # - false = isotropic material (same properties in all directions) + crystal_plasticity = true + + # ===== ExaCMech-Specific Settings ===== + [Materials.Model.ExaCMech] + # Model name from ExaCMech library + # This combines crystal structure + hardening law + kinetics + # Common options: + # FCC metals (aluminum, copper, nickel): + # - "evptn_FCC_A" = Linear Voce hardening + power law slip kinetics + # - "evptn_FCC_AH" = Nonlinear Voce hardening + power law slip kinetics + # - "evptn_FCC_B" = Kocks-Meckings single source DD model + MTS-like and phonon drag slip kinetics (temperature dependent) + # BCC metals (iron, tungsten): + # - "evptn_BCC_A" = Linear Voce hardening + power law slip kinetics + # - "evptn_BCC_B" = Kocks-Meckings single source DD model + MTS-like and phonon drag slip kinetics (temperature dependent) + # HCP metals (titanium, magnesium): + # - "evptn_HCP_A" = Kocks-Meckings single source DD model + MTS-like and phonon drag slip kinetics (temperature dependent) + shortcut = "evptn_FCC_A" + +# ===== Second Material Example (UMAT) ===== +[[Materials]] + material_name = "custom_polymer" + region_id = 2 + temperature = 298.0 + mech_type = "umat" + + [Materials.Properties] + num_props = 2 + # Direct property definition - useful for simple materials + # For polymer: [Young's modulus, Poisson's ratio] + values = [3000.0, 0.35] + + [Materials.State_Vars] + num_vars = 5 + # Initial state (zeros for undeformed material) + values = [0.0, 0.0, 0.0, 0.0, 0.0] + + [Materials.Model] + mech_type = "umat" + # UMATs are typically isotropic + crystal_plasticity = false + + [Materials.Model.UMAT] + # Path to compiled UMAT library (.so or .dll file) + # Can be absolute path or relative to working directory + library_path = "./my_umat.so" + + # Name of the UMAT function in the library + # Must match the function name in your Fortran/C code + function_name = "my_polymer_model" + + # When to load the library: + # - "persistent" = load once at start, keep in memory (fastest) + # - "lazy_load" = load when first needed (saves memory) + # - "load_on_setup" = reload for each setup (for debugging) + load_strategy = "persistent" + + # Allow loading external libraries? + # Set false for security if using pre-installed UMATs only + enable_dynamic_loading = true + + # Directories to search for the UMAT library + # Searched in order if library_path is just a filename + search_paths = ["./", "./umats/", "/shared/umat_libs/"] + +# ===================================== +# BOUNDARY CONDITIONS +# ===================================== +# Define how your model is loaded and constrained +# BCs can change over time using the time_info section + +[BCs] + # ===== Time-Dependent BC Control ===== + [BCs.time_info] + # How to specify when BCs change: + # Option 1: Based on cycle number (recommended for multi-step loading) + cycle_dependent = true + # Change BCs at cycle 100 and 200 + # A value of 1 is always required + cycles = [1, 100, 200] + + # Option 2: Based on simulation time (alternative method) + # Currently ignored but will be coming in a future release + # time_dependent = true + # times = [0.1, 0.2] # Change BCs at time 0.1 and 0.2 + + # Note: For all of the below BCs if we have changing BCs over several cycles then + # we need to have multiple [[BCs.velocity_bcs]] and [[BCs.velocity_gradient_bcs]] + + # ===== Velocity Boundary Conditions ===== + # Apply specific velocities to nodes/surfaces + # ( think constant engineering strain rate BCs ) + [[BCs.velocity_bcs]] + # Which boundary markers/node sets to apply this BC to + # These numbers come from your mesh file's boundary markers + essential_ids = [1, 2, 3] + + # Which velocity components to constrain for each boundary marker + # This uses a binary encoding: + # 0 = no constraints (free) + # 1 = constrain X velocity only + # 2 = constrain Y velocity only + # 3 = constrain Z velocity only + # 4 = constrain X and Y velocities + # 5 = constrain Y and Z velocities + # 6 = constrain X and Z velocities + # 7 = constrain all velocities (X, Y, and Z) + # Example: [3, 1, 2] means: + # - Boundary 1: fix Z velocity only + # - Boundary 2: fix X velocity only + # - Boundary 3: fix Y velocity only + essential_comps = [3, 1, 2] + + # Velocity values for each constrained component + # Format: [vx, vy, vz] for each boundary marker + # Order matches essential_ids, components match essential_comps + # Example below: all velocities are zero (fixed boundaries) + essential_vals = [0.0, 0.0, 0.0, # Boundary 1 velocities + 0.0, 0.0, 0.0, # Boundary 2 velocities + 0.0, 0.0, 0.0] # Boundary 3 velocities + + # ===== Velocity Gradient Boundary Conditions ===== + # Apply a velocity gradient to a boundary + # ( think constant true strain rate BCs ) + [[BCs.velocity_gradient_bcs]] + # Boundary markers for velocity gradient BC + essential_ids = [4] + + # Which components to constrain (same encoding as above) + # Typically use 7 (all components) for velocity gradient + essential_comps = [7] + + # Velocity gradient tensor (3x3 matrix) + # This defines the velocity: v = L·(x - origin) + # Each row is [L11, L12, L13; L21, L22, L23; L31, L32, L33] + # Example: uniaxial tension in Z at strain rate 0.001/s + velocity_gradient = [[0.0, 0.0, 0.0], # dVx/dx, dVx/dy, dVx/dz + [0.0, 0.0, 0.0], # dVy/dx, dVy/dy, dVy/dz + [0.0, 0.0, 0.001]] # dVz/dx, dVz/dy, dVz/dz + + # Reference point for velocity gradient (default: origin) + # Velocities are: v = L·(x - origin) + # Currently this is assummed constant over all time steps + # but in future this could change over time + origin = [0.0, 0.0, 0.0] + + # ================================================================= + # EXPERIMENTAL: Monotonic Z-Direction Loading Boundary Condition + # ================================================================= + # Enables a specialized boundary condition for uniaxial monotonic loading + # in the z-direction, commonly used for material characterization and + # constitutive model validation. + # + # BEHAVIOR: + # - Applies pure tension or compression in the z-direction + # - Automatically constrains lateral surfaces to prevent shear deformation + # - Maintains kinematic compatibility to avoid artificial stress concentrations + # - Produces clean uniaxial stress states ideal for comparing with + # experimental stress-strain curves + # + # TYPICAL USE CASES: + # - Single crystal tensile/compression testing + # - Polycrystal aggregate response validation + # - Material parameter calibration + # - Crystal plasticity model verification + # + # REQUIREMENTS & LIMITATIONS: + # - Best performance with auto_time_stepping = true + # - May cause convergence issues or crashes with fixed time stepping + # - Recommended for simple geometries (rectangular domains) + # - Not suitable for complex loading paths or multiaxial conditions + # + # WARNING: This is an experimental feature under active development. + # Verify results against known analytical solutions before + # using for production simulations. + # + expt_mono_def_flag = false + +# ===================================== +# TIME STEPPING CONTROL +# ===================================== +# How the simulation advances through time +# Choose ONE of: Custom, Auto, or Fixed + +[Time] + # Which time stepping method to use: + # - "custom" = user-defined time steps from file (most control) + # - "auto" = adaptive time stepping (recommended for efficiency) + # - "fixed" = constant time step (simplest) + # Time stepping priority (highest to lowest): + # 1. Custom (if [Time.Custom] exists) + # 2. Auto (if [Time.Auto] exists) + # 3. Fixed (if [Time.Fixed] exists or as default) + time_type = "custom" + + # ===== Restart Options (Optional) ===== + # Continue from a previous simulation + # Currently ignored but will be coming in a near-future release + # restart = true + # restart_time = 0.5 # Time to restart from + # restart_cycle = 100 # Cycle number to restart from + + # ===== Automatic Time Stepping ===== + # Adjusts time step based on convergence behavior + [Time.Auto] + # Starting time step size + # Smaller = more accurate but slower + # Typical range: 0.001 to 1.0 + dt_start = 0.1 + + # Minimum allowed time step + # Simulation fails if it needs smaller than this + # Too small = may never finish, too large = may not converge + dt_min = 0.001 + + # Maximum allowed time step + # Prevents steps from getting too large + # 1e9 = effectively no limit + dt_max = 1.0 + + # Reduction factor when convergence is difficult + # If solver struggles, multiply dt by this factor + # Must be between 0 and 1 (0.25 = reduce to 25%) + dt_scale = 0.25 + + # Target end time for simulation + t_final = 1.0 + + # ===== Fixed Time Stepping ===== + # Same time step throughout simulation + [Time.Fixed] + # Time step size (constant throughout) + dt = 0.01 + + # Simulation end time + t_final = 1.0 + + # ===== Custom Time Stepping ===== + # Full control over every time step + [Time.Custom] + # Total number of time steps to take + nsteps = 1000 + + # File containing time step sizes + # Format: one number per line (dt for each step) + # Line 1 = dt for step 1, Line 2 = dt for step 2, etc. + floc = "custom_dt.txt" + +# ===================================== +# SOLVER SETTINGS +# ===================================== +# Controls how the equations are solved numerically + +[Solvers] + # ===== Matrix Assembly Strategy ===== + # How to build and store the stiffness matrix: + # - "FULL" = build complete matrix (most memory, best for CPU-only problems) + # - "PA" = partial assembly (least memory, best for higher-order p-refinement elements) + # - "EA" = element assembly (less memory, best for large problems run on GPUs) + # GPU requires PA or EA, CPU works with all options + assembly = "FULL" + + # ===== Execution Model ===== + # Where to run computations: + # - "CPU" = single processor core (simple, good for debugging or CPU only problems) + # These later options require RAJA/MFEM/ExaConstit to have been compiled with support for these + # - "OPENMP" = multiple CPU cores (faster for medium problems) + # - "GPU" = graphics card (fastest for large problems) + rtmodel = "CPU" + + # ===== Integration Scheme ===== + # How to handle material incompressibility: + # - "FULL" = standard integration (general purpose) + # - "BBAR" = B-bar method (for nearly incompressible materials) + # Use BBAR is really useful if your material has Poisson's ratio > 0.45 + integ_model = "FULL" + + # ===== Linear Solver Settings ===== + # Solves Ax=b at each Newton iteration + [Solvers.Krylov] + # Maximum iterations before giving up + # Increase if solver fails to converge + # EA/PA may need 1000-5000 iterations + iter = 200 + + # Relative tolerance for convergence + # Stop when: ||residual|| < rel_tol * ||initial residual|| + # Smaller = more accurate but slower + rel_tol = 1e-10 + + # Absolute tolerance for convergence + # Stop when: ||residual|| < abs_tol + # Prevents over-solving when residual is already tiny + abs_tol = 1e-30 + + # Linear solver algorithm: + # - "CG" = Conjugate Gradient (fastest for well-behaved symmetric problems) + # - "GMRES" = General solver (works for any problem, reliable but uses more memory) + # - "MINRES" = Minimal Residual (for symmetric problems when CG fails) + # - "BiCGSTAB" = Memory-efficient alternative to GMRES (faster but less robust) + # Use CG for typical material models, GMRES if unsure or have convergence issues + solver = "GMRES" + + # Preconditioner to accelerate convergence: + # With multi-material systems you might find better convergence + # properties by testing out different preconditioners + # EA/PA will automatically run with JACOBI as the full matrix is + # not constructed + # - "JACOBI" = diagonal scaling (simple/fast, works everywhere but slow convergence) + # - "AMG" = Algebraic MultiGrid (fewer iterations but expensive setup, can fail on some problems) + # - "ILU" = Incomplete factorization (good middle-ground, useful for multi-material systems) + # - "L1GS" = advanced smoother (can help with multi-material systems with contrasting properties) + # - "CHEBYSHEV" = polynomial smoother (good for problems with multiple material scales) + # Try ILU / L1GS / CHEBYSHEV if JACOBI convergence is too slow + preconditioner = "JACOBI" + + # Output verbosity (0 = quiet, 1+ = show iterations) + print_level = 0 + + # ===== Nonlinear Solver Settings ===== + # Solves the overall nonlinear problem + [Solvers.NR] + # Maximum Newton-Raphson iterations per time step + # Increase if "failed to converge" errors occur + iter = 25 + + # Relative tolerance for equilibrium + # Stop when: force imbalance < rel_tol * applied force + rel_tol = 1e-5 + + # Absolute tolerance for equilibrium + # Stop when: force imbalance < abs_tol + abs_tol = 1e-10 + + # Nonlinear solver type: + # - "NR" = standard Newton-Raphson (usually sufficient) + # - "NRLS" = Newton with line search (for difficult convergence) + nl_solver = "NR" + +# ===================================== +# VISUALIZATION OUTPUT +# ===================================== +# Controls what visualization files are created for post-processing +# These files can be opened in ParaView, VisIt, or other tools + +[Visualizations] + # ===== Output Formats ===== + # Enable the formats you need (multiple can be true) + + # VisIt format (.visit files + data) + # Good for: VisIt software, time series data + visit = false + + # ParaView format (.pvtu/.vtu files) + # Good for: ParaView software, widely supported + # Recommended for most users + paraview = true + + # ADIOS2 format (high-performance I/O) + # Good for: very large simulations, supercomputers + adios2 = false + + # ===== Output Control ===== + # How often to write visualization files + # 1 = every time step (lots of files!) + # 10 = every 10th time step + # 100 = every 100th time step + output_frequency = 10 + + # ===== IMPORTANT: Visualization Output Location ===== + # This path is RELATIVE to the main output directory structure! + # Actual location will be: + # [PostProcessing.volume_averages.output_directory]/[basename]/[floc] + # + # Example: With output_directory = "./results", basename = "test", floc = "viz/": + # Actual path: ./results/test/viz/ + # + # Default "visualizations/" means files go to: + # [output_directory]/[basename]/visualizations/ + # + # Note: This is different from volume averages which go directly in: + # [output_directory]/[basename]/ + floc = "visualizations/" + +# ===================================== +# POST-PROCESSING OPTIONS +# ===================================== +# Analysis and data extraction from your simulation + +[PostProcessing] + # ===== Volume Averaging ===== + # Computes average quantities over the entire domain or by material + # Useful for: stress-strain curves, homogenized properties + [PostProcessing.volume_averages] + # Master switch for volume averaging + enabled = true + + # Which quantities to average: + # Each creates a separate output file with time history + + # Stress tensor (6 components: σxx, σyy, σzz, σxy, σyz, σxz) + stress = true + + # Deformation gradient tensor F (9 components) + # F relates current to reference configuration + def_grad = true + + # Euler strain tensor (6 components) + # Sometimes referred to as "true strain" or "logarithmic strain" in the 1D sense + # Note: "logarithmic strain" is a completely different measure when we move to 3D tensors + euler_strain = true + + # Integrated plastic work across entire volume + # Note: this is the only quantity that is the integrated quantity over the + # volume rather than a volume average value + # Useful for finding equivalent yield points across multiple + # loading directions + plastic_work = true + + # Equivalent plastic strain (scalar value) + # Single value representing accumulated plastic deformation + # Common measures: von Mises equivalent strain + # Useful for: failure prediction, hardening evolution + # Only available for ExaCMech models + eq_pl_strain = false + + # Elastic strain tensor (6 components) + # Only available for ExaCMech models + elastic_strain = false + + # How often to compute averages + # Must be multiple of visualization output_frequency + output_frequency = 1 + + # ===== OUTPUT DIRECTORY STRUCTURE ===== + # This setting determines the base directory for ALL simulation outputs + # The actual output structure will be: + # [output_directory]/[basename]/ <- Main simulation folder + # [output_directory]/[basename]/avg_*.txt <- Volume average files + # [output_directory]/[basename]/visualizations/ <- Visualization files + # [output_directory]/[basename]/restart/ <- Restart files (if enabled) + # + # Example: If output_directory = "./results" and basename = "tensile_test": + # ./results/tensile_test/ <- All outputs go here + # ./results/tensile_test/avg_stress_global.txt <- Note: filename NOT affected by basename + # ./results/tensile_test/avg_stress_region_aluminum_alloy_1.txt + # ./results/tensile_test/visualizations/solution_000010.vtu + # + # IMPORTANT: The basename only affects the subdirectory name, NOT the output filenames + # The actual filenames are controlled by the *_fname options below + output_directory = "./results/" + + # ===== MULTI-MATERIAL OUTPUT FILES ===== + # For simulations with multiple materials, EACH quantity generates: + # 1. A global average file: avg_[quantity]_global.txt + # - Contains volume-weighted average over ENTIRE domain + # - Useful for overall material response + # + # 2. Per-material files: avg_[quantity]_region_[material_name]_[region_id].txt + # - Contains average over that material region only + # - Useful for individual material behavior + # + # Example with two materials (aluminum and polymer): + # avg_stress_global.txt <- Combined response + # avg_stress_region_aluminum_alloy_1.txt <- Aluminum only (region 1) + # avg_stress_region_custom_polymer_2.txt <- Polymer only (region 2) + # + # File format (all files): + # Column 1: Time + # Column 2: Volume (total volume averaged over) + # Columns 3+: Component values + # Data is space-delimited, one row per time step + + # ===== Output File Names ===== + # These define the base names for averaged quantity files + # These are optional but the base names are provided down below + # Actual filenames will have _global or _region_[name]_[id] appended + + # Stress components file + # Columns: time, volume, σxx, σyy, σzz, σxy, σyz, σxz + # Creates: avg_stress_global.txt, avg_stress_region_*.txt + avg_stress_fname = "avg_stress.txt" + + # Deformation gradient file + # Columns: time, volume, F11, F12, F13, F21, F22, F23, F31, F32, F33 + # Creates: avg_def_grad_global.txt, avg_def_grad_region_*.txt + avg_def_grad_fname = "avg_def_grad.txt" + + # Euler strain file + # Columns: time, volume, εxx, εyy, εzz, εxy, εyz, εxz + # Creates: avg_euler_strain_global.txt, avg_euler_strain_region_*.txt + avg_euler_strain_fname = "avg_euler_strain.txt" + + # Plastic work file + # Columns: time, volume, plastic_work + # Creates: avg_pl_work_global.txt, avg_pl_work_region_*.txt + avg_pl_work_fname = "avg_pl_work.txt" + + # Equivalent plastic strain file + # Columns: time, volume, equivalent_plastic_strain + # Creates: avg_eq_pl_strain_global.txt, avg_eq_pl_strain_region_*.txt + avg_eq_pl_strain_fname = "avg_eq_pl_strain.txt" + + # Elastic strain file (ExaCMech only) + # Columns: time, volume, εe_xx, εe_yy, εe_zz, εe_xy, εe_yz, εe_xz + # Creates: avg_elastic_strain_global.txt, avg_elastic_strain_region_*.txt + avg_elastic_strain_fname = "avg_elastic_strain.txt" + + # ===== Crystal Orientation Analysis (Light-Up) ===== + # Tracks which crystals are favorably oriented for slip across all crystal systems + # Supports cubic, hexagonal, trigonal, rhombohedral, tetragonal, orthorhombic, monoclinic, triclinic + # Useful for: texture evolution, identifying active grains, powder diffraction simulation + [[PostProcessing.light_up]] + # Enable this analysis + enabled = true + + # Which material to analyze (must match Materials.material_name) + material_name = "aluminum_alloy" + + # Crystal system type - determines symmetry operations and lattice parameter requirements + # Supported values: 'CUBIC', 'HEXAGONAL', 'TRIGONAL', 'RHOMBOHEDRAL', + # 'TETRAGONAL', 'ORTHORHOMBIC', 'MONOCLINIC', 'TRICLINIC' + laue_type = 'CUBIC' + + # Crystal directions to monitor [h,k,l] + # These are Miller indices in crystal coordinates + # Examples: [1,1,1] = octahedral planes, [1,0,0] = cube faces, [1,1,0] = cube edges + # For hexagonal: [1,0,0] = basal, [0,0,1] = c-axis, [1,1,0] = prismatic + hkl_directions = [[1, 1, 1], [1, 0, 0], [1, 1, 0]] + + # Angular tolerance in radians + # Grains within this angle of target direction are considered "in-fiber" + # 0.0873 radians ≈ 5 degrees, 0.1745 radians ≈ 10 degrees + distance_tolerance = 0.0873 + + # Sample direction in lab coordinates [x,y,z] + # Used as reference for orientation analysis and lattice strain calculations + # [0,0,1] = Z direction (typical loading direction) + # [1,0,0] = X direction, [0,1,0] = Y direction + sample_direction = [0.0, 0.0, 1.0] + + # Crystal lattice parameters - requirements vary by crystal system: + # CUBIC: [a] (lattice parameter in Angstroms) + # HEXAGONAL: [a, c] (basal and c-axis parameters in Angstroms) + # TRIGONAL: [a, c] (basal and c-axis parameters in Angstroms) + # RHOMBOHEDRAL: [a, alpha] (lattice parameter in Angstroms, angle in radians) + # TETRAGONAL: [a, c] (basal and c-axis parameters in Angstroms) + # ORTHORHOMBIC: [a, b, c] (three lattice parameters in Angstroms) + # MONOCLINIC: [a, b, c, beta] (three lattice parameters in Angstroms, monoclinic angle in radians) + # TRICLINIC: [a, b, c, alpha, beta, gamma] (three lattice parameters in Angstroms, three angles in radians) + lattice_parameters = [3.6] # Cubic aluminum: a = 3.6 Angstroms + + # Base name for output files + # Creates files like: lattice_avg_directional_stiffness.txt, lattice_avg_dpeff.txt, lattice_avg_strains.txt... + # File naming automatically includes region number as well as quantity related to it + lattice_basename = "lattice_avg_" + + # Example: Hexagonal crystal system (e.g., titanium, zinc) + # [[PostProcessing.light_up]] + # enabled = true + # material_name = "titanium_alloy" + # laue_type = 'HEXAGONAL' + # hkl_directions = [[1, 0, 0], [0, 0, 1], [1, 1, 0]] # basal, c-axis, prismatic + # distance_tolerance = 0.0873 + # sample_direction = [0.0, 0.0, 1.0] + # lattice_parameters = [2.95, 4.68] # a = 2.95 Å, c = 4.68 Å for Ti + # lattice_basename = "ti_lattice_" + + # Example: Rhombohedral crystal system (e.g., some ceramics, bismuth) + # [[PostProcessing.light_up]] + # enabled = true + # material_name = "rhombohedral_ceramic" + # laue_type = 'RHOMBOHEDRAL' + # hkl_directions = [[1, 1, 1], [1, 0, 0], [1, 1, 0]] + # distance_tolerance = 0.0873 + # sample_direction = [0.0, 0.0, 1.0] + # lattice_parameters = [4.75, 1.0472] # a = 4.75 Å, alpha = 60° = 1.0472 radians + # lattice_basename = "rhombo_lattice_" + + # ===== Field Projections ===== + # Projects integration point data to nodes for visualization + [PostProcessing.projections] + # Field projection configuration for post-processing and visualization + # Format: "field_key" -> "Display Name in Visualization Tools" + # All fields listed below are available by default (except "all_state") + # + # ================================================================= + # GEOMETRIC FIELDS + # Basic geometric properties computed from finite element mesh + # ================================================================= + # "centroid" -> "Centroid" # Element centroid coordinates + # "volume" -> "Volume" # Element volume (or area in 2D) + # + # ================================================================= + # STRESS FIELDS + # Fundamental stress measures for solid mechanics analysis + # ================================================================= + # "cauchy" -> "Cauchy Stress" # Cauchy stress tensor (σ) + # "von_mises" -> "Von Mises Stress" # Von Mises equivalent stress (√(3/2 s:s)) + # "hydro" -> "Hydrostatic Stress" # Hydrostatic pressure (tr(σ)/3) + # + # ================================================================= + # GENERAL STATE VARIABLES + # Material state quantities from constitutive models + # ================================================================= + # "all_state" -> "All State Variables" # Complete state variable vector + # # (WARNING: Can be very large for complex models) + # + # ================================================================= + # EXACMECH CRYSTAL PLASTICITY FIELDS + # Specialized quantities for crystalline material modeling + # Available when using ExaCMech constitutive models + # ================================================================= + # "dpeff" -> "Equivalent Plastic Strain Rate" # √(2/3 ε̇ᵖ:ε̇ᵖ) + # "eps" -> "Equivalent Plastic Strain" # ∫√(2/3 ε̇ᵖ:ε̇ᵖ) dt + # "xtal_ori" -> "Crystal Orientations" # Crystal orientation tensors/quaternions + # "elastic_strain" -> "Elastic Strains" # Elastic strain tensor components in sample frame + # "hardness" -> "Hardness" # Crystal hardness parameters + # "shear_rate" -> "Shearing Rate" # Shear rate on slip systems + # + # ================================================================= + # CONFIGURATION + # ================================================================= + # + # Automatic field selection based on constitutive model + # Controls whether to automatically enable all fields compatible with your material model + auto_enable_compatible = true + # true -> Automatically project all fields supported by the active constitutive model + # (Geometric fields + Stress fields + model-specific state variables) + # Ignores 'enabled_projections' list when true + # Recommended for exploratory analysis and model validation + # false -> Use only fields specified in 'enabled_projections' list below + # Recommended for production runs or when targeting specific outputs + # Reduces computational overhead and output file size + # + # Manual field selection (only used when auto_enable_compatible = false) + # Specify which fields to project for visualization and analysis + # Leave empty ([]) to disable all projections + # Example configurations: + # enabled_projections = ["cauchy", "von_mises", "eps"] # Basic stress + plasticity + # enabled_projections = ["volume", "cauchy", "dpeff", "xtal_ori"] # Geometry + crystal fields + # enabled_projections = ["all_state"] # Everything (large output) + enabled_projections = [] + +# ===================================== +# MESH SETTINGS +# ===================================== +# Defines the geometry and discretization of your model + +[Mesh] + # Mesh source: + # - "file" = load from mesh file (most common) + # - "auto" = generate simple box mesh (for testing or bringing over voxelized data) + type = "file" + + # ===== File-Based Mesh ===== + # Path to mesh file (MFEM format .mesh or .mesh.gz) + # Can use absolute or relative paths + floc = "../../data/my_model.mesh" + + # ===== Mesh Refinement ===== + # Subdivide elements for higher accuracy + + # Serial refinement (before domain decomposition) + # 0 = no refinement, 1 = split each element into 8 (linear hex) (3D) + ref_ser = 0 + + # Parallel refinement (after domain decomposition) + # Use for better load balancing on many processors + ref_par = 0 + + # ===== Polynomial Order ===== + # Shape function order (higher = more accurate but expensive) + # 1 = linear elements (8-node hex, 4-node tet) + # 2 = quadratic elements (20-node hex, 10-node tet) + # 3+ = high-order elements (research use) + p_refinement = 1 + + # ===== Periodic Boundaries ===== + # Connect opposite faces for periodic simulations + # Used for: representative volume elements (RVEs) + # Currently ignored as we don't yet support PBCs yet + # periodicity = false + + # ===== Auto-Generated Mesh ===== + # Creates a simple box mesh (useful for testing) + [Mesh.Auto] + # Physical dimensions [X_length, Y_length, Z_length] + mxyz = [1.0, 1.0, 1.0] + + # Number of elements [X_elements, Y_elements, Z_elements] + # Total elements = product of these numbers + nxyz = [10, 10, 10] \ No newline at end of file diff --git a/workflows/exaconstit-calibrate/examples/parameter_creation.py b/workflows/exaconstit-calibrate/examples/parameter_creation.py new file mode 100644 index 0000000..ab340f4 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/parameter_creation.py @@ -0,0 +1,70 @@ +from __future__ import annotations + +import argparse +from pathlib import Path +from typing import Any, Sequence + +import numpy as np +import pandas as pd + +from workflow_common import ( + ArchiveDB, + CallablePropertyWriter, + CaseTemplater, + InfinityFailureHandler, + LocalBackend, + Manifest, + ObjectiveSpec, + Problem, + ProblemConfig, + SimCase, + StressStrainExtractor, + TemplatePathResolver, + TemplateTarget, + TextTableReader, + TextTableSpec, + configure_logging, +) +from workflows.optimization import ( + Bounds, + RunConfig, + build_reference_points, + derive_population_size, + run_nsga3, +) + +def write_properties(case_dir: Path, gene: Sequence[float], + names: Sequence[str], sim_case: SimCase) -> Path: + """Write material-model properties for one case. + + Produces a `properties.txt` file whose format matches what + the old `ExaConstit_Problems.py` wrote. Adapt the body to + whatever format your mechanics binary actually reads — the + framework doesn't care about the format, only that the file + exists and is referenced correctly in options.toml. + """ + path = case_dir / "properties.txt" + # Zip gene values with their names so the output is both + # machine-readable AND human-auditable for debugging. + gene_dict = dict(zip(names, gene)) + lines = [ + "8.920e-6", + "0.003435984", + "1.0e-10", + "168.4e0", + "121.4e0", + "75.2e0", + "44.0e0", + f"{gene_dict['mprime']:.6g}", + "1.0e0", + f"{gene_dict['h0']:.6g}", + f"{gene_dict['crss0']:.6g}", + "122.4e-3", + "0.0", + "5.0e9", + f"{gene_dict['crss0']:.6g}", + "0.0", + "-1.0307952", + ] + path.write_text("\n".join(lines) + "\n") + return path \ No newline at end of file diff --git a/workflows/exaconstit-calibrate/examples/plot_solutions.py b/workflows/exaconstit-calibrate/examples/plot_solutions.py new file mode 100644 index 0000000..e556345 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/plot_solutions.py @@ -0,0 +1,1623 @@ +""" +Plot the top-N optimized solutions vs the experimental reference. + +Companion to ``nsga3_calibration.py``. Once a calibration finishes, +point this script at the workspace and it will: + +1. Find the SQLite archive automatically (resolves + ``calibration.db`` in the workspace, or any unique ``*.db`` file). +2. Pull the top-N gene records from the archive — ranked either by + L2 norm of the fitness tuple (the "balanced winner" — closest to + the utopian origin) or by a single objective. Reuses + :func:`workflows.optimization.inspect_archive.select_top_genes`, + which is the same selection logic that powers the inspect-archive + CLI's ``--pareto-only`` view. +3. For each selected gene and each SimCase, reconstruct the + stress-strain curve from the archived ``avg_stress`` / + ``avg_def_grad`` tables. The case directories on disk DO NOT need + to still exist; the archive carries everything required. +4. Draw an interactive matplotlib figure overlaying: + * the experimental reference curve (thick black); + * each selected gene's simulated curve (semi-transparent line, + red-to-blue gradient by rank); + * a slider that fades curves below a chosen rank, so users can + interactively narrow focus from N to K without re-running; + * a click panel that prints the gene's parameter values when + the user clicks a curve. +5. Optionally produce a 2-D Pareto-front scatter (``--pareto``) for + any pair of objectives, with the L2-closest gene highlighted. + +Usage examples +-------------- + + # Top 10 by L2 norm (the headline plot): + python examples/plot_solutions.py calibration_run \\ + --top 10 --experimental experiments/exp1.csv experiments/exp2.csv + + # Top 5 ranked by objective 0 (e.g. stress-RMSE for exp1): + python examples/plot_solutions.py calibration_run \\ + --top 5 --objective 0 \\ + --experimental experiments/exp1.csv experiments/exp2.csv + + # 2-D Pareto-front scatter for objectives 0 vs 2: + python examples/plot_solutions.py calibration_run \\ + --pareto 0,2 --experimental experiments/exp1.csv experiments/exp2.csv + + # Save without showing (headless / batch / CI): + python examples/plot_solutions.py calibration_run \\ + --top 10 --save out.png --no-show \\ + --experimental experiments/exp1.csv experiments/exp2.csv + +Programmatic use (from a notebook): + + from examples.plot_solutions import plot_top_solutions_overlay + plot_top_solutions_overlay( + "calibration_run", top_n=10, mode="l2", + experimental_paths=["experiments/exp1.csv", + "experiments/exp2.csv"], + ) + +Architectural note +------------------ +This module is deliberately a thin orchestrator over functionality +that already lives in the framework: + +* archive resolution + run selection + top-N ranking → + :mod:`workflows.optimization.inspect_archive` (its public selection + API: ``select_top_genes``, ``pick_run``, ``resolve_archive_path``, + ``collect_all_genes``). +* simulated stress-strain reconstruction → + :meth:`ArchiveDB.load_case_outputs` (reconstitutes the on-disk + output tables from the archive's blob storage). +* strain/stress extraction → :class:`StressStrainExtractor`. +* curve overlay + Pareto scatter → + :func:`workflow_common.postprocess.plot_stress_strain_overlay` + and :func:`plot_pareto_front`. + +The only NEW logic in this file is the matplotlib interactivity +glue (Slider for opacity, pick events for click-to-show parameters) +and the per-SimCase subplot layout. Everything else is composed. +""" +from __future__ import annotations + +import argparse +import sys +from pathlib import Path +from typing import ( + Callable, Dict, List, Optional, Sequence, Tuple, Union, +) + +import numpy as np + +from workflow_common import ( + ArchiveDB, + GeneRecord, + PchipSmoother, + StressStrainExtractor, + load_experimental_csv, +) +from workflow_common.postprocess import plot_pareto_front +from workflow_common.results import CaseResultSet +from workflows.optimization.inspect_archive import ( + RankedGene, + collect_all_genes, + pick_run, + resolve_archive_path, + select_top_genes, +) + + +# --- Helpers ------------------------------------------------------------- + + +def _resolve_objective_index( + objective: Union[int, str], objective_labels: Sequence[str], +) -> int: + """Map an int-or-name objective specifier to an integer index. + + Names take precedence (so ``--objective stress_1`` works); a + bare numeric string falls through to int parse. Raises + :class:`ValueError` with the available labels listed if neither + matches. + """ + if isinstance(objective, str): + if objective in objective_labels: + return list(objective_labels).index(objective) + try: + idx = int(objective) + except ValueError: + raise ValueError( + f"objective {objective!r} not found. Available: " + f"{list(objective_labels)}" + ) + if not 0 <= idx < len(objective_labels): + raise ValueError( + f"objective index {idx} out of range " + f"[0, {len(objective_labels)})" + ) + return idx + if not 0 <= objective < len(objective_labels): + raise ValueError( + f"objective index {objective} out of range " + f"[0, {len(objective_labels)})" + ) + return objective + + +def _ranked_genes_for_mode( + archive: ArchiveDB, + run_id: str, + *, + objective_labels: Sequence[str], + top_n: int, + mode: str, + objective: Optional[Union[int, str]], +) -> List[RankedGene]: + """Pick the genes to plot using inspect_archive's selection logic. + + The three modes map to ``select_top_genes`` outputs: + + * ``"l2"`` — return the ``"l2"`` category list directly. + * ``"objective"`` — pick a single per-objective category by + label/index. + * ``"last-gen"`` — bypass ``select_top_genes`` and return every + gene from the highest gen_idx (no ranking; sorted by pop_idx + so the order matches what a user would see in + ``inspect_archive --gen ``). + """ + if mode == "last-gen": + all_genes = collect_all_genes(archive, run_id) + if not all_genes: + return [] + last_gen = max(g.gen_idx for g in all_genes) + last = sorted( + (g for g in all_genes if g.gen_idx == last_gen), + key=lambda g: g.pop_idx, + ) + l2_full = np.array([ + float(np.linalg.norm(g.fitness)) for g in last + ]) + if top_n <= 0 or top_n > len(last): + take = len(last) + else: + take = top_n + return [ + RankedGene( + gene=g, rank=i, category=f"gen={last_gen}", + score=float(g.pop_idx), l2_norm=float(l2_full[i]), + ) + for i, g in enumerate(last[:take]) + ] + + all_genes = collect_all_genes(archive, run_id) + if not all_genes: + return [] + + if mode == "l2": + selection = select_top_genes( + all_genes, + objective_labels=objective_labels, + top_n=top_n, + categories=("l2",), + ) + return selection.get("l2", []) + + if mode == "objective": + if objective is None: + raise ValueError("mode='objective' requires --objective") + idx = _resolve_objective_index(objective, objective_labels) + label = objective_labels[idx] + selection = select_top_genes( + all_genes, + objective_labels=objective_labels, + top_n=top_n, + categories=("per_objective",), + ) + return selection.get(f"obj:{label}", []) + + raise ValueError( + f"mode={mode!r} not recognized. Use 'l2', 'objective', or 'last-gen'." + ) + + +def _discover_sim_case_count( + archive: ArchiveDB, run_id: str, sample_gene: GeneRecord, + *, max_check: int = 16, +) -> int: + """Probe how many SimCases were archived for a sample gene. + + The archive doesn't store an explicit "n_sim_cases" field — it's + a derived property of which sim_case_idx values have rows under + a given ``(run_id, birth_gen, birth_gene)`` triple. We probe + incrementally until ``load_case_outputs`` returns ``None``. + + ``max_check`` caps the probe to avoid pathological loops if some + archive becomes sparse; 16 covers any reasonable calibration + setup (most have 1-4 SimCases). + """ + count = 0 + for idx in range(max_check): + outputs = archive.load_case_outputs( + run_id, + birth_gen=sample_gene.birth_gen, + birth_gene=sample_gene.birth_gene, + sim_case_idx=idx, + ) + if outputs is None: + break + count += 1 + return count + + +def _extract_curve( + case_result: CaseResultSet, extractor: StressStrainExtractor, + *, rank: int, sim_case_idx: int, +) -> Optional[Tuple[np.ndarray, np.ndarray]]: + """Run an extractor against one CaseResultSet, swallow + warn on failure. + + Bad data shouldn't crash the whole plot; print a one-line warning + and let the caller skip the curve. + """ + try: + strain, stress = extractor.extract(case_result) + return np.asarray(strain), np.asarray(stress) + except Exception as e: # noqa: BLE001 + print( + f"warning: skipping rank={rank} sim_case={sim_case_idx} " + f"(extractor failed: {e})", + file=sys.stderr, + ) + return None + + +def _load_or_extract_curve( + archive: ArchiveDB, + run_id: str, + *, + birth_gen: int, + birth_gene: int, + sim_case_idx: int, + extractor: StressStrainExtractor, + rank_label: int, + experimental_reference: Optional[Tuple[np.ndarray, np.ndarray]] = None, +) -> Optional[Tuple[np.ndarray, np.ndarray]]: + """Get an extracted curve for one (gene, sim_case). + + Preference order: + + 1. Archived curve from the ``case_curves`` table. This is the + FULL-range curve the framework stored at run time (window + stripped before extraction). Faster than re-extracting and + guaranteed to match what the optimizer scored against, + modulo the optimizer's own windowing on top. + 2. Re-extract from raw ``case_outputs`` using the supplied + extractor. Used for archives written before the + ``case_curves`` table existed, or for cases where the + run-time extraction failed but the raw outputs are + still useful. + + If ``experimental_reference`` is supplied as ``(exp_ind, + exp_dep)``, both the loaded ``ind`` and ``dep`` arrays are + sign-matched against their respective exp axes via + :func:`workflow_common.objectives.match_sign_to_reference`. + This catches the case where an extractor accidentally + absolute-values its output (a sign-stripped strain rate, an + evaluator that calls ``np.abs`` on its arrays before scoring + RMSE) — without correction, the plot shows the simulated curve + flipped relative to the experimental reference, which is + confusing. The correction is purely cosmetic; it doesn't + re-write the archive. + + Returns ``(independent, dependent)`` arrays or ``None`` if + neither path produces data. + """ + archived = archive.load_case_curve( + run_id, + birth_gen=birth_gen, birth_gene=birth_gene, + sim_case_idx=sim_case_idx, + ) + if archived is not None: + ind, dep, _ind_label, _dep_label = archived + else: + outputs = archive.load_case_outputs( + run_id, + birth_gen=birth_gen, birth_gene=birth_gene, + sim_case_idx=sim_case_idx, + ) + if outputs is None: + return None + pair = _extract_curve( + outputs, extractor, + rank=rank_label, sim_case_idx=sim_case_idx, + ) + if pair is None: + return None + ind, dep = pair + if experimental_reference is not None: + exp_ind = np.asarray(experimental_reference[0], dtype=float) + exp_dep = np.asarray(experimental_reference[1], dtype=float) + ind = np.asarray(ind, dtype=float) + dep = np.asarray(dep, dtype=float) + try: + # Dependent axis: bring exp_dep onto sim's grid via + # PCHIP, then element-wise np.copysign so each sim + # point inherits the sign of the experimental + # response at the same strain magnitude. ``np.abs`` + # on both x-arrays handles the cross-sign case + # (sim positive from absolute-valued extractor, exp + # negative from compression test) — the smoother + # works on monotonic |strain|, sign comes back via + # copysign. + smoother = PchipSmoother(strict_monotonic=False) + exp_dep_at_sim = smoother.sample_at( + np.abs(exp_ind), exp_dep, np.abs(ind), + ).y + dep = np.copysign(dep, exp_dep_at_sim) + # Independent axis: a scalar sign drawn from the + # experimental's last strain value gives the test + # direction unambiguously. (For cyclic data ending + # back at zero this fails; the framework targets + # monotonic loading.) + if exp_ind.size > 0: + ind = np.copysign(ind, exp_ind[-1]) + except (ValueError, RuntimeError) as e: + # Degenerate exp data (e.g. single point, all + # duplicates) → leave the curve as-is rather than + # crashing the plot. + print( + f"warning: sim/exp sign-match for sim_case={sim_case_idx} " + f"skipped — {e}", file=sys.stderr, + ) + return ind, dep + + +def _format_param_panel( + rg: RankedGene, param_names: Sequence[str], + objective_labels: Sequence[str], +) -> str: + """Pretty-print a gene's parameters for the click panel. + + Format is dense (monospace, line per param) since the panel sits + in a tight bottom strip of the figure. Includes fitness values + too because users almost always want to see "what trade-off did + this winner make?" alongside the parameters. + """ + lines = [ + f"rank {rg.rank} category={rg.category} " + f"score={rg.score:.4g} L2={rg.l2_norm:.4g}", + ] + for name, val in zip(param_names, rg.gene.gene_vector): + lines.append(f" {name} = {val:.6g}") + lines.append( + " fitness: " + + ", ".join( + f"{lbl}={v:.4g}" + for lbl, v in zip(objective_labels, rg.gene.fitness) + ) + ) + lines.append( + f" birth: gen={rg.gene.birth_gen}, " + f"gene={rg.gene.birth_gene}" + ) + return "\n".join(lines) + + +def _format_gene_panel( + g: GeneRecord, param_names: Sequence[str], + objective_labels: Sequence[str], + *, header: str = "", +) -> str: + """Pretty-print a plain GeneRecord for the Pareto-click panel. + + Same shape as :func:`_format_param_panel` but works directly on + a ``GeneRecord`` (no rank/category metadata, since Pareto-front + points aren't ranked relative to each other in the same way the + overlay's top-N list is). The optional ``header`` line lets the + caller annotate which point was clicked (e.g. its scatter + index or "L2 winner"). + """ + lines: List[str] = [] + if header: + lines.append(header) + lines.append( + f"L2={float(np.linalg.norm(g.fitness)):.4g} " + f"birth: gen={g.birth_gen}, gene={g.birth_gene}" + ) + for name, val in zip(param_names, g.gene_vector): + lines.append(f" {name} = {val:.6g}") + lines.append( + " fitness: " + + ", ".join( + f"{lbl}={v:.4g}" + for lbl, v in zip(objective_labels, g.fitness) + ) + ) + return "\n".join(lines) + + +def _resolve_experimental_for_case( + archive: ArchiveDB, + run_id: str, + sim_case_idx: int, + csv_fallback: Optional[Sequence[Optional[Union[str, Path]]]], +) -> Optional[Tuple[np.ndarray, np.ndarray, Optional[str]]]: + """Get experimental ``(strain, stress, label)`` for one SimCase. + + Resolution order: + + 1. Archive's ``experiments`` table — written by the driver at + run start. This is the "no manual ordering" path: the + SimCase index is the key, so users don't have to remember + which CSV maps to which experiment. + 2. CSV path at ``csv_fallback[sim_case_idx]`` — for archives + written before this feature existed, or for users who want + to override with different reference data post-hoc. + 3. ``None`` — no experimental overlay for this SimCase. + + Returns ``(strain, stress, label)`` on success, ``None`` if no + source resolved. + """ + # Archive first. + result = archive.load_experiment(run_id, sim_case_idx) + if result is not None: + label, df = result + if {"strain", "stress"}.issubset(df.columns): + return ( + df["strain"].to_numpy(), df["stress"].to_numpy(), label, + ) + cols = list(df.columns) + return ( + df[cols[0]].to_numpy(), df[cols[1]].to_numpy(), label, + ) + # CSV fallback. + if (csv_fallback is not None + and sim_case_idx < len(csv_fallback) + and csv_fallback[sim_case_idx] is not None): + path = Path(csv_fallback[sim_case_idx]) + try: + df = load_experimental_csv(path) + if {"strain", "stress"}.issubset(df.columns): + return ( + df["strain"].to_numpy(), df["stress"].to_numpy(), + str(path.name), + ) + cols = list(df.columns) + return ( + df[cols[0]].to_numpy(), df[cols[1]].to_numpy(), + str(path.name), + ) + except Exception as e: # noqa: BLE001 + print( + f"warning: could not load CSV {path} for SimCase " + f"{sim_case_idx}: {e}", + file=sys.stderr, + ) + return None + + +def _resolve_extractor_for_case( + archive: ArchiveDB, + run_id: str, + sim_case_idx: int, + *, + factory: Optional[Callable[[int], StressStrainExtractor]] = None, +) -> StressStrainExtractor: + """Pick a StressStrainExtractor for the given SimCase. + + Resolution order: + + 1. Archive's stored extractor config (written by the driver + at run start). This is the source of truth: it captures + the EXACT settings the optimizer used to score curves, + including ``strain_source``, ``strain_rate``, column-name + overrides, etc. Without this, a default-built extractor + can silently produce different curves from what the + optimizer saw. + 2. ``factory(sim_case_idx)`` if the caller supplied one. + Lets a Python caller of plot_top_solutions_overlay / + plot_pareto_front_with_l2_winner pass per-SimCase + extractor configs explicitly. + 3. ``StressStrainExtractor()`` default (ExaConstit z-axis + Biot-strain conventions). The "no idea what they used, + try defaults" fallback for archives written before + extractor configs were stored. + + The window field is STRIPPED from any archived extractor + before return: the plotter wants full-curve visibility (so + the user sees what's going on outside the optimization + region) and shades the window separately via + ``case_data["minmax_strain"]`` from the archive's + experiments.minmax_strain column. + """ + cfg = archive.load_extractor_config(run_id, sim_case_idx) + if cfg is not None: + try: + ext = StressStrainExtractor.from_dict(cfg) + # Plotter wants full curves with the window shaded as + # an overlay rather than data cropped at extraction. + if ext.window is not None: + ext = StressStrainExtractor.from_dict({ + **cfg, "window": None, + }) + return ext + except Exception as e: # noqa: BLE001 + print( + f"warning: archived extractor config for SimCase " + f"{sim_case_idx} failed to load ({e}); falling back", + file=sys.stderr, + ) + if factory is not None: + return factory(sim_case_idx) + return StressStrainExtractor() + + +def _slope_of(strain: np.ndarray, stress: np.ndarray) -> np.ndarray: + """Numerical slope dStress/dStrain via finite difference. + + Uses ``np.gradient`` so endpoints are handled with one-sided + differences and the slope array has the same length as the + inputs. This matches what ``StressStrainObjective``'s + slope-extraction does internally — deliberately so, because + if the optimizer scored a slope objective, the slope values + we plot here must be derived the same way to be comparable. + """ + return np.gradient(np.asarray(stress, dtype=float), + np.asarray(strain, dtype=float)) + + +def _lazy_matplotlib(): + """Soft dep on matplotlib with a useful error if missing.""" + try: + import matplotlib.pyplot as plt + from matplotlib.widgets import Slider + return plt, Slider + except ImportError as e: + raise RuntimeError( + "matplotlib is required for plot_solutions; install via " + "`pip install -e \".[plot]\"` or `pip install matplotlib`." + ) from e + + +# --- Public plotting functions ------------------------------------------- + + +def plot_top_solutions_overlay( + workspace: Union[str, Path], + *, + top_n: int = 10, + mode: str = "l2", + objective: Optional[Union[int, str]] = None, + run_id: Optional[str] = None, + experimental_paths: Optional[Sequence[Optional[Union[str, Path]]]] = None, + extractor_factory: Optional[ + Callable[[int], StressStrainExtractor] + ] = None, + show_slopes: bool = True, + save: Optional[Union[str, Path]] = None, + show: bool = True, +): + """Build the headline interactive figure. + + Layout: a 2-row × N-SimCase-column grid of subplots. + + * Top row — stress-strain. Top-N simulated curves overlaid on + the experimental reference (thick black). Curves colored by + rank with a coolwarm-reversed colormap so rank 0 is red. + * Bottom row — slope-strain (dStress/dStrain). Same overlay, + same colors. Computed via ``np.gradient`` for both sim and + exp so the curves correspond to what a slope objective + would have scored. Hidden if ``show_slopes=False`` or if + no experimental data is available. + + Below the subplots: + + * a slider that fades curves below a chosen rank, so users can + narrow visual focus from N to K interactively; + * a click panel that prints the gene's parameter values when + the user clicks a curve. + + Experimental data resolution (per SimCase): + + 1. The archive's ``experiments`` table (written by the driver + at run start). This is the canonical source — keyed by + ``sim_case_idx``, so users don't have to remember CSV + ordering. + 2. ``experimental_paths[sim_case_idx]`` if archive doesn't + have it. Lets archives written before the experiments + table existed still produce useful plots. + 3. None — the experimental overlay is omitted for that case. + + Args: + workspace: Workspace dir, or direct ``.db`` archive path. + top_n: Number of solutions to plot. ``0`` or negative shows + all available. + mode: ``"l2"`` (default), ``"objective"``, or ``"last-gen"``. + objective: Required for ``mode='objective'``; integer index + or label string. + run_id: Optional explicit run UUID (default: latest). + experimental_paths: Per-SimCase CSV fallbacks; only used + when the archive doesn't carry experimental data for + a given SimCase. + extractor_factory: ``f(sim_case_idx) -> StressStrainExtractor``; + lets per-SimCase extractor configs match what was used + at run time. Default: vanilla + ``StressStrainExtractor()`` for every case. + show_slopes: Include the slope-strain row. Default True. + save: Path to write the figure to (PNG / PDF / SVG). + show: Whether to call ``plt.show()`` blocking. Set False + for headless / batch. + + Returns: + The matplotlib ``Figure``. + """ + plt, Slider = _lazy_matplotlib() + + db_path = resolve_archive_path(Path(workspace), "calibration.db") + with ArchiveDB(db_path, readonly=True) as archive: + meta = pick_run(archive, run_id, latest_if_none=True) + run_id = meta.run_id + + ranked = _ranked_genes_for_mode( + archive, run_id, + objective_labels=meta.objective_labels, + top_n=top_n, mode=mode, objective=objective, + ) + if not ranked: + raise RuntimeError( + f"no plottable solutions in run {run_id} " + f"(mode={mode!r}). Either no genes have finite " + f"fitness, or the run hasn't completed gen 0." + ) + + n_sim_cases = _discover_sim_case_count( + archive, run_id, sample_gene=ranked[0].gene, + ) + if n_sim_cases == 0: + raise RuntimeError( + "best gene has no archived case outputs. " + "Was archiving disabled at run time?" + ) + + # Per-(sim_case, rank) (strain, stress) for simulated curves. + per_case_curves: List[ + List[Tuple[RankedGene, np.ndarray, np.ndarray]] + ] = [] + # Per-sim_case experimental (strain, stress, label) or None. + per_case_exp: List[ + Optional[Tuple[np.ndarray, np.ndarray, Optional[str]]] + ] = [] + # Per-sim_case (lo, hi) optimization window or None. Either + # side of the tuple may itself be None for "unbounded on + # that side." Plotter shades the corresponding strain region + # so users can see what range the optimizer scored against. + per_case_window: List[ + Optional[Tuple[Optional[float], Optional[float]]] + ] = [] + + for sc_idx in range(n_sim_cases): + extractor = _resolve_extractor_for_case( + archive, run_id, sc_idx, factory=extractor_factory, + ) + # Resolve experimental data FIRST so we can sign-match + # the simulated curves against it as we load them. This + # corrects archived data where an extractor accidentally + # absolute-values its output (e.g. a sign-stripped + # strain rate producing positive strain on a compression + # run): without correction the simulated curve plots + # mirrored relative to the experimental reference. + # The correction is applied in :func:`_load_or_extract_curve`. + exp_resolved = _resolve_experimental_for_case( + archive, run_id, sc_idx, experimental_paths, + ) + exp_reference: Optional[Tuple[np.ndarray, np.ndarray]] = None + if exp_resolved is not None: + exp_strain, exp_stress, _exp_label = exp_resolved + exp_reference = (exp_strain, exp_stress) + sc_curves: List[ + Tuple[RankedGene, np.ndarray, np.ndarray] + ] = [] + for rg in ranked: + pair = _load_or_extract_curve( + archive, run_id, + birth_gen=rg.gene.birth_gen, + birth_gene=rg.gene.birth_gene, + sim_case_idx=sc_idx, + extractor=extractor, + rank_label=rg.rank, + experimental_reference=exp_reference, + ) + if pair is None: + continue + sc_curves.append((rg, pair[0], pair[1])) + per_case_curves.append(sc_curves) + per_case_exp.append(exp_resolved) + # Pull the optimization window the optimizer was + # constrained to. Returns None on archives that pre-date + # the minmax_strain column (the read path tolerates that + # gracefully) and on SimCases that didn't supply one. + per_case_window.append( + archive.load_experiment_window(run_id, sc_idx) + ) + + # --- Lay out the figure ---------------------------------------- + n_cols = max(n_sim_cases, 1) + # Row layout: stress (top), slope (middle, optional), spacer, + # slider, panel. + has_slopes = show_slopes + if has_slopes: + # 5 stress + spacer + 5 slope + spacer + slider + panel. + height_ratios = [1] * 5 + [0.15] + [1] * 5 + [0.2, 0.5, 0.7] + fig = plt.figure(figsize=(6 * n_cols, 10)) + else: + height_ratios = [1] * 7 + [0.2, 0.5, 0.7] + fig = plt.figure(figsize=(6 * n_cols, 7.5)) + + gs = fig.add_gridspec( + nrows=len(height_ratios), ncols=n_cols, + height_ratios=height_ratios, + hspace=0.35, wspace=0.25, + ) + if has_slopes: + stress_axes = [fig.add_subplot(gs[0:5, c]) for c in range(n_cols)] + slope_axes = [fig.add_subplot(gs[6:11, c]) for c in range(n_cols)] + slider_ax = fig.add_subplot(gs[12, :]) + panel_ax = fig.add_subplot(gs[13, :]) + else: + stress_axes = [fig.add_subplot(gs[0:7, c]) for c in range(n_cols)] + slope_axes = [] + slider_ax = fig.add_subplot(gs[8, :]) + panel_ax = fig.add_subplot(gs[9, :]) + + panel_ax.axis("off") + panel_text = panel_ax.text( + 0.01, 0.95, + "click any curve to see its parameter values", + transform=panel_ax.transAxes, + va="top", ha="left", family="monospace", fontsize=9, + ) + + n_total = len(ranked) + cmap = plt.get_cmap("coolwarm_r") + line_to_rg: Dict[object, RankedGene] = {} + # line_groups[rank] = list of every Line2D for that rank across + # both stress and slope rows. Slider toggles them all together. + line_groups: List[List[object]] = [[] for _ in range(n_total)] + + for sc_idx in range(n_cols): + stress_ax = stress_axes[sc_idx] + slope_ax = slope_axes[sc_idx] if has_slopes else None + curves = per_case_curves[sc_idx] + exp_data = per_case_exp[sc_idx] + + for rg, strain, stress in curves: + color = cmap(rg.rank / max(n_total - 1, 1)) + line, = stress_ax.plot( + strain, stress, + color=color, alpha=0.7, linewidth=1.4, + picker=5, + label=f"rank {rg.rank}", + ) + line_to_rg[line] = rg + line_groups[rg.rank].append(line) + + if slope_ax is not None: + slope = _slope_of(strain, stress) + slope_line, = slope_ax.plot( + strain, slope, + color=color, alpha=0.7, linewidth=1.4, + picker=5, + label=f"rank {rg.rank}", + ) + line_to_rg[slope_line] = rg + line_groups[rg.rank].append(slope_line) + + # Experimental overlay — same source for both rows. + if exp_data is not None: + ex, ey, exp_label = exp_data + stress_ax.plot( + ex, ey, "k-", linewidth=2.5, label="experimental", + zorder=10, + ) + if slope_ax is not None: + exp_slope = _slope_of(ex, ey) + slope_ax.plot( + ex, exp_slope, "k-", linewidth=2.5, + label="experimental", zorder=10, + ) + + # Per-SimCase title using the experimental label if available. + title = f"SimCase {sc_idx}" + if exp_data is not None and exp_data[2]: + title += f" — {exp_data[2]}" + stress_ax.set_xlabel("Strain") + stress_ax.set_ylabel("Stress") + stress_ax.set_title(title) + stress_ax.grid(True, alpha=0.3) + + if slope_ax is not None: + slope_ax.set_xlabel("Strain") + slope_ax.set_ylabel("dStress/dStrain") + slope_ax.set_title(f"SimCase {sc_idx} — slope") + slope_ax.grid(True, alpha=0.3) + # Stress-strain slopes mix orders of magnitude AND + # signs: the elastic regime hits 10^5 MPa, plastic is + # 10^0 - 10^2, and the very first time steps often + # produce brief negative numerical artifacts when dt + # is tiny. Auto-scaling against ``max(|slope|)`` + # symmetrically lets those artifacts dictate the + # y-axis on BOTH sides — the majority-positive bulk + # gets squashed because matplotlib reserves equal + # vertical real estate for an all-but-empty negative + # half. + # + # The right rule: per-side decision. The side where + # the data actually lives gets its full range so the + # elastic spike stays visible. The other side, which + # is mostly outliers/numerical noise, gets clipped to + # the bulk's 90th-percentile envelope so it doesn't + # eat half the chart for a handful of points. + # + # When the data is genuinely two-sided (cyclic + # loading, repeated load reversals), neither side is + # a "minority" and both get the bulk clip — preserves + # symmetric behavior for the case where it's right. + all_slopes = [] + for _rg, s_strain, s_stress in curves: + all_slopes.append(_slope_of(s_strain, s_stress)) + if exp_data is not None: + all_slopes.append(_slope_of(exp_data[0], exp_data[1])) + if all_slopes: + concat = np.concatenate(all_slopes) + finite = concat[np.isfinite(concat)] + if finite.size: + abs_finite = np.abs(finite) + nonzero = abs_finite[abs_finite > 0] + # Bulk envelope — used to clip the minority + # side. 90th percentile catches the majority + # of the smooth response while excluding the + # top ~5% (typically elastic spikes or + # start-of-test artifacts). + bulk_clip = min( + 0, + 1.0, + ) + positives = finite[finite > 0] + negatives = finite[finite < 0] + n_pos, n_neg = positives.size, negatives.size + n_total = n_pos + n_neg + # 10% threshold: if one side has fewer than + # 10% of all signed points, treat it as the + # minority. Empirically this catches start- + # of-test numerical noise without misfiring + # on real two-sided responses. + MINORITY = 0.10 + if n_total == 0: + upper, lower = 1.0, -1.0 + elif n_neg == 0: + # All non-negative: full positive range, + # zero floor (nothing to show below). + upper = float(positives.max()) * 1.05 + lower = 0.0 + elif n_pos == 0: + upper = 0.0 + lower = float(negatives.min()) * 1.05 + elif n_neg / n_total < MINORITY: + # Mostly positive: full positive range, + # clip the negative tail to bulk envelope + # so the plastic bulk doesn't get crushed + # by a couple of dt-too-small noise dips. + upper = max( + float(positives.max()) * 1.05, + bulk_clip, + ) + lower = -bulk_clip + elif n_pos / n_total < MINORITY: + upper = bulk_clip + lower = min( + float(negatives.min()) * 1.05, + -bulk_clip, + ) + else: + # Genuinely two-sided: clip both ends to + # the bulk envelope; outliers extend off + # the chart visibly on whichever side + # they appeared. + upper = bulk_clip + lower = -bulk_clip + slope_ax.set_ylim(lower, upper) + # linthresh from the small end of the + # distribution — keeps the bulk in the LOG + # region rather than the LINEAR band, where + # log spacing actually helps with the + # multi-decade variation that the bulk + # exhibits. + if nonzero.size: + linthresh = max( + float(np.percentile(nonzero, 5)) * 0.5, + 1e-9, + ) + else: + linthresh = 1e-9 + slope_ax.set_yscale("symlog", linthresh=linthresh) + else: + # All non-finite slope values; keep a sensible + # default so the axis renders rather than + # crashing on the symlog setup. + slope_ax.set_yscale("symlog", linthresh=1.0) + + # Optimization-window shading. Shows the strain region the + # optimizer was constrained to via case_data["minmax_strain"]. + # Drawn under everything else (low zorder) so curves stay + # legible. None on either side means "unbounded on that + # side" — extend the shading to the axis edge there. + win = per_case_window[sc_idx] if sc_idx < len(per_case_window) else None + if win is not None and (win[0] is not None or win[1] is not None): + for ax_target in [stress_ax] + ([slope_ax] if slope_ax is not None else []): + xlo, xhi = ax_target.get_xlim() + lo = abs(win[0]) if win[0] is not None else xlo + hi = abs(win[1]) if win[1] is not None else xhi + # If the curves are negative-strain (compression), + # mirror the window onto the negative side so the + # shading lines up. We pick the sign of the + # experimental data if available, else of the first + # simulated curve. + ref = exp_data[0] if exp_data is not None else ( + curves[0][1] if curves else None + ) + if ref is not None and len(ref) > 0 and float(np.median(ref)) < 0: + lo, hi = -hi, -lo + ax_target.axvspan( + lo, hi, color="tab:green", alpha=0.08, zorder=0, + label="optimization window" if ax_target is stress_ax else None, + ) + + # --- Slider -------------------------------------------------------- + # The slider only makes sense when there are at least 2 ranks to + # toggle between. With n_total=1 matplotlib would raise an + # "identical low/high xlim" UserWarning trying to build a slider + # with valmin=valmax=1; the slider would also be functionless + # for the user. Hide the slider axes and skip widget creation. + slider = None + if n_total >= 2: + slider = Slider( + ax=slider_ax, + label="show top K", + valmin=1, valmax=n_total, valinit=n_total, valstep=1, + ) + + def _apply_visibility(k: int) -> None: + for r in range(n_total): + alpha = 0.7 if r < k else 0.07 + for line in line_groups[r]: + line.set_alpha(alpha) + fig.canvas.draw_idle() + + slider.on_changed(lambda val: _apply_visibility(int(val))) + else: + slider_ax.axis("off") + + # --- Click handler ------------------------------------------------- + def _on_pick(event): + rg = line_to_rg.get(event.artist) + if rg is None: + return + panel_text.set_text(_format_param_panel( + rg, meta.param_names, meta.objective_labels, + )) + fig.canvas.draw_idle() + + fig.canvas.mpl_connect("pick_event", _on_pick) + + label_for_title = ranked[0].category if ranked else "unranked" + fig.suptitle( + f"Top {n_total} solutions | ranked by {label_for_title} " + f"| run {run_id[:8]}", + fontsize=11, + ) + + if save is not None: + fig.savefig(str(save), dpi=120, bbox_inches="tight") + print(f"saved figure to {save}", file=sys.stderr) + if show: + plt.show() + fig._plot_solutions_slider = slider # type: ignore[attr-defined] + return fig + + +def plot_pareto_front_with_l2_winner( + workspace: Union[str, Path], + *, + objective_pair: Tuple[int, int] = (0, 1), + top_n: int = 0, + color_by: str = "subset_l2", + run_id: Optional[str] = None, + experimental_paths: Optional[Sequence[Optional[Union[str, Path]]]] = None, + extractor_factory: Optional[ + Callable[[int], StressStrainExtractor] + ] = None, + save: Optional[Union[str, Path]] = None, + show: bool = True, +): + """2-D Pareto-front scatter, clickable, with response-curve inset. + + Layout: a two-column figure. + + * Left column — the 2-D scatter of fitness pairs. Points are + colored by L2 norm (smaller = darker), the L2-closest gene + is marked in red, every point has ``picker=5`` so a click + fires a pick event. + * Right column — an inset axes that initially shows the + L2-closest gene's stress-strain response. When a user clicks + any scatter point, the inset redraws to show that gene's + simulated curves (one line per SimCase) overlaid against + the experimental references. A monospace text panel below + the inset shows the clicked gene's parameters and fitness. + + Selection: by default plots EVERY rank-0 (Pareto-front) gene + so the picture is the actual front. Pass ``top_n > 0`` to + restrict the scatter to the ``top_n`` lowest-L2 genes — useful + when the front has hundreds of points and the user only wants + to see the best balanced trade-offs. + + Experimental data is pulled archive-first via + :func:`_resolve_experimental_for_case` so users never need to + keep CSVs around once a run has completed. + + Args: + workspace: Workspace directory or direct ``.db`` path. + objective_pair: ``(x_idx, y_idx)`` objective indices to scatter. + top_n: If positive, restrict to the ``top_n`` lowest-L2 genes. + ``0`` (default) shows every rank-0 gene. + run_id: Optional explicit run UUID. + experimental_paths: CSV fallbacks per SimCase, used only when + the archive doesn't have experimental data. + extractor_factory: Per-SimCase extractor configs. + save: Path to write the figure. + show: Whether to ``plt.show()`` blocking. + + Returns: + The matplotlib ``Figure``. + """ + plt, _ = _lazy_matplotlib() + + db_path = resolve_archive_path(Path(workspace), "calibration.db") + + # Pre-load EVERYTHING the click handler will need: gene records, + # per-gene case_outputs for all SimCases, experimental refs. The + # alternative — keeping the DB connection open across user + # interaction — risks half-open connections and SQLite locking + # surprises. A few MB of in-memory DataFrames is the right + # tradeoff for a post-run analysis tool. + with ArchiveDB(db_path, readonly=True) as archive: + meta = pick_run(archive, run_id, latest_if_none=True) + run_id = meta.run_id + + all_genes = collect_all_genes(archive, run_id) + finite = [ + g for g in all_genes + if all(np.isfinite(v) for v in g.fitness) + ] + if not finite: + raise RuntimeError("no finite-fitness genes to plot.") + + a_idx, b_idx = objective_pair + n_obj = len(finite[0].fitness) + if not (0 <= a_idx < n_obj and 0 <= b_idx < n_obj): + raise ValueError( + f"objective_pair {objective_pair} out of range for " + f"{n_obj} objectives." + ) + + # Default selection: every rank-0 gene (the actual Pareto + # front). The archive stores rank in GeneRecord.rank, which + # is what selNSGA3 assigns — rank 0 is non-dominated. + if top_n is not None and top_n > 0: + # Top-N by L2 norm, dedup'd via shared logic with + # inspect_archive so the same genes appear in the + # `--pareto-only` table and on this plot. + selection = select_top_genes( + finite, + objective_labels=meta.objective_labels, + top_n=top_n, + categories=("l2",), + ) + chosen = [rg.gene for rg in selection.get("l2", [])] + mode_note = f"top-{top_n} by L2 norm" + else: + chosen = [ + g for g in finite + if g.rank is not None and g.rank == 0 + ] + # Dedup on gene-vector — elitism keeps the same + # solution across generations, and we don't want N + # copies of the same point sitting on top of each + # other in the scatter. + from workflows.optimization.inspect_archive import ( + dedup_on_gene_vector, + ) + chosen = dedup_on_gene_vector(chosen) + mode_note = "rank-0 Pareto front" + + if not chosen: + raise RuntimeError( + "no genes selected for Pareto plot. With top_n=0 " + "the run had no rank-0 genes (rank may not have " + "been recorded — check archive's gene table)." + ) + + # Discover SimCases via the first chosen gene. + n_sim_cases = _discover_sim_case_count( + archive, run_id, sample_gene=chosen[0], + ) + + # Resolve per-SimCase extractors ONCE — they don't vary + # across genes, so building them inside the gene loop is + # wasted work. Archive-stored configs are preferred over + # the user-supplied factory and the default fallback. + per_case_extractor: Dict[int, StressStrainExtractor] = { + sc_idx: _resolve_extractor_for_case( + archive, run_id, sc_idx, factory=extractor_factory, + ) + for sc_idx in range(n_sim_cases) + } + + # Per-SimCase experimental references — loaded BEFORE the + # gene loop so each loaded simulated curve can be + # sign-matched against its experimental reference at load + # time. Without sign-matching, archived curves where the + # extractor accidentally absolute-valued its output appear + # mirrored in the inset relative to the experimental + # reference, which is confusing. + per_case_exp: List[ + Optional[Tuple[np.ndarray, np.ndarray, Optional[str]]] + ] = [ + _resolve_experimental_for_case( + archive, run_id, sc_idx, experimental_paths, + ) + for sc_idx in range(n_sim_cases) + ] + per_case_exp_reference: Dict[ + int, Optional[Tuple[np.ndarray, np.ndarray]] + ] = {} + for sc_idx, exp_resolved in enumerate(per_case_exp): + if exp_resolved is None: + per_case_exp_reference[sc_idx] = None + else: + exp_strain, exp_stress, _exp_label = exp_resolved + per_case_exp_reference[sc_idx] = (exp_strain, exp_stress) + + # Pre-load per-gene case curves for the inset. Same shape + # as the overlay's per_case_curves, but indexed by clicked + # gene index rather than rank. + gene_to_curves: Dict[ + int, List[Tuple[int, np.ndarray, np.ndarray]] + ] = {} + for g_idx, g in enumerate(chosen): + sc_curves: List[Tuple[int, np.ndarray, np.ndarray]] = [] + for sc_idx in range(n_sim_cases): + pair = _load_or_extract_curve( + archive, run_id, + birth_gen=g.birth_gen, birth_gene=g.birth_gene, + sim_case_idx=sc_idx, + extractor=per_case_extractor[sc_idx], + rank_label=g_idx, + experimental_reference=per_case_exp_reference[sc_idx], + ) + if pair is None: + continue + sc_curves.append((sc_idx, pair[0], pair[1])) + gene_to_curves[g_idx] = sc_curves + + # --- Build the figure ---------------------------------------------- + fig = plt.figure(figsize=(13, 7)) + gs = fig.add_gridspec( + nrows=10, ncols=2, + height_ratios=[1] * 7 + [0.2, 0.5, 0.7], + width_ratios=[1.0, 1.1], + hspace=0.35, wspace=0.25, + ) + scatter_ax = fig.add_subplot(gs[0:7, 0]) + inset_ax = fig.add_subplot(gs[0:7, 1]) + panel_ax = fig.add_subplot(gs[9, :]) + panel_ax.axis("off") + panel_text = panel_ax.text( + 0.01, 0.95, + "click any scatter point to see its parameters and response", + transform=panel_ax.transAxes, + va="top", ha="left", family="monospace", fontsize=9, + ) + + # --- Scatter ------------------------------------------------------- + fits = np.array([g.fitness for g in chosen]) + pop_fit_2d = fits[:, [a_idx, b_idx]] + + # Two L2 norms are useful, depending on the question being asked: + # subset_l2: distance from utopian origin in the projected pair + # (a,b). This is what users typically want for "the best + # balanced point ON THIS PROJECTION" — the Pareto winner of + # what they're actually looking at. + # full_l2: distance across all objectives. Useful as a + # secondary signal showing how a point ranks globally vs + # just on the displayed pair. + # The L2 winner ring uses subset_l2 by default — Robert's ask: + # "the L2 circle should really be based on the objectives + # chosen and that should be what's most useful." + x_vals = pop_fit_2d[:, 0] + y_vals = pop_fit_2d[:, 1] + subset_l2 = np.sqrt(x_vals ** 2 + y_vals ** 2) + full_l2 = np.linalg.norm(fits, axis=1) + best_local_idx = int(np.argmin(subset_l2)) + + # Pick the color metric. Each mode highlights a different aspect + # of where points sit relative to the X=0 / Y=0 planes: + # - subset_l2: iso-curves are circles centered at origin in + # the projected pair. Points along a true Pareto front sit + # along an iso-curve at constant distance, so a uniform + # color along the front is the visual signal that the + # front is genuinely tradeoff-shaped here. + # - full_l2: same idea but with all objectives folded in. + # A point that looks balanced on (a,b) but is bad on the + # hidden objectives shows up as light here. + # - x: just the projected X value. Iso-curves are vertical + # lines. Reveals proximity to the Y=0 plane (how + # specialized-on-Y a point is — small X means good on + # X regardless of Y). + # - y: symmetric to x. Iso-curves are horizontal lines. + # Shows proximity to the X=0 plane. + # - asymmetry: |x - y| / (x + y). Zero means perfectly + # balanced on the pair, larger means more lopsided. Lets + # users see at a glance which points are specialists vs + # compromises. + color_metric_options = ("subset_l2", "full_l2", "x", "y", "asymmetry") + if color_by not in color_metric_options: + raise ValueError( + f"unknown color_by={color_by!r}; valid options: " + f"{color_metric_options}" + ) + if color_by == "subset_l2": + color_values = subset_l2 + cbar_label = ( + f"L2 of ({meta.objective_labels[a_idx]}, " + f"{meta.objective_labels[b_idx]})" + ) + elif color_by == "full_l2": + color_values = full_l2 + cbar_label = "L2 (all objectives)" + elif color_by == "x": + color_values = x_vals + cbar_label = meta.objective_labels[a_idx] + elif color_by == "y": + color_values = y_vals + cbar_label = meta.objective_labels[b_idx] + else: # asymmetry + # Guard against division by zero — x+y=0 only if both are + # zero (utopia point), which is a degenerate edge case. + denom = x_vals + y_vals + with np.errstate(divide="ignore", invalid="ignore"): + asym = np.where(denom > 0, np.abs(x_vals - y_vals) / denom, 0.0) + color_values = asym + cbar_label = ( + f"|{meta.objective_labels[a_idx]} - " + f"{meta.objective_labels[b_idx]}| / sum" + ) + + sc = scatter_ax.scatter( + pop_fit_2d[:, 0], pop_fit_2d[:, 1], + c=color_values, cmap="viridis", + s=40, picker=5, edgecolors="black", linewidths=0.4, + ) + # Mark the subset-L2 winner with a red ring on top. This is the + # gene most balanced on the displayed projection — answering + # "which point is the best compromise on what I'm looking at?" + scatter_ax.scatter( + pop_fit_2d[best_local_idx, 0], pop_fit_2d[best_local_idx, 1], + s=160, facecolors="none", edgecolors="red", linewidths=2.0, + label="L2 winner (subset)", zorder=5, + ) + # Utopian origin marker — same convention as plot_pareto_front + # in workflow_common.postprocess. + scatter_ax.scatter( + [0], [0], c="black", marker="+", s=80, label="utopia", + zorder=4, + ) + cbar = fig.colorbar(sc, ax=scatter_ax, shrink=0.85, pad=0.02) + cbar.set_label(cbar_label) + scatter_ax.set_xlabel(meta.objective_labels[a_idx]) + scatter_ax.set_ylabel(meta.objective_labels[b_idx]) + scatter_ax.set_title( + f"Pareto: {meta.objective_labels[a_idx]} vs " + f"{meta.objective_labels[b_idx]}\n" + f"({mode_note}, color={color_by})", + fontsize=10, + ) + scatter_ax.legend(loc="upper right", fontsize=8) + scatter_ax.grid(True, alpha=0.3) + + # --- Inset: response curve for the clicked gene -------------------- + inset_ax.set_xlabel("Strain") + inset_ax.set_ylabel("Stress") + inset_ax.set_title("Response of clicked gene", fontsize=10) + inset_ax.grid(True, alpha=0.3) + + def _draw_response(g_idx: int) -> None: + """Redraw the inset showing gene g_idx's sim curves vs experimental.""" + inset_ax.clear() + inset_ax.set_xlabel("Strain") + inset_ax.set_ylabel("Stress") + inset_ax.grid(True, alpha=0.3) + # Deterministic per-SimCase coloring. + sc_cmap = plt.get_cmap("tab10") + sim_curves = gene_to_curves.get(g_idx, []) + for sc_idx, strain, stress in sim_curves: + color = sc_cmap(sc_idx % 10) + inset_ax.plot( + strain, stress, + color=color, linewidth=1.6, + label=f"sim sc={sc_idx}", + ) + # Experimental reference (if any) for this SimCase. + if (sc_idx < len(per_case_exp) + and per_case_exp[sc_idx] is not None): + ex, ey, _exp_label = per_case_exp[sc_idx] + inset_ax.plot( + ex, ey, + color=color, linestyle="--", linewidth=2.0, + alpha=0.8, + label=f"exp sc={sc_idx}", + ) + gene = chosen[g_idx] + is_winner = g_idx == best_local_idx + title_extra = " (L2 winner)" if is_winner else "" + inset_ax.set_title( + f"Response — gene g{gene.birth_gen}.{gene.birth_gene}" + f"{title_extra}", + fontsize=10, + ) + if sim_curves: + inset_ax.legend(loc="best", fontsize=8) + + # Initialize the inset with the L2 winner's response. + _draw_response(best_local_idx) + panel_text.set_text(_format_gene_panel( + chosen[best_local_idx], meta.param_names, meta.objective_labels, + header=f"L2 winner — {len(chosen)} points on plot", + )) + + # --- Click handler ------------------------------------------------- + def _on_pick(event): + # PathCollection (scatter) pick events arrive with `ind` — + # an array of indices into the collection's data. A single + # click usually returns one index, but a dense cluster can + # return several; pick the first. + if event.artist is not sc: + return + if not hasattr(event, "ind") or len(event.ind) == 0: + return + g_idx = int(event.ind[0]) + _draw_response(g_idx) + panel_text.set_text(_format_gene_panel( + chosen[g_idx], meta.param_names, meta.objective_labels, + header=f"clicked point #{g_idx} of {len(chosen)}", + )) + fig.canvas.draw_idle() + + fig.canvas.mpl_connect("pick_event", _on_pick) + + fig.suptitle( + f"Pareto front (run {run_id[:8]}) — click points for details", + fontsize=11, + ) + + if save is not None: + fig.savefig(str(save), dpi=120, bbox_inches="tight") + print(f"saved figure to {save}", file=sys.stderr) + if show: + plt.show() + return fig + + +# --- CLI ----------------------------------------------------------------- + + +def _parse_pareto_pair(s: str) -> Tuple[int, int]: + """Parse ``--pareto 0,1`` into a tuple of ints with friendly errors.""" + try: + a, b = (int(x.strip()) for x in s.split(",")) + except (ValueError, AttributeError): + raise argparse.ArgumentTypeError( + f"--pareto must be 'i,j' (e.g. '0,1'); got {s!r}" + ) + return (a, b) + + +def _build_parser() -> argparse.ArgumentParser: + p = argparse.ArgumentParser( + description=( + "Plot top-N optimized solutions vs experimental " + "data from a calibration archive." + ), + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=( + "Examples:\n" + " Top 10 by L2 norm:\n" + " python examples/plot_solutions.py calibration_run \\\n" + " --top 10 --experimental experiments/exp1.csv \\\n" + " experiments/exp2.csv\n\n" + " Top 5 by objective 0:\n" + " python examples/plot_solutions.py calibration_run \\\n" + " --top 5 --objective 0 \\\n" + " --experimental experiments/exp1.csv\n\n" + " Pareto front for objectives 0 vs 2:\n" + " python examples/plot_solutions.py calibration_run \\\n" + " --pareto 0,2\n" + ), + ) + p.add_argument( + "workspace", type=Path, + help="Calibration workspace dir, or direct .db file path.", + ) + p.add_argument( + "--top", dest="top_n", type=int, default=10, + help="Number of top solutions to plot (default: 10). " + "Pass 0 or negative for all available.", + ) + p.add_argument( + "--mode", dest="mode", + choices=("l2", "objective", "last-gen"), default="l2", + help="Ranking mode (default: l2).", + ) + p.add_argument( + "--objective", dest="objective", default=None, + help="For --mode=objective: integer index or label name.", + ) + p.add_argument( + "--run-id", dest="run_id", default=None, + help="Archive run UUID (default: latest run).", + ) + p.add_argument( + "--experimental", dest="experimental", + nargs="*", type=Path, default=None, + help="CSV paths, one per SimCase (in SimCase order). " + "If fewer paths than SimCases are given, later cases " + "show no experimental overlay.", + ) + p.add_argument( + "--pareto", dest="pareto", + type=_parse_pareto_pair, default=None, + help="Produce a Pareto-front scatter for objectives i,j " + "(e.g. '0,1'). The subset-L2 winner is highlighted, all " + "points are clickable to reveal parameters and the " + "stress-strain response of every SimCase. Restricted to " + "--top N points if --top is set; otherwise plots every " + "rank-0 gene. NOTE: passing --pareto suppresses the " + "headline overlay by default — pass --overlay to also " + "plot the overlay.", + ) + p.add_argument( + "--overlay", dest="overlay", action="store_true", default=None, + help="Force the headline stress-strain overlay figure on " + "even when --pareto is given. Without this flag, " + "passing --pareto suppresses the overlay (the bare " + "command without --pareto still shows the overlay by " + "default).", + ) + p.add_argument( + "--color-by", dest="color_by", + choices=("subset_l2", "full_l2", "x", "y", "asymmetry"), + default="subset_l2", + help="Pareto-scatter color metric. 'subset_l2' (default) " + "colors by L2 distance in the projected (X,Y) plane — " + "iso-curves are circles, uniform color along the " + "front signals a true tradeoff curve. 'full_l2' uses " + "all objectives. 'x'/'y' color by a single axis " + "(showing proximity to the Y=0/X=0 plane). 'asymmetry' " + "highlights points that are specialists on one axis " + "vs balanced compromises.", + ) + p.add_argument( + "--save", dest="save", type=Path, default=None, + help="Save the headline figure to this path. Use with " + "--no-show for headless/CI usage.", + ) + p.add_argument( + "--save-pareto", dest="save_pareto", + type=Path, default=None, + help="Save the Pareto-front figure to this path.", + ) + p.add_argument( + "--no-show", dest="show", action="store_false", default=True, + help="Don't open a window (useful with --save in batch).", + ) + return p + + +def main(argv: Optional[Sequence[str]] = None) -> int: + args = _build_parser().parse_args(argv) + + # Auto-promote mode: if the user passed --objective without + # --mode, they almost certainly meant `--mode objective`. The + # alternative — silently using L2 ranking with the objective + # flag ignored — was the source of a real user-reported bug + # ("changing --objective shows the same plot"). So when + # --objective is supplied AND --mode is still the default + # "l2", flip mode to "objective". Users who really want L2 + # ranking with an objective set (a strange combination) can + # pass --mode l2 explicitly... and they'd see the same L2 + # plot they get without --objective, which is consistent. + # + # last-gen mode is incompatible with --objective (the former + # ignores ranking entirely). Error rather than silently + # picking one over the other. + if args.objective is not None: + if args.mode == "last-gen": + print( + "error: --objective is not compatible with " + "--mode last-gen (last-gen has no ranking step).", + file=sys.stderr, + ) + return 1 + if args.mode == "l2": + args.mode = "objective" + + # Resolve overlay default. Robert's report: passing --pareto + # alone should suppress the overlay, since users asking for a + # Pareto plot usually want only that. The --overlay flag is the + # explicit opt-in to also draw the overlay alongside Pareto. + # When --pareto is NOT given, the overlay is the headline + # figure and shows by default (the "user just runs the script + # to look at their results" path). + # + # args.overlay is None when neither flag was set; True when + # --overlay was passed. Resolve to a concrete bool here. + if args.overlay is True: + show_overlay = True + elif args.pareto is not None: + show_overlay = False + else: + show_overlay = True + + try: + if show_overlay: + plot_top_solutions_overlay( + args.workspace, + top_n=args.top_n, + mode=args.mode, + objective=args.objective, + run_id=args.run_id, + experimental_paths=args.experimental, + save=args.save, + show=args.show, + ) + if args.pareto is not None: + plot_pareto_front_with_l2_winner( + args.workspace, + objective_pair=args.pareto, + top_n=args.top_n, + color_by=args.color_by, + run_id=args.run_id, + experimental_paths=args.experimental, + save=args.save_pareto, + show=args.show, + ) + # No-op runs (neither overlay nor pareto) shouldn't happen + # given the resolution above, but guard for paranoia: the + # only way to reach this is --pareto unset AND --overlay + # explicitly false (which the new CLI doesn't expose), or + # the resolution logic above getting confused. + if not show_overlay and args.pareto is None: + print( + "error: nothing to plot. Pass --pareto i,j or " + "remove conflicting flags.", + file=sys.stderr, + ) + return 1 + except (ValueError, RuntimeError, FileNotFoundError) as e: + print(f"error: {e}", file=sys.stderr) + return 1 + except SystemExit as e: + return int(e.code) if e.code is not None else 1 + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/workflows/exaconstit-calibrate/examples/run_nsga3_slurm.sh b/workflows/exaconstit-calibrate/examples/run_nsga3_slurm.sh new file mode 100755 index 0000000..8f372e3 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/run_nsga3_slurm.sh @@ -0,0 +1,178 @@ +#!/bin/sh +# +# Slurm job script for the ExaConstit NSGA-III calibration example. +# +# Most users should only edit: +# 1. The #SBATCH lines. +# 2. The SIMPLE SETTINGS block. +# +# Submit with: +# +# sbatch run_nsga3_slurm.sh +# +# Keep this file in the same directory as nsga3_slurm_helpers.sh unless +# you set HELPER_SCRIPT below to the full path of that helper file. + +# ---------------------------------------------------------------------- +# 1. SLURM SETTINGS +# ---------------------------------------------------------------------- +# +# These lines ask Slurm for compute resources. +# +# Examples: +# +# One short debug job on two nodes: +# #SBATCH -A your_bank_name +# #SBATCH -N 2 +# #SBATCH -n 224 +# #SBATCH -t 01:00:00 +# #SBATCH -p pdebug +# +# A longer job on four nodes: +# #SBATCH -A your_bank_name +# #SBATCH -N 4 +# #SBATCH -n 448 +# #SBATCH -t 08:00:00 +# #SBATCH -p pbatch +# +# Edit the active lines below. Do not put quotes around these values. +# +#SBATCH -A your_bank_name +#SBATCH -N 2 +#SBATCH -n 224 +#SBATCH -t 01:00:00 +#SBATCH -p pdebug +#SBATCH -J exaconstit_nsga3 +#SBATCH -o exaconstit_nsga3.%j.out + +# ---------------------------------------------------------------------- +# 2. SIMPLE SETTINGS +# ---------------------------------------------------------------------- +# +# Change the text to the right of each equals sign. +# Do not add spaces around the equals sign. +# +# Correct: +# ACTION="all" +# +# Incorrect: +# ACTION = "all" + +# Python to use for this workflow. +# +# Example for this LLNL setup: +# PYTHON="/usr/tce/packages/python/python-3.12.2/bin/python" +# +# Example if your environment provides python on PATH: +# PYTHON="python3" +PYTHON="/usr/tce/packages/python/python-3.12.2/bin/python" + +# Where Flux's Python module is installed. +# +# Example for this LLNL setup: +# FLUX_PYTHONPATH="/usr/lib64/flux/python3.12" +# +# Example for a different Python version: +# FLUX_PYTHONPATH="/usr/lib64/flux/python3.11" +FLUX_PYTHONPATH="/usr/lib64/flux/python3.12" + +# What should this job do? +# +# Choose exactly one: +# ACTION="run" starts a new calibration run +# ACTION="resume-latest" resumes from the newest checkpoint +# ACTION="resume-from" resumes from CHECKPOINT_GEN or CHECKPOINT_PATH +# ACTION="inspect" prints tables from an existing run +# ACTION="plots" makes plots from an existing run +# ACTION="postprocess" does inspect and plots, but no new simulations +# ACTION="all" runs calibration, then inspect, then plots +ACTION="run" + +# Resume settings. These only matter when ACTION="resume-from". +# +# Resume from generation 15: +# CHECKPOINT_GEN="15" +# CHECKPOINT_PATH="" +# +# Resume from an exact checkpoint file: +# CHECKPOINT_GEN="" +# CHECKPOINT_PATH="calibration_run/checkpoint_files/checkpoint_gen_15.pkl" +CHECKPOINT_GEN="" +CHECKPOINT_PATH="" + +# Set this to "1" the first time you run, or after Python dependencies +# changed. Set it to "0" for normal runs so you do not spend allocation +# time reinstalling the package. +# +# Example first-time setup: +# INSTALL_PACKAGE="1" +# +# Example normal production run: +# INSTALL_PACKAGE="0" +INSTALL_PACKAGE="0" + +# Where nsga3_calibration.py writes case directories, checkpoints, +# logs, and the SQLite archive. +# +# Most users can leave this as-is. +WORKSPACE="calibration_run" + +# Where post-processing tables and figures should be written. +# +# Most users can leave this as-is. +POST_DIR="postprocess" + +# Number of best solutions to print or plot. +# +# Example: +# TOP_N="5" +# TOP_N="10" +# TOP_N="25" +TOP_N="10" + +# Optional path overrides. +# +# Most users should leave these blank because nsga3_calibration.py can +# find the ExaConstit checkout and common mechanics build directories. +# +# Example if your ExaConstit checkout is somewhere unusual: +# EXACONSTIT_ROOT="/usr/workspace/myname/ExaConstit" +# +# Example if mechanics is in a custom build directory: +# EXACONSTIT_MECHANICS="/usr/workspace/myname/ExaConstit/build_cpu/bin/mechanics" +# For most people, you just need to provide the ExaConstit binary location +EXACONSTIT_ROOT="" +EXACONSTIT_MECHANICS="" + +# Helper script with the actual shell functions. +# +# If nsga3_slurm_helpers.sh is in this same directory, leave this blank. +# +# If you keep the helper somewhere else, use the full path: +# HELPER_SCRIPT="/usr/workspace/myname/scripts/nsga3_slurm_helpers.sh" +HELPER_SCRIPT="" + +# ---------------------------------------------------------------------- +# 3. DO NOT EDIT BELOW THIS LINE FOR NORMAL USE +# ---------------------------------------------------------------------- + +set -eu + +SCRIPT_DIR=$(CDPATH= cd "$(dirname "$0")" && pwd) + +if [ -z "${HELPER_SCRIPT}" ]; then + HELPER_SCRIPT="${SCRIPT_DIR}/nsga3_slurm_helpers.sh" +fi + +if [ ! -f "${HELPER_SCRIPT}" ]; then + echo "ERROR: helper script not found:" + echo " ${HELPER_SCRIPT}" + echo + echo "Keep nsga3_slurm_helpers.sh next to run_nsga3_slurm.sh, or set" + echo "HELPER_SCRIPT to the helper's full path." + exit 2 +fi + +# Load helper functions, then run the workflow selected by ACTION. +. "${HELPER_SCRIPT}" +exa_nsga3_main diff --git a/workflows/exaconstit-calibrate/examples/template_options.toml b/workflows/exaconstit-calibrate/examples/template_options.toml new file mode 100644 index 0000000..192dcb9 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/template_options.toml @@ -0,0 +1,170 @@ +# ExaConstit NSGA-III calibration template. +# +# This file uses the newer ExaConstit options layout from examples/options.toml, +# but keeps only the pieces needed by examples/nsga3_calibration.py. +# +# The calibration driver renders this file once per simulation case. +# Values written with double-percent placeholder syntax are filled +# from SimCase.case_data in +# nsga3_calibration.py before mechanics runs. + +Version = "0.9.0" + +# Keep this fixed at "options" so output files land under +# results/options/, matching the reader paths configured by +# nsga3_calibration.py. +basename = "options" + +# Auto-generated meshes use this grain map. For file-based meshes this +# information may already live in the mesh element attributes. +grain_file = "%%grain_file%%" + +# ===================================== +# MATERIAL DEFINITIONS +# ===================================== + +[[Materials]] + material_name = "voce_fcc" + region_id = 1 + temperature = %%temperature_k%% + mech_type = "exacmech" + + [Materials.Properties] + floc = "%%properties_file%%" + num_props = 17 + + [Materials.State_Vars] + floc = "%%state_vars_file%%" + num_vars = 24 + + [Materials.Grain] + # Matches the legacy voce calibration setup. + ori_state_var_loc = 9 + ori_stride = 4 + ori_type = "quat" + num_grains = 500 + orientation_file = "%%ori_file%%" + grain_file = "%%grain_file%%" + + [Materials.Model] + mech_type = "exacmech" + crystal_plasticity = true + + [Materials.Model.ExaCMech] + # Old layout: + # xtal_type = "fcc" + # slip_type = "powervoce" + # + # New 0.9-style shortcut name: + # evptn_FCC_A = FCC + linear Voce hardening + power-law slip. + shortcut = "evptn_FCC_A" + +# ===================================== +# BOUNDARY CONDITIONS +# ===================================== + +[BCs] + [BCs.time_info] + cycle_dependent = true + cycles = [1] + + # This preserves the existing calibration behavior: the driver + # supplies per-experiment boundary values through %%essential_vals%%. + [[BCs.velocity_bcs]] + essential_ids = [1, 4] + essential_comps = [3, 3] + essential_vals = %%essential_vals%% + + # Keep the newer experimental monotonic deformation helper off by + # default. The calibration example controls loading explicitly via + # velocity_bcs above. + expt_mono_def_flag = false + +# ===================================== +# TIME STEPPING CONTROL +# ===================================== + +[Time] + time_type = "auto" + + [Time.Auto] + dt_start = %%dt_min%% + dt_min = %%dt_min%% + dt_max = %%dt_max%% + dt_scale = %%dt_scale%% + t_final = %%t_final%% + +# ===================================== +# SOLVER SETTINGS +# ===================================== + +[Solvers] + assembly = "EA" + rtmodel = "CPU" + integ_model = "FULL" + + [Solvers.Krylov] + iter = 1000 + rel_tol = 1e-7 + abs_tol = 1e-27 + solver = "CG" + preconditioner = "JACOBI" + print_level = 0 + + [Solvers.NR] + iter = 25 + rel_tol = 5e-5 + abs_tol = 5e-10 + nl_solver = "NR" + +# ===================================== +# VISUALIZATION OUTPUT +# ===================================== + +[Visualizations] + visit = false + paraview = false + adios2 = false + output_frequency = 1 + floc = "visualizations/" + +# ===================================== +# POST-PROCESSING OPTIONS +# ===================================== + +[PostProcessing] + [PostProcessing.volume_averages] + enabled = true + stress = true + def_grad = true + euler_strain = true + plastic_work = true + eq_pl_strain = true + elastic_strain = true + output_frequency = 1 + output_directory = "./results/" + + avg_stress_fname = "avg_stress.txt" + avg_def_grad_fname = "avg_def_grad.txt" + avg_euler_strain_fname = "avg_euler_strain.txt" + avg_pl_work_fname = "avg_pl_work.txt" + avg_eq_pl_strain_fname = "avg_eq_pl_strain.txt" + avg_elastic_strain_fname = "avg_elastic_strain.txt" + + [PostProcessing.projections] + auto_enable_compatible = false + enabled_projections = [] + +# ===================================== +# MESH SETTINGS +# ===================================== + +[Mesh] + type = "auto" + ref_ser = 1 + ref_par = 0 + p_refinement = %%p_refinement%% + + [Mesh.Auto] + mxyz = %%mesh_lengths%% + nxyz = %%mesh_cuts%% diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_global.txt new file mode 100644 index 0000000..f9b5354 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_global.txt @@ -0,0 +1,70 @@ + # Time Volume F11 F12 F13 F21 F22 F23 F31 F32 F33 + 5.00000000e-05 1.00000156e+00 9.99998350e-01 -1.84962184e-08 -5.66846862e-08 -8.91705016e-09 9.99998214e-01 -1.23795590e-07 -1.77937979e-07 2.55892896e-07 1.00000500e+00 + 2.06250000e-04 1.00000645e+00 9.99993194e-01 -7.62951971e-08 -2.33847635e-07 -3.67826043e-08 9.99992631e-01 -5.10652195e-07 -7.33968306e-07 1.05554536e-06 1.00002063e+00 + 6.94531250e-04 1.00002172e+00 9.99977084e-01 -2.56900365e-07 -7.87709412e-07 -1.23860238e-07 9.99975187e-01 -1.71953401e-06 -2.47131056e-06 3.55433392e-06 1.00006945e+00 + 2.22041016e-03 1.00006925e+00 9.99926658e-01 -7.73969082e-07 -2.77847366e-06 -3.70965413e-07 9.99920582e-01 -5.61775210e-06 -7.61207543e-06 1.15160494e-05 1.00022204e+00 + 4.60459595e-03 1.00009813e+00 9.99831231e-01 -2.78751469e-06 -1.73479784e-05 -4.03063251e-06 9.99806582e-01 -2.46207938e-05 -1.68848315e-06 3.58581525e-05 1.00046046e+00 + 6.46724110e-03 1.00010295e+00 9.99751214e-01 -6.14409726e-06 -2.70720587e-05 -8.50983666e-06 9.99705304e-01 -4.12131435e-05 5.26669821e-06 6.00831507e-05 1.00064673e+00 + 8.40749646e-03 1.00010584e+00 9.99667387e-01 -9.94364227e-06 -3.26621132e-05 -1.31620644e-05 9.99598209e-01 -5.58271783e-05 9.76063365e-06 8.35287520e-05 1.00084075e+00 + 1.04285958e-02 1.00010819e+00 9.99580472e-01 -1.34912718e-05 -3.55480452e-05 -1.76362744e-05 9.99485649e-01 -6.82051668e-05 1.29261268e-05 1.06184612e-04 1.00104286e+00 + 1.25339076e-02 1.00011031e+00 9.99490356e-01 -1.68056413e-05 -3.64573871e-05 -2.21636897e-05 9.99367725e-01 -7.81778058e-05 1.50306766e-05 1.27670681e-04 1.00125339e+00 + 1.47269407e-02 1.00011233e+00 9.99396824e-01 -2.00170646e-05 -3.57932403e-05 -2.68164382e-05 9.99244439e-01 -8.65142301e-05 1.61421739e-05 1.48588804e-04 1.00147270e+00 + 1.81535550e-02 1.00011532e+00 9.99251094e-01 -2.46886149e-05 -3.26732709e-05 -3.38661956e-05 9.99051358e-01 -9.71081725e-05 1.61007241e-05 1.79357447e-04 1.00181536e+00 + 2.17229449e-02 1.00011828e+00 9.99099425e-01 -2.95678349e-05 -2.89451155e-05 -4.13043649e-05 9.98850148e-01 -1.06855010e-04 1.54883263e-05 2.09973353e-04 1.00217230e+00 + 2.54410593e-02 1.00012127e+00 9.98941812e-01 -3.44906386e-05 -2.47997009e-05 -4.91171295e-05 9.98640296e-01 -1.16218679e-04 1.44316814e-05 2.40828452e-04 1.00254411e+00 + 2.93140952e-02 1.00012432e+00 9.98778093e-01 -3.93691292e-05 -2.05374196e-05 -5.73252134e-05 9.98421402e-01 -1.25306087e-04 1.31215347e-05 2.72119591e-04 1.00293141e+00 + 3.33485076e-02 1.00012744e+00 9.98608115e-01 -4.41314659e-05 -1.64970889e-05 -6.58840222e-05 9.98193016e-01 -1.34254071e-04 1.16888603e-05 3.03985330e-04 1.00333486e+00 + 3.75510205e-02 1.00013065e+00 9.98431497e-01 -4.88366889e-05 -1.26134168e-05 -7.47090691e-05 9.97954897e-01 -1.43435317e-04 1.00415440e-05 3.36881576e-04 1.00375511e+00 + 4.19286381e-02 1.00013395e+00 9.98247900e-01 -5.35998796e-05 -8.67130430e-06 -8.37813401e-05 9.97706732e-01 -1.52887119e-04 7.77828894e-06 3.70764997e-04 1.00419287e+00 + 4.64886564e-02 1.00013736e+00 9.98056993e-01 -5.85487696e-05 -4.77215069e-06 -9.31139680e-05 9.97448170e-01 -1.62596251e-04 4.71029267e-06 4.05703624e-04 1.00464887e+00 + 5.12386755e-02 1.00014089e+00 9.97858465e-01 -6.38067259e-05 -8.66597080e-07 -1.02683090e-04 9.97178814e-01 -1.72532196e-04 7.57670566e-07 4.41779027e-04 1.00512388e+00 + 5.61866121e-02 1.00014454e+00 9.97652036e-01 -6.94623975e-05 3.20431424e-06 -1.12436472e-04 9.96898207e-01 -1.82616767e-04 -4.07142368e-06 4.78802865e-04 1.00561867e+00 + 6.13407126e-02 1.00014832e+00 9.97437385e-01 -7.54279991e-05 7.36368938e-06 -1.22335209e-04 9.96605903e-01 -1.92824523e-04 -9.63971249e-06 5.16679729e-04 1.00613408e+00 + 6.67095674e-02 1.00015223e+00 9.97214210e-01 -8.17285911e-05 1.14029162e-05 -1.32315535e-04 9.96301410e-01 -2.03153279e-04 -1.57299138e-05 5.55472172e-04 1.00667097e+00 + 7.23021245e-02 1.00015628e+00 9.96982178e-01 -8.84432763e-05 1.53819474e-05 -1.42379879e-04 9.95984234e-01 -2.13553828e-04 -2.24094459e-05 5.95102530e-04 1.00723023e+00 + 7.81277047e-02 1.00016048e+00 9.96740891e-01 -9.56077521e-05 1.92549531e-05 -1.52547591e-04 9.95653913e-01 -2.23996200e-04 -2.96229587e-05 6.35628811e-04 1.00781278e+00 + 8.41960175e-02 1.00016483e+00 9.96489839e-01 -1.03216583e-04 2.30373399e-05 -1.62880863e-04 9.95310069e-01 -2.34588650e-04 -3.73538013e-05 6.77222474e-04 1.00841962e+00 + 9.05171766e-02 1.00016934e+00 9.96228495e-01 -1.11219010e-04 2.68172824e-05 -1.73407937e-04 9.94952301e-01 -2.45454155e-04 -4.56684375e-05 7.20050230e-04 1.00905173e+00 + 1.00393988e-01 1.00017658e+00 9.95820669e-01 -1.23836306e-04 3.22481862e-05 -1.89376671e-04 9.94394167e-01 -2.62068219e-04 -5.90023125e-05 7.86152925e-04 1.01003942e+00 + 1.10682333e-01 1.00018407e+00 9.95396554e-01 -1.36987588e-04 3.73908541e-05 -2.05480338e-04 9.93813591e-01 -2.78885529e-04 -7.31060587e-05 8.54201342e-04 1.01106826e+00 + 1.21399359e-01 1.00019182e+00 9.94955661e-01 -1.50667769e-04 4.21206147e-05 -2.21687055e-04 9.93209577e-01 -2.95970347e-04 -8.78265491e-05 9.24383834e-04 1.01213996e+00 + 1.32562927e-01 1.00019984e+00 9.94497399e-01 -1.64955716e-04 4.64689827e-05 -2.38064231e-04 9.92581177e-01 -3.13407000e-04 -1.03345563e-04 9.96722968e-04 1.01325632e+00 + 1.44191645e-01 1.00020813e+00 9.94021075e-01 -1.79930319e-04 5.05501326e-05 -2.54658986e-04 9.91927493e-01 -3.31356885e-04 -1.19810764e-04 1.07124763e-03 1.01441920e+00 + 1.56304892e-01 1.00021672e+00 9.93526040e-01 -1.95666426e-04 5.44921723e-05 -2.71436295e-04 9.91247526e-01 -3.49836582e-04 -1.37265068e-04 1.14786651e-03 1.01563053e+00 + 1.68922858e-01 1.00022560e+00 9.93011647e-01 -2.12201589e-04 5.82787669e-05 -2.88524182e-04 9.90540220e-01 -3.68743345e-04 -1.55498964e-04 1.22666291e-03 1.01689233e+00 + 1.82066573e-01 1.00023479e+00 9.92477148e-01 -2.29530093e-04 6.19303455e-05 -3.05969185e-04 9.89804567e-01 -3.88111875e-04 -1.74260087e-04 1.30795026e-03 1.01820671e+00 + 1.95757942e-01 1.00024429e+00 9.91921872e-01 -2.47660909e-04 6.54249816e-05 -3.23803663e-04 9.89039417e-01 -4.07995611e-04 -1.93597931e-04 1.39179474e-03 1.01957585e+00 + 2.10019785e-01 1.00025412e+00 9.91345186e-01 -2.66645939e-04 6.87408902e-05 -3.42000702e-04 9.88243524e-01 -4.28438562e-04 -2.13820021e-04 1.47822065e-03 1.02100204e+00 + 2.24875872e-01 1.00026429e+00 9.90746315e-01 -2.86502523e-04 7.18531705e-05 -3.60522923e-04 9.87415724e-01 -4.49407244e-04 -2.35128096e-04 1.56722767e-03 1.02248766e+00 + 2.40350962e-01 1.00027479e+00 9.90124494e-01 -3.07259538e-04 7.47357212e-05 -3.79329678e-04 9.86554781e-01 -4.70920914e-04 -2.57617802e-04 1.65888019e-03 1.02403518e+00 + 2.56470847e-01 1.00028566e+00 9.89478853e-01 -3.28904874e-04 7.73289205e-05 -3.98387239e-04 9.85659503e-01 -4.92851333e-04 -2.81233310e-04 1.75311505e-03 1.02564717e+00 + 2.73262395e-01 1.00029689e+00 9.88808495e-01 -3.51426710e-04 7.95891478e-05 -4.17649810e-04 9.84728658e-01 -5.15227551e-04 -3.05984626e-04 1.85007302e-03 1.02732634e+00 + 2.90753590e-01 1.00030849e+00 9.88112465e-01 -3.74819243e-04 8.14990535e-05 -4.37060637e-04 9.83761003e-01 -5.38122704e-04 -3.31990950e-04 1.95004267e-03 1.02907547e+00 + 3.08973585e-01 1.00032048e+00 9.87389805e-01 -3.99106530e-04 8.30745643e-05 -4.56585032e-04 9.82755236e-01 -5.61609102e-04 -3.59319924e-04 2.05333953e-03 1.03089748e+00 + 3.27952746e-01 1.00033288e+00 9.86639532e-01 -4.24429379e-04 8.43426788e-05 -4.76219795e-04 9.81710009e-01 -5.85619335e-04 -3.88004003e-04 2.15999815e-03 1.03279541e+00 + 3.47722706e-01 1.00034568e+00 9.85860592e-01 -4.50907428e-04 8.52769164e-05 -4.96030523e-04 9.80623977e-01 -6.10187086e-04 -4.18184587e-04 2.27007355e-03 1.03477242e+00 + 3.68316415e-01 1.00035891e+00 9.85051949e-01 -4.78590557e-04 8.58712253e-05 -5.15961220e-04 9.79495711e-01 -6.35274899e-04 -4.50201861e-04 2.38352978e-03 1.03683181e+00 + 3.89768194e-01 1.00037257e+00 9.84212618e-01 -5.07521181e-04 8.60299403e-05 -5.35849019e-04 9.78323659e-01 -6.60897056e-04 -4.84229640e-04 2.50018789e-03 1.03897701e+00 + 4.12113798e-01 1.00038668e+00 9.83341653e-01 -5.37729958e-04 8.55503152e-05 -5.55520564e-04 9.77106162e-01 -6.87034888e-04 -5.20170233e-04 2.61990689e-03 1.04121159e+00 + 4.35390468e-01 1.00040126e+00 9.82438076e-01 -5.69242539e-04 8.42968525e-05 -5.74878715e-04 9.75841523e-01 -7.13515322e-04 -5.57938119e-04 2.74243285e-03 1.04353928e+00 + 4.59637000e-01 1.00041631e+00 9.81500843e-01 -6.02083775e-04 8.21360492e-05 -5.93863176e-04 9.74528040e-01 -7.40200462e-04 -5.97617967e-04 2.86761346e-03 1.04596396e+00 + 4.84893803e-01 1.00043187e+00 9.80528856e-01 -6.36292220e-04 7.91032383e-05 -6.12445296e-04 9.73163991e-01 -7.67208828e-04 -6.39539193e-04 2.99583970e-03 1.04848967e+00 + 5.11202974e-01 1.00044793e+00 9.79521022e-01 -6.71892512e-04 7.53101403e-05 -6.30516821e-04 9.71747574e-01 -7.94677393e-04 -6.83905833e-04 3.12752360e-03 1.05112061e+00 + 5.38608360e-01 1.00046452e+00 9.78476237e-01 -7.08907363e-04 7.08113511e-05 -6.47989016e-04 9.70276939e-01 -8.22517601e-04 -7.30819899e-04 3.26254584e-03 1.05386118e+00 + 5.67155637e-01 1.00048166e+00 9.77393378e-01 -7.47370730e-04 6.56323127e-05 -6.64799978e-04 9.68750175e-01 -8.50615779e-04 -7.80313941e-04 3.40054401e-03 1.05671595e+00 + 5.96892384e-01 1.00049936e+00 9.76271331e-01 -7.87379799e-04 5.95184961e-05 -6.80908616e-04 9.67165302e-01 -8.79045710e-04 -8.32187254e-04 3.54116993e-03 1.05968966e+00 + 6.27868162e-01 1.00051766e+00 9.75108950e-01 -8.28993638e-04 5.22634895e-05 -6.96273117e-04 9.65520292e-01 -9.07802050e-04 -8.86526141e-04 3.68381703e-03 1.06278728e+00 + 6.60134598e-01 1.00053657e+00 9.73905027e-01 -8.72298656e-04 4.38578544e-05 -7.10793830e-04 9.63813115e-01 -9.36878465e-04 -9.43749332e-04 3.82774360e-03 1.06601397e+00 + 6.93745468e-01 1.00055611e+00 9.72658348e-01 -9.17361037e-04 3.45466602e-05 -7.24268785e-04 9.62041683e-01 -9.66014864e-04 -1.00421765e-03 3.97223618e-03 1.06937511e+00 + 7.28756791e-01 1.00057632e+00 9.71367647e-01 -9.64361159e-04 2.40335546e-05 -7.36597318e-04 9.60203908e-01 -9.94873638e-04 -1.06829173e-03 4.11668420e-03 1.07287630e+00 + 7.65226920e-01 1.00059722e+00 9.70031816e-01 -1.01333379e-03 1.21468611e-05 -7.47476869e-04 9.58297484e-01 -1.02327507e-03 -1.13632680e-03 4.26078321e-03 1.07652337e+00 + 8.03216637e-01 1.00061884e+00 9.68649705e-01 -1.06422197e-03 -1.37168203e-06 -7.56443357e-04 9.56320075e-01 -1.05108603e-03 -1.20777190e-03 4.40400145e-03 1.08032241e+00 + 8.42789259e-01 1.00064122e+00 9.67220184e-01 -1.11688812e-03 -1.60316789e-05 -7.63158388e-04 9.54269264e-01 -1.07807270e-03 -1.28193196e-03 4.54619186e-03 1.08427974e+00 + 8.84010740e-01 1.00066439e+00 9.65741931e-01 -1.17132391e-03 -3.21587952e-05 -7.67404000e-04 9.52142794e-01 -1.10415877e-03 -1.35836093e-03 4.68663976e-03 1.08840196e+00 + 9.26949783e-01 1.00068840e+00 9.64213599e-01 -1.22753082e-03 -4.98263349e-05 -7.69096293e-04 9.49938364e-01 -1.12904327e-03 -1.43802551e-03 4.82501186e-03 1.09269595e+00 + 9.71677953e-01 1.00071327e+00 9.62633984e-01 -1.28484284e-03 -6.84232571e-05 -7.67803623e-04 9.47653464e-01 -1.15304488e-03 -1.52200539e-03 4.96188638e-03 1.09716886e+00 + 1.01826980e+00 1.00073907e+00 9.61002476e-01 -1.34251678e-03 -8.74462880e-05 -7.63282000e-04 9.45284969e-01 -1.17615920e-03 -1.61097450e-03 5.09650796e-03 1.10182814e+00 + 1.06680297e+00 1.00076583e+00 9.59317939e-01 -1.40080612e-03 -1.06790146e-04 -7.55200854e-04 9.42830253e-01 -1.19865534e-03 -1.70468118e-03 5.22761202e-03 1.10668157e+00 + 1.11735835e+00 1.00079361e+00 9.57579486e-01 -1.45932417e-03 -1.26476397e-04 -7.43543283e-04 9.40286396e-01 -1.22045888e-03 -1.80396036e-03 5.35415003e-03 1.11173723e+00 + 1.17002021e+00 1.00082246e+00 9.55786084e-01 -1.51811558e-03 -1.47181890e-04 -7.28906672e-04 9.37650607e-01 -1.24166244e-03 -1.90889736e-03 5.47673172e-03 1.11700354e+00 + 1.20000000e+00 1.00083445e+00 9.54770799e-01 -1.55109051e-03 -1.59154169e-04 -7.19984587e-04 9.36154545e-01 -1.25326611e-03 -1.96915227e-03 5.54413827e-03 1.12000159e+00 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_region_default_1.txt new file mode 100644 index 0000000..f9b5354 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_def_grad_region_default_1.txt @@ -0,0 +1,70 @@ + # Time Volume F11 F12 F13 F21 F22 F23 F31 F32 F33 + 5.00000000e-05 1.00000156e+00 9.99998350e-01 -1.84962184e-08 -5.66846862e-08 -8.91705016e-09 9.99998214e-01 -1.23795590e-07 -1.77937979e-07 2.55892896e-07 1.00000500e+00 + 2.06250000e-04 1.00000645e+00 9.99993194e-01 -7.62951971e-08 -2.33847635e-07 -3.67826043e-08 9.99992631e-01 -5.10652195e-07 -7.33968306e-07 1.05554536e-06 1.00002063e+00 + 6.94531250e-04 1.00002172e+00 9.99977084e-01 -2.56900365e-07 -7.87709412e-07 -1.23860238e-07 9.99975187e-01 -1.71953401e-06 -2.47131056e-06 3.55433392e-06 1.00006945e+00 + 2.22041016e-03 1.00006925e+00 9.99926658e-01 -7.73969082e-07 -2.77847366e-06 -3.70965413e-07 9.99920582e-01 -5.61775210e-06 -7.61207543e-06 1.15160494e-05 1.00022204e+00 + 4.60459595e-03 1.00009813e+00 9.99831231e-01 -2.78751469e-06 -1.73479784e-05 -4.03063251e-06 9.99806582e-01 -2.46207938e-05 -1.68848315e-06 3.58581525e-05 1.00046046e+00 + 6.46724110e-03 1.00010295e+00 9.99751214e-01 -6.14409726e-06 -2.70720587e-05 -8.50983666e-06 9.99705304e-01 -4.12131435e-05 5.26669821e-06 6.00831507e-05 1.00064673e+00 + 8.40749646e-03 1.00010584e+00 9.99667387e-01 -9.94364227e-06 -3.26621132e-05 -1.31620644e-05 9.99598209e-01 -5.58271783e-05 9.76063365e-06 8.35287520e-05 1.00084075e+00 + 1.04285958e-02 1.00010819e+00 9.99580472e-01 -1.34912718e-05 -3.55480452e-05 -1.76362744e-05 9.99485649e-01 -6.82051668e-05 1.29261268e-05 1.06184612e-04 1.00104286e+00 + 1.25339076e-02 1.00011031e+00 9.99490356e-01 -1.68056413e-05 -3.64573871e-05 -2.21636897e-05 9.99367725e-01 -7.81778058e-05 1.50306766e-05 1.27670681e-04 1.00125339e+00 + 1.47269407e-02 1.00011233e+00 9.99396824e-01 -2.00170646e-05 -3.57932403e-05 -2.68164382e-05 9.99244439e-01 -8.65142301e-05 1.61421739e-05 1.48588804e-04 1.00147270e+00 + 1.81535550e-02 1.00011532e+00 9.99251094e-01 -2.46886149e-05 -3.26732709e-05 -3.38661956e-05 9.99051358e-01 -9.71081725e-05 1.61007241e-05 1.79357447e-04 1.00181536e+00 + 2.17229449e-02 1.00011828e+00 9.99099425e-01 -2.95678349e-05 -2.89451155e-05 -4.13043649e-05 9.98850148e-01 -1.06855010e-04 1.54883263e-05 2.09973353e-04 1.00217230e+00 + 2.54410593e-02 1.00012127e+00 9.98941812e-01 -3.44906386e-05 -2.47997009e-05 -4.91171295e-05 9.98640296e-01 -1.16218679e-04 1.44316814e-05 2.40828452e-04 1.00254411e+00 + 2.93140952e-02 1.00012432e+00 9.98778093e-01 -3.93691292e-05 -2.05374196e-05 -5.73252134e-05 9.98421402e-01 -1.25306087e-04 1.31215347e-05 2.72119591e-04 1.00293141e+00 + 3.33485076e-02 1.00012744e+00 9.98608115e-01 -4.41314659e-05 -1.64970889e-05 -6.58840222e-05 9.98193016e-01 -1.34254071e-04 1.16888603e-05 3.03985330e-04 1.00333486e+00 + 3.75510205e-02 1.00013065e+00 9.98431497e-01 -4.88366889e-05 -1.26134168e-05 -7.47090691e-05 9.97954897e-01 -1.43435317e-04 1.00415440e-05 3.36881576e-04 1.00375511e+00 + 4.19286381e-02 1.00013395e+00 9.98247900e-01 -5.35998796e-05 -8.67130430e-06 -8.37813401e-05 9.97706732e-01 -1.52887119e-04 7.77828894e-06 3.70764997e-04 1.00419287e+00 + 4.64886564e-02 1.00013736e+00 9.98056993e-01 -5.85487696e-05 -4.77215069e-06 -9.31139680e-05 9.97448170e-01 -1.62596251e-04 4.71029267e-06 4.05703624e-04 1.00464887e+00 + 5.12386755e-02 1.00014089e+00 9.97858465e-01 -6.38067259e-05 -8.66597080e-07 -1.02683090e-04 9.97178814e-01 -1.72532196e-04 7.57670566e-07 4.41779027e-04 1.00512388e+00 + 5.61866121e-02 1.00014454e+00 9.97652036e-01 -6.94623975e-05 3.20431424e-06 -1.12436472e-04 9.96898207e-01 -1.82616767e-04 -4.07142368e-06 4.78802865e-04 1.00561867e+00 + 6.13407126e-02 1.00014832e+00 9.97437385e-01 -7.54279991e-05 7.36368938e-06 -1.22335209e-04 9.96605903e-01 -1.92824523e-04 -9.63971249e-06 5.16679729e-04 1.00613408e+00 + 6.67095674e-02 1.00015223e+00 9.97214210e-01 -8.17285911e-05 1.14029162e-05 -1.32315535e-04 9.96301410e-01 -2.03153279e-04 -1.57299138e-05 5.55472172e-04 1.00667097e+00 + 7.23021245e-02 1.00015628e+00 9.96982178e-01 -8.84432763e-05 1.53819474e-05 -1.42379879e-04 9.95984234e-01 -2.13553828e-04 -2.24094459e-05 5.95102530e-04 1.00723023e+00 + 7.81277047e-02 1.00016048e+00 9.96740891e-01 -9.56077521e-05 1.92549531e-05 -1.52547591e-04 9.95653913e-01 -2.23996200e-04 -2.96229587e-05 6.35628811e-04 1.00781278e+00 + 8.41960175e-02 1.00016483e+00 9.96489839e-01 -1.03216583e-04 2.30373399e-05 -1.62880863e-04 9.95310069e-01 -2.34588650e-04 -3.73538013e-05 6.77222474e-04 1.00841962e+00 + 9.05171766e-02 1.00016934e+00 9.96228495e-01 -1.11219010e-04 2.68172824e-05 -1.73407937e-04 9.94952301e-01 -2.45454155e-04 -4.56684375e-05 7.20050230e-04 1.00905173e+00 + 1.00393988e-01 1.00017658e+00 9.95820669e-01 -1.23836306e-04 3.22481862e-05 -1.89376671e-04 9.94394167e-01 -2.62068219e-04 -5.90023125e-05 7.86152925e-04 1.01003942e+00 + 1.10682333e-01 1.00018407e+00 9.95396554e-01 -1.36987588e-04 3.73908541e-05 -2.05480338e-04 9.93813591e-01 -2.78885529e-04 -7.31060587e-05 8.54201342e-04 1.01106826e+00 + 1.21399359e-01 1.00019182e+00 9.94955661e-01 -1.50667769e-04 4.21206147e-05 -2.21687055e-04 9.93209577e-01 -2.95970347e-04 -8.78265491e-05 9.24383834e-04 1.01213996e+00 + 1.32562927e-01 1.00019984e+00 9.94497399e-01 -1.64955716e-04 4.64689827e-05 -2.38064231e-04 9.92581177e-01 -3.13407000e-04 -1.03345563e-04 9.96722968e-04 1.01325632e+00 + 1.44191645e-01 1.00020813e+00 9.94021075e-01 -1.79930319e-04 5.05501326e-05 -2.54658986e-04 9.91927493e-01 -3.31356885e-04 -1.19810764e-04 1.07124763e-03 1.01441920e+00 + 1.56304892e-01 1.00021672e+00 9.93526040e-01 -1.95666426e-04 5.44921723e-05 -2.71436295e-04 9.91247526e-01 -3.49836582e-04 -1.37265068e-04 1.14786651e-03 1.01563053e+00 + 1.68922858e-01 1.00022560e+00 9.93011647e-01 -2.12201589e-04 5.82787669e-05 -2.88524182e-04 9.90540220e-01 -3.68743345e-04 -1.55498964e-04 1.22666291e-03 1.01689233e+00 + 1.82066573e-01 1.00023479e+00 9.92477148e-01 -2.29530093e-04 6.19303455e-05 -3.05969185e-04 9.89804567e-01 -3.88111875e-04 -1.74260087e-04 1.30795026e-03 1.01820671e+00 + 1.95757942e-01 1.00024429e+00 9.91921872e-01 -2.47660909e-04 6.54249816e-05 -3.23803663e-04 9.89039417e-01 -4.07995611e-04 -1.93597931e-04 1.39179474e-03 1.01957585e+00 + 2.10019785e-01 1.00025412e+00 9.91345186e-01 -2.66645939e-04 6.87408902e-05 -3.42000702e-04 9.88243524e-01 -4.28438562e-04 -2.13820021e-04 1.47822065e-03 1.02100204e+00 + 2.24875872e-01 1.00026429e+00 9.90746315e-01 -2.86502523e-04 7.18531705e-05 -3.60522923e-04 9.87415724e-01 -4.49407244e-04 -2.35128096e-04 1.56722767e-03 1.02248766e+00 + 2.40350962e-01 1.00027479e+00 9.90124494e-01 -3.07259538e-04 7.47357212e-05 -3.79329678e-04 9.86554781e-01 -4.70920914e-04 -2.57617802e-04 1.65888019e-03 1.02403518e+00 + 2.56470847e-01 1.00028566e+00 9.89478853e-01 -3.28904874e-04 7.73289205e-05 -3.98387239e-04 9.85659503e-01 -4.92851333e-04 -2.81233310e-04 1.75311505e-03 1.02564717e+00 + 2.73262395e-01 1.00029689e+00 9.88808495e-01 -3.51426710e-04 7.95891478e-05 -4.17649810e-04 9.84728658e-01 -5.15227551e-04 -3.05984626e-04 1.85007302e-03 1.02732634e+00 + 2.90753590e-01 1.00030849e+00 9.88112465e-01 -3.74819243e-04 8.14990535e-05 -4.37060637e-04 9.83761003e-01 -5.38122704e-04 -3.31990950e-04 1.95004267e-03 1.02907547e+00 + 3.08973585e-01 1.00032048e+00 9.87389805e-01 -3.99106530e-04 8.30745643e-05 -4.56585032e-04 9.82755236e-01 -5.61609102e-04 -3.59319924e-04 2.05333953e-03 1.03089748e+00 + 3.27952746e-01 1.00033288e+00 9.86639532e-01 -4.24429379e-04 8.43426788e-05 -4.76219795e-04 9.81710009e-01 -5.85619335e-04 -3.88004003e-04 2.15999815e-03 1.03279541e+00 + 3.47722706e-01 1.00034568e+00 9.85860592e-01 -4.50907428e-04 8.52769164e-05 -4.96030523e-04 9.80623977e-01 -6.10187086e-04 -4.18184587e-04 2.27007355e-03 1.03477242e+00 + 3.68316415e-01 1.00035891e+00 9.85051949e-01 -4.78590557e-04 8.58712253e-05 -5.15961220e-04 9.79495711e-01 -6.35274899e-04 -4.50201861e-04 2.38352978e-03 1.03683181e+00 + 3.89768194e-01 1.00037257e+00 9.84212618e-01 -5.07521181e-04 8.60299403e-05 -5.35849019e-04 9.78323659e-01 -6.60897056e-04 -4.84229640e-04 2.50018789e-03 1.03897701e+00 + 4.12113798e-01 1.00038668e+00 9.83341653e-01 -5.37729958e-04 8.55503152e-05 -5.55520564e-04 9.77106162e-01 -6.87034888e-04 -5.20170233e-04 2.61990689e-03 1.04121159e+00 + 4.35390468e-01 1.00040126e+00 9.82438076e-01 -5.69242539e-04 8.42968525e-05 -5.74878715e-04 9.75841523e-01 -7.13515322e-04 -5.57938119e-04 2.74243285e-03 1.04353928e+00 + 4.59637000e-01 1.00041631e+00 9.81500843e-01 -6.02083775e-04 8.21360492e-05 -5.93863176e-04 9.74528040e-01 -7.40200462e-04 -5.97617967e-04 2.86761346e-03 1.04596396e+00 + 4.84893803e-01 1.00043187e+00 9.80528856e-01 -6.36292220e-04 7.91032383e-05 -6.12445296e-04 9.73163991e-01 -7.67208828e-04 -6.39539193e-04 2.99583970e-03 1.04848967e+00 + 5.11202974e-01 1.00044793e+00 9.79521022e-01 -6.71892512e-04 7.53101403e-05 -6.30516821e-04 9.71747574e-01 -7.94677393e-04 -6.83905833e-04 3.12752360e-03 1.05112061e+00 + 5.38608360e-01 1.00046452e+00 9.78476237e-01 -7.08907363e-04 7.08113511e-05 -6.47989016e-04 9.70276939e-01 -8.22517601e-04 -7.30819899e-04 3.26254584e-03 1.05386118e+00 + 5.67155637e-01 1.00048166e+00 9.77393378e-01 -7.47370730e-04 6.56323127e-05 -6.64799978e-04 9.68750175e-01 -8.50615779e-04 -7.80313941e-04 3.40054401e-03 1.05671595e+00 + 5.96892384e-01 1.00049936e+00 9.76271331e-01 -7.87379799e-04 5.95184961e-05 -6.80908616e-04 9.67165302e-01 -8.79045710e-04 -8.32187254e-04 3.54116993e-03 1.05968966e+00 + 6.27868162e-01 1.00051766e+00 9.75108950e-01 -8.28993638e-04 5.22634895e-05 -6.96273117e-04 9.65520292e-01 -9.07802050e-04 -8.86526141e-04 3.68381703e-03 1.06278728e+00 + 6.60134598e-01 1.00053657e+00 9.73905027e-01 -8.72298656e-04 4.38578544e-05 -7.10793830e-04 9.63813115e-01 -9.36878465e-04 -9.43749332e-04 3.82774360e-03 1.06601397e+00 + 6.93745468e-01 1.00055611e+00 9.72658348e-01 -9.17361037e-04 3.45466602e-05 -7.24268785e-04 9.62041683e-01 -9.66014864e-04 -1.00421765e-03 3.97223618e-03 1.06937511e+00 + 7.28756791e-01 1.00057632e+00 9.71367647e-01 -9.64361159e-04 2.40335546e-05 -7.36597318e-04 9.60203908e-01 -9.94873638e-04 -1.06829173e-03 4.11668420e-03 1.07287630e+00 + 7.65226920e-01 1.00059722e+00 9.70031816e-01 -1.01333379e-03 1.21468611e-05 -7.47476869e-04 9.58297484e-01 -1.02327507e-03 -1.13632680e-03 4.26078321e-03 1.07652337e+00 + 8.03216637e-01 1.00061884e+00 9.68649705e-01 -1.06422197e-03 -1.37168203e-06 -7.56443357e-04 9.56320075e-01 -1.05108603e-03 -1.20777190e-03 4.40400145e-03 1.08032241e+00 + 8.42789259e-01 1.00064122e+00 9.67220184e-01 -1.11688812e-03 -1.60316789e-05 -7.63158388e-04 9.54269264e-01 -1.07807270e-03 -1.28193196e-03 4.54619186e-03 1.08427974e+00 + 8.84010740e-01 1.00066439e+00 9.65741931e-01 -1.17132391e-03 -3.21587952e-05 -7.67404000e-04 9.52142794e-01 -1.10415877e-03 -1.35836093e-03 4.68663976e-03 1.08840196e+00 + 9.26949783e-01 1.00068840e+00 9.64213599e-01 -1.22753082e-03 -4.98263349e-05 -7.69096293e-04 9.49938364e-01 -1.12904327e-03 -1.43802551e-03 4.82501186e-03 1.09269595e+00 + 9.71677953e-01 1.00071327e+00 9.62633984e-01 -1.28484284e-03 -6.84232571e-05 -7.67803623e-04 9.47653464e-01 -1.15304488e-03 -1.52200539e-03 4.96188638e-03 1.09716886e+00 + 1.01826980e+00 1.00073907e+00 9.61002476e-01 -1.34251678e-03 -8.74462880e-05 -7.63282000e-04 9.45284969e-01 -1.17615920e-03 -1.61097450e-03 5.09650796e-03 1.10182814e+00 + 1.06680297e+00 1.00076583e+00 9.59317939e-01 -1.40080612e-03 -1.06790146e-04 -7.55200854e-04 9.42830253e-01 -1.19865534e-03 -1.70468118e-03 5.22761202e-03 1.10668157e+00 + 1.11735835e+00 1.00079361e+00 9.57579486e-01 -1.45932417e-03 -1.26476397e-04 -7.43543283e-04 9.40286396e-01 -1.22045888e-03 -1.80396036e-03 5.35415003e-03 1.11173723e+00 + 1.17002021e+00 1.00082246e+00 9.55786084e-01 -1.51811558e-03 -1.47181890e-04 -7.28906672e-04 9.37650607e-01 -1.24166244e-03 -1.90889736e-03 5.47673172e-03 1.11700354e+00 + 1.20000000e+00 1.00083445e+00 9.54770799e-01 -1.55109051e-03 -1.59154169e-04 -7.19984587e-04 9.36154545e-01 -1.25326611e-03 -1.96915227e-03 5.54413827e-03 1.12000159e+00 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_global.txt new file mode 100644 index 0000000..5d9b14d --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_global.txt @@ -0,0 +1,70 @@ + # Time Volume Ee11 Ee22 Ee33 Ee23 Ee13 Ee12 + 5.00000000e-05 1.00000156e+00 -6.52013073e-08 -5.97936310e-08 7.28502963e-07 3.45182917e-08 -3.57852082e-08 6.12148327e-10 + 2.06250000e-04 1.00000645e+00 -2.68957051e-07 -2.46650053e-07 3.00503720e-06 1.42384596e-07 -1.47612881e-07 2.52686095e-09 + 6.94531250e-04 1.00002172e+00 -9.05709931e-07 -8.30589312e-07 1.01188403e-05 4.79434009e-07 -4.97063589e-07 8.52741736e-09 + 2.22041016e-03 1.00006925e+00 -2.91225111e-06 -2.64889063e-06 3.22580272e-05 1.53065829e-06 -1.58227518e-06 2.62430111e-08 + 4.60459595e-03 1.00009813e+00 -3.97076821e-06 -5.01054952e-06 4.54759638e-05 2.43543480e-06 -2.32007153e-06 -5.48978424e-07 + 6.46724110e-03 1.00010295e+00 -3.93860268e-06 -5.36296780e-06 4.73212657e-05 2.57872457e-06 -2.35800859e-06 -9.26377382e-07 + 8.40749646e-03 1.00010584e+00 -3.97496267e-06 -5.39462816e-06 4.83007254e-05 2.61158945e-06 -2.37888128e-06 -1.13719379e-06 + 1.04285958e-02 1.00010819e+00 -4.05124642e-06 -5.38559335e-06 4.90854332e-05 2.60748018e-06 -2.41106814e-06 -1.27637843e-06 + 1.25339076e-02 1.00011031e+00 -4.13745727e-06 -5.37296772e-06 4.98009255e-05 2.59475395e-06 -2.44829089e-06 -1.39809418e-06 + 1.47269407e-02 1.00011233e+00 -4.23992464e-06 -5.35766896e-06 5.04884952e-05 2.58310242e-06 -2.48594454e-06 -1.51061843e-06 + 1.81535550e-02 1.00011532e+00 -4.41146324e-06 -5.33576067e-06 5.15140000e-05 2.56595128e-06 -2.55701787e-06 -1.65738911e-06 + 2.17229449e-02 1.00011828e+00 -4.57796769e-06 -5.32089312e-06 5.25581503e-05 2.54992556e-06 -2.64561914e-06 -1.79300897e-06 + 2.54410593e-02 1.00012127e+00 -4.73933113e-06 -5.30902712e-06 5.36379171e-05 2.53006779e-06 -2.74562321e-06 -1.92409375e-06 + 2.93140952e-02 1.00012432e+00 -4.89803093e-06 -5.30988734e-06 5.47540971e-05 2.50736021e-06 -2.84920865e-06 -2.05368859e-06 + 3.33485076e-02 1.00012744e+00 -5.05152954e-06 -5.33242791e-06 5.59110155e-05 2.48636148e-06 -2.94891290e-06 -2.17996544e-06 + 3.75510205e-02 1.00013065e+00 -5.19197970e-06 -5.38208898e-06 5.71136216e-05 2.46865091e-06 -3.04449601e-06 -2.30679572e-06 + 4.19286381e-02 1.00013395e+00 -5.32749661e-06 -5.45140627e-06 5.83623310e-05 2.45571277e-06 -3.13720758e-06 -2.43621406e-06 + 4.64886564e-02 1.00013736e+00 -5.46017167e-06 -5.53634801e-06 5.96603472e-05 2.44644077e-06 -3.23025982e-06 -2.56405701e-06 + 5.12386755e-02 1.00014089e+00 -5.59387302e-06 -5.63739263e-06 6.10076953e-05 2.43893842e-06 -3.32617874e-06 -2.68697055e-06 + 5.61866121e-02 1.00014454e+00 -5.73385182e-06 -5.74874615e-06 6.24086297e-05 2.43370473e-06 -3.42713299e-06 -2.80142179e-06 + 6.13407126e-02 1.00014832e+00 -5.87798238e-06 -5.87148445e-06 6.38648901e-05 2.43074917e-06 -3.53389034e-06 -2.91162757e-06 + 6.67095674e-02 1.00015223e+00 -6.01956587e-06 -6.01042812e-06 6.53773719e-05 2.42785445e-06 -3.64697345e-06 -3.01850026e-06 + 7.23021245e-02 1.00015628e+00 -6.16158872e-06 -6.16437060e-06 6.69439195e-05 2.42581408e-06 -3.76292995e-06 -3.12308441e-06 + 7.81277047e-02 1.00016048e+00 -6.30776886e-06 -6.33207041e-06 6.85655002e-05 2.42532320e-06 -3.87918606e-06 -3.22522159e-06 + 8.41960175e-02 1.00016483e+00 -6.45788900e-06 -6.51154316e-06 7.02454703e-05 2.42439527e-06 -3.99638215e-06 -3.32606018e-06 + 9.05171766e-02 1.00016934e+00 -6.61616744e-06 -6.69766342e-06 7.19874050e-05 2.42183742e-06 -4.11296261e-06 -3.42642018e-06 + 1.00393988e-01 1.00017658e+00 -6.86691525e-06 -6.98759658e-06 7.46834668e-05 2.42313687e-06 -4.28746784e-06 -3.57297489e-06 + 1.10682333e-01 1.00018407e+00 -7.13005577e-06 -7.29181441e-06 7.74622889e-05 2.43150666e-06 -4.46271825e-06 -3.71909046e-06 + 1.21399359e-01 1.00019182e+00 -7.40071529e-06 -7.61122370e-06 8.03242505e-05 2.44579697e-06 -4.63848697e-06 -3.87009415e-06 + 1.32562927e-01 1.00019984e+00 -7.68007824e-06 -7.94379900e-06 8.32689786e-05 2.46307545e-06 -4.81659330e-06 -4.02662794e-06 + 1.44191645e-01 1.00020813e+00 -7.96838850e-06 -8.28604890e-06 8.62960357e-05 2.48250147e-06 -4.99675667e-06 -4.18552378e-06 + 1.56304892e-01 1.00021672e+00 -8.26782005e-06 -8.63729487e-06 8.94054571e-05 2.50365329e-06 -5.17903750e-06 -4.34592454e-06 + 1.68922858e-01 1.00022560e+00 -8.57760543e-06 -9.00220377e-06 9.25993540e-05 2.52661673e-06 -5.36527099e-06 -4.50716767e-06 + 1.82066573e-01 1.00023479e+00 -8.89731634e-06 -9.38110931e-06 9.58795180e-05 2.55014502e-06 -5.55585070e-06 -4.66988337e-06 + 1.95757942e-01 1.00024429e+00 -9.22698743e-06 -9.77181672e-06 9.92467108e-05 2.57465836e-06 -5.74965404e-06 -4.83516060e-06 + 2.10019785e-01 1.00025412e+00 -9.56441475e-06 -1.01731968e-05 1.02699369e-04 2.60158433e-06 -5.94470664e-06 -5.00632529e-06 + 2.24875872e-01 1.00026429e+00 -9.90921003e-06 -1.05865815e-05 1.06238235e-04 2.63065311e-06 -6.14039279e-06 -5.18281267e-06 + 2.40350962e-01 1.00027479e+00 -1.02680897e-05 -1.10112361e-05 1.09867366e-04 2.66310519e-06 -6.33950773e-06 -5.36188242e-06 + 2.56470847e-01 1.00028566e+00 -1.06386208e-05 -1.14494785e-05 1.13584060e-04 2.69593869e-06 -6.54281044e-06 -5.54328646e-06 + 2.73262395e-01 1.00029689e+00 -1.10192911e-05 -1.19021044e-05 1.17386303e-04 2.72737418e-06 -6.75136135e-06 -5.72674164e-06 + 2.90753590e-01 1.00030849e+00 -1.14087042e-05 -1.23705585e-05 1.21273005e-04 2.75754718e-06 -6.96514256e-06 -5.91263311e-06 + 3.08973585e-01 1.00032048e+00 -1.18073277e-05 -1.28553467e-05 1.25242946e-04 2.78748640e-06 -7.18329691e-06 -6.10021788e-06 + 3.27952746e-01 1.00033288e+00 -1.22162960e-05 -1.33561070e-05 1.29294772e-04 2.81656372e-06 -7.40543363e-06 -6.28980404e-06 + 3.47722706e-01 1.00034568e+00 -1.26352549e-05 -1.38720025e-05 1.33427550e-04 2.84409260e-06 -7.63161753e-06 -6.48214744e-06 + 3.68316415e-01 1.00035891e+00 -1.30616029e-05 -1.44047163e-05 1.37639307e-04 2.87179434e-06 -7.86126034e-06 -6.67685886e-06 + 3.89768194e-01 1.00037257e+00 -1.34929014e-05 -1.49553139e-05 1.41927577e-04 2.90066331e-06 -8.09203044e-06 -6.87248474e-06 + 4.12113798e-01 1.00038668e+00 -1.39303489e-05 -1.55247210e-05 1.46289126e-04 2.92989226e-06 -8.32323143e-06 -7.06850171e-06 + 4.35390468e-01 1.00040126e+00 -1.43763679e-05 -1.61118598e-05 1.50720362e-04 2.95774279e-06 -8.55416736e-06 -7.26605016e-06 + 4.59637000e-01 1.00041631e+00 -1.48337517e-05 -1.67157898e-05 1.55217045e-04 2.98538900e-06 -8.78339744e-06 -7.46610366e-06 + 4.84893803e-01 1.00043187e+00 -1.53026365e-05 -1.73346415e-05 1.59775619e-04 3.01413818e-06 -9.00988011e-06 -7.66914984e-06 + 5.11202974e-01 1.00044793e+00 -1.57811209e-05 -1.79681681e-05 1.64393778e-04 3.04379125e-06 -9.23674227e-06 -7.87337174e-06 + 5.38608360e-01 1.00046452e+00 -1.62673184e-05 -1.86162493e-05 1.69068577e-04 3.07309395e-06 -9.46468047e-06 -8.07684503e-06 + 5.67155637e-01 1.00048166e+00 -1.67601935e-05 -1.92781446e-05 1.73796058e-04 3.09995271e-06 -9.69154255e-06 -8.28026806e-06 + 5.96892384e-01 1.00049936e+00 -1.72610746e-05 -1.99530704e-05 1.78569697e-04 3.12484836e-06 -9.91410426e-06 -8.48442595e-06 + 6.27868162e-01 1.00051766e+00 -1.77684813e-05 -2.06424745e-05 1.83383391e-04 3.14863529e-06 -1.01296972e-05 -8.69034434e-06 + 6.60134598e-01 1.00053657e+00 -1.82801663e-05 -2.13467144e-05 1.88231626e-04 3.17297229e-06 -1.03384731e-05 -8.89739925e-06 + 6.93745468e-01 1.00055611e+00 -1.87957068e-05 -2.20650686e-05 1.93108162e-04 3.19985787e-06 -1.05402343e-05 -9.10404549e-06 + 7.28756791e-01 1.00057632e+00 -1.93125283e-05 -2.27980968e-05 1.98002290e-04 3.22831832e-06 -1.07343079e-05 -9.30970943e-06 + 7.65226920e-01 1.00059722e+00 -1.98296633e-05 -2.35444854e-05 2.02907345e-04 3.25420190e-06 -1.09207311e-05 -9.51287014e-06 + 8.03216637e-01 1.00061884e+00 -2.03489836e-05 -2.42996311e-05 2.07820331e-04 3.27683872e-06 -1.11063556e-05 -9.70974864e-06 + 8.42789259e-01 1.00064122e+00 -2.08652143e-05 -2.50721241e-05 2.12737291e-04 3.29511289e-06 -1.12962495e-05 -9.89818401e-06 + 8.84010740e-01 1.00066439e+00 -2.13735081e-05 -2.58589221e-05 2.17650907e-04 3.31229075e-06 -1.14782428e-05 -1.00800696e-05 + 9.26949783e-01 1.00068840e+00 -2.18751499e-05 -2.66539593e-05 2.22552054e-04 3.32928744e-06 -1.16449055e-05 -1.02533782e-05 + 9.71677953e-01 1.00071327e+00 -2.23680159e-05 -2.74540657e-05 2.27435271e-04 3.34078650e-06 -1.17955189e-05 -1.04158891e-05 + 1.01826980e+00 1.00073907e+00 -2.28468808e-05 -2.82540412e-05 2.32291225e-04 3.35153705e-06 -1.19316959e-05 -1.05634402e-05 + 1.06680297e+00 1.00076583e+00 -2.33209479e-05 -2.90458218e-05 2.37120193e-04 3.37939420e-06 -1.20569176e-05 -1.06974281e-05 + 1.11735835e+00 1.00079361e+00 -2.37948264e-05 -2.98294887e-05 2.41905898e-04 3.42098560e-06 -1.21715363e-05 -1.08153447e-05 + 1.17002021e+00 1.00082246e+00 -2.42663437e-05 -3.06040361e-05 2.46630037e-04 3.46749877e-06 -1.22687975e-05 -1.09233698e-05 + 1.20000000e+00 1.00083445e+00 -2.45324797e-05 -3.10324592e-05 2.49236362e-04 3.49265813e-06 -1.23160767e-05 -1.09794436e-05 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_region_default_1.txt new file mode 100644 index 0000000..5d9b14d --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_elastic_strain_region_default_1.txt @@ -0,0 +1,70 @@ + # Time Volume Ee11 Ee22 Ee33 Ee23 Ee13 Ee12 + 5.00000000e-05 1.00000156e+00 -6.52013073e-08 -5.97936310e-08 7.28502963e-07 3.45182917e-08 -3.57852082e-08 6.12148327e-10 + 2.06250000e-04 1.00000645e+00 -2.68957051e-07 -2.46650053e-07 3.00503720e-06 1.42384596e-07 -1.47612881e-07 2.52686095e-09 + 6.94531250e-04 1.00002172e+00 -9.05709931e-07 -8.30589312e-07 1.01188403e-05 4.79434009e-07 -4.97063589e-07 8.52741736e-09 + 2.22041016e-03 1.00006925e+00 -2.91225111e-06 -2.64889063e-06 3.22580272e-05 1.53065829e-06 -1.58227518e-06 2.62430111e-08 + 4.60459595e-03 1.00009813e+00 -3.97076821e-06 -5.01054952e-06 4.54759638e-05 2.43543480e-06 -2.32007153e-06 -5.48978424e-07 + 6.46724110e-03 1.00010295e+00 -3.93860268e-06 -5.36296780e-06 4.73212657e-05 2.57872457e-06 -2.35800859e-06 -9.26377382e-07 + 8.40749646e-03 1.00010584e+00 -3.97496267e-06 -5.39462816e-06 4.83007254e-05 2.61158945e-06 -2.37888128e-06 -1.13719379e-06 + 1.04285958e-02 1.00010819e+00 -4.05124642e-06 -5.38559335e-06 4.90854332e-05 2.60748018e-06 -2.41106814e-06 -1.27637843e-06 + 1.25339076e-02 1.00011031e+00 -4.13745727e-06 -5.37296772e-06 4.98009255e-05 2.59475395e-06 -2.44829089e-06 -1.39809418e-06 + 1.47269407e-02 1.00011233e+00 -4.23992464e-06 -5.35766896e-06 5.04884952e-05 2.58310242e-06 -2.48594454e-06 -1.51061843e-06 + 1.81535550e-02 1.00011532e+00 -4.41146324e-06 -5.33576067e-06 5.15140000e-05 2.56595128e-06 -2.55701787e-06 -1.65738911e-06 + 2.17229449e-02 1.00011828e+00 -4.57796769e-06 -5.32089312e-06 5.25581503e-05 2.54992556e-06 -2.64561914e-06 -1.79300897e-06 + 2.54410593e-02 1.00012127e+00 -4.73933113e-06 -5.30902712e-06 5.36379171e-05 2.53006779e-06 -2.74562321e-06 -1.92409375e-06 + 2.93140952e-02 1.00012432e+00 -4.89803093e-06 -5.30988734e-06 5.47540971e-05 2.50736021e-06 -2.84920865e-06 -2.05368859e-06 + 3.33485076e-02 1.00012744e+00 -5.05152954e-06 -5.33242791e-06 5.59110155e-05 2.48636148e-06 -2.94891290e-06 -2.17996544e-06 + 3.75510205e-02 1.00013065e+00 -5.19197970e-06 -5.38208898e-06 5.71136216e-05 2.46865091e-06 -3.04449601e-06 -2.30679572e-06 + 4.19286381e-02 1.00013395e+00 -5.32749661e-06 -5.45140627e-06 5.83623310e-05 2.45571277e-06 -3.13720758e-06 -2.43621406e-06 + 4.64886564e-02 1.00013736e+00 -5.46017167e-06 -5.53634801e-06 5.96603472e-05 2.44644077e-06 -3.23025982e-06 -2.56405701e-06 + 5.12386755e-02 1.00014089e+00 -5.59387302e-06 -5.63739263e-06 6.10076953e-05 2.43893842e-06 -3.32617874e-06 -2.68697055e-06 + 5.61866121e-02 1.00014454e+00 -5.73385182e-06 -5.74874615e-06 6.24086297e-05 2.43370473e-06 -3.42713299e-06 -2.80142179e-06 + 6.13407126e-02 1.00014832e+00 -5.87798238e-06 -5.87148445e-06 6.38648901e-05 2.43074917e-06 -3.53389034e-06 -2.91162757e-06 + 6.67095674e-02 1.00015223e+00 -6.01956587e-06 -6.01042812e-06 6.53773719e-05 2.42785445e-06 -3.64697345e-06 -3.01850026e-06 + 7.23021245e-02 1.00015628e+00 -6.16158872e-06 -6.16437060e-06 6.69439195e-05 2.42581408e-06 -3.76292995e-06 -3.12308441e-06 + 7.81277047e-02 1.00016048e+00 -6.30776886e-06 -6.33207041e-06 6.85655002e-05 2.42532320e-06 -3.87918606e-06 -3.22522159e-06 + 8.41960175e-02 1.00016483e+00 -6.45788900e-06 -6.51154316e-06 7.02454703e-05 2.42439527e-06 -3.99638215e-06 -3.32606018e-06 + 9.05171766e-02 1.00016934e+00 -6.61616744e-06 -6.69766342e-06 7.19874050e-05 2.42183742e-06 -4.11296261e-06 -3.42642018e-06 + 1.00393988e-01 1.00017658e+00 -6.86691525e-06 -6.98759658e-06 7.46834668e-05 2.42313687e-06 -4.28746784e-06 -3.57297489e-06 + 1.10682333e-01 1.00018407e+00 -7.13005577e-06 -7.29181441e-06 7.74622889e-05 2.43150666e-06 -4.46271825e-06 -3.71909046e-06 + 1.21399359e-01 1.00019182e+00 -7.40071529e-06 -7.61122370e-06 8.03242505e-05 2.44579697e-06 -4.63848697e-06 -3.87009415e-06 + 1.32562927e-01 1.00019984e+00 -7.68007824e-06 -7.94379900e-06 8.32689786e-05 2.46307545e-06 -4.81659330e-06 -4.02662794e-06 + 1.44191645e-01 1.00020813e+00 -7.96838850e-06 -8.28604890e-06 8.62960357e-05 2.48250147e-06 -4.99675667e-06 -4.18552378e-06 + 1.56304892e-01 1.00021672e+00 -8.26782005e-06 -8.63729487e-06 8.94054571e-05 2.50365329e-06 -5.17903750e-06 -4.34592454e-06 + 1.68922858e-01 1.00022560e+00 -8.57760543e-06 -9.00220377e-06 9.25993540e-05 2.52661673e-06 -5.36527099e-06 -4.50716767e-06 + 1.82066573e-01 1.00023479e+00 -8.89731634e-06 -9.38110931e-06 9.58795180e-05 2.55014502e-06 -5.55585070e-06 -4.66988337e-06 + 1.95757942e-01 1.00024429e+00 -9.22698743e-06 -9.77181672e-06 9.92467108e-05 2.57465836e-06 -5.74965404e-06 -4.83516060e-06 + 2.10019785e-01 1.00025412e+00 -9.56441475e-06 -1.01731968e-05 1.02699369e-04 2.60158433e-06 -5.94470664e-06 -5.00632529e-06 + 2.24875872e-01 1.00026429e+00 -9.90921003e-06 -1.05865815e-05 1.06238235e-04 2.63065311e-06 -6.14039279e-06 -5.18281267e-06 + 2.40350962e-01 1.00027479e+00 -1.02680897e-05 -1.10112361e-05 1.09867366e-04 2.66310519e-06 -6.33950773e-06 -5.36188242e-06 + 2.56470847e-01 1.00028566e+00 -1.06386208e-05 -1.14494785e-05 1.13584060e-04 2.69593869e-06 -6.54281044e-06 -5.54328646e-06 + 2.73262395e-01 1.00029689e+00 -1.10192911e-05 -1.19021044e-05 1.17386303e-04 2.72737418e-06 -6.75136135e-06 -5.72674164e-06 + 2.90753590e-01 1.00030849e+00 -1.14087042e-05 -1.23705585e-05 1.21273005e-04 2.75754718e-06 -6.96514256e-06 -5.91263311e-06 + 3.08973585e-01 1.00032048e+00 -1.18073277e-05 -1.28553467e-05 1.25242946e-04 2.78748640e-06 -7.18329691e-06 -6.10021788e-06 + 3.27952746e-01 1.00033288e+00 -1.22162960e-05 -1.33561070e-05 1.29294772e-04 2.81656372e-06 -7.40543363e-06 -6.28980404e-06 + 3.47722706e-01 1.00034568e+00 -1.26352549e-05 -1.38720025e-05 1.33427550e-04 2.84409260e-06 -7.63161753e-06 -6.48214744e-06 + 3.68316415e-01 1.00035891e+00 -1.30616029e-05 -1.44047163e-05 1.37639307e-04 2.87179434e-06 -7.86126034e-06 -6.67685886e-06 + 3.89768194e-01 1.00037257e+00 -1.34929014e-05 -1.49553139e-05 1.41927577e-04 2.90066331e-06 -8.09203044e-06 -6.87248474e-06 + 4.12113798e-01 1.00038668e+00 -1.39303489e-05 -1.55247210e-05 1.46289126e-04 2.92989226e-06 -8.32323143e-06 -7.06850171e-06 + 4.35390468e-01 1.00040126e+00 -1.43763679e-05 -1.61118598e-05 1.50720362e-04 2.95774279e-06 -8.55416736e-06 -7.26605016e-06 + 4.59637000e-01 1.00041631e+00 -1.48337517e-05 -1.67157898e-05 1.55217045e-04 2.98538900e-06 -8.78339744e-06 -7.46610366e-06 + 4.84893803e-01 1.00043187e+00 -1.53026365e-05 -1.73346415e-05 1.59775619e-04 3.01413818e-06 -9.00988011e-06 -7.66914984e-06 + 5.11202974e-01 1.00044793e+00 -1.57811209e-05 -1.79681681e-05 1.64393778e-04 3.04379125e-06 -9.23674227e-06 -7.87337174e-06 + 5.38608360e-01 1.00046452e+00 -1.62673184e-05 -1.86162493e-05 1.69068577e-04 3.07309395e-06 -9.46468047e-06 -8.07684503e-06 + 5.67155637e-01 1.00048166e+00 -1.67601935e-05 -1.92781446e-05 1.73796058e-04 3.09995271e-06 -9.69154255e-06 -8.28026806e-06 + 5.96892384e-01 1.00049936e+00 -1.72610746e-05 -1.99530704e-05 1.78569697e-04 3.12484836e-06 -9.91410426e-06 -8.48442595e-06 + 6.27868162e-01 1.00051766e+00 -1.77684813e-05 -2.06424745e-05 1.83383391e-04 3.14863529e-06 -1.01296972e-05 -8.69034434e-06 + 6.60134598e-01 1.00053657e+00 -1.82801663e-05 -2.13467144e-05 1.88231626e-04 3.17297229e-06 -1.03384731e-05 -8.89739925e-06 + 6.93745468e-01 1.00055611e+00 -1.87957068e-05 -2.20650686e-05 1.93108162e-04 3.19985787e-06 -1.05402343e-05 -9.10404549e-06 + 7.28756791e-01 1.00057632e+00 -1.93125283e-05 -2.27980968e-05 1.98002290e-04 3.22831832e-06 -1.07343079e-05 -9.30970943e-06 + 7.65226920e-01 1.00059722e+00 -1.98296633e-05 -2.35444854e-05 2.02907345e-04 3.25420190e-06 -1.09207311e-05 -9.51287014e-06 + 8.03216637e-01 1.00061884e+00 -2.03489836e-05 -2.42996311e-05 2.07820331e-04 3.27683872e-06 -1.11063556e-05 -9.70974864e-06 + 8.42789259e-01 1.00064122e+00 -2.08652143e-05 -2.50721241e-05 2.12737291e-04 3.29511289e-06 -1.12962495e-05 -9.89818401e-06 + 8.84010740e-01 1.00066439e+00 -2.13735081e-05 -2.58589221e-05 2.17650907e-04 3.31229075e-06 -1.14782428e-05 -1.00800696e-05 + 9.26949783e-01 1.00068840e+00 -2.18751499e-05 -2.66539593e-05 2.22552054e-04 3.32928744e-06 -1.16449055e-05 -1.02533782e-05 + 9.71677953e-01 1.00071327e+00 -2.23680159e-05 -2.74540657e-05 2.27435271e-04 3.34078650e-06 -1.17955189e-05 -1.04158891e-05 + 1.01826980e+00 1.00073907e+00 -2.28468808e-05 -2.82540412e-05 2.32291225e-04 3.35153705e-06 -1.19316959e-05 -1.05634402e-05 + 1.06680297e+00 1.00076583e+00 -2.33209479e-05 -2.90458218e-05 2.37120193e-04 3.37939420e-06 -1.20569176e-05 -1.06974281e-05 + 1.11735835e+00 1.00079361e+00 -2.37948264e-05 -2.98294887e-05 2.41905898e-04 3.42098560e-06 -1.21715363e-05 -1.08153447e-05 + 1.17002021e+00 1.00082246e+00 -2.42663437e-05 -3.06040361e-05 2.46630037e-04 3.46749877e-06 -1.22687975e-05 -1.09233698e-05 + 1.20000000e+00 1.00083445e+00 -2.45324797e-05 -3.10324592e-05 2.49236362e-04 3.49265813e-06 -1.23160767e-05 -1.09794436e-05 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_global.txt new file mode 100644 index 0000000..4dd44a3 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_global.txt @@ -0,0 +1,70 @@ + # Time Volume Equiv_Plastic_Stra + 5.00000000e-05 1.00000156e+00 0.00000000e+00 + 2.06250000e-04 1.00000645e+00 4.38622126e-57 + 6.94531250e-04 1.00002172e+00 3.17095312e-30 + 2.22041016e-03 1.00006925e+00 6.03269776e-07 + 4.60459595e-03 1.00009813e+00 1.71114334e-04 + 6.46724110e-03 1.00010295e+00 3.69825112e-04 + 8.40749646e-03 1.00010584e+00 5.80325020e-04 + 1.04285958e-02 1.00010819e+00 7.98940539e-04 + 1.25339076e-02 1.00011031e+00 1.02571369e-03 + 1.47269407e-02 1.00011233e+00 1.26106867e-03 + 1.81535550e-02 1.00011532e+00 1.62745955e-03 + 2.17229449e-02 1.00011828e+00 2.00797549e-03 + 2.54410593e-02 1.00012127e+00 2.40336026e-03 + 2.93140952e-02 1.00012432e+00 2.81425795e-03 + 3.33485076e-02 1.00012744e+00 3.24135201e-03 + 3.75510205e-02 1.00013065e+00 3.68540814e-03 + 4.19286381e-02 1.00013395e+00 4.14716651e-03 + 4.64886564e-02 1.00013736e+00 4.62738735e-03 + 5.12386755e-02 1.00014089e+00 5.12684736e-03 + 5.61866121e-02 1.00014454e+00 5.64632501e-03 + 6.13407126e-02 1.00014832e+00 6.18669912e-03 + 6.67095674e-02 1.00015223e+00 6.74885130e-03 + 7.23021245e-02 1.00015628e+00 7.33367665e-03 + 7.81277047e-02 1.00016048e+00 7.94208419e-03 + 8.41960175e-02 1.00016483e+00 8.57504947e-03 + 9.05171766e-02 1.00016934e+00 9.23360207e-03 + 1.00393988e-01 1.00017658e+00 1.02609318e-02 + 1.10682333e-01 1.00018407e+00 1.13294070e-02 + 1.21399359e-01 1.00019182e+00 1.24406791e-02 + 1.32562927e-01 1.00019984e+00 1.35964806e-02 + 1.44191645e-01 1.00020813e+00 1.47986375e-02 + 1.56304892e-01 1.00021672e+00 1.60489992e-02 + 1.68922858e-01 1.00022560e+00 1.73495018e-02 + 1.82066573e-01 1.00023479e+00 1.87021746e-02 + 1.95757942e-01 1.00024429e+00 2.01090552e-02 + 2.10019785e-01 1.00025412e+00 2.15722458e-02 + 2.24875872e-01 1.00026429e+00 2.30939421e-02 + 2.40350962e-01 1.00027479e+00 2.46764107e-02 + 2.56470847e-01 1.00028566e+00 2.63220429e-02 + 2.73262395e-01 1.00029689e+00 2.80332955e-02 + 2.90753590e-01 1.00030849e+00 2.98126717e-02 + 3.08973585e-01 1.00032048e+00 3.16627716e-02 + 3.27952746e-01 1.00033288e+00 3.35862965e-02 + 3.47722706e-01 1.00034568e+00 3.55860564e-02 + 3.68316415e-01 1.00035891e+00 3.76649095e-02 + 3.89768194e-01 1.00037257e+00 3.98257771e-02 + 4.12113798e-01 1.00038668e+00 4.20717141e-02 + 4.35390468e-01 1.00040126e+00 4.44059177e-02 + 4.59637000e-01 1.00041631e+00 4.68316482e-02 + 4.84893803e-01 1.00043187e+00 4.93522507e-02 + 5.11202974e-01 1.00044793e+00 5.19711845e-02 + 5.38608360e-01 1.00046452e+00 5.46919997e-02 + 5.67155637e-01 1.00048166e+00 5.75183670e-02 + 5.96892384e-01 1.00049936e+00 6.04540547e-02 + 6.27868162e-01 1.00051766e+00 6.35029488e-02 + 6.60134598e-01 1.00053657e+00 6.66690118e-02 + 6.93745468e-01 1.00055611e+00 6.99562731e-02 + 7.28756791e-01 1.00057632e+00 7.33689565e-02 + 7.65226920e-01 1.00059722e+00 7.69114060e-02 + 8.03216637e-01 1.00061884e+00 8.05880251e-02 + 8.42789259e-01 1.00064122e+00 8.44032998e-02 + 8.84010740e-01 1.00066439e+00 8.83619773e-02 + 9.26949783e-01 1.00068840e+00 9.24690430e-02 + 9.71677953e-01 1.00071327e+00 9.67294710e-02 + 1.01826980e+00 1.00073907e+00 1.01148174e-01 + 1.06680297e+00 1.00076583e+00 1.05730289e-01 + 1.11735835e+00 1.00079361e+00 1.10481071e-01 + 1.17002021e+00 1.00082246e+00 1.15405930e-01 + 1.20000000e+00 1.00083445e+00 1.18202002e-01 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_region_default_1.txt new file mode 100644 index 0000000..4dd44a3 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_eq_pl_strain_region_default_1.txt @@ -0,0 +1,70 @@ + # Time Volume Equiv_Plastic_Stra + 5.00000000e-05 1.00000156e+00 0.00000000e+00 + 2.06250000e-04 1.00000645e+00 4.38622126e-57 + 6.94531250e-04 1.00002172e+00 3.17095312e-30 + 2.22041016e-03 1.00006925e+00 6.03269776e-07 + 4.60459595e-03 1.00009813e+00 1.71114334e-04 + 6.46724110e-03 1.00010295e+00 3.69825112e-04 + 8.40749646e-03 1.00010584e+00 5.80325020e-04 + 1.04285958e-02 1.00010819e+00 7.98940539e-04 + 1.25339076e-02 1.00011031e+00 1.02571369e-03 + 1.47269407e-02 1.00011233e+00 1.26106867e-03 + 1.81535550e-02 1.00011532e+00 1.62745955e-03 + 2.17229449e-02 1.00011828e+00 2.00797549e-03 + 2.54410593e-02 1.00012127e+00 2.40336026e-03 + 2.93140952e-02 1.00012432e+00 2.81425795e-03 + 3.33485076e-02 1.00012744e+00 3.24135201e-03 + 3.75510205e-02 1.00013065e+00 3.68540814e-03 + 4.19286381e-02 1.00013395e+00 4.14716651e-03 + 4.64886564e-02 1.00013736e+00 4.62738735e-03 + 5.12386755e-02 1.00014089e+00 5.12684736e-03 + 5.61866121e-02 1.00014454e+00 5.64632501e-03 + 6.13407126e-02 1.00014832e+00 6.18669912e-03 + 6.67095674e-02 1.00015223e+00 6.74885130e-03 + 7.23021245e-02 1.00015628e+00 7.33367665e-03 + 7.81277047e-02 1.00016048e+00 7.94208419e-03 + 8.41960175e-02 1.00016483e+00 8.57504947e-03 + 9.05171766e-02 1.00016934e+00 9.23360207e-03 + 1.00393988e-01 1.00017658e+00 1.02609318e-02 + 1.10682333e-01 1.00018407e+00 1.13294070e-02 + 1.21399359e-01 1.00019182e+00 1.24406791e-02 + 1.32562927e-01 1.00019984e+00 1.35964806e-02 + 1.44191645e-01 1.00020813e+00 1.47986375e-02 + 1.56304892e-01 1.00021672e+00 1.60489992e-02 + 1.68922858e-01 1.00022560e+00 1.73495018e-02 + 1.82066573e-01 1.00023479e+00 1.87021746e-02 + 1.95757942e-01 1.00024429e+00 2.01090552e-02 + 2.10019785e-01 1.00025412e+00 2.15722458e-02 + 2.24875872e-01 1.00026429e+00 2.30939421e-02 + 2.40350962e-01 1.00027479e+00 2.46764107e-02 + 2.56470847e-01 1.00028566e+00 2.63220429e-02 + 2.73262395e-01 1.00029689e+00 2.80332955e-02 + 2.90753590e-01 1.00030849e+00 2.98126717e-02 + 3.08973585e-01 1.00032048e+00 3.16627716e-02 + 3.27952746e-01 1.00033288e+00 3.35862965e-02 + 3.47722706e-01 1.00034568e+00 3.55860564e-02 + 3.68316415e-01 1.00035891e+00 3.76649095e-02 + 3.89768194e-01 1.00037257e+00 3.98257771e-02 + 4.12113798e-01 1.00038668e+00 4.20717141e-02 + 4.35390468e-01 1.00040126e+00 4.44059177e-02 + 4.59637000e-01 1.00041631e+00 4.68316482e-02 + 4.84893803e-01 1.00043187e+00 4.93522507e-02 + 5.11202974e-01 1.00044793e+00 5.19711845e-02 + 5.38608360e-01 1.00046452e+00 5.46919997e-02 + 5.67155637e-01 1.00048166e+00 5.75183670e-02 + 5.96892384e-01 1.00049936e+00 6.04540547e-02 + 6.27868162e-01 1.00051766e+00 6.35029488e-02 + 6.60134598e-01 1.00053657e+00 6.66690118e-02 + 6.93745468e-01 1.00055611e+00 6.99562731e-02 + 7.28756791e-01 1.00057632e+00 7.33689565e-02 + 7.65226920e-01 1.00059722e+00 7.69114060e-02 + 8.03216637e-01 1.00061884e+00 8.05880251e-02 + 8.42789259e-01 1.00064122e+00 8.44032998e-02 + 8.84010740e-01 1.00066439e+00 8.83619773e-02 + 9.26949783e-01 1.00068840e+00 9.24690430e-02 + 9.71677953e-01 1.00071327e+00 9.67294710e-02 + 1.01826980e+00 1.00073907e+00 1.01148174e-01 + 1.06680297e+00 1.00076583e+00 1.05730289e-01 + 1.11735835e+00 1.00079361e+00 1.10481071e-01 + 1.17002021e+00 1.00082246e+00 1.15405930e-01 + 1.20000000e+00 1.00083445e+00 1.18202002e-01 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global copy.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global copy.txt new file mode 100644 index 0000000..a460180 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global copy.txt @@ -0,0 +1,63 @@ + # Time Volume E11 E22 E33 E23 E13 E12 + 5.00000000e-05 9.99998436e-01 1.64988818e-06 1.78646132e-06 -5.00003741e-06 -6.60483513e-08 1.17311770e-07 1.37067237e-08 + 2.06250000e-04 9.99993550e-01 6.80584388e-06 7.36920606e-06 -2.06256366e-05 -2.72449396e-07 4.83917359e-07 5.65401921e-08 + 6.94531250e-04 9.99978280e-01 2.29187441e-05 2.48158022e-05 -6.94603445e-05 -9.17452130e-07 1.62962140e-06 1.90394360e-07 + 2.22041016e-03 9.99930760e-01 7.33679176e-05 7.94443440e-05 -2.22114816e-04 -2.94974797e-06 5.19644768e-06 5.72486145e-07 + 4.60459595e-03 9.99901977e-01 1.68899474e-04 1.93577899e-04 -4.60776811e-04 -5.62016524e-06 9.51691877e-06 3.40286583e-06 + 6.46724110e-03 9.99897218e-01 2.49034998e-04 2.94976224e-04 -6.47350115e-04 -9.43738927e-06 1.08910941e-05 7.32238953e-06 + 8.40749646e-03 9.99894393e-01 3.33020444e-04 4.02223178e-04 -8.41808299e-04 -1.38512416e-05 1.14259134e-05 1.15458055e-05 + 1.04285958e-02 9.99892111e-01 4.20132868e-04 5.14976013e-04 -1.04448956e-03 -1.89887617e-05 1.12687120e-05 1.55545433e-05 + 1.25339076e-02 9.99890059e-01 5.10493631e-04 6.33135323e-04 -1.25574700e-03 -2.47468438e-05 1.06479357e-05 1.94729447e-05 + 1.47269407e-02 9.99888107e-01 6.04317507e-04 7.56709443e-04 -1.47594923e-03 -3.10461514e-05 9.72456781e-06 2.34016351e-05 + 1.81535550e-02 9.99885307e-01 7.50623904e-04 9.50343344e-04 -1.82030633e-03 -4.11720510e-05 8.12818229e-06 2.92586016e-05 + 2.17229449e-02 9.99882544e-01 9.02984872e-04 1.15224820e-03 -2.17938938e-03 -5.16890002e-05 6.50433978e-06 3.54070415e-05 + 2.54410593e-02 9.99879766e-01 1.06142886e-03 1.36294017e-03 -2.55384462e-03 -6.25278123e-05 4.88199771e-06 4.17902193e-05 + 2.93140952e-02 9.99876947e-01 1.22611402e-03 1.58285412e-03 -2.94434813e-03 -7.37498519e-05 3.30297126e-06 4.83740453e-05 + 3.33485076e-02 9.99874073e-01 1.39722209e-03 1.81244119e-03 -3.35160724e-03 -8.53593441e-05 1.88331987e-06 5.51113877e-05 + 3.75510205e-02 9.99871132e-01 1.57513141e-03 2.05198244e-03 -3.77636215e-03 -9.73939919e-05 6.36400555e-07 6.19716223e-05 + 4.19286381e-02 9.99868113e-01 1.76021473e-03 2.30179665e-03 -4.21938758e-03 -1.09818030e-04 -3.43886591e-07 6.89880816e-05 + 4.64886564e-02 9.99865009e-01 1.95280823e-03 2.56227204e-03 -4.68149461e-03 -1.22696171e-04 -8.96585763e-07 7.62346446e-05 + 5.12386755e-02 9.99861816e-01 2.15324870e-03 2.83382651e-03 -5.16353251e-03 -1.36095081e-04 -1.03170550e-06 8.37700287e-05 + 5.61866121e-02 9.99858527e-01 2.36187170e-03 3.11691298e-03 -5.66639064e-03 -1.49958012e-04 -8.38338243e-07 9.15489732e-05 + 6.13407126e-02 9.99855139e-01 2.57902489e-03 3.41200460e-03 -6.19100061e-03 -1.64277955e-04 -3.54069895e-07 9.95780080e-05 + 6.93939948e-02 9.99850095e-01 2.91807251e-03 3.87360640e-03 -7.01234073e-03 -1.86392962e-04 1.01381140e-06 1.12080610e-04 + 7.77828304e-02 9.99844925e-01 3.27097771e-03 4.35482036e-03 -7.87003237e-03 -2.09153135e-04 3.03434565e-06 1.25151440e-04 + 8.65212008e-02 9.99839626e-01 3.63858934e-03 4.85619407e-03 -8.76577907e-03 -2.32669795e-04 5.57308718e-06 1.38829441e-04 + 9.56236699e-02 9.99834196e-01 4.02150860e-03 5.37858388e-03 -9.70137257e-03 -2.57031680e-04 8.55485420e-06 1.53101792e-04 + 1.05105409e-01 9.99828631e-01 4.42036746e-03 5.92287659e-03 -1.06786982e-02 -2.82433020e-04 1.19628991e-05 1.67866111e-04 + 1.14982220e-01 9.99822932e-01 4.83597567e-03 6.48984672e-03 -1.16997402e-02 -3.08936084e-04 1.57264823e-05 1.83044054e-04 + 1.25270565e-01 9.99817099e-01 5.26899354e-03 7.08048558e-03 -1.27665878e-02 -3.36626964e-04 1.97977643e-05 1.98585132e-04 + 1.35987591e-01 9.99811134e-01 5.72008443e-03 7.69584987e-03 -1.38814419e-02 -3.65563514e-04 2.41527029e-05 2.14568980e-04 + 1.47151159e-01 9.99805040e-01 6.19000965e-03 8.33697112e-03 -1.50466218e-02 -3.95791634e-04 2.87826854e-05 2.31152569e-04 + 1.58779877e-01 9.99798818e-01 6.67960559e-03 9.00488170e-03 -1.62645731e-02 -4.27329217e-04 3.36501161e-05 2.48444507e-04 + 1.70893124e-01 9.99792474e-01 7.18973148e-03 9.70067025e-03 -1.75378754e-02 -4.60200587e-04 3.87621057e-05 2.66431295e-04 + 1.83511090e-01 9.99786013e-01 7.72119068e-03 1.04255620e-02 -1.88692512e-02 -4.94424703e-04 4.41296469e-05 2.85143647e-04 + 1.96654805e-01 9.99779441e-01 8.27487587e-03 1.11807787e-02 -2.02615754e-02 -5.30070592e-04 4.97622477e-05 3.04583437e-04 + 2.17191859e-01 9.99770658e-01 9.14052827e-03 1.23618305e-02 -2.24483359e-02 -5.85899906e-04 5.87336957e-05 3.34698897e-04 + 2.38584623e-01 9.99761924e-01 1.00425782e-02 1.35922824e-02 -2.47409155e-02 -6.44319477e-04 6.82660459e-05 3.65677486e-04 + 2.60868753e-01 9.99753272e-01 1.09826043e-02 1.48741634e-02 -2.71451152e-02 -7.05674509e-04 7.84127870e-05 3.97610463e-04 + 2.84081388e-01 9.99744740e-01 1.19620886e-02 1.62097493e-02 -2.96671157e-02 -7.70095129e-04 8.94242339e-05 4.30511962e-04 + 3.08261217e-01 9.99736370e-01 1.29828278e-02 1.76011636e-02 -3.23135085e-02 -8.37696053e-04 1.01368787e-04 4.64301283e-04 + 3.33448538e-01 9.99728205e-01 1.40466883e-02 1.90506239e-02 -3.50913290e-02 -9.08567571e-04 1.14191813e-04 4.98979890e-04 + 3.59685331e-01 9.99720297e-01 1.51555983e-02 2.05604580e-02 -3.80080941e-02 -9.82882301e-04 1.27973567e-04 5.34590395e-04 + 3.87015323e-01 9.99712700e-01 1.63115028e-02 2.21331555e-02 -4.10718423e-02 -1.06084988e-03 1.42687637e-04 5.71048348e-04 + 4.15484065e-01 9.99705477e-01 1.75164741e-02 2.37712644e-02 -4.42911791e-02 -1.14270992e-03 1.58132558e-04 6.08178201e-04 + 4.45139005e-01 9.99698696e-01 1.87723549e-02 2.54777486e-02 -4.76753252e-02 -1.22865889e-03 1.73966824e-04 6.45881378e-04 + 4.76029568e-01 9.99692430e-01 2.00812334e-02 2.72555314e-02 -5.12341718e-02 -1.31886051e-03 1.89968202e-04 6.83836572e-04 + 5.08207237e-01 9.99686759e-01 2.14452422e-02 2.91077037e-02 -5.49783412e-02 -1.41360923e-03 2.05970334e-04 7.21734191e-04 + 5.41725642e-01 9.99681773e-01 2.28666025e-02 3.10374870e-02 -5.89192546e-02 -1.51328920e-03 2.21846498e-04 7.59612616e-04 + 5.76640647e-01 9.99677572e-01 2.43479107e-02 3.30479570e-02 -6.30692067e-02 -1.61824184e-03 2.37723039e-04 7.97706137e-04 + 6.13010445e-01 9.99674261e-01 2.58917450e-02 3.51424307e-02 -6.74414484e-02 -1.72890072e-03 2.53935670e-04 8.35860337e-04 + 6.50895651e-01 9.99671957e-01 2.75001860e-02 3.73249453e-02 -7.20502796e-02 -1.84571937e-03 2.70855369e-04 8.73982930e-04 + 6.90359406e-01 9.99670788e-01 2.91759086e-02 3.95992216e-02 -7.69111493e-02 -1.96884604e-03 2.88330664e-04 9.11820773e-04 + 7.31467486e-01 9.99670896e-01 3.09217671e-02 4.19690457e-02 -8.20407785e-02 -2.09898477e-03 3.05954472e-04 9.49383694e-04 + 7.74288401e-01 9.99672435e-01 3.27404562e-02 4.44386288e-02 -8.74572896e-02 -2.23704369e-03 3.23690280e-04 9.86765134e-04 + 8.18893522e-01 9.99675573e-01 3.46351496e-02 4.70120353e-02 -9.31803448e-02 -2.38329866e-03 3.41327084e-04 1.02396078e-03 + 8.65357189e-01 9.99680496e-01 3.66095806e-02 4.96930832e-02 -9.92313184e-02 -2.53831923e-03 3.59326351e-04 1.06067748e-03 + 9.13756842e-01 9.99687406e-01 3.86670828e-02 5.24862761e-02 -1.05633476e-01 -2.70252104e-03 3.77724962e-04 1.09671465e-03 + 9.64173148e-01 9.99696526e-01 4.08112833e-02 5.53961765e-02 -1.12412186e-01 -2.87607898e-03 3.96697658e-04 1.13177909e-03 + 1.01669013e+00 9.99708102e-01 4.30462325e-02 5.84272366e-02 -1.19595170e-01 -3.05960203e-03 4.17271122e-04 1.16431871e-03 + 1.07139533e+00 9.99722402e-01 4.53773375e-02 6.15829157e-02 -1.27212756e-01 -3.25333324e-03 4.40311978e-04 1.19235937e-03 + 1.12837990e+00 9.99739724e-01 4.78105586e-02 6.48665343e-02 -1.35298198e-01 -3.45757488e-03 4.66432959e-04 1.21602455e-03 + 1.18773883e+00 9.99760394e-01 5.03515297e-02 6.82821450e-02 -1.43888019e-01 -3.67227977e-03 4.94381086e-04 1.23560940e-03 + 1.20000000e+00 9.99759023e-01 5.08749042e-02 6.89841082e-02 -1.45684030e-01 -3.71659097e-03 5.00064806e-04 1.23935776e-03 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global.txt new file mode 100644 index 0000000..5da2d63 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_global.txt @@ -0,0 +1,70 @@ + # Time Volume E11 E22 E33 E23 E13 E12 + 5.00000000e-05 1.00000156e+00 -1.64987386e-06 -1.78644680e-06 4.99996259e-06 6.60489760e-08 -1.17310942e-07 -1.37067125e-08 + 2.06250000e-04 1.00000645e+00 -6.80567452e-06 -7.36903992e-06 2.06243634e-05 2.72452079e-07 -4.83901329e-07 -5.65402321e-08 + 6.94531250e-04 1.00002172e+00 -2.29170130e-05 -2.48141246e-05 6.94459068e-05 9.17462289e-07 -1.62943467e-06 -1.90395397e-07 + 2.22041016e-03 1.00006925e+00 -7.33503261e-05 -7.94272245e-05 2.21967260e-04 2.94980399e-06 -5.19446856e-06 -5.72612240e-07 + 4.60459595e-03 1.00009813e+00 -1.68812021e-04 -1.93473291e-04 4.60142773e-04 5.62627098e-06 -9.51151943e-06 -3.41084185e-06 + 6.46724110e-03 1.00010295e+00 -2.48878741e-04 -2.94824866e-04 6.46099195e-04 9.45377475e-06 -1.08886313e-05 -7.33255158e-06 + 8.40749646e-03 1.00010584e+00 -3.32779773e-04 -4.02030070e-04 8.39693377e-04 1.38848305e-05 -1.14289046e-05 -1.15648074e-05 + 1.04285958e-02 1.00010819e+00 -4.19792901e-04 -5.14743202e-04 1.04123413e-03 1.90424059e-05 -1.12815085e-05 -1.55843789e-05 + 1.25339076e-02 1.00011031e+00 -5.10034573e-04 -6.32868791e-04 1.25104238e-03 2.48202492e-05 -1.06769358e-05 -1.95164207e-05 + 1.47269407e-02 1.00011233e+00 -6.03722672e-04 -7.56410373e-04 1.46945165e-03 3.11346796e-05 -9.78322728e-06 -2.34624687e-05 + 1.81535550e-02 1.00011532e+00 -7.49749610e-04 -9.49982071e-04 1.81042852e-03 4.12620011e-05 -8.23780007e-06 -2.93494904e-05 + 2.17229449e-02 1.00011828e+00 -9.01794636e-04 -1.15182362e-03 2.16524049e-03 5.17430142e-05 -6.67568858e-06 -3.55422511e-05 + 2.54410593e-02 1.00012127e+00 -1.05987183e-03 -1.36246357e-03 2.53443295e-03 6.25424325e-05 -5.12920050e-06 -4.19523679e-05 + 2.93140952e-02 1.00012432e+00 -1.22415325e-03 -1.58232133e-03 2.91857147e-03 7.37056757e-05 -3.65293901e-06 -4.85468887e-05 + 3.33485076e-02 1.00012744e+00 -1.39479991e-03 -1.81186674e-03 3.31824235e-03 8.52342734e-05 -2.35017674e-06 -5.52680712e-05 + 3.75510205e-02 1.00013065e+00 -1.57220560e-03 -2.05136186e-03 3.73405340e-03 9.71713419e-05 -1.23437065e-06 -6.21038768e-05 + 4.19286381e-02 1.00013395e+00 -1.75672120e-03 -2.30114377e-03 4.16663445e-03 1.09477481e-04 -3.99120510e-07 -6.91034628e-05 + 4.64886564e-02 1.00013736e+00 -1.94869165e-03 -2.56158783e-03 4.61663805e-03 1.22194178e-04 1.08288760e-08 -7.63386823e-05 + 5.12386755e-02 1.00014089e+00 -2.14844224e-03 -2.83311982e-03 5.08474013e-03 1.35378338e-04 -1.99053655e-08 -8.38608359e-05 + 5.61866121e-02 1.00014454e+00 -2.35627013e-03 -3.11622744e-03 5.57164073e-03 1.48975529e-04 -4.08495423e-07 -9.16896645e-05 + 6.13407126e-02 1.00014832e+00 -2.57251101e-03 -3.41139085e-03 6.07806462e-03 1.62951588e-04 -1.12468231e-06 -9.97626983e-05 + 6.67095674e-02 1.00015223e+00 -2.79748808e-03 -3.71913846e-03 6.60476195e-03 1.77339898e-04 -2.16330771e-06 -1.08061775e-04 + 7.23021245e-02 1.00015628e+00 -3.03155481e-03 -4.04000415e-03 7.15250901e-03 1.92126989e-04 -3.52830598e-06 -1.16629588e-04 + 7.81277047e-02 1.00016048e+00 -3.27513057e-03 -4.37449280e-03 7.72210878e-03 2.07357729e-04 -5.21461864e-06 -1.25495637e-04 + 8.41960175e-02 1.00016483e+00 -3.52875181e-03 -4.72303011e-03 8.31439172e-03 2.23065438e-04 -7.20619744e-06 -1.34690489e-04 + 9.05171766e-02 1.00016934e+00 -3.79297412e-03 -5.08606342e-03 8.93021640e-03 2.39274052e-04 -9.49266217e-06 -1.44204800e-04 + 1.00393988e-01 1.00017658e+00 -4.20570853e-03 -5.65319501e-03 9.89012715e-03 2.64397013e-04 -1.34744940e-05 -1.58920414e-04 + 1.10682333e-01 1.00018407e+00 -4.63546601e-03 -6.24414279e-03 1.08870420e-02 2.90436163e-04 -1.79869903e-05 -1.74029150e-04 + 1.21399359e-01 1.00019182e+00 -5.08280826e-03 -6.86004632e-03 1.19222615e-02 3.17457565e-04 -2.30142035e-05 -1.89516956e-04 + 1.32562927e-01 1.00019984e+00 -5.54840486e-03 -7.50200843e-03 1.29971214e-02 3.45434766e-04 -2.86303191e-05 -2.05463478e-04 + 1.44191645e-01 1.00020813e+00 -6.03303614e-03 -8.17109425e-03 1.41129932e-02 3.74307158e-04 -3.48520603e-05 -2.21938901e-04 + 1.56304892e-01 1.00021672e+00 -6.53744375e-03 -8.86848625e-03 1.52712835e-02 4.04025594e-04 -4.16374267e-05 -2.38970207e-04 + 1.68922858e-01 1.00022560e+00 -7.06237576e-03 -9.59544035e-03 1.64734340e-02 4.34687054e-04 -4.88891360e-05 -2.56649212e-04 + 1.82066573e-01 1.00023479e+00 -7.60869260e-03 -1.03531820e-02 1.77209214e-02 4.66436817e-04 -5.64706471e-05 -2.75004811e-04 + 1.95757942e-01 1.00024429e+00 -8.17718209e-03 -1.11430991e-02 1.90152571e-02 4.99287695e-04 -6.44165823e-05 -2.94066905e-04 + 2.10019785e-01 1.00025412e+00 -8.76860319e-03 -1.19667006e-02 2.03579865e-02 5.33236664e-04 -7.28899109e-05 -3.13857860e-04 + 2.24875872e-01 1.00026429e+00 -9.38387080e-03 -1.28254315e-02 2.17506886e-02 5.68306306e-04 -8.20021324e-05 -3.34377499e-04 + 2.40350962e-01 1.00027479e+00 -1.00238994e-02 -1.37208358e-02 2.31949751e-02 6.04526527e-04 -9.18121604e-05 -3.55630525e-04 + 2.56470847e-01 1.00028566e+00 -1.06897240e-02 -1.46544384e-02 2.46924893e-02 6.41934801e-04 -1.02319367e-04 -3.77604517e-04 + 2.73262395e-01 1.00029689e+00 -1.13824207e-02 -1.56278303e-02 2.62449049e-02 6.80595074e-04 -1.13547817e-04 -4.00281506e-04 + 2.90753590e-01 1.00030849e+00 -1.21031382e-02 -1.66426420e-02 2.78539249e-02 7.20624864e-04 -1.25562959e-04 -4.23641530e-04 + 3.08973585e-01 1.00032048e+00 -1.28530455e-02 -1.77005996e-02 2.95212797e-02 7.62156619e-04 -1.38388340e-04 -4.47692314e-04 + 3.27952746e-01 1.00033288e+00 -1.36333532e-02 -1.88035096e-02 3.12487258e-02 8.05249365e-04 -1.52024568e-04 -4.72520240e-04 + 3.47722706e-01 1.00034568e+00 -1.44453625e-02 -1.99532113e-02 3.30380438e-02 8.49924313e-04 -1.66551628e-04 -4.98238409e-04 + 3.68316415e-01 1.00035891e+00 -1.52903764e-02 -2.11516733e-02 3.48910355e-02 8.96191479e-04 -1.82138398e-04 -5.24861314e-04 + 3.89768194e-01 1.00037257e+00 -1.61696644e-02 -2.24010362e-02 3.68095224e-02 9.43965067e-04 -1.98912978e-04 -5.52344138e-04 + 4.12113798e-01 1.00038668e+00 -1.70844764e-02 -2.37036027e-02 3.87953422e-02 9.93194768e-04 -2.16917875e-04 -5.80630541e-04 + 4.35390468e-01 1.00040126e+00 -1.80361178e-02 -2.50617673e-02 4.08503454e-02 1.04384657e-03 -2.36168593e-04 -6.09702300e-04 + 4.59637000e-01 1.00041631e+00 -1.90259861e-02 -2.64779878e-02 4.29763925e-02 1.09592034e-03 -2.56762846e-04 -6.39561246e-04 + 4.84893803e-01 1.00043187e+00 -2.00555637e-02 -2.79548014e-02 4.51753491e-02 1.14956913e-03 -2.78839642e-04 -6.70234963e-04 + 5.11202974e-01 1.00044793e+00 -2.11263526e-02 -2.94948934e-02 4.74490824e-02 1.20494992e-03 -3.02440038e-04 -7.01702437e-04 + 5.38608360e-01 1.00046452e+00 -2.22399003e-02 -3.11010774e-02 4.97994580e-02 1.26205860e-03 -3.27581144e-04 -7.33952836e-04 + 5.67155637e-01 1.00048166e+00 -2.33978001e-02 -3.27763041e-02 5.22283342e-02 1.32078026e-03 -3.54258892e-04 -7.66996936e-04 + 5.96892384e-01 1.00049936e+00 -2.46016757e-02 -3.45236880e-02 5.47375574e-02 1.38092067e-03 -3.82480964e-04 -8.00892442e-04 + 6.27868162e-01 1.00051766e+00 -2.58532165e-02 -3.63464800e-02 5.73289560e-02 1.44219234e-03 -4.12372908e-04 -8.35678162e-04 + 6.60134598e-01 1.00053657e+00 -2.71542174e-02 -3.82480332e-02 6.00043349e-02 1.50423981e-03 -4.44133306e-04 -8.71378920e-04 + 6.93745468e-01 1.00055611e+00 -2.85065219e-02 -4.02318675e-02 6.27654675e-02 1.56683741e-03 -4.77816460e-04 -9.07954412e-04 + 7.28756791e-01 1.00057632e+00 -2.99120735e-02 -4.23016169e-02 6.56140888e-02 1.62984389e-03 -5.13719216e-04 -9.45483976e-04 + 7.65226920e-01 1.00059722e+00 -3.13726896e-02 -4.44612770e-02 6.85518885e-02 1.69319959e-03 -5.52076385e-04 -9.83856842e-04 + 8.03216637e-01 1.00061884e+00 -3.28902796e-02 -4.67150137e-02 7.15805057e-02 1.75670850e-03 -5.92711648e-04 -1.02281939e-03 + 8.42789259e-01 1.00064122e+00 -3.44667818e-02 -4.90672272e-02 7.47015185e-02 1.82041357e-03 -6.35044814e-04 -1.06214020e-03 + 8.84010740e-01 1.00066439e+00 -3.61044008e-02 -5.15222907e-02 7.79164370e-02 1.88399687e-03 -6.78980238e-04 -1.10172445e-03 + 9.26949783e-01 1.00068840e+00 -3.78054283e-02 -5.40847905e-02 8.12266904e-02 1.94742995e-03 -7.25003233e-04 -1.14155906e-03 + 9.71677953e-01 1.00071327e+00 -3.95720579e-02 -5.67597287e-02 8.46336195e-02 2.01087349e-03 -7.73357152e-04 -1.18104980e-03 + 1.01826980e+00 1.00073907e+00 -4.14058779e-02 -5.95530309e-02 8.81384718e-02 2.07395921e-03 -8.24136115e-04 -1.21965053e-03 + 1.06680297e+00 1.00076583e+00 -4.33091289e-02 -6.24702637e-02 9.17423915e-02 2.13593840e-03 -8.77152457e-04 -1.25733406e-03 + 1.11735835e+00 1.00079361e+00 -4.52838344e-02 -6.55175729e-02 9.54464019e-02 2.19632148e-03 -9.32805269e-04 -1.29387689e-03 + 1.17002021e+00 1.00082246e+00 -4.73322611e-02 -6.87012101e-02 9.92513921e-02 2.25538803e-03 -9.91393636e-04 -1.32966177e-03 + 1.20000000e+00 1.00083445e+00 -4.84970434e-02 -7.05202032e-02 1.01393587e-01 2.28810468e-03 -1.02497986e-03 -1.34959621e-03 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_region_default_1.txt new file mode 100644 index 0000000..5da2d63 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_euler_strain_region_default_1.txt @@ -0,0 +1,70 @@ + # Time Volume E11 E22 E33 E23 E13 E12 + 5.00000000e-05 1.00000156e+00 -1.64987386e-06 -1.78644680e-06 4.99996259e-06 6.60489760e-08 -1.17310942e-07 -1.37067125e-08 + 2.06250000e-04 1.00000645e+00 -6.80567452e-06 -7.36903992e-06 2.06243634e-05 2.72452079e-07 -4.83901329e-07 -5.65402321e-08 + 6.94531250e-04 1.00002172e+00 -2.29170130e-05 -2.48141246e-05 6.94459068e-05 9.17462289e-07 -1.62943467e-06 -1.90395397e-07 + 2.22041016e-03 1.00006925e+00 -7.33503261e-05 -7.94272245e-05 2.21967260e-04 2.94980399e-06 -5.19446856e-06 -5.72612240e-07 + 4.60459595e-03 1.00009813e+00 -1.68812021e-04 -1.93473291e-04 4.60142773e-04 5.62627098e-06 -9.51151943e-06 -3.41084185e-06 + 6.46724110e-03 1.00010295e+00 -2.48878741e-04 -2.94824866e-04 6.46099195e-04 9.45377475e-06 -1.08886313e-05 -7.33255158e-06 + 8.40749646e-03 1.00010584e+00 -3.32779773e-04 -4.02030070e-04 8.39693377e-04 1.38848305e-05 -1.14289046e-05 -1.15648074e-05 + 1.04285958e-02 1.00010819e+00 -4.19792901e-04 -5.14743202e-04 1.04123413e-03 1.90424059e-05 -1.12815085e-05 -1.55843789e-05 + 1.25339076e-02 1.00011031e+00 -5.10034573e-04 -6.32868791e-04 1.25104238e-03 2.48202492e-05 -1.06769358e-05 -1.95164207e-05 + 1.47269407e-02 1.00011233e+00 -6.03722672e-04 -7.56410373e-04 1.46945165e-03 3.11346796e-05 -9.78322728e-06 -2.34624687e-05 + 1.81535550e-02 1.00011532e+00 -7.49749610e-04 -9.49982071e-04 1.81042852e-03 4.12620011e-05 -8.23780007e-06 -2.93494904e-05 + 2.17229449e-02 1.00011828e+00 -9.01794636e-04 -1.15182362e-03 2.16524049e-03 5.17430142e-05 -6.67568858e-06 -3.55422511e-05 + 2.54410593e-02 1.00012127e+00 -1.05987183e-03 -1.36246357e-03 2.53443295e-03 6.25424325e-05 -5.12920050e-06 -4.19523679e-05 + 2.93140952e-02 1.00012432e+00 -1.22415325e-03 -1.58232133e-03 2.91857147e-03 7.37056757e-05 -3.65293901e-06 -4.85468887e-05 + 3.33485076e-02 1.00012744e+00 -1.39479991e-03 -1.81186674e-03 3.31824235e-03 8.52342734e-05 -2.35017674e-06 -5.52680712e-05 + 3.75510205e-02 1.00013065e+00 -1.57220560e-03 -2.05136186e-03 3.73405340e-03 9.71713419e-05 -1.23437065e-06 -6.21038768e-05 + 4.19286381e-02 1.00013395e+00 -1.75672120e-03 -2.30114377e-03 4.16663445e-03 1.09477481e-04 -3.99120510e-07 -6.91034628e-05 + 4.64886564e-02 1.00013736e+00 -1.94869165e-03 -2.56158783e-03 4.61663805e-03 1.22194178e-04 1.08288760e-08 -7.63386823e-05 + 5.12386755e-02 1.00014089e+00 -2.14844224e-03 -2.83311982e-03 5.08474013e-03 1.35378338e-04 -1.99053655e-08 -8.38608359e-05 + 5.61866121e-02 1.00014454e+00 -2.35627013e-03 -3.11622744e-03 5.57164073e-03 1.48975529e-04 -4.08495423e-07 -9.16896645e-05 + 6.13407126e-02 1.00014832e+00 -2.57251101e-03 -3.41139085e-03 6.07806462e-03 1.62951588e-04 -1.12468231e-06 -9.97626983e-05 + 6.67095674e-02 1.00015223e+00 -2.79748808e-03 -3.71913846e-03 6.60476195e-03 1.77339898e-04 -2.16330771e-06 -1.08061775e-04 + 7.23021245e-02 1.00015628e+00 -3.03155481e-03 -4.04000415e-03 7.15250901e-03 1.92126989e-04 -3.52830598e-06 -1.16629588e-04 + 7.81277047e-02 1.00016048e+00 -3.27513057e-03 -4.37449280e-03 7.72210878e-03 2.07357729e-04 -5.21461864e-06 -1.25495637e-04 + 8.41960175e-02 1.00016483e+00 -3.52875181e-03 -4.72303011e-03 8.31439172e-03 2.23065438e-04 -7.20619744e-06 -1.34690489e-04 + 9.05171766e-02 1.00016934e+00 -3.79297412e-03 -5.08606342e-03 8.93021640e-03 2.39274052e-04 -9.49266217e-06 -1.44204800e-04 + 1.00393988e-01 1.00017658e+00 -4.20570853e-03 -5.65319501e-03 9.89012715e-03 2.64397013e-04 -1.34744940e-05 -1.58920414e-04 + 1.10682333e-01 1.00018407e+00 -4.63546601e-03 -6.24414279e-03 1.08870420e-02 2.90436163e-04 -1.79869903e-05 -1.74029150e-04 + 1.21399359e-01 1.00019182e+00 -5.08280826e-03 -6.86004632e-03 1.19222615e-02 3.17457565e-04 -2.30142035e-05 -1.89516956e-04 + 1.32562927e-01 1.00019984e+00 -5.54840486e-03 -7.50200843e-03 1.29971214e-02 3.45434766e-04 -2.86303191e-05 -2.05463478e-04 + 1.44191645e-01 1.00020813e+00 -6.03303614e-03 -8.17109425e-03 1.41129932e-02 3.74307158e-04 -3.48520603e-05 -2.21938901e-04 + 1.56304892e-01 1.00021672e+00 -6.53744375e-03 -8.86848625e-03 1.52712835e-02 4.04025594e-04 -4.16374267e-05 -2.38970207e-04 + 1.68922858e-01 1.00022560e+00 -7.06237576e-03 -9.59544035e-03 1.64734340e-02 4.34687054e-04 -4.88891360e-05 -2.56649212e-04 + 1.82066573e-01 1.00023479e+00 -7.60869260e-03 -1.03531820e-02 1.77209214e-02 4.66436817e-04 -5.64706471e-05 -2.75004811e-04 + 1.95757942e-01 1.00024429e+00 -8.17718209e-03 -1.11430991e-02 1.90152571e-02 4.99287695e-04 -6.44165823e-05 -2.94066905e-04 + 2.10019785e-01 1.00025412e+00 -8.76860319e-03 -1.19667006e-02 2.03579865e-02 5.33236664e-04 -7.28899109e-05 -3.13857860e-04 + 2.24875872e-01 1.00026429e+00 -9.38387080e-03 -1.28254315e-02 2.17506886e-02 5.68306306e-04 -8.20021324e-05 -3.34377499e-04 + 2.40350962e-01 1.00027479e+00 -1.00238994e-02 -1.37208358e-02 2.31949751e-02 6.04526527e-04 -9.18121604e-05 -3.55630525e-04 + 2.56470847e-01 1.00028566e+00 -1.06897240e-02 -1.46544384e-02 2.46924893e-02 6.41934801e-04 -1.02319367e-04 -3.77604517e-04 + 2.73262395e-01 1.00029689e+00 -1.13824207e-02 -1.56278303e-02 2.62449049e-02 6.80595074e-04 -1.13547817e-04 -4.00281506e-04 + 2.90753590e-01 1.00030849e+00 -1.21031382e-02 -1.66426420e-02 2.78539249e-02 7.20624864e-04 -1.25562959e-04 -4.23641530e-04 + 3.08973585e-01 1.00032048e+00 -1.28530455e-02 -1.77005996e-02 2.95212797e-02 7.62156619e-04 -1.38388340e-04 -4.47692314e-04 + 3.27952746e-01 1.00033288e+00 -1.36333532e-02 -1.88035096e-02 3.12487258e-02 8.05249365e-04 -1.52024568e-04 -4.72520240e-04 + 3.47722706e-01 1.00034568e+00 -1.44453625e-02 -1.99532113e-02 3.30380438e-02 8.49924313e-04 -1.66551628e-04 -4.98238409e-04 + 3.68316415e-01 1.00035891e+00 -1.52903764e-02 -2.11516733e-02 3.48910355e-02 8.96191479e-04 -1.82138398e-04 -5.24861314e-04 + 3.89768194e-01 1.00037257e+00 -1.61696644e-02 -2.24010362e-02 3.68095224e-02 9.43965067e-04 -1.98912978e-04 -5.52344138e-04 + 4.12113798e-01 1.00038668e+00 -1.70844764e-02 -2.37036027e-02 3.87953422e-02 9.93194768e-04 -2.16917875e-04 -5.80630541e-04 + 4.35390468e-01 1.00040126e+00 -1.80361178e-02 -2.50617673e-02 4.08503454e-02 1.04384657e-03 -2.36168593e-04 -6.09702300e-04 + 4.59637000e-01 1.00041631e+00 -1.90259861e-02 -2.64779878e-02 4.29763925e-02 1.09592034e-03 -2.56762846e-04 -6.39561246e-04 + 4.84893803e-01 1.00043187e+00 -2.00555637e-02 -2.79548014e-02 4.51753491e-02 1.14956913e-03 -2.78839642e-04 -6.70234963e-04 + 5.11202974e-01 1.00044793e+00 -2.11263526e-02 -2.94948934e-02 4.74490824e-02 1.20494992e-03 -3.02440038e-04 -7.01702437e-04 + 5.38608360e-01 1.00046452e+00 -2.22399003e-02 -3.11010774e-02 4.97994580e-02 1.26205860e-03 -3.27581144e-04 -7.33952836e-04 + 5.67155637e-01 1.00048166e+00 -2.33978001e-02 -3.27763041e-02 5.22283342e-02 1.32078026e-03 -3.54258892e-04 -7.66996936e-04 + 5.96892384e-01 1.00049936e+00 -2.46016757e-02 -3.45236880e-02 5.47375574e-02 1.38092067e-03 -3.82480964e-04 -8.00892442e-04 + 6.27868162e-01 1.00051766e+00 -2.58532165e-02 -3.63464800e-02 5.73289560e-02 1.44219234e-03 -4.12372908e-04 -8.35678162e-04 + 6.60134598e-01 1.00053657e+00 -2.71542174e-02 -3.82480332e-02 6.00043349e-02 1.50423981e-03 -4.44133306e-04 -8.71378920e-04 + 6.93745468e-01 1.00055611e+00 -2.85065219e-02 -4.02318675e-02 6.27654675e-02 1.56683741e-03 -4.77816460e-04 -9.07954412e-04 + 7.28756791e-01 1.00057632e+00 -2.99120735e-02 -4.23016169e-02 6.56140888e-02 1.62984389e-03 -5.13719216e-04 -9.45483976e-04 + 7.65226920e-01 1.00059722e+00 -3.13726896e-02 -4.44612770e-02 6.85518885e-02 1.69319959e-03 -5.52076385e-04 -9.83856842e-04 + 8.03216637e-01 1.00061884e+00 -3.28902796e-02 -4.67150137e-02 7.15805057e-02 1.75670850e-03 -5.92711648e-04 -1.02281939e-03 + 8.42789259e-01 1.00064122e+00 -3.44667818e-02 -4.90672272e-02 7.47015185e-02 1.82041357e-03 -6.35044814e-04 -1.06214020e-03 + 8.84010740e-01 1.00066439e+00 -3.61044008e-02 -5.15222907e-02 7.79164370e-02 1.88399687e-03 -6.78980238e-04 -1.10172445e-03 + 9.26949783e-01 1.00068840e+00 -3.78054283e-02 -5.40847905e-02 8.12266904e-02 1.94742995e-03 -7.25003233e-04 -1.14155906e-03 + 9.71677953e-01 1.00071327e+00 -3.95720579e-02 -5.67597287e-02 8.46336195e-02 2.01087349e-03 -7.73357152e-04 -1.18104980e-03 + 1.01826980e+00 1.00073907e+00 -4.14058779e-02 -5.95530309e-02 8.81384718e-02 2.07395921e-03 -8.24136115e-04 -1.21965053e-03 + 1.06680297e+00 1.00076583e+00 -4.33091289e-02 -6.24702637e-02 9.17423915e-02 2.13593840e-03 -8.77152457e-04 -1.25733406e-03 + 1.11735835e+00 1.00079361e+00 -4.52838344e-02 -6.55175729e-02 9.54464019e-02 2.19632148e-03 -9.32805269e-04 -1.29387689e-03 + 1.17002021e+00 1.00082246e+00 -4.73322611e-02 -6.87012101e-02 9.92513921e-02 2.25538803e-03 -9.91393636e-04 -1.32966177e-03 + 1.20000000e+00 1.00083445e+00 -4.84970434e-02 -7.05202032e-02 1.01393587e-01 2.28810468e-03 -1.02497986e-03 -1.34959621e-03 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_global.txt new file mode 100644 index 0000000..2bffeb3 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_global.txt @@ -0,0 +1,70 @@ + # Time Volume Plastic_Work + 5.00000000e-05 1.00000156e+00 0.00000000e+00 + 2.06250000e-04 1.00000645e+00 1.36358016e-59 + 6.94531250e-04 1.00002172e+00 3.31947133e-32 + 2.22041016e-03 1.00006925e+00 1.65894349e-08 + 4.60459595e-03 1.00009813e+00 5.37295074e-06 + 6.46724110e-03 1.00010295e+00 1.22050855e-05 + 8.40749646e-03 1.00010584e+00 1.98870797e-05 + 1.04285958e-02 1.00010819e+00 2.82045982e-05 + 1.25339076e-02 1.00011031e+00 3.71138635e-05 + 1.47269407e-02 1.00011233e+00 4.66091275e-05 + 1.81535550e-02 1.00011532e+00 6.18742258e-05 + 2.17229449e-02 1.00011828e+00 7.82083067e-05 + 2.54410593e-02 1.00012127e+00 9.56605448e-05 + 2.93140952e-02 1.00012432e+00 1.14293809e-04 + 3.33485076e-02 1.00012744e+00 1.34184498e-04 + 3.75510205e-02 1.00013065e+00 1.55411029e-04 + 4.19286381e-02 1.00013395e+00 1.78060390e-04 + 4.64886564e-02 1.00013736e+00 2.02229000e-04 + 5.12386755e-02 1.00014089e+00 2.28020237e-04 + 5.61866121e-02 1.00014454e+00 2.55548152e-04 + 6.13407126e-02 1.00014832e+00 2.84932617e-04 + 6.67095674e-02 1.00015223e+00 3.16303490e-04 + 7.23021245e-02 1.00015628e+00 3.49800419e-04 + 7.81277047e-02 1.00016048e+00 3.85573456e-04 + 8.41960175e-02 1.00016483e+00 4.23781920e-04 + 9.05171766e-02 1.00016934e+00 4.64597566e-04 + 1.00393988e-01 1.00017658e+00 5.30809645e-04 + 1.10682333e-01 1.00018407e+00 6.02390280e-04 + 1.21399359e-01 1.00019182e+00 6.79746012e-04 + 1.32562927e-01 1.00019984e+00 7.63311237e-04 + 1.44191645e-01 1.00020813e+00 8.53548598e-04 + 1.56304892e-01 1.00021672e+00 9.50955456e-04 + 1.68922858e-01 1.00022560e+00 1.05606226e-03 + 1.82066573e-01 1.00023479e+00 1.16943234e-03 + 1.95757942e-01 1.00024429e+00 1.29167122e-03 + 2.10019785e-01 1.00025412e+00 1.42342599e-03 + 2.24875872e-01 1.00026429e+00 1.56538397e-03 + 2.40350962e-01 1.00027479e+00 1.71827780e-03 + 2.56470847e-01 1.00028566e+00 1.88288013e-03 + 2.73262395e-01 1.00029689e+00 2.06001377e-03 + 2.90753590e-01 1.00030849e+00 2.25055382e-03 + 3.08973585e-01 1.00032048e+00 2.45542606e-03 + 3.27952746e-01 1.00033288e+00 2.67561119e-03 + 3.47722706e-01 1.00034568e+00 2.91214499e-03 + 3.68316415e-01 1.00035891e+00 3.16612580e-03 + 3.89768194e-01 1.00037257e+00 3.43871424e-03 + 4.12113798e-01 1.00038668e+00 3.73113237e-03 + 4.35390468e-01 1.00040126e+00 4.04466335e-03 + 4.59637000e-01 1.00041631e+00 4.38066009e-03 + 4.84893803e-01 1.00043187e+00 4.74054635e-03 + 5.11202974e-01 1.00044793e+00 5.12581667e-03 + 5.38608360e-01 1.00046452e+00 5.53803223e-03 + 5.67155637e-01 1.00048166e+00 5.97882811e-03 + 5.96892384e-01 1.00049936e+00 6.44991879e-03 + 6.27868162e-01 1.00051766e+00 6.95309001e-03 + 6.60134598e-01 1.00053657e+00 7.49019970e-03 + 6.93745468e-01 1.00055611e+00 8.06318622e-03 + 7.28756791e-01 1.00057632e+00 8.67405492e-03 + 7.65226920e-01 1.00059722e+00 9.32489196e-03 + 8.03216637e-01 1.00061884e+00 1.00178580e-02 + 8.42789259e-01 1.00064122e+00 1.07552005e-02 + 8.84010740e-01 1.00066439e+00 1.15392106e-02 + 9.26949783e-01 1.00068840e+00 1.23722583e-02 + 9.71677953e-01 1.00071327e+00 1.32567878e-02 + 1.01826980e+00 1.00073907e+00 1.41953143e-02 + 1.06680297e+00 1.00076583e+00 1.51904063e-02 + 1.11735835e+00 1.00079361e+00 1.62447144e-02 + 1.17002021e+00 1.00082246e+00 1.73609554e-02 + 1.20000000e+00 1.00083445e+00 1.80019490e-02 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_region_default_1.txt new file mode 100644 index 0000000..2bffeb3 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_pl_work_region_default_1.txt @@ -0,0 +1,70 @@ + # Time Volume Plastic_Work + 5.00000000e-05 1.00000156e+00 0.00000000e+00 + 2.06250000e-04 1.00000645e+00 1.36358016e-59 + 6.94531250e-04 1.00002172e+00 3.31947133e-32 + 2.22041016e-03 1.00006925e+00 1.65894349e-08 + 4.60459595e-03 1.00009813e+00 5.37295074e-06 + 6.46724110e-03 1.00010295e+00 1.22050855e-05 + 8.40749646e-03 1.00010584e+00 1.98870797e-05 + 1.04285958e-02 1.00010819e+00 2.82045982e-05 + 1.25339076e-02 1.00011031e+00 3.71138635e-05 + 1.47269407e-02 1.00011233e+00 4.66091275e-05 + 1.81535550e-02 1.00011532e+00 6.18742258e-05 + 2.17229449e-02 1.00011828e+00 7.82083067e-05 + 2.54410593e-02 1.00012127e+00 9.56605448e-05 + 2.93140952e-02 1.00012432e+00 1.14293809e-04 + 3.33485076e-02 1.00012744e+00 1.34184498e-04 + 3.75510205e-02 1.00013065e+00 1.55411029e-04 + 4.19286381e-02 1.00013395e+00 1.78060390e-04 + 4.64886564e-02 1.00013736e+00 2.02229000e-04 + 5.12386755e-02 1.00014089e+00 2.28020237e-04 + 5.61866121e-02 1.00014454e+00 2.55548152e-04 + 6.13407126e-02 1.00014832e+00 2.84932617e-04 + 6.67095674e-02 1.00015223e+00 3.16303490e-04 + 7.23021245e-02 1.00015628e+00 3.49800419e-04 + 7.81277047e-02 1.00016048e+00 3.85573456e-04 + 8.41960175e-02 1.00016483e+00 4.23781920e-04 + 9.05171766e-02 1.00016934e+00 4.64597566e-04 + 1.00393988e-01 1.00017658e+00 5.30809645e-04 + 1.10682333e-01 1.00018407e+00 6.02390280e-04 + 1.21399359e-01 1.00019182e+00 6.79746012e-04 + 1.32562927e-01 1.00019984e+00 7.63311237e-04 + 1.44191645e-01 1.00020813e+00 8.53548598e-04 + 1.56304892e-01 1.00021672e+00 9.50955456e-04 + 1.68922858e-01 1.00022560e+00 1.05606226e-03 + 1.82066573e-01 1.00023479e+00 1.16943234e-03 + 1.95757942e-01 1.00024429e+00 1.29167122e-03 + 2.10019785e-01 1.00025412e+00 1.42342599e-03 + 2.24875872e-01 1.00026429e+00 1.56538397e-03 + 2.40350962e-01 1.00027479e+00 1.71827780e-03 + 2.56470847e-01 1.00028566e+00 1.88288013e-03 + 2.73262395e-01 1.00029689e+00 2.06001377e-03 + 2.90753590e-01 1.00030849e+00 2.25055382e-03 + 3.08973585e-01 1.00032048e+00 2.45542606e-03 + 3.27952746e-01 1.00033288e+00 2.67561119e-03 + 3.47722706e-01 1.00034568e+00 2.91214499e-03 + 3.68316415e-01 1.00035891e+00 3.16612580e-03 + 3.89768194e-01 1.00037257e+00 3.43871424e-03 + 4.12113798e-01 1.00038668e+00 3.73113237e-03 + 4.35390468e-01 1.00040126e+00 4.04466335e-03 + 4.59637000e-01 1.00041631e+00 4.38066009e-03 + 4.84893803e-01 1.00043187e+00 4.74054635e-03 + 5.11202974e-01 1.00044793e+00 5.12581667e-03 + 5.38608360e-01 1.00046452e+00 5.53803223e-03 + 5.67155637e-01 1.00048166e+00 5.97882811e-03 + 5.96892384e-01 1.00049936e+00 6.44991879e-03 + 6.27868162e-01 1.00051766e+00 6.95309001e-03 + 6.60134598e-01 1.00053657e+00 7.49019970e-03 + 6.93745468e-01 1.00055611e+00 8.06318622e-03 + 7.28756791e-01 1.00057632e+00 8.67405492e-03 + 7.65226920e-01 1.00059722e+00 9.32489196e-03 + 8.03216637e-01 1.00061884e+00 1.00178580e-02 + 8.42789259e-01 1.00064122e+00 1.07552005e-02 + 8.84010740e-01 1.00066439e+00 1.15392106e-02 + 9.26949783e-01 1.00068840e+00 1.23722583e-02 + 9.71677953e-01 1.00071327e+00 1.32567878e-02 + 1.01826980e+00 1.00073907e+00 1.41953143e-02 + 1.06680297e+00 1.00076583e+00 1.51904063e-02 + 1.11735835e+00 1.00079361e+00 1.62447144e-02 + 1.17002021e+00 1.00082246e+00 1.73609554e-02 + 1.20000000e+00 1.00083445e+00 1.80019490e-02 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global copy.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global copy.txt new file mode 100644 index 0000000..3591ecc --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global copy.txt @@ -0,0 +1,63 @@ + # Time Volume Sxx Syy Szz Sxy Sxz Syz + 5.00000000e-05 9.99998436e-01 -1.15438016e-14 -1.17951784e-14 -6.42983008e-04 4.06656919e-16 8.21398965e-16 8.86975495e-17 + 2.06250000e-04 9.99993550e-01 -4.25634510e-13 -4.29366326e-13 -2.65234750e-03 1.27395729e-14 -1.92240117e-14 3.04365975e-15 + 6.94531250e-04 9.99978280e-01 -1.36294679e-11 -1.37292789e-11 -8.93202744e-03 4.55870714e-13 -7.35919466e-13 1.03603043e-13 + 2.22041016e-03 9.99930760e-01 -7.07067300e-11 -2.80658159e-10 -2.84803206e-02 3.69802595e-11 1.18577358e-10 5.11152472e-11 + 4.60459595e-03 9.99901977e-01 -9.66715638e-10 3.94276702e-10 -4.03351500e-02 -4.63152398e-10 2.99332068e-10 -3.15202359e-12 + 6.46724110e-03 9.99897218e-01 -3.56309221e-10 -1.39719952e-09 -4.23034880e-02 -1.56516319e-11 -2.78544897e-10 5.76434479e-10 + 8.40749646e-03 9.99894393e-01 -2.78702908e-12 -2.14243516e-11 -4.34777055e-02 -1.32028160e-11 -4.90680293e-11 -4.09336434e-13 + 1.04285958e-02 9.99892111e-01 -1.35196247e-12 -1.28748833e-11 -4.44292269e-02 -3.98153471e-11 -1.58125351e-11 6.14934554e-13 + 1.25339076e-02 9.99890059e-01 -2.71489557e-12 -5.53343022e-12 -4.52877602e-02 -2.27315198e-11 -3.93802528e-12 5.26102204e-13 + 1.47269407e-02 9.99888107e-01 -1.23466878e-09 -1.32690472e-09 -4.61060975e-02 -2.05351296e-09 -1.56505680e-09 -1.71568762e-12 + 1.81535550e-02 9.99885307e-01 -6.51200170e-12 -5.04687393e-12 -4.72959467e-02 -2.46007635e-11 -9.73838949e-12 5.44202033e-13 + 2.17229449e-02 9.99882544e-01 -2.28413686e-12 -2.15040129e-12 -4.84740811e-02 -1.12793016e-11 -2.65352514e-12 -4.18627943e-13 + 2.54410593e-02 9.99879766e-01 1.29257591e-12 -9.80421311e-13 -4.96616380e-02 -5.05913767e-12 -2.54979330e-12 -6.99517279e-13 + 2.93140952e-02 9.99876947e-01 -2.23000299e-12 -4.80897131e-12 -5.08699810e-02 -1.24177098e-11 5.14419101e-12 7.41433851e-13 + 3.33485076e-02 9.99874073e-01 1.28936548e-13 -1.17013103e-12 -5.21047295e-02 -4.80590390e-12 4.71718792e-12 3.32816815e-13 + 3.75510205e-02 9.99871132e-01 1.17996715e-12 -1.02364533e-12 -5.33718859e-02 -2.07663173e-12 -1.28745352e-12 -8.83998085e-14 + 4.19286381e-02 9.99868113e-01 1.44124691e-12 -3.89201441e-13 -5.46759754e-02 -3.22302223e-12 -5.48559591e-12 -5.57536869e-13 + 4.64886564e-02 9.99865009e-01 1.14162126e-12 1.35286605e-13 -5.60199674e-02 -2.53976795e-12 -7.24327002e-12 -5.80135528e-13 + 5.12386755e-02 9.99861816e-01 -8.94573332e-14 -4.43000955e-13 -5.74069323e-02 -5.48139856e-12 -6.20037629e-12 -6.27442861e-13 + 5.61866121e-02 9.99858527e-01 -7.17610122e-13 -1.00227162e-12 -5.88390510e-02 -7.99044575e-12 -6.38930357e-12 -7.76033492e-13 + 6.13407126e-02 9.99855139e-01 -3.17630957e-11 -2.80103571e-11 -6.03189260e-02 -5.36661708e-10 -5.68974373e-10 -8.98187119e-11 + 6.93939948e-02 9.99850095e-01 -8.24340911e-12 -4.65761449e-12 -6.26047279e-02 -3.81105377e-11 -2.27794421e-11 -2.51902174e-12 + 7.77828304e-02 9.99844925e-01 -3.99831826e-12 -5.06428872e-12 -6.49599864e-02 -3.12235590e-11 -1.50785577e-11 -4.06052772e-13 + 8.65212008e-02 9.99839626e-01 1.87324601e-12 -8.66005887e-13 -6.73883290e-02 -1.47854438e-11 -1.14372534e-11 3.72549758e-12 + 9.56236699e-02 9.99834196e-01 1.44484325e-12 8.03844220e-14 -6.98924149e-02 -1.61902980e-11 -9.77868039e-12 9.97502044e-13 + 1.05105409e-01 9.99828631e-01 -1.58576119e-12 -2.03124015e-12 -7.24750488e-02 -1.46952884e-11 8.33502200e-12 2.26920153e-12 + 1.14982220e-01 9.99822932e-01 -4.75735164e-12 -3.55155196e-12 -7.51384622e-02 -1.31308036e-11 1.14578221e-11 1.01318920e-12 + 1.25270565e-01 9.99817099e-01 -1.22048513e-12 -2.99028039e-12 -7.78845126e-02 -4.44296288e-12 9.65977794e-12 1.14929077e-12 + 1.35987591e-01 9.99811134e-01 2.59308342e-12 -1.40365816e-12 -8.07150565e-02 4.33336089e-12 5.04847811e-12 1.19454071e-12 + 1.47151159e-01 9.99805040e-01 9.14514826e-13 -1.84491795e-12 -8.36319499e-02 8.59410349e-13 5.55081257e-12 5.30313868e-13 + 1.58779877e-01 9.99798818e-01 -4.05867171e-12 -3.52363513e-12 -8.66369691e-02 -8.69631251e-12 9.41391639e-12 9.30427933e-13 + 1.70893124e-01 9.99792474e-01 -8.30278872e-12 -6.77314180e-12 -8.97313996e-02 -2.51985076e-11 1.70541406e-12 5.31573589e-13 + 1.83511090e-01 9.99786013e-01 -6.64888465e-12 -2.66634075e-12 -9.29163379e-02 -2.04778561e-11 1.86724007e-12 8.82553620e-13 + 1.96654805e-01 9.99779441e-01 -4.59604053e-12 5.54291923e-10 -9.61930496e-02 1.38648077e-10 1.07364442e-10 4.24260016e-10 + 2.17191859e-01 9.99770658e-01 -5.99168688e-12 -2.37075427e-12 -1.01210864e-01 -6.16540018e-11 -8.91415768e-12 1.72713949e-11 + 2.38584623e-01 9.99761924e-01 9.38465087e-12 1.24131895e-11 -1.06334252e-01 -3.82503976e-11 -1.52763958e-11 1.36018426e-11 + 2.60868753e-01 9.99753272e-01 7.45421263e-11 5.69211682e-11 -1.11560755e-01 4.20674815e-11 -2.80599840e-11 2.72966720e-11 + 2.84081388e-01 9.99744740e-01 1.37757725e-10 1.12754450e-10 -1.16887429e-01 1.06239483e-10 -4.95918658e-11 4.21685083e-11 + 3.08261217e-01 9.99736370e-01 1.23779307e-10 1.20542616e-10 -1.22311650e-01 6.89742060e-11 -7.84941376e-11 4.39422104e-11 + 3.33448538e-01 9.99728205e-01 1.74445015e-10 1.54256857e-10 -1.27830074e-01 9.71045858e-11 -1.45697377e-10 3.57199603e-11 + 3.59685331e-01 9.99720297e-01 2.67048533e-10 2.24334090e-10 -1.33438560e-01 1.54506885e-10 -2.22887490e-10 3.48120149e-11 + 3.87015323e-01 9.99712700e-01 3.65455905e-10 2.97070790e-10 -1.39132382e-01 2.24147845e-10 -2.56661523e-10 3.67218062e-11 + 4.15484065e-01 9.99705477e-01 5.08314122e-10 3.76076344e-10 -1.44906315e-01 3.36866459e-10 -2.56606226e-10 5.54250903e-11 + 4.45139005e-01 9.99698696e-01 5.64578237e-10 3.77618418e-10 -1.50754282e-01 3.61318101e-10 -2.20669988e-10 6.04236300e-11 + 4.76029568e-01 9.99692430e-01 5.90907137e-10 3.80209324e-10 -1.56670442e-01 3.04717960e-10 -2.56388214e-10 5.13316844e-11 + 5.08207237e-01 9.99686759e-01 8.35630298e-10 4.87769821e-10 -1.62648170e-01 4.80501688e-10 -2.52644490e-10 7.88005222e-11 + 5.41725642e-01 9.99681773e-01 1.09252919e-09 5.78177628e-10 -1.68679946e-01 5.92928373e-10 -2.59147441e-10 7.57350624e-11 + 5.76640647e-01 9.99677572e-01 1.30659032e-09 5.70326423e-10 -1.74757286e-01 4.78627304e-10 -6.32036711e-10 8.45609250e-11 + 6.13010445e-01 9.99674261e-01 1.73841704e-09 6.76927192e-10 -1.80871901e-01 4.10267821e-10 -9.75258970e-10 6.37301658e-11 + 6.50895651e-01 9.99671957e-01 2.28292944e-09 1.09112961e-09 -1.87014791e-01 4.70396759e-10 -1.26450185e-09 3.97183263e-11 + 6.90359406e-01 9.99670788e-01 2.48997152e-09 1.27395201e-09 -1.93175992e-01 2.03342833e-10 -1.38864661e-09 1.09111558e-10 + 7.31467486e-01 9.99670896e-01 2.91660633e-09 1.23171218e-09 -1.99344529e-01 4.37139713e-10 -1.07603086e-09 2.57190231e-10 + 7.74288401e-01 9.99672435e-01 3.34640710e-09 1.00583608e-09 -2.05509453e-01 5.69943664e-10 -7.24407338e-10 3.10980227e-10 + 8.18893522e-01 9.99675573e-01 3.35013045e-09 3.01704158e-10 -2.11660412e-01 -8.90057492e-11 -9.00186682e-10 1.66989017e-10 + 8.65357189e-01 9.99680496e-01 3.76373860e-09 -5.25935413e-12 -2.17786085e-01 -7.43317470e-11 4.48234033e-10 1.76984365e-10 + 9.13756842e-01 9.99687406e-01 4.30185184e-09 2.08850140e-10 -2.23874873e-01 -4.46329298e-10 1.22435211e-09 7.00366921e-12 + 9.64173148e-01 9.99696526e-01 4.24628049e-09 3.26472751e-10 -2.29915721e-01 -7.71149947e-10 1.04224271e-09 1.26073705e-10 + 1.01669013e+00 9.99708102e-01 4.17877879e-09 1.02008877e-09 -2.35895180e-01 -9.44208807e-10 1.15778464e-09 -6.06397376e-10 + 1.07139533e+00 9.99722402e-01 4.82596228e-09 1.37826508e-09 -2.41799759e-01 -1.68927741e-09 -5.52407666e-11 -1.06312674e-09 + 1.12837990e+00 9.99739724e-01 4.51625765e-09 3.03989291e-10 -2.47615395e-01 -4.08484851e-09 5.26023389e-10 -1.48085234e-09 + 1.18773883e+00 9.99760394e-01 5.48781979e-09 -1.82564686e-09 -2.53330882e-01 -6.12255160e-09 5.98445815e-09 -2.34394203e-09 + 1.20000000e+00 9.99759023e-01 2.61685452e-11 -1.09238373e-11 -2.54504369e-01 3.85584170e-11 -7.33548595e-12 8.02673036e-12 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global.txt new file mode 100644 index 0000000..b9e23fe --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_global.txt @@ -0,0 +1,70 @@ + # Time Volume Sxx Syy Szz Sxy Sxz Syz + 5.00000000e-05 1.00000156e+00 1.21038456e-14 1.21280482e-14 6.42975026e-04 2.31406498e-16 1.06522289e-15 -6.87555586e-17 + 2.06250000e-04 1.00000645e+00 4.26120026e-13 4.28838045e-13 2.65222940e-03 -1.34576235e-14 1.93526415e-14 -3.74845963e-15 + 6.94531250e-04 1.00002172e+00 1.36322112e-11 1.37220196e-11 8.93073348e-03 -4.63022685e-13 7.36986367e-13 -1.10480431e-13 + 2.22041016e-03 1.00006925e+00 -1.78469679e-11 1.65132296e-10 2.84676846e-02 4.84890041e-11 9.48052910e-11 -4.88204743e-12 + 4.60459595e-03 1.00009813e+00 1.04711135e-09 -2.64506171e-10 4.03244212e-02 1.70785901e-11 -4.62169345e-10 -4.53172180e-11 + 6.46724110e-03 1.00010295e+00 8.39996824e-10 1.74364922e-09 4.22940923e-02 3.27115109e-10 -2.00526810e-10 -5.43533441e-10 + 8.40749646e-03 1.00010584e+00 1.35728685e-12 -1.39083962e-11 4.34688223e-02 -1.72646669e-11 -2.97484721e-11 -5.71559651e-13 + 1.04285958e-02 1.00010819e+00 -1.22760553e-13 -4.97395092e-12 4.44204610e-02 -2.77716175e-11 -1.21135454e-11 -2.78669442e-14 + 1.25339076e-02 1.00011031e+00 -2.00403548e-13 -2.08893888e-12 4.52786760e-02 -6.07808187e-12 -6.33601826e-12 4.29126491e-13 + 1.47269407e-02 1.00011233e+00 3.94844564e-10 4.90950605e-11 4.60964482e-02 -1.70165106e-09 -1.27260465e-09 1.11661340e-10 + 1.81535550e-02 1.00011532e+00 1.08484414e-14 1.33524187e-12 4.72849483e-02 3.25554756e-12 -2.59471759e-12 2.78999575e-12 + 2.17229449e-02 1.00011828e+00 4.32292781e-14 2.21608782e-12 4.84611478e-02 8.50342310e-12 8.89583873e-13 1.10449259e-12 + 2.54410593e-02 1.00012127e+00 -2.60127590e-13 8.63295344e-13 4.96460722e-02 2.26480648e-12 1.21337866e-13 2.06257393e-13 + 2.93140952e-02 1.00012432e+00 -2.12444887e-12 -2.28776647e-12 5.08509268e-02 -2.08845160e-12 4.87247773e-12 1.30129661e-12 + 3.33485076e-02 1.00012744e+00 3.32259958e-13 2.20238994e-12 5.20812749e-02 2.80120628e-12 -2.16330838e-12 -1.19351614e-13 + 3.75510205e-02 1.00013065e+00 -8.71318379e-13 1.40840436e-12 5.33432568e-02 5.81115371e-13 -6.31504592e-13 2.42284876e-13 + 4.19286381e-02 1.00013395e+00 -6.72606475e-13 7.70841928e-13 5.46411212e-02 1.30786220e-12 1.20678747e-12 4.56345404e-13 + 4.64886564e-02 1.00013736e+00 -4.75536929e-13 3.72711767e-13 5.59778401e-02 -3.04707642e-13 1.09455955e-12 5.47644086e-13 + 5.12386755e-02 1.00014089e+00 1.37903962e-13 4.39966044e-13 5.73562615e-02 2.60650077e-12 7.63736922e-13 5.39684392e-13 + 5.61866121e-02 1.00014454e+00 4.53920929e-13 8.10661353e-13 5.87783729e-02 5.84868620e-12 2.15935386e-12 9.24963272e-13 + 6.13407126e-02 1.00014832e+00 4.93923323e-13 6.58039384e-13 6.02466982e-02 4.38204544e-12 -3.29281935e-13 8.12941419e-13 + 6.67095674e-02 1.00015223e+00 2.48031602e-13 9.47811757e-13 6.17632045e-02 2.31390998e-12 -2.17233660e-12 3.14153424e-13 + 7.23021245e-02 1.00015628e+00 -6.37458824e-13 5.52072192e-13 6.33297982e-02 -8.06972956e-14 -7.28901432e-13 -4.18027151e-14 + 7.81277047e-02 1.00016048e+00 -1.17325785e-12 -6.57479369e-14 6.49483954e-02 -2.47935430e-12 1.31694435e-12 -2.81458222e-13 + 8.41960175e-02 1.00016483e+00 -1.44291958e-12 2.27917538e-13 6.66210144e-02 -3.78919257e-12 2.14971341e-12 -2.55140701e-13 + 9.05171766e-02 1.00016934e+00 1.25799566e-10 1.46253908e-10 6.83494258e-02 3.67365402e-10 -1.18691711e-10 7.52520691e-11 + 1.00393988e-01 1.00017658e+00 -5.02263951e-12 1.12581685e-12 7.10182076e-02 -4.99190342e-12 8.80775110e-13 -2.07501101e-12 + 1.10682333e-01 1.00018407e+00 -7.14802860e-12 2.05556993e-12 7.37650406e-02 -1.41126833e-11 -3.62992144e-12 -2.82208188e-12 + 1.21399359e-01 1.00019182e+00 -5.69503270e-12 1.92601333e-13 7.65915851e-02 -1.48233656e-11 -9.92650862e-12 -2.83334123e-12 + 1.32562927e-01 1.00019984e+00 -1.06413954e-11 -1.94443697e-12 7.94993662e-02 -2.18817797e-11 -9.14138372e-12 -3.08204794e-12 + 1.44191645e-01 1.00020813e+00 -1.28122833e-11 -2.92915168e-12 8.24898901e-02 -3.10482778e-11 -1.95356608e-12 -2.39067432e-12 + 1.56304892e-01 1.00021672e+00 -1.69777216e-11 5.46669510e-13 8.55641000e-02 -3.41551399e-11 5.19353048e-13 -3.17095229e-12 + 1.68922858e-01 1.00022560e+00 -2.02036842e-11 -4.71708438e-12 8.87229742e-02 -4.31347555e-11 8.24805604e-12 -5.11311784e-12 + 1.82066573e-01 1.00023479e+00 -2.09145191e-11 -1.04014643e-11 9.19674957e-02 -5.09061125e-11 1.53002617e-11 -5.39250846e-12 + 1.95757942e-01 1.00024429e+00 -2.10569532e-11 -1.10130949e-11 9.52980708e-02 -5.36406337e-11 1.68185438e-11 -6.57584487e-12 + 2.10019785e-01 1.00025412e+00 -2.58136032e-11 -1.23966530e-11 9.87149279e-02 -6.42799152e-11 5.74784380e-12 -1.08682199e-11 + 2.24875872e-01 1.00026429e+00 -3.05152935e-11 -1.66768891e-11 1.02218321e-01 -7.99263271e-11 -5.88512917e-12 -8.74603943e-12 + 2.40350962e-01 1.00027479e+00 -3.55002371e-11 -1.87511779e-11 1.05808206e-01 -9.28076174e-11 -2.80176054e-12 -7.01710006e-12 + 2.56470847e-01 1.00028566e+00 -4.45961203e-11 -2.33714479e-11 1.09484581e-01 -1.11322883e-10 7.28651065e-12 -9.54285324e-12 + 2.73262395e-01 1.00029689e+00 -5.07805029e-11 -2.66928451e-11 1.13247025e-01 -1.22096923e-10 1.34772736e-11 -1.33208902e-11 + 2.90753590e-01 1.00030849e+00 -5.38036150e-11 -2.89353420e-11 1.17094873e-01 -1.26742014e-10 9.00504570e-12 -1.70708098e-11 + 3.08973585e-01 1.00032048e+00 -5.20335669e-11 -2.76733102e-11 1.21027296e-01 -1.25761518e-10 1.02661236e-11 -1.71569930e-11 + 3.27952746e-01 1.00033288e+00 -5.63288355e-11 -3.06735510e-11 1.25043107e-01 -1.41438261e-10 2.19191952e-11 -1.68587321e-11 + 3.47722706e-01 1.00034568e+00 -7.03698557e-11 -3.95746862e-11 1.29140948e-01 -1.78989605e-10 2.09001824e-11 -1.83062512e-11 + 3.68316415e-01 1.00035891e+00 -7.75732555e-11 -4.31857145e-11 1.33318983e-01 -2.31673375e-10 5.60846510e-12 -2.63515932e-11 + 3.89768194e-01 1.00037257e+00 -9.11499228e-11 -5.18226283e-11 1.37575096e-01 -2.94626694e-10 -2.61081483e-11 -3.20811286e-11 + 4.12113798e-01 1.00038668e+00 -1.10769861e-10 -6.55036204e-11 1.41907012e-01 -3.57360639e-10 -5.90262125e-12 -3.64316042e-11 + 4.35390468e-01 1.00040126e+00 -1.45364346e-10 -8.71814839e-11 1.46312241e-01 -4.88173776e-10 5.47777810e-11 -3.58664496e-11 + 4.59637000e-01 1.00041631e+00 -1.87518975e-10 -1.15399036e-10 1.50787729e-01 -6.50709975e-10 9.60853647e-11 -4.28906163e-11 + 4.84893803e-01 1.00043187e+00 -2.01648593e-10 -1.21483537e-10 1.55330068e-01 -7.42277035e-10 6.14761991e-11 -5.63225564e-11 + 5.11202974e-01 1.00044793e+00 -2.30152895e-10 -1.37035525e-10 1.59935509e-01 -8.35053015e-10 7.27899268e-13 -7.60034339e-11 + 5.38608360e-01 1.00046452e+00 -3.00850935e-10 -1.75508083e-10 1.64600152e-01 -1.00582792e-09 -4.28199118e-11 -1.00570917e-10 + 5.67155637e-01 1.00048166e+00 -3.65317276e-10 -2.08122992e-10 1.69319732e-01 -1.23986547e-09 -2.94835440e-11 -1.12768087e-10 + 5.96892384e-01 1.00049936e+00 -4.26591896e-10 -2.23634280e-10 1.74089466e-01 -1.50986599e-09 5.67494909e-11 -1.21468521e-10 + 6.27868162e-01 1.00051766e+00 -5.51376923e-10 -2.52811929e-10 1.78904390e-01 -1.93606700e-09 8.40882056e-11 -1.56495556e-10 + 6.60134598e-01 1.00053657e+00 -7.75007973e-10 -3.49601564e-10 1.83759342e-01 -2.60220479e-09 -2.12531385e-10 -2.77843063e-10 + 6.93745468e-01 1.00055611e+00 -8.78547554e-10 -6.09811713e-10 1.88648517e-01 -3.44733916e-09 -5.31175074e-10 -3.25042720e-10 + 7.28756791e-01 1.00057632e+00 -1.14975264e-09 -7.89429099e-10 1.93565999e-01 -4.50187086e-09 -6.67600383e-10 -4.58227993e-10 + 7.65226920e-01 1.00059722e+00 -1.46991266e-09 -9.27289963e-10 1.98505606e-01 -5.61848647e-09 -8.76845604e-10 -5.96092702e-10 + 8.03216637e-01 1.00061884e+00 -1.83116760e-09 -1.08114438e-09 2.03460653e-01 -6.85493932e-09 -1.00115275e-09 -8.38041525e-10 + 8.42789259e-01 1.00064122e+00 -2.01902139e-09 -1.32820734e-09 2.08423674e-01 -8.11487573e-09 -6.52368339e-10 -8.90495411e-10 + 8.84010740e-01 1.00066439e+00 -2.32466613e-09 -1.51269240e-09 2.13388167e-01 -9.68415804e-09 -3.84502118e-10 -9.95052511e-10 + 9.26949783e-01 1.00068840e+00 -2.85410331e-09 -2.01457768e-09 2.18346729e-01 -1.20032907e-08 -9.83694051e-10 -1.28924280e-09 + 9.71677953e-01 1.00071327e+00 -3.20105750e-09 -2.62595579e-09 2.23291645e-01 -1.35277206e-08 -1.98291837e-09 -1.60972310e-09 + 1.01826980e+00 1.00073907e+00 -4.26716380e-09 -3.60466209e-09 2.28214738e-01 -1.65799354e-08 -4.08881849e-09 -2.51502276e-09 + 1.06680297e+00 1.00076583e+00 -5.17573850e-09 -4.15913321e-09 2.33107911e-01 -1.98505250e-08 -4.08066685e-09 -3.18339055e-09 + 1.11735835e+00 1.00079361e+00 -6.57417063e-09 -4.75207583e-09 2.37962087e-01 -2.45439431e-08 -6.46099509e-09 -3.76923013e-09 + 1.17002021e+00 1.00082246e+00 -7.78733810e-09 -4.77147499e-09 2.42768679e-01 -2.80140243e-08 -7.87636859e-09 -4.51894000e-09 + 1.20000000e+00 1.00083445e+00 -6.99039894e-10 -4.27031977e-10 2.45424804e-01 -4.93423862e-09 -1.01815961e-09 -4.04024039e-10 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_region_default_1.txt new file mode 100644 index 0000000..b9e23fe --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m1/avg_stress_region_default_1.txt @@ -0,0 +1,70 @@ + # Time Volume Sxx Syy Szz Sxy Sxz Syz + 5.00000000e-05 1.00000156e+00 1.21038456e-14 1.21280482e-14 6.42975026e-04 2.31406498e-16 1.06522289e-15 -6.87555586e-17 + 2.06250000e-04 1.00000645e+00 4.26120026e-13 4.28838045e-13 2.65222940e-03 -1.34576235e-14 1.93526415e-14 -3.74845963e-15 + 6.94531250e-04 1.00002172e+00 1.36322112e-11 1.37220196e-11 8.93073348e-03 -4.63022685e-13 7.36986367e-13 -1.10480431e-13 + 2.22041016e-03 1.00006925e+00 -1.78469679e-11 1.65132296e-10 2.84676846e-02 4.84890041e-11 9.48052910e-11 -4.88204743e-12 + 4.60459595e-03 1.00009813e+00 1.04711135e-09 -2.64506171e-10 4.03244212e-02 1.70785901e-11 -4.62169345e-10 -4.53172180e-11 + 6.46724110e-03 1.00010295e+00 8.39996824e-10 1.74364922e-09 4.22940923e-02 3.27115109e-10 -2.00526810e-10 -5.43533441e-10 + 8.40749646e-03 1.00010584e+00 1.35728685e-12 -1.39083962e-11 4.34688223e-02 -1.72646669e-11 -2.97484721e-11 -5.71559651e-13 + 1.04285958e-02 1.00010819e+00 -1.22760553e-13 -4.97395092e-12 4.44204610e-02 -2.77716175e-11 -1.21135454e-11 -2.78669442e-14 + 1.25339076e-02 1.00011031e+00 -2.00403548e-13 -2.08893888e-12 4.52786760e-02 -6.07808187e-12 -6.33601826e-12 4.29126491e-13 + 1.47269407e-02 1.00011233e+00 3.94844564e-10 4.90950605e-11 4.60964482e-02 -1.70165106e-09 -1.27260465e-09 1.11661340e-10 + 1.81535550e-02 1.00011532e+00 1.08484414e-14 1.33524187e-12 4.72849483e-02 3.25554756e-12 -2.59471759e-12 2.78999575e-12 + 2.17229449e-02 1.00011828e+00 4.32292781e-14 2.21608782e-12 4.84611478e-02 8.50342310e-12 8.89583873e-13 1.10449259e-12 + 2.54410593e-02 1.00012127e+00 -2.60127590e-13 8.63295344e-13 4.96460722e-02 2.26480648e-12 1.21337866e-13 2.06257393e-13 + 2.93140952e-02 1.00012432e+00 -2.12444887e-12 -2.28776647e-12 5.08509268e-02 -2.08845160e-12 4.87247773e-12 1.30129661e-12 + 3.33485076e-02 1.00012744e+00 3.32259958e-13 2.20238994e-12 5.20812749e-02 2.80120628e-12 -2.16330838e-12 -1.19351614e-13 + 3.75510205e-02 1.00013065e+00 -8.71318379e-13 1.40840436e-12 5.33432568e-02 5.81115371e-13 -6.31504592e-13 2.42284876e-13 + 4.19286381e-02 1.00013395e+00 -6.72606475e-13 7.70841928e-13 5.46411212e-02 1.30786220e-12 1.20678747e-12 4.56345404e-13 + 4.64886564e-02 1.00013736e+00 -4.75536929e-13 3.72711767e-13 5.59778401e-02 -3.04707642e-13 1.09455955e-12 5.47644086e-13 + 5.12386755e-02 1.00014089e+00 1.37903962e-13 4.39966044e-13 5.73562615e-02 2.60650077e-12 7.63736922e-13 5.39684392e-13 + 5.61866121e-02 1.00014454e+00 4.53920929e-13 8.10661353e-13 5.87783729e-02 5.84868620e-12 2.15935386e-12 9.24963272e-13 + 6.13407126e-02 1.00014832e+00 4.93923323e-13 6.58039384e-13 6.02466982e-02 4.38204544e-12 -3.29281935e-13 8.12941419e-13 + 6.67095674e-02 1.00015223e+00 2.48031602e-13 9.47811757e-13 6.17632045e-02 2.31390998e-12 -2.17233660e-12 3.14153424e-13 + 7.23021245e-02 1.00015628e+00 -6.37458824e-13 5.52072192e-13 6.33297982e-02 -8.06972956e-14 -7.28901432e-13 -4.18027151e-14 + 7.81277047e-02 1.00016048e+00 -1.17325785e-12 -6.57479369e-14 6.49483954e-02 -2.47935430e-12 1.31694435e-12 -2.81458222e-13 + 8.41960175e-02 1.00016483e+00 -1.44291958e-12 2.27917538e-13 6.66210144e-02 -3.78919257e-12 2.14971341e-12 -2.55140701e-13 + 9.05171766e-02 1.00016934e+00 1.25799566e-10 1.46253908e-10 6.83494258e-02 3.67365402e-10 -1.18691711e-10 7.52520691e-11 + 1.00393988e-01 1.00017658e+00 -5.02263951e-12 1.12581685e-12 7.10182076e-02 -4.99190342e-12 8.80775110e-13 -2.07501101e-12 + 1.10682333e-01 1.00018407e+00 -7.14802860e-12 2.05556993e-12 7.37650406e-02 -1.41126833e-11 -3.62992144e-12 -2.82208188e-12 + 1.21399359e-01 1.00019182e+00 -5.69503270e-12 1.92601333e-13 7.65915851e-02 -1.48233656e-11 -9.92650862e-12 -2.83334123e-12 + 1.32562927e-01 1.00019984e+00 -1.06413954e-11 -1.94443697e-12 7.94993662e-02 -2.18817797e-11 -9.14138372e-12 -3.08204794e-12 + 1.44191645e-01 1.00020813e+00 -1.28122833e-11 -2.92915168e-12 8.24898901e-02 -3.10482778e-11 -1.95356608e-12 -2.39067432e-12 + 1.56304892e-01 1.00021672e+00 -1.69777216e-11 5.46669510e-13 8.55641000e-02 -3.41551399e-11 5.19353048e-13 -3.17095229e-12 + 1.68922858e-01 1.00022560e+00 -2.02036842e-11 -4.71708438e-12 8.87229742e-02 -4.31347555e-11 8.24805604e-12 -5.11311784e-12 + 1.82066573e-01 1.00023479e+00 -2.09145191e-11 -1.04014643e-11 9.19674957e-02 -5.09061125e-11 1.53002617e-11 -5.39250846e-12 + 1.95757942e-01 1.00024429e+00 -2.10569532e-11 -1.10130949e-11 9.52980708e-02 -5.36406337e-11 1.68185438e-11 -6.57584487e-12 + 2.10019785e-01 1.00025412e+00 -2.58136032e-11 -1.23966530e-11 9.87149279e-02 -6.42799152e-11 5.74784380e-12 -1.08682199e-11 + 2.24875872e-01 1.00026429e+00 -3.05152935e-11 -1.66768891e-11 1.02218321e-01 -7.99263271e-11 -5.88512917e-12 -8.74603943e-12 + 2.40350962e-01 1.00027479e+00 -3.55002371e-11 -1.87511779e-11 1.05808206e-01 -9.28076174e-11 -2.80176054e-12 -7.01710006e-12 + 2.56470847e-01 1.00028566e+00 -4.45961203e-11 -2.33714479e-11 1.09484581e-01 -1.11322883e-10 7.28651065e-12 -9.54285324e-12 + 2.73262395e-01 1.00029689e+00 -5.07805029e-11 -2.66928451e-11 1.13247025e-01 -1.22096923e-10 1.34772736e-11 -1.33208902e-11 + 2.90753590e-01 1.00030849e+00 -5.38036150e-11 -2.89353420e-11 1.17094873e-01 -1.26742014e-10 9.00504570e-12 -1.70708098e-11 + 3.08973585e-01 1.00032048e+00 -5.20335669e-11 -2.76733102e-11 1.21027296e-01 -1.25761518e-10 1.02661236e-11 -1.71569930e-11 + 3.27952746e-01 1.00033288e+00 -5.63288355e-11 -3.06735510e-11 1.25043107e-01 -1.41438261e-10 2.19191952e-11 -1.68587321e-11 + 3.47722706e-01 1.00034568e+00 -7.03698557e-11 -3.95746862e-11 1.29140948e-01 -1.78989605e-10 2.09001824e-11 -1.83062512e-11 + 3.68316415e-01 1.00035891e+00 -7.75732555e-11 -4.31857145e-11 1.33318983e-01 -2.31673375e-10 5.60846510e-12 -2.63515932e-11 + 3.89768194e-01 1.00037257e+00 -9.11499228e-11 -5.18226283e-11 1.37575096e-01 -2.94626694e-10 -2.61081483e-11 -3.20811286e-11 + 4.12113798e-01 1.00038668e+00 -1.10769861e-10 -6.55036204e-11 1.41907012e-01 -3.57360639e-10 -5.90262125e-12 -3.64316042e-11 + 4.35390468e-01 1.00040126e+00 -1.45364346e-10 -8.71814839e-11 1.46312241e-01 -4.88173776e-10 5.47777810e-11 -3.58664496e-11 + 4.59637000e-01 1.00041631e+00 -1.87518975e-10 -1.15399036e-10 1.50787729e-01 -6.50709975e-10 9.60853647e-11 -4.28906163e-11 + 4.84893803e-01 1.00043187e+00 -2.01648593e-10 -1.21483537e-10 1.55330068e-01 -7.42277035e-10 6.14761991e-11 -5.63225564e-11 + 5.11202974e-01 1.00044793e+00 -2.30152895e-10 -1.37035525e-10 1.59935509e-01 -8.35053015e-10 7.27899268e-13 -7.60034339e-11 + 5.38608360e-01 1.00046452e+00 -3.00850935e-10 -1.75508083e-10 1.64600152e-01 -1.00582792e-09 -4.28199118e-11 -1.00570917e-10 + 5.67155637e-01 1.00048166e+00 -3.65317276e-10 -2.08122992e-10 1.69319732e-01 -1.23986547e-09 -2.94835440e-11 -1.12768087e-10 + 5.96892384e-01 1.00049936e+00 -4.26591896e-10 -2.23634280e-10 1.74089466e-01 -1.50986599e-09 5.67494909e-11 -1.21468521e-10 + 6.27868162e-01 1.00051766e+00 -5.51376923e-10 -2.52811929e-10 1.78904390e-01 -1.93606700e-09 8.40882056e-11 -1.56495556e-10 + 6.60134598e-01 1.00053657e+00 -7.75007973e-10 -3.49601564e-10 1.83759342e-01 -2.60220479e-09 -2.12531385e-10 -2.77843063e-10 + 6.93745468e-01 1.00055611e+00 -8.78547554e-10 -6.09811713e-10 1.88648517e-01 -3.44733916e-09 -5.31175074e-10 -3.25042720e-10 + 7.28756791e-01 1.00057632e+00 -1.14975264e-09 -7.89429099e-10 1.93565999e-01 -4.50187086e-09 -6.67600383e-10 -4.58227993e-10 + 7.65226920e-01 1.00059722e+00 -1.46991266e-09 -9.27289963e-10 1.98505606e-01 -5.61848647e-09 -8.76845604e-10 -5.96092702e-10 + 8.03216637e-01 1.00061884e+00 -1.83116760e-09 -1.08114438e-09 2.03460653e-01 -6.85493932e-09 -1.00115275e-09 -8.38041525e-10 + 8.42789259e-01 1.00064122e+00 -2.01902139e-09 -1.32820734e-09 2.08423674e-01 -8.11487573e-09 -6.52368339e-10 -8.90495411e-10 + 8.84010740e-01 1.00066439e+00 -2.32466613e-09 -1.51269240e-09 2.13388167e-01 -9.68415804e-09 -3.84502118e-10 -9.95052511e-10 + 9.26949783e-01 1.00068840e+00 -2.85410331e-09 -2.01457768e-09 2.18346729e-01 -1.20032907e-08 -9.83694051e-10 -1.28924280e-09 + 9.71677953e-01 1.00071327e+00 -3.20105750e-09 -2.62595579e-09 2.23291645e-01 -1.35277206e-08 -1.98291837e-09 -1.60972310e-09 + 1.01826980e+00 1.00073907e+00 -4.26716380e-09 -3.60466209e-09 2.28214738e-01 -1.65799354e-08 -4.08881849e-09 -2.51502276e-09 + 1.06680297e+00 1.00076583e+00 -5.17573850e-09 -4.15913321e-09 2.33107911e-01 -1.98505250e-08 -4.08066685e-09 -3.18339055e-09 + 1.11735835e+00 1.00079361e+00 -6.57417063e-09 -4.75207583e-09 2.37962087e-01 -2.45439431e-08 -6.46099509e-09 -3.76923013e-09 + 1.17002021e+00 1.00082246e+00 -7.78733810e-09 -4.77147499e-09 2.42768679e-01 -2.80140243e-08 -7.87636859e-09 -4.51894000e-09 + 1.20000000e+00 1.00083445e+00 -6.99039894e-10 -4.27031977e-10 2.45424804e-01 -4.93423862e-09 -1.01815961e-09 -4.04024039e-10 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_global.txt new file mode 100644 index 0000000..8942ced --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_global.txt @@ -0,0 +1,61 @@ + # Time Volume F11 F12 F13 F21 F22 F23 F31 F32 F33 + 5.00000000e-02 9.99984364e-01 1.00001650e+00 1.84982920e-07 5.66620597e-07 8.91682017e-08 1.00001787e+00 1.23796114e-06 1.77961123e-06 -2.55893657e-06 9.99950000e-01 + 2.06250000e-01 9.99935759e-01 1.00006819e+00 7.08975750e-07 2.62792447e-06 3.42953820e-07 1.00007384e+00 5.25944604e-06 7.02180271e-06 -1.07466402e-05 9.99793750e-01 + 4.50390625e-01 9.99909751e-01 1.00016716e+00 3.01526328e-06 1.78460623e-05 4.34670524e-06 1.00019312e+00 2.54847625e-05 -2.91102157e-08 -3.69871256e-05 9.99549610e-01 + 6.41125488e-01 9.99905645e-01 1.00024951e+00 6.61894342e-06 2.67718113e-05 9.03779694e-06 1.00029755e+00 4.22326845e-05 -6.61482335e-06 -6.18594136e-05 9.99358875e-01 + 8.39807638e-01 9.99903137e-01 1.00033553e+00 1.03818785e-05 3.15340367e-05 1.36450841e-05 1.00040792e+00 5.67418478e-05 -1.09181552e-05 -8.58909758e-05 9.99160194e-01 + 1.04676821e+00 9.99901078e-01 1.00042474e+00 1.38097550e-05 3.35723602e-05 1.81509646e-05 1.00052392e+00 6.85541935e-05 -1.38021838e-05 -1.08727213e-04 9.98953233e-01 + 1.26235214e+00 9.99899206e-01 1.00051727e+00 1.70494421e-05 3.35718206e-05 2.27657139e-05 1.00064548e+00 7.79603120e-05 -1.54694556e-05 -1.30380934e-04 9.98737650e-01 + 1.48691873e+00 9.99897410e-01 1.00061346e+00 2.01692140e-05 3.20357903e-05 2.74307274e-05 1.00077254e+00 8.61288759e-05 -1.60345087e-05 -1.51796556e-04 9.98513083e-01 + 1.72084227e+00 9.99895640e-01 1.00071359e+00 2.33040059e-05 2.97482888e-05 3.22377975e-05 1.00090514e+00 9.35922774e-05 -1.60129857e-05 -1.73218754e-04 9.98279160e-01 + 2.08634779e+00 9.99893048e-01 1.00086999e+00 2.82391937e-05 2.54123093e-05 3.99215427e-05 1.00111274e+00 1.03792872e-04 -1.49950641e-05 -2.05042089e-04 9.97913655e-01 + 2.46708271e+00 9.99890445e-01 1.00103259e+00 3.31814330e-05 2.06151321e-05 4.80440523e-05 1.00132962e+00 1.13622925e-04 -1.34964075e-05 -2.37187359e-04 9.97532920e-01 + 2.86368158e+00 9.99887810e-01 1.00120156e+00 3.80233325e-05 1.58041863e-05 5.65950083e-05 1.00155626e+00 1.23208345e-04 -1.17662286e-05 -2.69879566e-04 9.97136322e-01 + 3.27680541e+00 9.99885127e-01 1.00137720e+00 4.27791021e-05 1.11531480e-05 6.54972829e-05 1.00179304e+00 1.32915615e-04 -9.79505403e-06 -3.03535500e-04 9.96723199e-01 + 3.70714273e+00 9.99882382e-01 1.00155997e+00 4.75143003e-05 6.36539450e-06 7.46789969e-05 1.00204022e+00 1.42977445e-04 -7.14972408e-06 -3.38322208e-04 9.96292862e-01 + 4.15541077e+00 9.99879566e-01 1.00175020e+00 5.24039789e-05 1.46245442e-06 8.41695915e-05 1.00229819e+00 1.53342610e-04 -3.45448756e-06 -3.74285867e-04 9.95844595e-01 + 4.62235665e+00 9.99876673e-01 1.00194826e+00 5.76153279e-05 -3.61998846e-06 9.39326232e-05 1.00256740e+00 1.63924811e-04 1.36271291e-06 -4.11505721e-04 9.95377649e-01 + 5.10875860e+00 9.99873698e-01 1.00215448e+00 6.31792179e-05 -9.03371299e-06 1.03838676e-04 1.00284831e+00 1.74686008e-04 7.23915356e-06 -4.49801747e-04 9.94891248e-01 + 5.61542731e+00 9.99870636e-01 1.00236925e+00 6.91109181e-05 -1.47443325e-05 1.13929562e-04 1.00314140e+00 1.85634851e-04 1.40823240e-05 -4.89170728e-04 9.94384580e-01 + 6.14320721e+00 9.99867483e-01 1.00259295e+00 7.54585736e-05 -2.05709882e-05 1.24253162e-04 1.00344719e+00 1.96783084e-04 2.17294582e-05 -5.29657733e-04 9.93856801e-01 + 6.69297793e+00 9.99864237e-01 1.00282596e+00 8.23770178e-05 -2.65151397e-05 1.34796429e-04 1.00376625e+00 2.08102258e-04 3.02373477e-05 -5.71246741e-04 9.93307031e-01 + 7.26565578e+00 9.99860893e-01 1.00306879e+00 8.98551697e-05 -3.25553006e-05 1.45585674e-04 1.00409903e+00 2.19720593e-04 3.94773317e-05 -6.14217436e-04 9.92734354e-01 + 7.86219520e+00 9.99857450e-01 1.00332195e+00 9.78606823e-05 -3.87700081e-05 1.56707143e-04 1.00444604e+00 2.31746310e-04 4.94344894e-05 -6.58715980e-04 9.92137816e-01 + 8.48359042e+00 9.99853903e-01 1.00358586e+00 1.06348806e-04 -4.51639076e-05 1.68188044e-04 1.00480797e+00 2.44108334e-04 6.01087310e-05 -7.04798350e-04 9.91516422e-01 + 9.13087712e+00 9.99850251e-01 1.00386094e+00 1.15264928e-04 -5.16299082e-05 1.79995958e-04 1.00518548e+00 2.56808901e-04 7.14078436e-05 -7.52679355e-04 9.90869136e-01 + 9.80513409e+00 9.99846491e-01 1.00414778e+00 1.24583286e-04 -5.82164126e-05 1.92114403e-04 1.00557919e+00 2.69812982e-04 8.33074598e-05 -8.02405850e-04 9.90194881e-01 + 1.05074851e+01 9.99842622e-01 1.00444691e+00 1.34282481e-04 -6.49752362e-05 2.04503541e-04 1.00598977e+00 2.83108295e-04 9.58299959e-05 -8.54045707e-04 9.89492531e-01 + 1.16049086e+01 9.99837000e-01 1.00491499e+00 1.49429151e-04 -7.54447989e-05 2.23348064e-04 1.00663263e+00 3.03616548e-04 1.15672511e-04 -9.34745693e-04 9.88395110e-01 + 1.27480580e+01 9.99831265e-01 1.00540327e+00 1.65415056e-04 -8.64152438e-05 2.42660712e-04 1.00730374e+00 3.25080005e-04 1.36715064e-04 -1.01919181e-03 9.87251963e-01 + 1.39388387e+01 9.99825419e-01 1.00591275e+00 1.82431494e-04 -9.79421080e-05 2.62599520e-04 1.00800431e+00 3.47424597e-04 1.58934600e-04 -1.10731282e-03 9.86061185e-01 + 1.51792352e+01 9.99819468e-01 1.00644441e+00 2.00407412e-04 -1.10147440e-04 2.83190522e-04 1.00873568e+00 3.70532555e-04 1.82417285e-04 -1.19900347e-03 9.84820791e-01 + 1.64713149e+01 9.99813418e-01 1.00699916e+00 2.19290428e-04 -1.23043322e-04 3.04534938e-04 1.00949938e+00 3.94493910e-04 2.07202737e-04 -1.29444029e-03 9.83528714e-01 + 1.78172313e+01 9.99807275e-01 1.00757802e+00 2.39024299e-04 -1.36701658e-04 3.26670658e-04 1.01029694e+00 4.19318585e-04 2.33437032e-04 -1.39380976e-03 9.82182801e-01 + 1.92192275e+01 9.99801048e-01 1.00818212e+00 2.59543843e-04 -1.51188522e-04 3.49552663e-04 1.01112990e+00 4.45064061e-04 2.61263784e-04 -1.49739499e-03 9.80780809e-01 + 2.06796402e+01 9.99794747e-01 1.00881268e+00 2.80876822e-04 -1.66613699e-04 3.73232305e-04 1.01199987e+00 4.71859027e-04 2.90807404e-04 -1.60564603e-03 9.79320400e-01 + 2.29615351e+01 9.99786710e-01 1.00980088e+00 3.14334612e-04 -1.91705504e-04 4.09919626e-04 1.01336504e+00 5.14135167e-04 3.38927873e-04 -1.77669488e-03 9.77038511e-01 + 2.53385090e+01 9.99778825e-01 1.01083363e+00 3.49118974e-04 -2.18775172e-04 4.48018693e-04 1.01479331e+00 5.58360276e-04 3.91334880e-04 -1.95645771e-03 9.74661543e-01 + 2.78145234e+01 9.99771134e-01 1.01191343e+00 3.85136735e-04 -2.48080198e-04 4.87897874e-04 1.01628753e+00 6.04714511e-04 4.48314345e-04 -2.14560176e-03 9.72185536e-01 + 3.03937051e+01 9.99763684e-01 1.01304280e+00 4.22406148e-04 -2.79841327e-04 5.29648084e-04 1.01785082e+00 6.53341582e-04 5.10008990e-04 -2.34460567e-03 9.69606362e-01 + 3.30803527e+01 9.99756528e-01 1.01422425e+00 4.60775181e-04 -3.14187385e-04 5.73243267e-04 1.01948668e+00 7.04231773e-04 5.76475022e-04 -2.55360288e-03 9.66919722e-01 + 3.58789439e+01 9.99749723e-01 1.01546035e+00 5.00035848e-04 -3.51375836e-04 6.18644934e-04 1.02119887e+00 7.57658516e-04 6.47874970e-04 -2.77339759e-03 9.64121140e-01 + 3.87941431e+01 9.99743335e-01 1.01675358e+00 5.40091701e-04 -3.92081381e-04 6.66064860e-04 1.02299160e+00 8.13553747e-04 7.24186285e-04 -3.00442276e-03 9.61205949e-01 + 4.18308089e+01 9.99737432e-01 1.01810684e+00 5.80515421e-04 -4.36695463e-04 7.15420929e-04 1.02486906e+00 8.72361332e-04 8.05481586e-04 -3.24780879e-03 9.58169293e-01 + 4.49940025e+01 9.99732089e-01 1.01952335e+00 6.20891089e-04 -4.85467867e-04 7.66853113e-04 1.02683557e+00 9.34171084e-04 8.91959331e-04 -3.50433625e-03 9.55006110e-01 + 4.82889958e+01 9.99727392e-01 1.02100619e+00 6.60845408e-04 -5.38805201e-04 8.20407551e-04 1.02889603e+00 9.99245422e-04 9.83752446e-04 -3.77525089e-03 9.51711127e-01 + 5.17212805e+01 9.99723435e-01 1.02255869e+00 7.00264016e-04 -5.96954610e-04 8.76280887e-04 1.03105561e+00 1.06783183e-03 1.08086936e-03 -4.06172413e-03 9.48278853e-01 + 5.52965771e+01 9.99720318e-01 1.02418452e+00 7.39089479e-04 -6.60146928e-04 9.34478380e-04 1.03331965e+00 1.13994335e-03 1.18441931e-03 -4.36486603e-03 9.44703567e-01 + 5.90208444e+01 9.99718152e-01 1.02588689e+00 7.77028643e-04 -7.29508639e-04 9.94897989e-04 1.03569449e+00 1.21536374e-03 1.29680388e-03 -4.68562049e-03 9.40979311e-01 + 6.29002894e+01 9.99717059e-01 1.02766927e+00 8.13905598e-04 -8.05715808e-04 1.05775290e-03 1.03818688e+00 1.29382168e-03 1.41992010e-03 -5.02439897e-03 9.37099877e-01 + 6.69413780e+01 9.99717171e-01 1.02953655e+00 8.49871468e-04 -8.88906832e-04 1.12337503e-03 1.04080274e+00 1.37568572e-03 1.55325526e-03 -5.38285720e-03 9.33058799e-01 + 7.11508453e+01 9.99718632e-01 1.03149291e+00 8.85141477e-04 -9.79232455e-04 1.19164581e-03 1.04354945e+00 1.46131487e-03 1.69594115e-03 -5.76371076e-03 9.28849343e-01 + 7.55357071e+01 9.99721604e-01 1.03354315e+00 9.19674343e-04 -1.07615904e-03 1.26268031e-03 1.04643456e+00 1.55023978e-03 1.84776622e-03 -6.16873059e-03 9.24464492e-01 + 8.01032714e+01 9.99726262e-01 1.03569304e+00 9.53355596e-04 -1.17953241e-03 1.33704496e-03 1.04946553e+00 1.64249241e-03 2.00932452e-03 -6.59853083e-03 9.19896937e-01 + 8.48611510e+01 9.99732796e-01 1.03794788e+00 9.85506347e-04 -1.28996596e-03 1.41482619e-03 1.05265116e+00 1.73854505e-03 2.18119754e-03 -7.05448490e-03 9.15139066e-01 + 8.98172755e+01 9.99741417e-01 1.04031329e+00 1.01523030e-03 -1.40858835e-03 1.49557814e-03 1.05600097e+00 1.83816020e-03 2.36565805e-03 -7.53672090e-03 9.10182950e-01 + 9.49799051e+01 9.99752360e-01 1.04279632e+00 1.04188740e-03 -1.53564613e-03 1.57889382e-03 1.05952412e+00 1.94077088e-03 2.56413533e-03 -8.04599932e-03 9.05020327e-01 + 1.00357644e+02 9.99765879e-01 1.04540443e+00 1.06439987e-03 -1.67123281e-03 1.66320028e-03 1.06323046e+00 2.04703703e-03 2.77979383e-03 -8.58406368e-03 8.99642594e-01 + 1.05959456e+02 9.99782255e-01 1.04814666e+00 1.08172041e-03 -1.81480613e-03 1.74600716e-03 1.06712957e+00 2.15641487e-03 3.01385434e-03 -9.15192102e-03 8.94040785e-01 + 1.11794677e+02 9.99801800e-01 1.05103302e+00 1.09343125e-03 -1.96561201e-03 1.82707126e-03 1.07123155e+00 2.26809865e-03 3.26481031e-03 -9.75025138e-03 8.88205565e-01 + 1.17873031e+02 9.99824854e-01 1.05407296e+00 1.09847101e-03 -2.12199027e-03 1.90645318e-03 1.07554859e+00 2.38108335e-03 3.53299508e-03 -1.03798473e-02 8.82127207e-01 + 1.20000000e+02 9.99824800e-01 1.05514085e+00 1.09929381e-03 -2.17638012e-03 1.93348929e-03 1.07706464e+00 2.42027069e-03 3.62769437e-03 -1.06011240e-02 8.80000244e-01 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_region_default_1.txt new file mode 100644 index 0000000..8942ced --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_def_grad_region_default_1.txt @@ -0,0 +1,61 @@ + # Time Volume F11 F12 F13 F21 F22 F23 F31 F32 F33 + 5.00000000e-02 9.99984364e-01 1.00001650e+00 1.84982920e-07 5.66620597e-07 8.91682017e-08 1.00001787e+00 1.23796114e-06 1.77961123e-06 -2.55893657e-06 9.99950000e-01 + 2.06250000e-01 9.99935759e-01 1.00006819e+00 7.08975750e-07 2.62792447e-06 3.42953820e-07 1.00007384e+00 5.25944604e-06 7.02180271e-06 -1.07466402e-05 9.99793750e-01 + 4.50390625e-01 9.99909751e-01 1.00016716e+00 3.01526328e-06 1.78460623e-05 4.34670524e-06 1.00019312e+00 2.54847625e-05 -2.91102157e-08 -3.69871256e-05 9.99549610e-01 + 6.41125488e-01 9.99905645e-01 1.00024951e+00 6.61894342e-06 2.67718113e-05 9.03779694e-06 1.00029755e+00 4.22326845e-05 -6.61482335e-06 -6.18594136e-05 9.99358875e-01 + 8.39807638e-01 9.99903137e-01 1.00033553e+00 1.03818785e-05 3.15340367e-05 1.36450841e-05 1.00040792e+00 5.67418478e-05 -1.09181552e-05 -8.58909758e-05 9.99160194e-01 + 1.04676821e+00 9.99901078e-01 1.00042474e+00 1.38097550e-05 3.35723602e-05 1.81509646e-05 1.00052392e+00 6.85541935e-05 -1.38021838e-05 -1.08727213e-04 9.98953233e-01 + 1.26235214e+00 9.99899206e-01 1.00051727e+00 1.70494421e-05 3.35718206e-05 2.27657139e-05 1.00064548e+00 7.79603120e-05 -1.54694556e-05 -1.30380934e-04 9.98737650e-01 + 1.48691873e+00 9.99897410e-01 1.00061346e+00 2.01692140e-05 3.20357903e-05 2.74307274e-05 1.00077254e+00 8.61288759e-05 -1.60345087e-05 -1.51796556e-04 9.98513083e-01 + 1.72084227e+00 9.99895640e-01 1.00071359e+00 2.33040059e-05 2.97482888e-05 3.22377975e-05 1.00090514e+00 9.35922774e-05 -1.60129857e-05 -1.73218754e-04 9.98279160e-01 + 2.08634779e+00 9.99893048e-01 1.00086999e+00 2.82391937e-05 2.54123093e-05 3.99215427e-05 1.00111274e+00 1.03792872e-04 -1.49950641e-05 -2.05042089e-04 9.97913655e-01 + 2.46708271e+00 9.99890445e-01 1.00103259e+00 3.31814330e-05 2.06151321e-05 4.80440523e-05 1.00132962e+00 1.13622925e-04 -1.34964075e-05 -2.37187359e-04 9.97532920e-01 + 2.86368158e+00 9.99887810e-01 1.00120156e+00 3.80233325e-05 1.58041863e-05 5.65950083e-05 1.00155626e+00 1.23208345e-04 -1.17662286e-05 -2.69879566e-04 9.97136322e-01 + 3.27680541e+00 9.99885127e-01 1.00137720e+00 4.27791021e-05 1.11531480e-05 6.54972829e-05 1.00179304e+00 1.32915615e-04 -9.79505403e-06 -3.03535500e-04 9.96723199e-01 + 3.70714273e+00 9.99882382e-01 1.00155997e+00 4.75143003e-05 6.36539450e-06 7.46789969e-05 1.00204022e+00 1.42977445e-04 -7.14972408e-06 -3.38322208e-04 9.96292862e-01 + 4.15541077e+00 9.99879566e-01 1.00175020e+00 5.24039789e-05 1.46245442e-06 8.41695915e-05 1.00229819e+00 1.53342610e-04 -3.45448756e-06 -3.74285867e-04 9.95844595e-01 + 4.62235665e+00 9.99876673e-01 1.00194826e+00 5.76153279e-05 -3.61998846e-06 9.39326232e-05 1.00256740e+00 1.63924811e-04 1.36271291e-06 -4.11505721e-04 9.95377649e-01 + 5.10875860e+00 9.99873698e-01 1.00215448e+00 6.31792179e-05 -9.03371299e-06 1.03838676e-04 1.00284831e+00 1.74686008e-04 7.23915356e-06 -4.49801747e-04 9.94891248e-01 + 5.61542731e+00 9.99870636e-01 1.00236925e+00 6.91109181e-05 -1.47443325e-05 1.13929562e-04 1.00314140e+00 1.85634851e-04 1.40823240e-05 -4.89170728e-04 9.94384580e-01 + 6.14320721e+00 9.99867483e-01 1.00259295e+00 7.54585736e-05 -2.05709882e-05 1.24253162e-04 1.00344719e+00 1.96783084e-04 2.17294582e-05 -5.29657733e-04 9.93856801e-01 + 6.69297793e+00 9.99864237e-01 1.00282596e+00 8.23770178e-05 -2.65151397e-05 1.34796429e-04 1.00376625e+00 2.08102258e-04 3.02373477e-05 -5.71246741e-04 9.93307031e-01 + 7.26565578e+00 9.99860893e-01 1.00306879e+00 8.98551697e-05 -3.25553006e-05 1.45585674e-04 1.00409903e+00 2.19720593e-04 3.94773317e-05 -6.14217436e-04 9.92734354e-01 + 7.86219520e+00 9.99857450e-01 1.00332195e+00 9.78606823e-05 -3.87700081e-05 1.56707143e-04 1.00444604e+00 2.31746310e-04 4.94344894e-05 -6.58715980e-04 9.92137816e-01 + 8.48359042e+00 9.99853903e-01 1.00358586e+00 1.06348806e-04 -4.51639076e-05 1.68188044e-04 1.00480797e+00 2.44108334e-04 6.01087310e-05 -7.04798350e-04 9.91516422e-01 + 9.13087712e+00 9.99850251e-01 1.00386094e+00 1.15264928e-04 -5.16299082e-05 1.79995958e-04 1.00518548e+00 2.56808901e-04 7.14078436e-05 -7.52679355e-04 9.90869136e-01 + 9.80513409e+00 9.99846491e-01 1.00414778e+00 1.24583286e-04 -5.82164126e-05 1.92114403e-04 1.00557919e+00 2.69812982e-04 8.33074598e-05 -8.02405850e-04 9.90194881e-01 + 1.05074851e+01 9.99842622e-01 1.00444691e+00 1.34282481e-04 -6.49752362e-05 2.04503541e-04 1.00598977e+00 2.83108295e-04 9.58299959e-05 -8.54045707e-04 9.89492531e-01 + 1.16049086e+01 9.99837000e-01 1.00491499e+00 1.49429151e-04 -7.54447989e-05 2.23348064e-04 1.00663263e+00 3.03616548e-04 1.15672511e-04 -9.34745693e-04 9.88395110e-01 + 1.27480580e+01 9.99831265e-01 1.00540327e+00 1.65415056e-04 -8.64152438e-05 2.42660712e-04 1.00730374e+00 3.25080005e-04 1.36715064e-04 -1.01919181e-03 9.87251963e-01 + 1.39388387e+01 9.99825419e-01 1.00591275e+00 1.82431494e-04 -9.79421080e-05 2.62599520e-04 1.00800431e+00 3.47424597e-04 1.58934600e-04 -1.10731282e-03 9.86061185e-01 + 1.51792352e+01 9.99819468e-01 1.00644441e+00 2.00407412e-04 -1.10147440e-04 2.83190522e-04 1.00873568e+00 3.70532555e-04 1.82417285e-04 -1.19900347e-03 9.84820791e-01 + 1.64713149e+01 9.99813418e-01 1.00699916e+00 2.19290428e-04 -1.23043322e-04 3.04534938e-04 1.00949938e+00 3.94493910e-04 2.07202737e-04 -1.29444029e-03 9.83528714e-01 + 1.78172313e+01 9.99807275e-01 1.00757802e+00 2.39024299e-04 -1.36701658e-04 3.26670658e-04 1.01029694e+00 4.19318585e-04 2.33437032e-04 -1.39380976e-03 9.82182801e-01 + 1.92192275e+01 9.99801048e-01 1.00818212e+00 2.59543843e-04 -1.51188522e-04 3.49552663e-04 1.01112990e+00 4.45064061e-04 2.61263784e-04 -1.49739499e-03 9.80780809e-01 + 2.06796402e+01 9.99794747e-01 1.00881268e+00 2.80876822e-04 -1.66613699e-04 3.73232305e-04 1.01199987e+00 4.71859027e-04 2.90807404e-04 -1.60564603e-03 9.79320400e-01 + 2.29615351e+01 9.99786710e-01 1.00980088e+00 3.14334612e-04 -1.91705504e-04 4.09919626e-04 1.01336504e+00 5.14135167e-04 3.38927873e-04 -1.77669488e-03 9.77038511e-01 + 2.53385090e+01 9.99778825e-01 1.01083363e+00 3.49118974e-04 -2.18775172e-04 4.48018693e-04 1.01479331e+00 5.58360276e-04 3.91334880e-04 -1.95645771e-03 9.74661543e-01 + 2.78145234e+01 9.99771134e-01 1.01191343e+00 3.85136735e-04 -2.48080198e-04 4.87897874e-04 1.01628753e+00 6.04714511e-04 4.48314345e-04 -2.14560176e-03 9.72185536e-01 + 3.03937051e+01 9.99763684e-01 1.01304280e+00 4.22406148e-04 -2.79841327e-04 5.29648084e-04 1.01785082e+00 6.53341582e-04 5.10008990e-04 -2.34460567e-03 9.69606362e-01 + 3.30803527e+01 9.99756528e-01 1.01422425e+00 4.60775181e-04 -3.14187385e-04 5.73243267e-04 1.01948668e+00 7.04231773e-04 5.76475022e-04 -2.55360288e-03 9.66919722e-01 + 3.58789439e+01 9.99749723e-01 1.01546035e+00 5.00035848e-04 -3.51375836e-04 6.18644934e-04 1.02119887e+00 7.57658516e-04 6.47874970e-04 -2.77339759e-03 9.64121140e-01 + 3.87941431e+01 9.99743335e-01 1.01675358e+00 5.40091701e-04 -3.92081381e-04 6.66064860e-04 1.02299160e+00 8.13553747e-04 7.24186285e-04 -3.00442276e-03 9.61205949e-01 + 4.18308089e+01 9.99737432e-01 1.01810684e+00 5.80515421e-04 -4.36695463e-04 7.15420929e-04 1.02486906e+00 8.72361332e-04 8.05481586e-04 -3.24780879e-03 9.58169293e-01 + 4.49940025e+01 9.99732089e-01 1.01952335e+00 6.20891089e-04 -4.85467867e-04 7.66853113e-04 1.02683557e+00 9.34171084e-04 8.91959331e-04 -3.50433625e-03 9.55006110e-01 + 4.82889958e+01 9.99727392e-01 1.02100619e+00 6.60845408e-04 -5.38805201e-04 8.20407551e-04 1.02889603e+00 9.99245422e-04 9.83752446e-04 -3.77525089e-03 9.51711127e-01 + 5.17212805e+01 9.99723435e-01 1.02255869e+00 7.00264016e-04 -5.96954610e-04 8.76280887e-04 1.03105561e+00 1.06783183e-03 1.08086936e-03 -4.06172413e-03 9.48278853e-01 + 5.52965771e+01 9.99720318e-01 1.02418452e+00 7.39089479e-04 -6.60146928e-04 9.34478380e-04 1.03331965e+00 1.13994335e-03 1.18441931e-03 -4.36486603e-03 9.44703567e-01 + 5.90208444e+01 9.99718152e-01 1.02588689e+00 7.77028643e-04 -7.29508639e-04 9.94897989e-04 1.03569449e+00 1.21536374e-03 1.29680388e-03 -4.68562049e-03 9.40979311e-01 + 6.29002894e+01 9.99717059e-01 1.02766927e+00 8.13905598e-04 -8.05715808e-04 1.05775290e-03 1.03818688e+00 1.29382168e-03 1.41992010e-03 -5.02439897e-03 9.37099877e-01 + 6.69413780e+01 9.99717171e-01 1.02953655e+00 8.49871468e-04 -8.88906832e-04 1.12337503e-03 1.04080274e+00 1.37568572e-03 1.55325526e-03 -5.38285720e-03 9.33058799e-01 + 7.11508453e+01 9.99718632e-01 1.03149291e+00 8.85141477e-04 -9.79232455e-04 1.19164581e-03 1.04354945e+00 1.46131487e-03 1.69594115e-03 -5.76371076e-03 9.28849343e-01 + 7.55357071e+01 9.99721604e-01 1.03354315e+00 9.19674343e-04 -1.07615904e-03 1.26268031e-03 1.04643456e+00 1.55023978e-03 1.84776622e-03 -6.16873059e-03 9.24464492e-01 + 8.01032714e+01 9.99726262e-01 1.03569304e+00 9.53355596e-04 -1.17953241e-03 1.33704496e-03 1.04946553e+00 1.64249241e-03 2.00932452e-03 -6.59853083e-03 9.19896937e-01 + 8.48611510e+01 9.99732796e-01 1.03794788e+00 9.85506347e-04 -1.28996596e-03 1.41482619e-03 1.05265116e+00 1.73854505e-03 2.18119754e-03 -7.05448490e-03 9.15139066e-01 + 8.98172755e+01 9.99741417e-01 1.04031329e+00 1.01523030e-03 -1.40858835e-03 1.49557814e-03 1.05600097e+00 1.83816020e-03 2.36565805e-03 -7.53672090e-03 9.10182950e-01 + 9.49799051e+01 9.99752360e-01 1.04279632e+00 1.04188740e-03 -1.53564613e-03 1.57889382e-03 1.05952412e+00 1.94077088e-03 2.56413533e-03 -8.04599932e-03 9.05020327e-01 + 1.00357644e+02 9.99765879e-01 1.04540443e+00 1.06439987e-03 -1.67123281e-03 1.66320028e-03 1.06323046e+00 2.04703703e-03 2.77979383e-03 -8.58406368e-03 8.99642594e-01 + 1.05959456e+02 9.99782255e-01 1.04814666e+00 1.08172041e-03 -1.81480613e-03 1.74600716e-03 1.06712957e+00 2.15641487e-03 3.01385434e-03 -9.15192102e-03 8.94040785e-01 + 1.11794677e+02 9.99801800e-01 1.05103302e+00 1.09343125e-03 -1.96561201e-03 1.82707126e-03 1.07123155e+00 2.26809865e-03 3.26481031e-03 -9.75025138e-03 8.88205565e-01 + 1.17873031e+02 9.99824854e-01 1.05407296e+00 1.09847101e-03 -2.12199027e-03 1.90645318e-03 1.07554859e+00 2.38108335e-03 3.53299508e-03 -1.03798473e-02 8.82127207e-01 + 1.20000000e+02 9.99824800e-01 1.05514085e+00 1.09929381e-03 -2.17638012e-03 1.93348929e-03 1.07706464e+00 2.42027069e-03 3.62769437e-03 -1.06011240e-02 8.80000244e-01 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_global.txt new file mode 100644 index 0000000..4bf4fb2 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_global.txt @@ -0,0 +1,61 @@ + # Time Volume Ee11 Ee22 Ee33 Ee23 Ee13 Ee12 + 5.00000000e-02 9.99984364e-01 6.52003825e-07 5.97931386e-07 -7.28543824e-06 -3.45217001e-07 3.57865458e-07 -6.10392343e-09 + 2.06250000e-01 9.99935759e-01 2.70919917e-06 2.45910974e-06 -2.99409069e-05 -1.42185673e-06 1.46827408e-06 -2.32850540e-08 + 4.50390625e-01 9.99909751e-01 3.60933531e-06 4.65625706e-06 -4.18236912e-05 -2.25534397e-06 2.12169909e-06 5.74713505e-07 + 6.41125488e-01 9.99905645e-01 3.58532871e-06 4.89965655e-06 -4.33697199e-05 -2.36220578e-06 2.15151641e-06 9.13068315e-07 + 8.39807638e-01 9.99903137e-01 3.63753731e-06 4.90831606e-06 -4.42289530e-05 -2.37865204e-06 2.17619019e-06 1.09052126e-06 + 1.04676821e+00 9.99901078e-01 3.71307652e-06 4.89393340e-06 -4.49373720e-05 -2.36569049e-06 2.21080822e-06 1.22020841e-06 + 1.26235214e+00 9.99899206e-01 3.80320636e-06 4.87649452e-06 -4.55872737e-05 -2.35081250e-06 2.24676952e-06 1.33873103e-06 + 1.48691873e+00 9.99897410e-01 3.90936724e-06 4.85737408e-06 -4.62230085e-05 -2.33628919e-06 2.28710170e-06 1.44405441e-06 + 1.72084227e+00 9.99895640e-01 4.01955208e-06 4.84212896e-06 -4.68665829e-05 -2.32240072e-06 2.33841758e-06 1.54041770e-06 + 2.08634779e+00 9.99893048e-01 4.17691793e-06 4.82123605e-06 -4.78534395e-05 -2.30084519e-06 2.43068773e-06 1.67739474e-06 + 2.46708271e+00 9.99890445e-01 4.32816925e-06 4.80664364e-06 -4.88731635e-05 -2.27505023e-06 2.53218899e-06 1.81025405e-06 + 2.86368158e+00 9.99887810e-01 4.47583771e-06 4.81028744e-06 -4.99272874e-05 -2.24917529e-06 2.63254547e-06 1.93993717e-06 + 3.27680541e+00 9.99885127e-01 4.60923468e-06 4.84216450e-06 -5.10227574e-05 -2.22564513e-06 2.72672417e-06 2.06884204e-06 + 3.70714273e+00 9.99882382e-01 4.73260496e-06 4.89720641e-06 -5.21605708e-05 -2.20412802e-06 2.81648724e-06 2.20119417e-06 + 4.15541077e+00 9.99879566e-01 4.85116415e-06 4.96876130e-06 -5.33448938e-05 -2.18765478e-06 2.90616352e-06 2.33123694e-06 + 4.62235665e+00 9.99876673e-01 4.96901405e-06 5.05688393e-06 -5.45764685e-05 -2.17469321e-06 2.99819949e-06 2.45354163e-06 + 5.10875860e+00 9.99873698e-01 5.09278798e-06 5.15388304e-06 -5.58595836e-05 -2.16466205e-06 3.09571218e-06 2.56618843e-06 + 5.61542731e+00 9.99870636e-01 5.21780176e-06 5.26426957e-06 -5.71971448e-05 -2.15718225e-06 3.20082141e-06 2.67275741e-06 + 6.14320721e+00 9.99867483e-01 5.33991714e-06 5.39159023e-06 -5.85877513e-05 -2.14890084e-06 3.31236577e-06 2.77764703e-06 + 6.69297793e+00 9.99864237e-01 5.46522227e-06 5.53309961e-06 -6.00301709e-05 -2.14158278e-06 3.42384773e-06 2.87944033e-06 + 7.26565578e+00 9.99860893e-01 5.59426370e-06 5.68783233e-06 -6.15288700e-05 -2.13621253e-06 3.53567894e-06 2.97703017e-06 + 7.86219520e+00 9.99857450e-01 5.72996730e-06 5.85001690e-06 -6.30872955e-05 -2.13222581e-06 3.64820948e-06 3.07152161e-06 + 8.48359042e+00 9.99853903e-01 5.87585390e-06 6.02107148e-06 -6.47029101e-05 -2.13221705e-06 3.76278190e-06 3.16448491e-06 + 9.13087712e+00 9.99850251e-01 6.03250474e-06 6.19911014e-06 -6.63750751e-05 -2.13481069e-06 3.87942287e-06 3.25699018e-06 + 9.80513409e+00 9.99846491e-01 6.19752318e-06 6.38517568e-06 -6.81053634e-05 -2.14017208e-06 3.99715135e-06 3.35171361e-06 + 1.05074851e+01 9.99842622e-01 6.37029307e-06 6.58025845e-06 -6.98953511e-05 -2.14798496e-06 4.11689020e-06 3.45014922e-06 + 1.16049086e+01 9.99837000e-01 6.64141596e-06 6.88169417e-06 -7.26598321e-05 -2.16307806e-06 4.30046587e-06 3.60376061e-06 + 1.27480580e+01 9.99831265e-01 6.92581177e-06 7.18990711e-06 -7.55090459e-05 -2.18155665e-06 4.48724688e-06 3.76203691e-06 + 1.39388387e+01 9.99825419e-01 7.21950060e-06 7.50960386e-06 -7.84455456e-05 -2.20283324e-06 4.67708716e-06 3.92296084e-06 + 1.51792352e+01 9.99819468e-01 7.52294893e-06 7.84279523e-06 -8.14714471e-05 -2.22922136e-06 4.87004847e-06 4.08640056e-06 + 1.64713149e+01 9.99813418e-01 7.84028172e-06 8.18670427e-06 -8.45900161e-05 -2.26037724e-06 5.06654551e-06 4.25232353e-06 + 1.78172313e+01 9.99807275e-01 8.16951285e-06 8.54439909e-06 -8.78013653e-05 -2.29592321e-06 5.26865439e-06 4.42137775e-06 + 1.92192275e+01 9.99801048e-01 8.50811154e-06 8.91739999e-06 -9.11055463e-05 -2.33320474e-06 5.47742721e-06 4.59443399e-06 + 2.06796402e+01 9.99794747e-01 8.85606549e-06 9.30423304e-06 -9.45025076e-05 -2.37043985e-06 5.69129608e-06 4.77255684e-06 + 2.29615351e+01 9.99786710e-01 9.39276142e-06 9.89123815e-06 -9.96988109e-05 -2.43252802e-06 6.01593018e-06 5.04716340e-06 + 2.53385090e+01 9.99778825e-01 9.94641688e-06 1.04905200e-05 -1.05002561e-04 -2.50570360e-06 6.34447883e-06 5.32722859e-06 + 2.78145234e+01 9.99771134e-01 1.05152441e-05 1.11055036e-05 -1.10405679e-04 -2.58576598e-06 6.67894424e-06 5.61460190e-06 + 3.03937051e+01 9.99763684e-01 1.10933205e-05 1.17367441e-05 -1.15905928e-04 -2.67425246e-06 7.02209093e-06 5.90814193e-06 + 3.30803527e+01 9.99756528e-01 1.16784322e-05 1.23800399e-05 -1.21503501e-04 -2.77475514e-06 7.37360988e-06 6.20587301e-06 + 3.58789439e+01 9.99749723e-01 1.22695526e-05 1.30340906e-05 -1.27195449e-04 -2.88832657e-06 7.73161934e-06 6.50752194e-06 + 3.87941431e+01 9.99743335e-01 1.28633599e-05 1.37006954e-05 -1.32972731e-04 -3.01141998e-06 8.09303691e-06 6.81191204e-06 + 4.18308089e+01 9.99737432e-01 1.34602952e-05 1.43784426e-05 -1.38822837e-04 -3.13792231e-06 8.45676776e-06 7.12083388e-06 + 4.49940025e+01 9.99732089e-01 1.40646528e-05 1.50661032e-05 -1.44742584e-04 -3.26570418e-06 8.82328157e-06 7.43390299e-06 + 4.82889958e+01 9.99727392e-01 1.46769655e-05 1.57656544e-05 -1.50728304e-04 -3.39322385e-06 9.19404292e-06 7.75020299e-06 + 5.17212805e+01 9.99723435e-01 1.52970740e-05 1.64730011e-05 -1.56774823e-04 -3.52335846e-06 9.56644717e-06 8.06398828e-06 + 5.52965771e+01 9.99720318e-01 1.59266916e-05 1.71852474e-05 -1.62874685e-04 -3.66016630e-06 9.94002955e-06 8.37282317e-06 + 5.90208444e+01 9.99718152e-01 1.65646582e-05 1.79060189e-05 -1.69013687e-04 -3.80458083e-06 1.03173698e-05 8.67565949e-06 + 6.29002894e+01 9.99717059e-01 1.72102965e-05 1.86351206e-05 -1.75181973e-04 -3.95696233e-06 1.06961571e-05 8.97468857e-06 + 6.69413780e+01 9.99717171e-01 1.78524393e-05 1.93774393e-05 -1.81371570e-04 -4.12242457e-06 1.10737605e-05 9.26983510e-06 + 7.11508453e+01 9.99718632e-01 1.84842965e-05 2.01327626e-05 -1.87567339e-04 -4.30598276e-06 1.14453829e-05 9.55750222e-06 + 7.55357071e+01 9.99721604e-01 1.91110865e-05 2.08966482e-05 -1.93757440e-04 -4.50100899e-06 1.18144157e-05 9.83398950e-06 + 8.01032714e+01 9.99726262e-01 1.97353444e-05 2.16691496e-05 -1.99933975e-04 -4.72034147e-06 1.21783212e-05 1.01013466e-05 + 8.48611510e+01 9.99732796e-01 2.03578597e-05 2.24645673e-05 -2.06075542e-04 -4.97059013e-06 1.25350620e-05 1.03631581e-05 + 8.98172755e+01 9.99741417e-01 2.09792973e-05 2.32716938e-05 -2.12169539e-04 -5.24835180e-06 1.28802572e-05 1.06112252e-05 + 9.49799051e+01 9.99752360e-01 2.15925564e-05 2.40963873e-05 -2.18210631e-04 -5.55622802e-06 1.32179025e-05 1.08463173e-05 + 1.00357644e+02 9.99765879e-01 2.21916416e-05 2.49394085e-05 -2.24181472e-04 -5.88887813e-06 1.35474984e-05 1.10674413e-05 + 1.05959456e+02 9.99782255e-01 2.27735551e-05 2.57971757e-05 -2.30074871e-04 -6.24634687e-06 1.38717431e-05 1.12646143e-05 + 1.11794677e+02 9.99801800e-01 2.33358084e-05 2.66529312e-05 -2.35883521e-04 -6.62773237e-06 1.41825609e-05 1.14567370e-05 + 1.17873031e+02 9.99824854e-01 2.38633226e-05 2.75054961e-05 -2.41600966e-04 -7.02035782e-06 1.44847908e-05 1.16546761e-05 + 1.20000000e+02 9.99824800e-01 2.40370555e-05 2.78006827e-05 -2.43574276e-04 -7.15659410e-06 1.45902015e-05 1.17221246e-05 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_region_default_1.txt new file mode 100644 index 0000000..4bf4fb2 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_elastic_strain_region_default_1.txt @@ -0,0 +1,61 @@ + # Time Volume Ee11 Ee22 Ee33 Ee23 Ee13 Ee12 + 5.00000000e-02 9.99984364e-01 6.52003825e-07 5.97931386e-07 -7.28543824e-06 -3.45217001e-07 3.57865458e-07 -6.10392343e-09 + 2.06250000e-01 9.99935759e-01 2.70919917e-06 2.45910974e-06 -2.99409069e-05 -1.42185673e-06 1.46827408e-06 -2.32850540e-08 + 4.50390625e-01 9.99909751e-01 3.60933531e-06 4.65625706e-06 -4.18236912e-05 -2.25534397e-06 2.12169909e-06 5.74713505e-07 + 6.41125488e-01 9.99905645e-01 3.58532871e-06 4.89965655e-06 -4.33697199e-05 -2.36220578e-06 2.15151641e-06 9.13068315e-07 + 8.39807638e-01 9.99903137e-01 3.63753731e-06 4.90831606e-06 -4.42289530e-05 -2.37865204e-06 2.17619019e-06 1.09052126e-06 + 1.04676821e+00 9.99901078e-01 3.71307652e-06 4.89393340e-06 -4.49373720e-05 -2.36569049e-06 2.21080822e-06 1.22020841e-06 + 1.26235214e+00 9.99899206e-01 3.80320636e-06 4.87649452e-06 -4.55872737e-05 -2.35081250e-06 2.24676952e-06 1.33873103e-06 + 1.48691873e+00 9.99897410e-01 3.90936724e-06 4.85737408e-06 -4.62230085e-05 -2.33628919e-06 2.28710170e-06 1.44405441e-06 + 1.72084227e+00 9.99895640e-01 4.01955208e-06 4.84212896e-06 -4.68665829e-05 -2.32240072e-06 2.33841758e-06 1.54041770e-06 + 2.08634779e+00 9.99893048e-01 4.17691793e-06 4.82123605e-06 -4.78534395e-05 -2.30084519e-06 2.43068773e-06 1.67739474e-06 + 2.46708271e+00 9.99890445e-01 4.32816925e-06 4.80664364e-06 -4.88731635e-05 -2.27505023e-06 2.53218899e-06 1.81025405e-06 + 2.86368158e+00 9.99887810e-01 4.47583771e-06 4.81028744e-06 -4.99272874e-05 -2.24917529e-06 2.63254547e-06 1.93993717e-06 + 3.27680541e+00 9.99885127e-01 4.60923468e-06 4.84216450e-06 -5.10227574e-05 -2.22564513e-06 2.72672417e-06 2.06884204e-06 + 3.70714273e+00 9.99882382e-01 4.73260496e-06 4.89720641e-06 -5.21605708e-05 -2.20412802e-06 2.81648724e-06 2.20119417e-06 + 4.15541077e+00 9.99879566e-01 4.85116415e-06 4.96876130e-06 -5.33448938e-05 -2.18765478e-06 2.90616352e-06 2.33123694e-06 + 4.62235665e+00 9.99876673e-01 4.96901405e-06 5.05688393e-06 -5.45764685e-05 -2.17469321e-06 2.99819949e-06 2.45354163e-06 + 5.10875860e+00 9.99873698e-01 5.09278798e-06 5.15388304e-06 -5.58595836e-05 -2.16466205e-06 3.09571218e-06 2.56618843e-06 + 5.61542731e+00 9.99870636e-01 5.21780176e-06 5.26426957e-06 -5.71971448e-05 -2.15718225e-06 3.20082141e-06 2.67275741e-06 + 6.14320721e+00 9.99867483e-01 5.33991714e-06 5.39159023e-06 -5.85877513e-05 -2.14890084e-06 3.31236577e-06 2.77764703e-06 + 6.69297793e+00 9.99864237e-01 5.46522227e-06 5.53309961e-06 -6.00301709e-05 -2.14158278e-06 3.42384773e-06 2.87944033e-06 + 7.26565578e+00 9.99860893e-01 5.59426370e-06 5.68783233e-06 -6.15288700e-05 -2.13621253e-06 3.53567894e-06 2.97703017e-06 + 7.86219520e+00 9.99857450e-01 5.72996730e-06 5.85001690e-06 -6.30872955e-05 -2.13222581e-06 3.64820948e-06 3.07152161e-06 + 8.48359042e+00 9.99853903e-01 5.87585390e-06 6.02107148e-06 -6.47029101e-05 -2.13221705e-06 3.76278190e-06 3.16448491e-06 + 9.13087712e+00 9.99850251e-01 6.03250474e-06 6.19911014e-06 -6.63750751e-05 -2.13481069e-06 3.87942287e-06 3.25699018e-06 + 9.80513409e+00 9.99846491e-01 6.19752318e-06 6.38517568e-06 -6.81053634e-05 -2.14017208e-06 3.99715135e-06 3.35171361e-06 + 1.05074851e+01 9.99842622e-01 6.37029307e-06 6.58025845e-06 -6.98953511e-05 -2.14798496e-06 4.11689020e-06 3.45014922e-06 + 1.16049086e+01 9.99837000e-01 6.64141596e-06 6.88169417e-06 -7.26598321e-05 -2.16307806e-06 4.30046587e-06 3.60376061e-06 + 1.27480580e+01 9.99831265e-01 6.92581177e-06 7.18990711e-06 -7.55090459e-05 -2.18155665e-06 4.48724688e-06 3.76203691e-06 + 1.39388387e+01 9.99825419e-01 7.21950060e-06 7.50960386e-06 -7.84455456e-05 -2.20283324e-06 4.67708716e-06 3.92296084e-06 + 1.51792352e+01 9.99819468e-01 7.52294893e-06 7.84279523e-06 -8.14714471e-05 -2.22922136e-06 4.87004847e-06 4.08640056e-06 + 1.64713149e+01 9.99813418e-01 7.84028172e-06 8.18670427e-06 -8.45900161e-05 -2.26037724e-06 5.06654551e-06 4.25232353e-06 + 1.78172313e+01 9.99807275e-01 8.16951285e-06 8.54439909e-06 -8.78013653e-05 -2.29592321e-06 5.26865439e-06 4.42137775e-06 + 1.92192275e+01 9.99801048e-01 8.50811154e-06 8.91739999e-06 -9.11055463e-05 -2.33320474e-06 5.47742721e-06 4.59443399e-06 + 2.06796402e+01 9.99794747e-01 8.85606549e-06 9.30423304e-06 -9.45025076e-05 -2.37043985e-06 5.69129608e-06 4.77255684e-06 + 2.29615351e+01 9.99786710e-01 9.39276142e-06 9.89123815e-06 -9.96988109e-05 -2.43252802e-06 6.01593018e-06 5.04716340e-06 + 2.53385090e+01 9.99778825e-01 9.94641688e-06 1.04905200e-05 -1.05002561e-04 -2.50570360e-06 6.34447883e-06 5.32722859e-06 + 2.78145234e+01 9.99771134e-01 1.05152441e-05 1.11055036e-05 -1.10405679e-04 -2.58576598e-06 6.67894424e-06 5.61460190e-06 + 3.03937051e+01 9.99763684e-01 1.10933205e-05 1.17367441e-05 -1.15905928e-04 -2.67425246e-06 7.02209093e-06 5.90814193e-06 + 3.30803527e+01 9.99756528e-01 1.16784322e-05 1.23800399e-05 -1.21503501e-04 -2.77475514e-06 7.37360988e-06 6.20587301e-06 + 3.58789439e+01 9.99749723e-01 1.22695526e-05 1.30340906e-05 -1.27195449e-04 -2.88832657e-06 7.73161934e-06 6.50752194e-06 + 3.87941431e+01 9.99743335e-01 1.28633599e-05 1.37006954e-05 -1.32972731e-04 -3.01141998e-06 8.09303691e-06 6.81191204e-06 + 4.18308089e+01 9.99737432e-01 1.34602952e-05 1.43784426e-05 -1.38822837e-04 -3.13792231e-06 8.45676776e-06 7.12083388e-06 + 4.49940025e+01 9.99732089e-01 1.40646528e-05 1.50661032e-05 -1.44742584e-04 -3.26570418e-06 8.82328157e-06 7.43390299e-06 + 4.82889958e+01 9.99727392e-01 1.46769655e-05 1.57656544e-05 -1.50728304e-04 -3.39322385e-06 9.19404292e-06 7.75020299e-06 + 5.17212805e+01 9.99723435e-01 1.52970740e-05 1.64730011e-05 -1.56774823e-04 -3.52335846e-06 9.56644717e-06 8.06398828e-06 + 5.52965771e+01 9.99720318e-01 1.59266916e-05 1.71852474e-05 -1.62874685e-04 -3.66016630e-06 9.94002955e-06 8.37282317e-06 + 5.90208444e+01 9.99718152e-01 1.65646582e-05 1.79060189e-05 -1.69013687e-04 -3.80458083e-06 1.03173698e-05 8.67565949e-06 + 6.29002894e+01 9.99717059e-01 1.72102965e-05 1.86351206e-05 -1.75181973e-04 -3.95696233e-06 1.06961571e-05 8.97468857e-06 + 6.69413780e+01 9.99717171e-01 1.78524393e-05 1.93774393e-05 -1.81371570e-04 -4.12242457e-06 1.10737605e-05 9.26983510e-06 + 7.11508453e+01 9.99718632e-01 1.84842965e-05 2.01327626e-05 -1.87567339e-04 -4.30598276e-06 1.14453829e-05 9.55750222e-06 + 7.55357071e+01 9.99721604e-01 1.91110865e-05 2.08966482e-05 -1.93757440e-04 -4.50100899e-06 1.18144157e-05 9.83398950e-06 + 8.01032714e+01 9.99726262e-01 1.97353444e-05 2.16691496e-05 -1.99933975e-04 -4.72034147e-06 1.21783212e-05 1.01013466e-05 + 8.48611510e+01 9.99732796e-01 2.03578597e-05 2.24645673e-05 -2.06075542e-04 -4.97059013e-06 1.25350620e-05 1.03631581e-05 + 8.98172755e+01 9.99741417e-01 2.09792973e-05 2.32716938e-05 -2.12169539e-04 -5.24835180e-06 1.28802572e-05 1.06112252e-05 + 9.49799051e+01 9.99752360e-01 2.15925564e-05 2.40963873e-05 -2.18210631e-04 -5.55622802e-06 1.32179025e-05 1.08463173e-05 + 1.00357644e+02 9.99765879e-01 2.21916416e-05 2.49394085e-05 -2.24181472e-04 -5.88887813e-06 1.35474984e-05 1.10674413e-05 + 1.05959456e+02 9.99782255e-01 2.27735551e-05 2.57971757e-05 -2.30074871e-04 -6.24634687e-06 1.38717431e-05 1.12646143e-05 + 1.11794677e+02 9.99801800e-01 2.33358084e-05 2.66529312e-05 -2.35883521e-04 -6.62773237e-06 1.41825609e-05 1.14567370e-05 + 1.17873031e+02 9.99824854e-01 2.38633226e-05 2.75054961e-05 -2.41600966e-04 -7.02035782e-06 1.44847908e-05 1.16546761e-05 + 1.20000000e+02 9.99824800e-01 2.40370555e-05 2.78006827e-05 -2.43574276e-04 -7.15659410e-06 1.45902015e-05 1.17221246e-05 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_global.txt new file mode 100644 index 0000000..ad41f13 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_global.txt @@ -0,0 +1,61 @@ + # Time Volume Equiv_Plastic_Stra + 5.00000000e-02 9.99984364e-01 2.38346921e-35 + 2.06250000e-01 9.99935759e-01 8.02785799e-07 + 4.50390625e-01 9.99909751e-01 1.88612160e-04 + 6.41125488e-01 9.99905645e-01 3.93935005e-04 + 8.39807638e-01 9.99903137e-01 6.10005165e-04 + 1.04676821e+00 9.99901078e-01 8.34296422e-04 + 1.26235214e+00 9.99899206e-01 1.06702071e-03 + 1.48691873e+00 9.99897410e-01 1.30874481e-03 + 1.72084227e+00 9.99895640e-01 1.55999865e-03 + 2.08634779e+00 9.99893048e-01 1.95175158e-03 + 2.46708271e+00 9.99890445e-01 2.35910283e-03 + 2.86368158e+00 9.99887810e-01 2.78273070e-03 + 3.27680541e+00 9.99885127e-01 3.22343234e-03 + 3.70714273e+00 9.99882382e-01 3.68202967e-03 + 4.15541077e+00 9.99879566e-01 4.15933775e-03 + 4.62235665e+00 9.99876673e-01 4.65621794e-03 + 5.10875860e+00 9.99873698e-01 5.17351275e-03 + 5.61542731e+00 9.99870636e-01 5.71214057e-03 + 6.14320721e+00 9.99867483e-01 6.27304338e-03 + 6.69297793e+00 9.99864237e-01 6.85716713e-03 + 7.26565578e+00 9.99860893e-01 7.46556009e-03 + 7.86219520e+00 9.99857450e-01 8.09931462e-03 + 8.48359042e+00 9.99853903e-01 8.75956579e-03 + 9.13087712e+00 9.99850251e-01 9.44750278e-03 + 9.80513409e+00 9.99846491e-01 1.01643473e-02 + 1.05074851e+01 9.99842622e-01 1.09113654e-02 + 1.16049086e+01 9.99837000e-01 1.20794837e-02 + 1.27480580e+01 9.99831265e-01 1.32973426e-02 + 1.39388387e+01 9.99825419e-01 1.45672148e-02 + 1.51792352e+01 9.99819468e-01 1.58914572e-02 + 1.64713149e+01 9.99813418e-01 1.72725212e-02 + 1.78172313e+01 9.99807275e-01 1.87129816e-02 + 1.92192275e+01 9.99801048e-01 2.02155395e-02 + 2.06796402e+01 9.99794747e-01 2.17830576e-02 + 2.29615351e+01 9.99786710e-01 2.42382204e-02 + 2.53385090e+01 9.99778825e-01 2.68021792e-02 + 2.78145234e+01 9.99771134e-01 2.94802357e-02 + 3.03937051e+01 9.99763684e-01 3.22778956e-02 + 3.30803527e+01 9.99756528e-01 3.52008758e-02 + 3.58789439e+01 9.99749723e-01 3.82551495e-02 + 3.87941431e+01 9.99743335e-01 4.14470445e-02 + 4.18308089e+01 9.99737432e-01 4.47832927e-02 + 4.49940025e+01 9.99732089e-01 4.82709995e-02 + 4.82889958e+01 9.99727392e-01 5.19176004e-02 + 5.17212805e+01 9.99723435e-01 5.57309870e-02 + 5.52965771e+01 9.99720318e-01 5.97195559e-02 + 5.90208444e+01 9.99718152e-01 6.38919896e-02 + 6.29002894e+01 9.99717059e-01 6.82574300e-02 + 6.69413780e+01 9.99717171e-01 7.28256482e-02 + 7.11508453e+01 9.99718632e-01 7.76070611e-02 + 7.55357071e+01 9.99721604e-01 8.26127978e-02 + 8.01032714e+01 9.99726262e-01 8.78546055e-02 + 8.48611510e+01 9.99732796e-01 9.33450620e-02 + 8.98172755e+01 9.99741417e-01 9.90974038e-02 + 9.49799051e+01 9.99752360e-01 1.05125526e-01 + 1.00357644e+02 9.99765879e-01 1.11444465e-01 + 1.05959456e+02 9.99782255e-01 1.18070052e-01 + 1.11794677e+02 9.99801800e-01 1.25018596e-01 + 1.17873031e+02 9.99824854e-01 1.32308290e-01 + 1.20000000e+02 9.99824800e-01 1.34865446e-01 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_region_default_1.txt new file mode 100644 index 0000000..ad41f13 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_eq_pl_strain_region_default_1.txt @@ -0,0 +1,61 @@ + # Time Volume Equiv_Plastic_Stra + 5.00000000e-02 9.99984364e-01 2.38346921e-35 + 2.06250000e-01 9.99935759e-01 8.02785799e-07 + 4.50390625e-01 9.99909751e-01 1.88612160e-04 + 6.41125488e-01 9.99905645e-01 3.93935005e-04 + 8.39807638e-01 9.99903137e-01 6.10005165e-04 + 1.04676821e+00 9.99901078e-01 8.34296422e-04 + 1.26235214e+00 9.99899206e-01 1.06702071e-03 + 1.48691873e+00 9.99897410e-01 1.30874481e-03 + 1.72084227e+00 9.99895640e-01 1.55999865e-03 + 2.08634779e+00 9.99893048e-01 1.95175158e-03 + 2.46708271e+00 9.99890445e-01 2.35910283e-03 + 2.86368158e+00 9.99887810e-01 2.78273070e-03 + 3.27680541e+00 9.99885127e-01 3.22343234e-03 + 3.70714273e+00 9.99882382e-01 3.68202967e-03 + 4.15541077e+00 9.99879566e-01 4.15933775e-03 + 4.62235665e+00 9.99876673e-01 4.65621794e-03 + 5.10875860e+00 9.99873698e-01 5.17351275e-03 + 5.61542731e+00 9.99870636e-01 5.71214057e-03 + 6.14320721e+00 9.99867483e-01 6.27304338e-03 + 6.69297793e+00 9.99864237e-01 6.85716713e-03 + 7.26565578e+00 9.99860893e-01 7.46556009e-03 + 7.86219520e+00 9.99857450e-01 8.09931462e-03 + 8.48359042e+00 9.99853903e-01 8.75956579e-03 + 9.13087712e+00 9.99850251e-01 9.44750278e-03 + 9.80513409e+00 9.99846491e-01 1.01643473e-02 + 1.05074851e+01 9.99842622e-01 1.09113654e-02 + 1.16049086e+01 9.99837000e-01 1.20794837e-02 + 1.27480580e+01 9.99831265e-01 1.32973426e-02 + 1.39388387e+01 9.99825419e-01 1.45672148e-02 + 1.51792352e+01 9.99819468e-01 1.58914572e-02 + 1.64713149e+01 9.99813418e-01 1.72725212e-02 + 1.78172313e+01 9.99807275e-01 1.87129816e-02 + 1.92192275e+01 9.99801048e-01 2.02155395e-02 + 2.06796402e+01 9.99794747e-01 2.17830576e-02 + 2.29615351e+01 9.99786710e-01 2.42382204e-02 + 2.53385090e+01 9.99778825e-01 2.68021792e-02 + 2.78145234e+01 9.99771134e-01 2.94802357e-02 + 3.03937051e+01 9.99763684e-01 3.22778956e-02 + 3.30803527e+01 9.99756528e-01 3.52008758e-02 + 3.58789439e+01 9.99749723e-01 3.82551495e-02 + 3.87941431e+01 9.99743335e-01 4.14470445e-02 + 4.18308089e+01 9.99737432e-01 4.47832927e-02 + 4.49940025e+01 9.99732089e-01 4.82709995e-02 + 4.82889958e+01 9.99727392e-01 5.19176004e-02 + 5.17212805e+01 9.99723435e-01 5.57309870e-02 + 5.52965771e+01 9.99720318e-01 5.97195559e-02 + 5.90208444e+01 9.99718152e-01 6.38919896e-02 + 6.29002894e+01 9.99717059e-01 6.82574300e-02 + 6.69413780e+01 9.99717171e-01 7.28256482e-02 + 7.11508453e+01 9.99718632e-01 7.76070611e-02 + 7.55357071e+01 9.99721604e-01 8.26127978e-02 + 8.01032714e+01 9.99726262e-01 8.78546055e-02 + 8.48611510e+01 9.99732796e-01 9.33450620e-02 + 8.98172755e+01 9.99741417e-01 9.90974038e-02 + 9.49799051e+01 9.99752360e-01 1.05125526e-01 + 1.00357644e+02 9.99765879e-01 1.11444465e-01 + 1.05959456e+02 9.99782255e-01 1.18070052e-01 + 1.11794677e+02 9.99801800e-01 1.25018596e-01 + 1.17873031e+02 9.99824854e-01 1.32308290e-01 + 1.20000000e+02 9.99824800e-01 1.34865446e-01 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_global.txt new file mode 100644 index 0000000..a64e7a1 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_global.txt @@ -0,0 +1,61 @@ + # Time Volume E11 E22 E33 E23 E13 E12 + 5.00000000e-02 9.99984364e-01 1.64995260e-05 1.78652662e-05 -5.00037416e-05 -6.60455400e-07 1.17315494e-06 1.37067735e-07 + 2.06250000e-01 9.99935759e-01 6.81831552e-05 7.38358797e-05 -2.06313674e-04 -2.74302523e-06 4.82556533e-06 5.25840969e-07 + 4.50390625e-01 9.99909751e-01 1.67114223e-04 1.93066120e-04 -4.50694109e-04 -5.74338591e-06 8.91512251e-06 3.67910719e-06 + 6.41125488e-01 9.99905645e-01 2.49418816e-04 2.97416971e-04 -6.41740732e-04 -9.79405339e-06 1.00922046e-05 7.82238060e-06 + 8.39807638e-01 9.99903137e-01 3.35364741e-04 4.07675614e-04 -8.40864035e-04 -1.45395751e-05 1.03289721e-05 1.20009210e-05 + 1.04676821e+00 9.99901078e-01 4.24464275e-04 5.23512944e-04 -1.04841071e-03 -2.00326938e-05 9.91304052e-06 1.59588942e-05 + 1.26235214e+00 9.99899206e-01 5.16866211e-04 6.44862855e-04 -1.26474268e-03 -2.61352078e-05 9.08510034e-06 1.98745595e-05 + 1.48691873e+00 9.99897410e-01 6.12895364e-04 7.71649401e-04 -1.49023778e-03 -3.27346033e-05 8.03917636e-06 2.37525854e-05 + 1.72084227e+00 9.99895640e-01 7.12830367e-04 9.03919045e-04 -1.72529057e-03 -3.96867179e-05 6.90968015e-06 2.77059354e-05 + 2.08634779e+00 9.99893048e-01 8.68850725e-04 1.11089790e-03 -2.09289196e-03 -5.04513892e-05 5.25361093e-06 3.39819758e-05 + 2.46708271e+00 9.99890445e-01 1.03098817e-03 1.32699019e-03 -2.47624027e-03 -6.15544007e-05 3.60471768e-06 4.04723111e-05 + 2.86368158e+00 9.99887810e-01 1.19939383e-03 1.55265787e-03 -2.87602891e-03 -7.30447280e-05 2.06255419e-06 4.71175197e-05 + 3.27680541e+00 9.99885127e-01 1.37435878e-03 1.78825518e-03 -3.29298344e-03 -8.49461898e-05 7.19016288e-07 5.38855052e-05 + 3.70714273e+00 9.99882382e-01 1.55631783e-03 2.03402252e-03 -3.72786317e-03 -9.72245501e-05 -3.58335623e-07 6.07722644e-05 + 4.15541077e+00 9.99879566e-01 1.74560918e-03 2.29033212e-03 -4.18146301e-03 -1.09927620e-04 -9.70864815e-07 6.78785285e-05 + 4.62235665e+00 9.99876673e-01 1.94257150e-03 2.55758616e-03 -4.65461521e-03 -1.23137363e-04 -1.11505811e-06 7.52680545e-05 + 5.10875860e+00 9.99873698e-01 2.14753037e-03 2.83623352e-03 -5.14819124e-03 -1.36781890e-04 -8.99087116e-07 8.28905340e-05 + 5.61542731e+00 9.99870636e-01 2.36084818e-03 3.12671810e-03 -5.66310390e-03 -1.50854148e-04 -3.52348422e-07 9.07729375e-05 + 6.14320721e+00 9.99867483e-01 2.58288865e-03 3.42951791e-03 -6.20030949e-03 -1.65369558e-04 5.34574274e-07 9.89613843e-05 + 6.69297793e+00 9.99864237e-01 2.81400896e-03 3.74515381e-03 -6.76081011e-03 -1.80333213e-04 1.78912324e-06 1.07524313e-04 + 7.26565578e+00 9.99860893e-01 3.05470315e-03 4.07404576e-03 -7.34565612e-03 -1.95818779e-04 3.35751496e-06 1.16467258e-04 + 7.86219520e+00 9.99857450e-01 3.30545618e-03 4.41666188e-03 -7.95594869e-03 -2.11842981e-04 5.19227284e-06 1.25814693e-04 + 8.48359042e+00 9.99853903e-01 3.56663862e-03 4.77362509e-03 -8.59284267e-03 -2.28467865e-04 7.29059841e-06 1.35555583e-04 + 9.13087712e+00 9.99850251e-01 3.83867435e-03 5.14554611e-03 -9.25754947e-03 -2.45797662e-04 9.66020454e-06 1.45644509e-04 + 9.80513409e+00 9.99846491e-01 4.12208670e-03 5.53298047e-03 -9.95134013e-03 -2.63871496e-04 1.22639816e-05 1.56058087e-04 + 1.05074851e+01 9.99842622e-01 4.41739221e-03 5.93653634e-03 -1.06755487e-02 -2.82727358e-04 1.50863702e-05 1.66763481e-04 + 1.16049086e+01 9.99837000e-01 4.87894961e-03 6.56739892e-03 -1.18102194e-02 -3.12292240e-04 1.96700804e-05 1.83187326e-04 + 1.27480580e+01 9.99831265e-01 5.35974419e-03 7.22469992e-03 -1.29961972e-02 -3.43179281e-04 2.45849901e-05 2.00182185e-04 + 1.39388387e+01 9.99825419e-01 5.86066920e-03 7.90945908e-03 -1.42359828e-02 -3.75383432e-04 2.97893014e-05 2.17911422e-04 + 1.51792352e+01 9.99819468e-01 6.38258868e-03 8.62280664e-03 -1.55322150e-02 -4.08904722e-04 3.52613520e-05 2.36343873e-04 + 1.64713149e+01 9.99813418e-01 6.92628467e-03 9.36603755e-03 -1.68876793e-02 -4.43777145e-04 4.10120287e-05 2.55493794e-04 + 1.78172313e+01 9.99807275e-01 7.49265697e-03 1.01404168e-02 -1.83053178e-02 -4.80079813e-04 4.70745296e-05 2.75341821e-04 + 1.92192275e+01 9.99801048e-01 8.08268686e-03 1.09472202e-02 -1.97882397e-02 -5.17914856e-04 5.34832592e-05 2.95823297e-04 + 2.06796402e+01 9.99794747e-01 8.69743219e-03 1.17877418e-02 -2.13397328e-02 -5.57429906e-04 6.02400073e-05 3.16965014e-04 + 2.29615351e+01 9.99786710e-01 9.65852635e-03 1.31023636e-02 -2.37778947e-02 -6.19739249e-04 7.12129246e-05 3.49708794e-04 + 2.53385090e+01 9.99778825e-01 1.06599443e-02 1.44720699e-02 -2.63358935e-02 -6.85113483e-04 8.32290892e-05 3.83471227e-04 + 2.78145234e+01 9.99771134e-01 1.17036955e-02 1.58988646e-02 -2.90204689e-02 -7.53757608e-04 9.62828017e-05 4.18351531e-04 + 3.03937051e+01 9.99763684e-01 1.27918053e-02 1.73849073e-02 -3.18388177e-02 -8.25797081e-04 1.10312110e-04 4.54365115e-04 + 3.30803527e+01 9.99756528e-01 1.39261974e-02 1.89326348e-02 -3.47986323e-02 -9.01260997e-04 1.25256115e-04 4.91387720e-04 + 3.58789439e+01 9.99749723e-01 1.51088253e-02 2.05446332e-02 -3.79081429e-02 -9.80359175e-04 1.41036235e-04 5.29261363e-04 + 3.87941431e+01 9.99743335e-01 1.63415053e-02 2.22238035e-02 -4.11761640e-02 -1.06328967e-03 1.57254425e-04 5.67997988e-04 + 4.18308089e+01 9.99737432e-01 1.76263784e-02 2.39729262e-02 -4.46121455e-02 -1.15032116e-03 1.73701629e-04 6.07312775e-04 + 4.49940025e+01 9.99732089e-01 1.89658305e-02 2.57947542e-02 -4.82262305e-02 -1.24172768e-03 1.90300654e-04 6.47032284e-04 + 4.82889958e+01 9.99727392e-01 2.03620452e-02 2.76924574e-02 -5.20293177e-02 -1.33791303e-03 2.06849939e-04 6.86958773e-04 + 5.17212805e+01 9.99723435e-01 2.18173489e-02 2.96692848e-02 -5.60331315e-02 -1.43924155e-03 2.23162826e-04 7.27084226e-04 + 5.52965771e+01 9.99720318e-01 2.33343157e-02 3.17284652e-02 -6.02503008e-02 -1.54616404e-03 2.39607960e-04 7.67334305e-04 + 5.90208444e+01 9.99718152e-01 2.49149948e-02 3.38739707e-02 -6.46944449e-02 -1.65916223e-03 2.56705267e-04 8.07473767e-04 + 6.29002894e+01 9.99717059e-01 2.65615626e-02 3.61098959e-02 -6.93802691e-02 -1.77847633e-03 2.74949596e-04 8.47466105e-04 + 6.69413780e+01 9.99717171e-01 2.82774223e-02 3.84393740e-02 -7.43236738e-02 -1.90460528e-03 2.93901546e-04 8.87473390e-04 + 7.11508453e+01 9.99718632e-01 3.00651669e-02 4.08665978e-02 -7.95418798e-02 -2.03855423e-03 3.12922648e-04 9.27474204e-04 + 7.55357071e+01 9.99721604e-01 3.19278384e-02 4.33956327e-02 -8.50535658e-02 -2.18130482e-03 3.32078059e-04 9.67434664e-04 + 8.01032714e+01 9.99726262e-01 3.38691933e-02 4.60301522e-02 -9.08790086e-02 -2.33297964e-03 3.51619407e-04 1.00748364e-03 + 8.48611510e+01 9.99732796e-01 3.58924137e-02 4.87746716e-02 -9.70402612e-02 -2.49380407e-03 3.71338780e-04 1.04728100e-03 + 8.98172755e+01 9.99741417e-01 3.80007500e-02 5.16339692e-02 -1.03561348e-01 -2.66376775e-03 3.91557343e-04 1.08616217e-03 + 9.49799051e+01 9.99752360e-01 4.01985513e-02 5.46120891e-02 -1.10468497e-01 -2.84335204e-03 4.12645205e-04 1.12360658e-03 + 1.00357644e+02 9.99765879e-01 4.24902777e-02 5.77132522e-02 -1.17790379e-01 -3.03279936e-03 4.35913503e-04 1.15841702e-03 + 1.05959456e+02 9.99782255e-01 4.48815040e-02 6.09409949e-02 -1.25558405e-01 -3.23266267e-03 4.62033146e-04 1.18906121e-03 + 1.11794677e+02 9.99801800e-01 4.73782855e-02 6.42988075e-02 -1.33807048e-01 -3.44347656e-03 4.90407278e-04 1.21527890e-03 + 1.17873031e+02 9.99824854e-01 4.99858553e-02 6.77913579e-02 -1.42574226e-01 -3.66593545e-03 5.21915334e-04 1.23669046e-03 + 1.20000000e+02 9.99824800e-01 5.08965215e-02 6.90079394e-02 -1.45685097e-01 -3.74396453e-03 5.33117665e-04 1.24332915e-03 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_region_default_1.txt new file mode 100644 index 0000000..a64e7a1 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_euler_strain_region_default_1.txt @@ -0,0 +1,61 @@ + # Time Volume E11 E22 E33 E23 E13 E12 + 5.00000000e-02 9.99984364e-01 1.64995260e-05 1.78652662e-05 -5.00037416e-05 -6.60455400e-07 1.17315494e-06 1.37067735e-07 + 2.06250000e-01 9.99935759e-01 6.81831552e-05 7.38358797e-05 -2.06313674e-04 -2.74302523e-06 4.82556533e-06 5.25840969e-07 + 4.50390625e-01 9.99909751e-01 1.67114223e-04 1.93066120e-04 -4.50694109e-04 -5.74338591e-06 8.91512251e-06 3.67910719e-06 + 6.41125488e-01 9.99905645e-01 2.49418816e-04 2.97416971e-04 -6.41740732e-04 -9.79405339e-06 1.00922046e-05 7.82238060e-06 + 8.39807638e-01 9.99903137e-01 3.35364741e-04 4.07675614e-04 -8.40864035e-04 -1.45395751e-05 1.03289721e-05 1.20009210e-05 + 1.04676821e+00 9.99901078e-01 4.24464275e-04 5.23512944e-04 -1.04841071e-03 -2.00326938e-05 9.91304052e-06 1.59588942e-05 + 1.26235214e+00 9.99899206e-01 5.16866211e-04 6.44862855e-04 -1.26474268e-03 -2.61352078e-05 9.08510034e-06 1.98745595e-05 + 1.48691873e+00 9.99897410e-01 6.12895364e-04 7.71649401e-04 -1.49023778e-03 -3.27346033e-05 8.03917636e-06 2.37525854e-05 + 1.72084227e+00 9.99895640e-01 7.12830367e-04 9.03919045e-04 -1.72529057e-03 -3.96867179e-05 6.90968015e-06 2.77059354e-05 + 2.08634779e+00 9.99893048e-01 8.68850725e-04 1.11089790e-03 -2.09289196e-03 -5.04513892e-05 5.25361093e-06 3.39819758e-05 + 2.46708271e+00 9.99890445e-01 1.03098817e-03 1.32699019e-03 -2.47624027e-03 -6.15544007e-05 3.60471768e-06 4.04723111e-05 + 2.86368158e+00 9.99887810e-01 1.19939383e-03 1.55265787e-03 -2.87602891e-03 -7.30447280e-05 2.06255419e-06 4.71175197e-05 + 3.27680541e+00 9.99885127e-01 1.37435878e-03 1.78825518e-03 -3.29298344e-03 -8.49461898e-05 7.19016288e-07 5.38855052e-05 + 3.70714273e+00 9.99882382e-01 1.55631783e-03 2.03402252e-03 -3.72786317e-03 -9.72245501e-05 -3.58335623e-07 6.07722644e-05 + 4.15541077e+00 9.99879566e-01 1.74560918e-03 2.29033212e-03 -4.18146301e-03 -1.09927620e-04 -9.70864815e-07 6.78785285e-05 + 4.62235665e+00 9.99876673e-01 1.94257150e-03 2.55758616e-03 -4.65461521e-03 -1.23137363e-04 -1.11505811e-06 7.52680545e-05 + 5.10875860e+00 9.99873698e-01 2.14753037e-03 2.83623352e-03 -5.14819124e-03 -1.36781890e-04 -8.99087116e-07 8.28905340e-05 + 5.61542731e+00 9.99870636e-01 2.36084818e-03 3.12671810e-03 -5.66310390e-03 -1.50854148e-04 -3.52348422e-07 9.07729375e-05 + 6.14320721e+00 9.99867483e-01 2.58288865e-03 3.42951791e-03 -6.20030949e-03 -1.65369558e-04 5.34574274e-07 9.89613843e-05 + 6.69297793e+00 9.99864237e-01 2.81400896e-03 3.74515381e-03 -6.76081011e-03 -1.80333213e-04 1.78912324e-06 1.07524313e-04 + 7.26565578e+00 9.99860893e-01 3.05470315e-03 4.07404576e-03 -7.34565612e-03 -1.95818779e-04 3.35751496e-06 1.16467258e-04 + 7.86219520e+00 9.99857450e-01 3.30545618e-03 4.41666188e-03 -7.95594869e-03 -2.11842981e-04 5.19227284e-06 1.25814693e-04 + 8.48359042e+00 9.99853903e-01 3.56663862e-03 4.77362509e-03 -8.59284267e-03 -2.28467865e-04 7.29059841e-06 1.35555583e-04 + 9.13087712e+00 9.99850251e-01 3.83867435e-03 5.14554611e-03 -9.25754947e-03 -2.45797662e-04 9.66020454e-06 1.45644509e-04 + 9.80513409e+00 9.99846491e-01 4.12208670e-03 5.53298047e-03 -9.95134013e-03 -2.63871496e-04 1.22639816e-05 1.56058087e-04 + 1.05074851e+01 9.99842622e-01 4.41739221e-03 5.93653634e-03 -1.06755487e-02 -2.82727358e-04 1.50863702e-05 1.66763481e-04 + 1.16049086e+01 9.99837000e-01 4.87894961e-03 6.56739892e-03 -1.18102194e-02 -3.12292240e-04 1.96700804e-05 1.83187326e-04 + 1.27480580e+01 9.99831265e-01 5.35974419e-03 7.22469992e-03 -1.29961972e-02 -3.43179281e-04 2.45849901e-05 2.00182185e-04 + 1.39388387e+01 9.99825419e-01 5.86066920e-03 7.90945908e-03 -1.42359828e-02 -3.75383432e-04 2.97893014e-05 2.17911422e-04 + 1.51792352e+01 9.99819468e-01 6.38258868e-03 8.62280664e-03 -1.55322150e-02 -4.08904722e-04 3.52613520e-05 2.36343873e-04 + 1.64713149e+01 9.99813418e-01 6.92628467e-03 9.36603755e-03 -1.68876793e-02 -4.43777145e-04 4.10120287e-05 2.55493794e-04 + 1.78172313e+01 9.99807275e-01 7.49265697e-03 1.01404168e-02 -1.83053178e-02 -4.80079813e-04 4.70745296e-05 2.75341821e-04 + 1.92192275e+01 9.99801048e-01 8.08268686e-03 1.09472202e-02 -1.97882397e-02 -5.17914856e-04 5.34832592e-05 2.95823297e-04 + 2.06796402e+01 9.99794747e-01 8.69743219e-03 1.17877418e-02 -2.13397328e-02 -5.57429906e-04 6.02400073e-05 3.16965014e-04 + 2.29615351e+01 9.99786710e-01 9.65852635e-03 1.31023636e-02 -2.37778947e-02 -6.19739249e-04 7.12129246e-05 3.49708794e-04 + 2.53385090e+01 9.99778825e-01 1.06599443e-02 1.44720699e-02 -2.63358935e-02 -6.85113483e-04 8.32290892e-05 3.83471227e-04 + 2.78145234e+01 9.99771134e-01 1.17036955e-02 1.58988646e-02 -2.90204689e-02 -7.53757608e-04 9.62828017e-05 4.18351531e-04 + 3.03937051e+01 9.99763684e-01 1.27918053e-02 1.73849073e-02 -3.18388177e-02 -8.25797081e-04 1.10312110e-04 4.54365115e-04 + 3.30803527e+01 9.99756528e-01 1.39261974e-02 1.89326348e-02 -3.47986323e-02 -9.01260997e-04 1.25256115e-04 4.91387720e-04 + 3.58789439e+01 9.99749723e-01 1.51088253e-02 2.05446332e-02 -3.79081429e-02 -9.80359175e-04 1.41036235e-04 5.29261363e-04 + 3.87941431e+01 9.99743335e-01 1.63415053e-02 2.22238035e-02 -4.11761640e-02 -1.06328967e-03 1.57254425e-04 5.67997988e-04 + 4.18308089e+01 9.99737432e-01 1.76263784e-02 2.39729262e-02 -4.46121455e-02 -1.15032116e-03 1.73701629e-04 6.07312775e-04 + 4.49940025e+01 9.99732089e-01 1.89658305e-02 2.57947542e-02 -4.82262305e-02 -1.24172768e-03 1.90300654e-04 6.47032284e-04 + 4.82889958e+01 9.99727392e-01 2.03620452e-02 2.76924574e-02 -5.20293177e-02 -1.33791303e-03 2.06849939e-04 6.86958773e-04 + 5.17212805e+01 9.99723435e-01 2.18173489e-02 2.96692848e-02 -5.60331315e-02 -1.43924155e-03 2.23162826e-04 7.27084226e-04 + 5.52965771e+01 9.99720318e-01 2.33343157e-02 3.17284652e-02 -6.02503008e-02 -1.54616404e-03 2.39607960e-04 7.67334305e-04 + 5.90208444e+01 9.99718152e-01 2.49149948e-02 3.38739707e-02 -6.46944449e-02 -1.65916223e-03 2.56705267e-04 8.07473767e-04 + 6.29002894e+01 9.99717059e-01 2.65615626e-02 3.61098959e-02 -6.93802691e-02 -1.77847633e-03 2.74949596e-04 8.47466105e-04 + 6.69413780e+01 9.99717171e-01 2.82774223e-02 3.84393740e-02 -7.43236738e-02 -1.90460528e-03 2.93901546e-04 8.87473390e-04 + 7.11508453e+01 9.99718632e-01 3.00651669e-02 4.08665978e-02 -7.95418798e-02 -2.03855423e-03 3.12922648e-04 9.27474204e-04 + 7.55357071e+01 9.99721604e-01 3.19278384e-02 4.33956327e-02 -8.50535658e-02 -2.18130482e-03 3.32078059e-04 9.67434664e-04 + 8.01032714e+01 9.99726262e-01 3.38691933e-02 4.60301522e-02 -9.08790086e-02 -2.33297964e-03 3.51619407e-04 1.00748364e-03 + 8.48611510e+01 9.99732796e-01 3.58924137e-02 4.87746716e-02 -9.70402612e-02 -2.49380407e-03 3.71338780e-04 1.04728100e-03 + 8.98172755e+01 9.99741417e-01 3.80007500e-02 5.16339692e-02 -1.03561348e-01 -2.66376775e-03 3.91557343e-04 1.08616217e-03 + 9.49799051e+01 9.99752360e-01 4.01985513e-02 5.46120891e-02 -1.10468497e-01 -2.84335204e-03 4.12645205e-04 1.12360658e-03 + 1.00357644e+02 9.99765879e-01 4.24902777e-02 5.77132522e-02 -1.17790379e-01 -3.03279936e-03 4.35913503e-04 1.15841702e-03 + 1.05959456e+02 9.99782255e-01 4.48815040e-02 6.09409949e-02 -1.25558405e-01 -3.23266267e-03 4.62033146e-04 1.18906121e-03 + 1.11794677e+02 9.99801800e-01 4.73782855e-02 6.42988075e-02 -1.33807048e-01 -3.44347656e-03 4.90407278e-04 1.21527890e-03 + 1.17873031e+02 9.99824854e-01 4.99858553e-02 6.77913579e-02 -1.42574226e-01 -3.66593545e-03 5.21915334e-04 1.23669046e-03 + 1.20000000e+02 9.99824800e-01 5.08965215e-02 6.90079394e-02 -1.45685097e-01 -3.74396453e-03 5.33117665e-04 1.24332915e-03 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_global.txt new file mode 100644 index 0000000..4d54a34 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_global.txt @@ -0,0 +1,61 @@ + # Time Volume Plastic_Work + 5.00000000e-02 9.99984364e-01 1.79635968e-37 + 2.06250000e-01 9.99935759e-01 2.01544672e-08 + 4.50390625e-01 9.99909751e-01 5.46566788e-06 + 6.41125488e-01 9.99905645e-01 1.20034931e-05 + 8.39807638e-01 9.99903137e-01 1.92938619e-05 + 1.04676821e+00 9.99901078e-01 2.71689233e-05 + 1.26235214e+00 9.99899206e-01 3.55964835e-05 + 1.48691873e+00 9.99897410e-01 4.45747585e-05 + 1.72084227e+00 9.99895640e-01 5.41203773e-05 + 2.08634779e+00 9.99893048e-01 6.94553337e-05 + 2.46708271e+00 9.99890445e-01 8.58616868e-05 + 2.86368158e+00 9.99887810e-01 1.03406869e-04 + 3.27680541e+00 9.99885127e-01 1.22164342e-04 + 3.70714273e+00 9.99882382e-01 1.42211750e-04 + 4.15541077e+00 9.99879566e-01 1.63637782e-04 + 4.62235665e+00 9.99876673e-01 1.86538012e-04 + 5.10875860e+00 9.99873698e-01 2.11018152e-04 + 5.61542731e+00 9.99870636e-01 2.37189991e-04 + 6.14320721e+00 9.99867483e-01 2.65175178e-04 + 6.69297793e+00 9.99864237e-01 2.95107501e-04 + 7.26565578e+00 9.99860893e-01 3.27127855e-04 + 7.86219520e+00 9.99857450e-01 3.61388187e-04 + 8.48359042e+00 9.99853903e-01 3.98052962e-04 + 9.13087712e+00 9.99850251e-01 4.37297326e-04 + 9.80513409e+00 9.99846491e-01 4.79310539e-04 + 1.05074851e+01 9.99842622e-01 5.24296950e-04 + 1.16049086e+01 9.99837000e-01 5.97543522e-04 + 1.27480580e+01 9.99831265e-01 6.77021164e-04 + 1.39388387e+01 9.99825419e-01 7.63226334e-04 + 1.51792352e+01 9.99819468e-01 8.56698090e-04 + 1.64713149e+01 9.99813418e-01 9.58019410e-04 + 1.78172313e+01 9.99807275e-01 1.06781389e-03 + 1.92192275e+01 9.99801048e-01 1.18675185e-03 + 2.06796402e+01 9.99794747e-01 1.31555213e-03 + 2.29615351e+01 9.99786710e-01 1.52859490e-03 + 2.53385090e+01 9.99778825e-01 1.76311397e-03 + 2.78145234e+01 9.99771134e-01 2.02084751e-03 + 3.03937051e+01 9.99763684e-01 2.30365528e-03 + 3.30803527e+01 9.99756528e-01 2.61353217e-03 + 3.58789439e+01 9.99749723e-01 2.95261775e-03 + 3.87941431e+01 9.99743335e-01 3.32320154e-03 + 4.18308089e+01 9.99737432e-01 3.72770751e-03 + 4.49940025e+01 9.99732089e-01 4.16870448e-03 + 4.82889958e+01 9.99727392e-01 4.64893196e-03 + 5.17212805e+01 9.99723435e-01 5.17131789e-03 + 5.52965771e+01 9.99720318e-01 5.73897968e-03 + 5.90208444e+01 9.99718152e-01 6.35522166e-03 + 6.29002894e+01 9.99717059e-01 7.02354560e-03 + 6.69413780e+01 9.99717171e-01 7.74769081e-03 + 7.11508453e+01 9.99718632e-01 8.53159448e-03 + 7.55357071e+01 9.99721604e-01 9.37942579e-03 + 8.01032714e+01 9.99726262e-01 1.02955630e-02 + 8.48611510e+01 9.99732796e-01 1.12846370e-02 + 8.98172755e+01 9.99741417e-01 1.23515215e-02 + 9.49799051e+01 9.99752360e-01 1.35014301e-02 + 1.00357644e+02 9.99765879e-01 1.47399305e-02 + 1.05959456e+02 9.99782255e-01 1.60729388e-02 + 1.11794677e+02 9.99801800e-01 1.75065822e-02 + 1.17873031e+02 9.99824854e-01 1.90473159e-02 + 1.20000000e+02 9.99824800e-01 1.95920945e-02 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_region_default_1.txt new file mode 100644 index 0000000..4d54a34 --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_pl_work_region_default_1.txt @@ -0,0 +1,61 @@ + # Time Volume Plastic_Work + 5.00000000e-02 9.99984364e-01 1.79635968e-37 + 2.06250000e-01 9.99935759e-01 2.01544672e-08 + 4.50390625e-01 9.99909751e-01 5.46566788e-06 + 6.41125488e-01 9.99905645e-01 1.20034931e-05 + 8.39807638e-01 9.99903137e-01 1.92938619e-05 + 1.04676821e+00 9.99901078e-01 2.71689233e-05 + 1.26235214e+00 9.99899206e-01 3.55964835e-05 + 1.48691873e+00 9.99897410e-01 4.45747585e-05 + 1.72084227e+00 9.99895640e-01 5.41203773e-05 + 2.08634779e+00 9.99893048e-01 6.94553337e-05 + 2.46708271e+00 9.99890445e-01 8.58616868e-05 + 2.86368158e+00 9.99887810e-01 1.03406869e-04 + 3.27680541e+00 9.99885127e-01 1.22164342e-04 + 3.70714273e+00 9.99882382e-01 1.42211750e-04 + 4.15541077e+00 9.99879566e-01 1.63637782e-04 + 4.62235665e+00 9.99876673e-01 1.86538012e-04 + 5.10875860e+00 9.99873698e-01 2.11018152e-04 + 5.61542731e+00 9.99870636e-01 2.37189991e-04 + 6.14320721e+00 9.99867483e-01 2.65175178e-04 + 6.69297793e+00 9.99864237e-01 2.95107501e-04 + 7.26565578e+00 9.99860893e-01 3.27127855e-04 + 7.86219520e+00 9.99857450e-01 3.61388187e-04 + 8.48359042e+00 9.99853903e-01 3.98052962e-04 + 9.13087712e+00 9.99850251e-01 4.37297326e-04 + 9.80513409e+00 9.99846491e-01 4.79310539e-04 + 1.05074851e+01 9.99842622e-01 5.24296950e-04 + 1.16049086e+01 9.99837000e-01 5.97543522e-04 + 1.27480580e+01 9.99831265e-01 6.77021164e-04 + 1.39388387e+01 9.99825419e-01 7.63226334e-04 + 1.51792352e+01 9.99819468e-01 8.56698090e-04 + 1.64713149e+01 9.99813418e-01 9.58019410e-04 + 1.78172313e+01 9.99807275e-01 1.06781389e-03 + 1.92192275e+01 9.99801048e-01 1.18675185e-03 + 2.06796402e+01 9.99794747e-01 1.31555213e-03 + 2.29615351e+01 9.99786710e-01 1.52859490e-03 + 2.53385090e+01 9.99778825e-01 1.76311397e-03 + 2.78145234e+01 9.99771134e-01 2.02084751e-03 + 3.03937051e+01 9.99763684e-01 2.30365528e-03 + 3.30803527e+01 9.99756528e-01 2.61353217e-03 + 3.58789439e+01 9.99749723e-01 2.95261775e-03 + 3.87941431e+01 9.99743335e-01 3.32320154e-03 + 4.18308089e+01 9.99737432e-01 3.72770751e-03 + 4.49940025e+01 9.99732089e-01 4.16870448e-03 + 4.82889958e+01 9.99727392e-01 4.64893196e-03 + 5.17212805e+01 9.99723435e-01 5.17131789e-03 + 5.52965771e+01 9.99720318e-01 5.73897968e-03 + 5.90208444e+01 9.99718152e-01 6.35522166e-03 + 6.29002894e+01 9.99717059e-01 7.02354560e-03 + 6.69413780e+01 9.99717171e-01 7.74769081e-03 + 7.11508453e+01 9.99718632e-01 8.53159448e-03 + 7.55357071e+01 9.99721604e-01 9.37942579e-03 + 8.01032714e+01 9.99726262e-01 1.02955630e-02 + 8.48611510e+01 9.99732796e-01 1.12846370e-02 + 8.98172755e+01 9.99741417e-01 1.23515215e-02 + 9.49799051e+01 9.99752360e-01 1.35014301e-02 + 1.00357644e+02 9.99765879e-01 1.47399305e-02 + 1.05959456e+02 9.99782255e-01 1.60729388e-02 + 1.11794677e+02 9.99801800e-01 1.75065822e-02 + 1.17873031e+02 9.99824854e-01 1.90473159e-02 + 1.20000000e+02 9.99824800e-01 1.95920945e-02 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_global.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_global.txt new file mode 100644 index 0000000..8eca5bc --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_global.txt @@ -0,0 +1,61 @@ + # Time Volume Sxx Syy Szz Sxy Sxz Syz + 5.00000000e-02 9.99984364e-01 -1.17286159e-11 -1.19304241e-11 -6.43018930e-03 1.00328942e-13 -2.17874083e-14 7.09617246e-14 + 2.06250000e-01 9.99935759e-01 -1.31670047e-10 -5.21193558e-10 -2.64245624e-02 4.93367045e-11 1.26785759e-10 6.96794602e-11 + 4.50390625e-01 9.99909751e-01 -1.94003027e-09 5.99584187e-10 -3.71387594e-02 -7.15524599e-10 5.76057152e-10 -4.34355708e-10 + 6.41125488e-01 9.99905645e-01 -7.37240254e-10 -1.18796608e-09 -3.88392983e-02 -4.87786742e-11 -5.13311427e-10 1.98904186e-10 + 8.39807638e-01 9.99903137e-01 1.87417892e-14 -3.58427812e-11 -3.98838238e-02 -7.09257282e-11 -4.71687096e-11 6.90440160e-13 + 1.04676821e+00 9.99901078e-01 -5.91699105e-12 -2.06183075e-11 -4.07446049e-02 -6.31761049e-11 -4.75040286e-12 -2.97275779e-13 + 1.26235214e+00 9.99899206e-01 -2.20925937e-12 -3.73258914e-12 -4.15297061e-02 -1.61436408e-11 -5.67324342e-12 -1.70517899e-13 + 1.48691873e+00 9.99897410e-01 -2.17896218e-12 -2.97932324e-12 -4.22848079e-02 -1.20735804e-11 -1.28003166e-12 1.38029994e-13 + 1.72084227e+00 9.99895640e-01 -6.89812359e-10 -5.60776275e-10 -4.30303950e-02 -1.38265560e-09 -3.63552605e-10 -4.36119920e-11 + 2.08634779e+00 9.99893048e-01 2.21002118e-13 -1.25316600e-12 -4.41400204e-02 -7.93220449e-12 -4.46846373e-12 -1.33110330e-12 + 2.46708271e+00 9.99890445e-01 1.68828021e-12 -1.40362850e-12 -4.52577035e-02 -6.32225364e-12 -2.46245782e-12 -4.40985258e-13 + 2.86368158e+00 9.99887810e-01 -1.39776424e-12 -2.13732926e-12 -4.63925807e-02 -9.55895417e-12 5.99257288e-12 8.73918280e-13 + 3.27680541e+00 9.99885127e-01 7.98250464e-13 -1.17957844e-12 -4.75515736e-02 -2.78961266e-12 1.18580741e-12 2.33088054e-13 + 3.70714273e+00 9.99882382e-01 1.77132713e-12 -4.84099568e-13 -4.87409397e-02 -3.55522264e-12 -5.15533799e-12 -6.46977059e-13 + 4.15541077e+00 9.99879566e-01 1.76159291e-12 5.11906476e-13 -4.99642706e-02 -2.01453370e-12 -7.27217063e-12 -7.08641578e-13 + 4.62235665e+00 9.99876673e-01 -2.04605239e-14 -3.31793828e-13 -5.12249262e-02 -5.53037054e-12 -6.53180468e-12 -8.94027278e-13 + 5.10875860e+00 9.99873698e-01 -8.44477917e-13 -1.10263464e-12 -5.25252424e-02 -8.36504528e-12 -6.63104675e-12 -9.33028059e-13 + 5.61542731e+00 9.99870636e-01 -1.53134378e-12 -1.26531074e-12 -5.38679544e-02 -8.99067573e-12 -4.32678574e-12 -6.70212765e-13 + 6.14320721e+00 9.99867483e-01 -2.08900058e-12 -1.45400626e-12 -5.52552188e-02 -1.13938649e-11 -4.12250369e-12 -5.25344129e-13 + 6.69297793e+00 9.99864237e-01 -1.39112172e-12 -1.02025900e-12 -5.66886689e-02 -1.04169559e-11 -5.68993449e-12 -8.97989848e-14 + 7.26565578e+00 9.99860893e-01 -4.29781148e-14 -7.42368567e-13 -5.81705456e-02 -5.07064299e-12 -2.62252972e-12 4.81626045e-13 + 7.86219520e+00 9.99857450e-01 2.41572535e-13 -4.79388453e-13 -5.97028225e-02 -4.67576045e-12 -3.31190202e-12 8.33292106e-13 + 8.48359042e+00 9.99853903e-01 3.25356378e-13 -1.14870438e-13 -6.12872695e-02 -4.79068315e-12 -2.82100198e-12 1.44096315e-13 + 9.13087712e+00 9.99850251e-01 2.28509742e-13 -6.57441013e-13 -6.29259516e-02 -4.25444713e-12 1.65342557e-12 6.50833383e-13 + 9.80513409e+00 9.99846491e-01 -1.71934270e-12 -1.06178272e-12 -6.46206684e-02 -5.58448626e-12 4.94468003e-12 5.62488821e-13 + 1.05074851e+01 9.99842622e-01 -7.00428033e-11 1.58109694e-11 -6.63730805e-02 -1.86242102e-10 1.85696241e-10 8.61639787e-11 + 1.16049086e+01 9.99837000e-01 1.47367541e-12 -3.10068713e-12 -6.90793921e-02 8.33668133e-13 9.14918455e-12 1.88116001e-12 + 1.27480580e+01 9.99831265e-01 1.08421175e-12 -3.51651138e-12 -7.18666670e-02 -2.55644026e-12 5.77660122e-12 -2.13599800e-13 + 1.39388387e+01 9.99825419e-01 -6.12159934e-12 -7.37818146e-12 -7.47368079e-02 -1.50561034e-11 6.75330759e-12 -7.46131449e-13 + 1.51792352e+01 9.99819468e-01 -1.30420749e-11 -1.11497395e-11 -7.76910305e-02 -3.14989588e-11 4.82083847e-12 -1.59546114e-13 + 1.64713149e+01 9.99813418e-01 -9.42791593e-12 -3.69788044e-12 -8.07303410e-02 -1.94953774e-11 9.61689624e-12 2.85848761e-12 + 1.78172313e+01 9.99807275e-01 -9.00013358e-12 -4.99864711e-12 -8.38559249e-02 -2.23459563e-11 7.88442723e-12 4.90210025e-12 + 1.92192275e+01 9.99801048e-01 -8.70483291e-12 -2.51554705e-12 -8.70686865e-02 -2.40209269e-11 2.76077143e-12 4.28414893e-12 + 2.06796402e+01 9.99794747e-01 7.84451786e-11 8.01901394e-10 -9.03694378e-02 3.39892371e-10 -2.60719318e-11 5.42923357e-10 + 2.29615351e+01 9.99786710e-01 8.77176380e-11 7.09938653e-11 -9.54120787e-02 4.37145286e-11 1.31256018e-11 4.32982796e-11 + 2.53385090e+01 9.99778825e-01 9.91060940e-11 9.85591557e-11 -1.00548320e-01 4.29676758e-11 2.00240588e-12 6.02109969e-11 + 2.78145234e+01 9.99771134e-01 1.43048946e-10 1.23149298e-10 -1.05775544e-01 5.97915596e-11 -7.82846626e-11 3.87101228e-11 + 3.03937051e+01 9.99763684e-01 1.67614186e-10 1.31252038e-10 -1.11090209e-01 7.83870502e-11 -1.23036631e-10 3.08544861e-11 + 3.30803527e+01 9.99756528e-01 2.08156475e-10 1.72523338e-10 -1.16488028e-01 8.66937972e-11 -1.25607737e-10 3.06951550e-11 + 3.58789439e+01 9.99749723e-01 2.78907322e-10 2.55941455e-10 -1.21963986e-01 1.31478808e-10 -1.28706271e-10 3.99589020e-11 + 3.87941431e+01 9.99743335e-01 3.49633346e-10 2.70724070e-10 -1.27512579e-01 2.01401728e-10 -1.54499633e-10 1.70643596e-11 + 4.18308089e+01 9.99737432e-01 4.68634186e-10 3.37594960e-10 -1.33128321e-01 2.35719142e-10 -2.47509754e-10 -6.46898327e-12 + 4.49940025e+01 9.99732089e-01 5.07668365e-10 3.15035690e-10 -1.38805414e-01 1.72474078e-10 -2.16838676e-10 1.90684497e-11 + 4.82889958e+01 9.99727392e-01 7.35462985e-10 4.31647827e-10 -1.44537089e-01 2.84316929e-10 -1.67941482e-10 2.65865942e-11 + 5.17212805e+01 9.99723435e-01 1.09079124e-09 5.80868299e-10 -1.50315593e-01 4.23658774e-10 -1.93348712e-10 3.65591468e-11 + 5.52965771e+01 9.99720318e-01 1.46711857e-09 7.33191182e-10 -1.56132682e-01 4.24256616e-10 -5.98921237e-10 3.12932725e-12 + 5.90208444e+01 9.99718152e-01 1.89869192e-09 1.00389909e-09 -1.61979958e-01 2.76256676e-10 -1.00510360e-09 -4.61570026e-11 + 6.29002894e+01 9.99717059e-01 2.27284032e-09 1.28494451e-09 -1.67848590e-01 2.00673837e-10 -1.44117629e-09 -3.46606184e-11 + 6.69413780e+01 9.99717171e-01 2.72653954e-09 1.31252295e-09 -1.73728204e-01 2.74353395e-10 -1.15776538e-09 -2.79672505e-12 + 7.11508453e+01 9.99718632e-01 2.84397144e-09 1.09278902e-09 -1.79608945e-01 5.74771895e-10 -4.99280286e-10 1.20789071e-10 + 7.55357071e+01 9.99721604e-01 2.98811415e-09 7.52384967e-10 -1.85480230e-01 6.66017962e-10 -3.03313805e-11 1.67221152e-10 + 8.01032714e+01 9.99726262e-01 3.17104579e-09 4.13741525e-10 -1.91332020e-01 -5.99919753e-12 7.69955055e-11 -2.09739159e-11 + 8.48611510e+01 9.99732796e-01 3.70082532e-09 2.43160232e-10 -1.97153473e-01 -1.75985306e-10 1.29209882e-09 -3.37749489e-12 + 8.98172755e+01 9.99741417e-01 3.35223603e-09 6.98013714e-11 -2.02933702e-01 -7.47243616e-10 1.32135323e-09 -9.89102584e-12 + 9.49799051e+01 9.99752360e-01 4.12317873e-09 6.85692157e-10 -2.08660800e-01 -1.01521965e-09 1.18892048e-09 -3.79303115e-10 + 1.00357644e+02 9.99765879e-01 4.21540463e-09 1.46660215e-09 -2.14321372e-01 -1.55417307e-09 -7.79685188e-10 -6.86281249e-10 + 1.05959456e+02 9.99782255e-01 2.27736476e-09 -8.76402713e-10 -2.19901664e-01 -3.73640993e-09 5.66555364e-10 -7.43386901e-10 + 1.11794677e+02 9.99801800e-01 2.50998868e-09 -2.91972540e-09 -2.25390382e-01 -6.11149567e-09 3.17082810e-09 -2.17943610e-09 + 1.17873031e+02 9.99824854e-01 1.39777757e-09 -6.93034871e-09 -2.30776112e-01 -7.71429702e-09 6.58059316e-09 -2.39948168e-09 + 1.20000000e+02 9.99824800e-01 1.95726748e-10 -4.34564212e-11 -2.32630337e-01 7.87356588e-11 5.22192696e-11 8.14534180e-11 diff --git a/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_region_default_1.txt b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_region_default_1.txt new file mode 100644 index 0000000..8eca5bc --- /dev/null +++ b/workflows/exaconstit-calibrate/examples/voce_ea_1m3/avg_stress_region_default_1.txt @@ -0,0 +1,61 @@ + # Time Volume Sxx Syy Szz Sxy Sxz Syz + 5.00000000e-02 9.99984364e-01 -1.17286159e-11 -1.19304241e-11 -6.43018930e-03 1.00328942e-13 -2.17874083e-14 7.09617246e-14 + 2.06250000e-01 9.99935759e-01 -1.31670047e-10 -5.21193558e-10 -2.64245624e-02 4.93367045e-11 1.26785759e-10 6.96794602e-11 + 4.50390625e-01 9.99909751e-01 -1.94003027e-09 5.99584187e-10 -3.71387594e-02 -7.15524599e-10 5.76057152e-10 -4.34355708e-10 + 6.41125488e-01 9.99905645e-01 -7.37240254e-10 -1.18796608e-09 -3.88392983e-02 -4.87786742e-11 -5.13311427e-10 1.98904186e-10 + 8.39807638e-01 9.99903137e-01 1.87417892e-14 -3.58427812e-11 -3.98838238e-02 -7.09257282e-11 -4.71687096e-11 6.90440160e-13 + 1.04676821e+00 9.99901078e-01 -5.91699105e-12 -2.06183075e-11 -4.07446049e-02 -6.31761049e-11 -4.75040286e-12 -2.97275779e-13 + 1.26235214e+00 9.99899206e-01 -2.20925937e-12 -3.73258914e-12 -4.15297061e-02 -1.61436408e-11 -5.67324342e-12 -1.70517899e-13 + 1.48691873e+00 9.99897410e-01 -2.17896218e-12 -2.97932324e-12 -4.22848079e-02 -1.20735804e-11 -1.28003166e-12 1.38029994e-13 + 1.72084227e+00 9.99895640e-01 -6.89812359e-10 -5.60776275e-10 -4.30303950e-02 -1.38265560e-09 -3.63552605e-10 -4.36119920e-11 + 2.08634779e+00 9.99893048e-01 2.21002118e-13 -1.25316600e-12 -4.41400204e-02 -7.93220449e-12 -4.46846373e-12 -1.33110330e-12 + 2.46708271e+00 9.99890445e-01 1.68828021e-12 -1.40362850e-12 -4.52577035e-02 -6.32225364e-12 -2.46245782e-12 -4.40985258e-13 + 2.86368158e+00 9.99887810e-01 -1.39776424e-12 -2.13732926e-12 -4.63925807e-02 -9.55895417e-12 5.99257288e-12 8.73918280e-13 + 3.27680541e+00 9.99885127e-01 7.98250464e-13 -1.17957844e-12 -4.75515736e-02 -2.78961266e-12 1.18580741e-12 2.33088054e-13 + 3.70714273e+00 9.99882382e-01 1.77132713e-12 -4.84099568e-13 -4.87409397e-02 -3.55522264e-12 -5.15533799e-12 -6.46977059e-13 + 4.15541077e+00 9.99879566e-01 1.76159291e-12 5.11906476e-13 -4.99642706e-02 -2.01453370e-12 -7.27217063e-12 -7.08641578e-13 + 4.62235665e+00 9.99876673e-01 -2.04605239e-14 -3.31793828e-13 -5.12249262e-02 -5.53037054e-12 -6.53180468e-12 -8.94027278e-13 + 5.10875860e+00 9.99873698e-01 -8.44477917e-13 -1.10263464e-12 -5.25252424e-02 -8.36504528e-12 -6.63104675e-12 -9.33028059e-13 + 5.61542731e+00 9.99870636e-01 -1.53134378e-12 -1.26531074e-12 -5.38679544e-02 -8.99067573e-12 -4.32678574e-12 -6.70212765e-13 + 6.14320721e+00 9.99867483e-01 -2.08900058e-12 -1.45400626e-12 -5.52552188e-02 -1.13938649e-11 -4.12250369e-12 -5.25344129e-13 + 6.69297793e+00 9.99864237e-01 -1.39112172e-12 -1.02025900e-12 -5.66886689e-02 -1.04169559e-11 -5.68993449e-12 -8.97989848e-14 + 7.26565578e+00 9.99860893e-01 -4.29781148e-14 -7.42368567e-13 -5.81705456e-02 -5.07064299e-12 -2.62252972e-12 4.81626045e-13 + 7.86219520e+00 9.99857450e-01 2.41572535e-13 -4.79388453e-13 -5.97028225e-02 -4.67576045e-12 -3.31190202e-12 8.33292106e-13 + 8.48359042e+00 9.99853903e-01 3.25356378e-13 -1.14870438e-13 -6.12872695e-02 -4.79068315e-12 -2.82100198e-12 1.44096315e-13 + 9.13087712e+00 9.99850251e-01 2.28509742e-13 -6.57441013e-13 -6.29259516e-02 -4.25444713e-12 1.65342557e-12 6.50833383e-13 + 9.80513409e+00 9.99846491e-01 -1.71934270e-12 -1.06178272e-12 -6.46206684e-02 -5.58448626e-12 4.94468003e-12 5.62488821e-13 + 1.05074851e+01 9.99842622e-01 -7.00428033e-11 1.58109694e-11 -6.63730805e-02 -1.86242102e-10 1.85696241e-10 8.61639787e-11 + 1.16049086e+01 9.99837000e-01 1.47367541e-12 -3.10068713e-12 -6.90793921e-02 8.33668133e-13 9.14918455e-12 1.88116001e-12 + 1.27480580e+01 9.99831265e-01 1.08421175e-12 -3.51651138e-12 -7.18666670e-02 -2.55644026e-12 5.77660122e-12 -2.13599800e-13 + 1.39388387e+01 9.99825419e-01 -6.12159934e-12 -7.37818146e-12 -7.47368079e-02 -1.50561034e-11 6.75330759e-12 -7.46131449e-13 + 1.51792352e+01 9.99819468e-01 -1.30420749e-11 -1.11497395e-11 -7.76910305e-02 -3.14989588e-11 4.82083847e-12 -1.59546114e-13 + 1.64713149e+01 9.99813418e-01 -9.42791593e-12 -3.69788044e-12 -8.07303410e-02 -1.94953774e-11 9.61689624e-12 2.85848761e-12 + 1.78172313e+01 9.99807275e-01 -9.00013358e-12 -4.99864711e-12 -8.38559249e-02 -2.23459563e-11 7.88442723e-12 4.90210025e-12 + 1.92192275e+01 9.99801048e-01 -8.70483291e-12 -2.51554705e-12 -8.70686865e-02 -2.40209269e-11 2.76077143e-12 4.28414893e-12 + 2.06796402e+01 9.99794747e-01 7.84451786e-11 8.01901394e-10 -9.03694378e-02 3.39892371e-10 -2.60719318e-11 5.42923357e-10 + 2.29615351e+01 9.99786710e-01 8.77176380e-11 7.09938653e-11 -9.54120787e-02 4.37145286e-11 1.31256018e-11 4.32982796e-11 + 2.53385090e+01 9.99778825e-01 9.91060940e-11 9.85591557e-11 -1.00548320e-01 4.29676758e-11 2.00240588e-12 6.02109969e-11 + 2.78145234e+01 9.99771134e-01 1.43048946e-10 1.23149298e-10 -1.05775544e-01 5.97915596e-11 -7.82846626e-11 3.87101228e-11 + 3.03937051e+01 9.99763684e-01 1.67614186e-10 1.31252038e-10 -1.11090209e-01 7.83870502e-11 -1.23036631e-10 3.08544861e-11 + 3.30803527e+01 9.99756528e-01 2.08156475e-10 1.72523338e-10 -1.16488028e-01 8.66937972e-11 -1.25607737e-10 3.06951550e-11 + 3.58789439e+01 9.99749723e-01 2.78907322e-10 2.55941455e-10 -1.21963986e-01 1.31478808e-10 -1.28706271e-10 3.99589020e-11 + 3.87941431e+01 9.99743335e-01 3.49633346e-10 2.70724070e-10 -1.27512579e-01 2.01401728e-10 -1.54499633e-10 1.70643596e-11 + 4.18308089e+01 9.99737432e-01 4.68634186e-10 3.37594960e-10 -1.33128321e-01 2.35719142e-10 -2.47509754e-10 -6.46898327e-12 + 4.49940025e+01 9.99732089e-01 5.07668365e-10 3.15035690e-10 -1.38805414e-01 1.72474078e-10 -2.16838676e-10 1.90684497e-11 + 4.82889958e+01 9.99727392e-01 7.35462985e-10 4.31647827e-10 -1.44537089e-01 2.84316929e-10 -1.67941482e-10 2.65865942e-11 + 5.17212805e+01 9.99723435e-01 1.09079124e-09 5.80868299e-10 -1.50315593e-01 4.23658774e-10 -1.93348712e-10 3.65591468e-11 + 5.52965771e+01 9.99720318e-01 1.46711857e-09 7.33191182e-10 -1.56132682e-01 4.24256616e-10 -5.98921237e-10 3.12932725e-12 + 5.90208444e+01 9.99718152e-01 1.89869192e-09 1.00389909e-09 -1.61979958e-01 2.76256676e-10 -1.00510360e-09 -4.61570026e-11 + 6.29002894e+01 9.99717059e-01 2.27284032e-09 1.28494451e-09 -1.67848590e-01 2.00673837e-10 -1.44117629e-09 -3.46606184e-11 + 6.69413780e+01 9.99717171e-01 2.72653954e-09 1.31252295e-09 -1.73728204e-01 2.74353395e-10 -1.15776538e-09 -2.79672505e-12 + 7.11508453e+01 9.99718632e-01 2.84397144e-09 1.09278902e-09 -1.79608945e-01 5.74771895e-10 -4.99280286e-10 1.20789071e-10 + 7.55357071e+01 9.99721604e-01 2.98811415e-09 7.52384967e-10 -1.85480230e-01 6.66017962e-10 -3.03313805e-11 1.67221152e-10 + 8.01032714e+01 9.99726262e-01 3.17104579e-09 4.13741525e-10 -1.91332020e-01 -5.99919753e-12 7.69955055e-11 -2.09739159e-11 + 8.48611510e+01 9.99732796e-01 3.70082532e-09 2.43160232e-10 -1.97153473e-01 -1.75985306e-10 1.29209882e-09 -3.37749489e-12 + 8.98172755e+01 9.99741417e-01 3.35223603e-09 6.98013714e-11 -2.02933702e-01 -7.47243616e-10 1.32135323e-09 -9.89102584e-12 + 9.49799051e+01 9.99752360e-01 4.12317873e-09 6.85692157e-10 -2.08660800e-01 -1.01521965e-09 1.18892048e-09 -3.79303115e-10 + 1.00357644e+02 9.99765879e-01 4.21540463e-09 1.46660215e-09 -2.14321372e-01 -1.55417307e-09 -7.79685188e-10 -6.86281249e-10 + 1.05959456e+02 9.99782255e-01 2.27736476e-09 -8.76402713e-10 -2.19901664e-01 -3.73640993e-09 5.66555364e-10 -7.43386901e-10 + 1.11794677e+02 9.99801800e-01 2.50998868e-09 -2.91972540e-09 -2.25390382e-01 -6.11149567e-09 3.17082810e-09 -2.17943610e-09 + 1.17873031e+02 9.99824854e-01 1.39777757e-09 -6.93034871e-09 -2.30776112e-01 -7.71429702e-09 6.58059316e-09 -2.39948168e-09 + 1.20000000e+02 9.99824800e-01 1.95726748e-10 -4.34564212e-11 -2.32630337e-01 7.87356588e-11 5.22192696e-11 8.14534180e-11 diff --git a/workflows/exaconstit-calibrate/pyproject.toml b/workflows/exaconstit-calibrate/pyproject.toml new file mode 100644 index 0000000..e4bdd04 --- /dev/null +++ b/workflows/exaconstit-calibrate/pyproject.toml @@ -0,0 +1,134 @@ +[build-system] +requires = ["setuptools>=77.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "exaconstit-calibrate" +version = "0.1.0" +description = "Calibration infrastructure for ExaConstit: NSGA-III parameter fitting, yield-surface fitting (planned), and a code-agnostic framework for driving external simulation codes from Python." +readme = "README.md" +requires-python = ">=3.10" +license = "BSD-3-Clause" +license-files = ["LICENSE"] +authors = [ + { name = "Lawrence Livermore National Laboratory" }, +] +keywords = [ + "ExaConstit", + "crystal plasticity", + "material parameter calibration", + "yield surface", + "NSGA-III", + "genetic algorithm", + "finite element", + "LLNL", +] +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Science/Research", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS :: MacOS X", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Topic :: Scientific/Engineering :: Physics", +] + +# Runtime dependencies required by the core framework. Kept minimal +# on purpose: numpy, pandas, scipy are the only hard requirements. +# SQLite is stdlib. DEAP, matplotlib, and flux-core are all optional. +dependencies = [ + "numpy>=1.21", + "pandas>=1.3", + "scipy>=1.7", +] + +[project.optional-dependencies] +# The NSGA-III driver needs the rcarson3 DEAP fork for the +# U-NSGA-III niching step. Installable from the fork's GitHub URL; +# users who pin the SHA can do so via direct-url installs instead. +nsga3 = [ + "deap @ git+https://github.com/rcarson3/deap.git", +] +# Soft dep for the postprocess plotting helpers. Framework data +# loading / analysis works fine without this. +plot = [ + "matplotlib>=3.5", +] +# Flux is an HPC scheduler; its Python bindings are available only +# on systems where flux-core is installed. Listing here makes the +# intent explicit even though "pip install" won't always succeed. +flux = [ + "flux-python>=0.50", +] +# Test suite dependencies. "pip install .[test]" is what CI runs. +test = [ + "pytest>=7", + "matplotlib>=3.5", + "deap @ git+https://github.com/rcarson3/deap.git", +] +# Catch-all for "install everything installable on a laptop". Drop +# the flux entry from this set since it fails on non-HPC dev +# machines. +all = [ + "matplotlib>=3.5", + "deap @ git+https://github.com/rcarson3/deap.git", +] + +[project.urls] +Homepage = "https://github.com/LLNL/ExaConstit" +Repository = "https://github.com/LLNL/ExaConstit" +Documentation = "https://github.com/LLNL/ExaConstit/tree/main/workflows" + +# --- Package discovery -------------------------------------------------- +# +# Two top-level packages ship together: +# +# workflow_common/ - the framework (code-agnostic; no DEAP dep) +# workflows/ - currently contains workflows.optimization with +# the NSGA-III driver. Uses the "workflows" +# top-level name because that's where the +# pre-refactor scripts live in the ExaConstit +# repo; keeping the name preserves import paths +# in migrated user code. +# +# The distribution name ("exaconstit-calibrate") and the importable +# names ("workflow_common", "workflows") are deliberately different: +# the distribution name captures scope (CP calibration, yield-surface +# fitting to come), while the importable names capture the role of +# each subpackage. Renaming the imports would break every migrated +# driver. + +[tool.setuptools.packages.find] +where = ["."] +include = ["workflow_common*", "workflows*"] +exclude = ["tests*"] + +# Include the two top-level Markdown docs inside the workflow_common +# package so pip-installed users can find them via +# importlib.resources.files("workflow_common") / "ARCHITECTURE.md". +[tool.setuptools.package-data] +workflow_common = ["*.md"] + +# --- Tooling ------------------------------------------------------------ + +[tool.pytest.ini_options] +minversion = "7.0" +# tests/conftest.py adds the repo root to sys.path so the in-tree +# workflow_common imports cleanly during development. No rootdir +# override needed beyond that. +testpaths = ["tests"] +addopts = [ + "-ra", + "--strict-markers", +] +# Warnings-as-errors for anything that looks like a real bug in +# framework code. DEAP's fork emits a few deprecation warnings from +# older numpy APIs; filter those. +filterwarnings = [ + "error", + "ignore::DeprecationWarning:deap.*", + "ignore::DeprecationWarning:pandas.*", + # SyntaxWarning from the rcarson3 DEAP fork's older regex patterns. + "ignore::SyntaxWarning", +] diff --git a/workflows/exaconstit-calibrate/test_script.sh b/workflows/exaconstit-calibrate/test_script.sh new file mode 100644 index 0000000..3605c4e --- /dev/null +++ b/workflows/exaconstit-calibrate/test_script.sh @@ -0,0 +1,6 @@ + PY=/usr/tce/packages/python/python-3.12.2/bin/python + export PYTHONPATH=/usr/lib64/flux/python3.12${PYTHONPATH:+:$PYTHONPATH} + + $PY -m pip install -e ".[test,nsga3,plot]" + cd examples + srun -n 1 --pty --mpi=none --mpibind=off flux start $PY nsga3_calibration.py --backend flux \ No newline at end of file diff --git a/workflows/exaconstit-calibrate/tests/conftest.py b/workflows/exaconstit-calibrate/tests/conftest.py new file mode 100644 index 0000000..a2c66a3 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/conftest.py @@ -0,0 +1,272 @@ +""" +Shared pytest fixtures for the workflow_common test suite. + +What's here +----------- +Fixtures that are reused across multiple test files. Two broad +categories: + +1. **Workspace fixtures** - provide throwaway directories for tests + to write into. pytest's built-in ``tmp_path`` handles the basics; + the ``workspace`` fixture below adds the repo path to sys.path + and configures logging so individual tests do not have to. + +2. **Fake simulation fixtures** - produce a small executable Python + script that mimics just enough of ExaConstit's output conventions + for the integration tests to exercise the full pipeline without + requiring the real simulation code to be installed. + +Why a separate "fake binary" instead of mocking the backend +----------------------------------------------------------- +Integration tests that mock out the backend only verify the Python +code paths. They cannot catch bugs in how environment variables are +set, how working directories are created, how output files are +written and closed, how subprocess stdio is captured, or how timeouts +interact with partially-written files. All of those have bitten real +ExaConstit workflows at least once. Exercising a real subprocess +with a stand-in binary catches those bugs; pure mocks do not. + +The fake binary is tiny - about 30 lines of Python with no imports +beyond the stdlib - so tests that use it stay fast (tens of +milliseconds each). +""" +from __future__ import annotations + +import os +import stat +import sys +import textwrap +from pathlib import Path +from typing import Callable + +import pytest + +# Make workflow_common importable without install when running the +# tests directly against a source checkout. The repository root is +# two levels up from this file (``tests/conftest.py``). When +# workflow_common is pip-installed (e.g. `pip install .[test]`) +# the repo-root path is harmless: importing still picks up the +# installed package because site-packages is earlier on sys.path +# than the insert at position 0 would place it... actually, to +# favor the INSTALLED package, append rather than insert. That way +# developers doing "pip install -e ." hit the editable install +# first, and users running pytest in a source checkout without +# install still get a working import chain via the appended path. +REPO_ROOT = Path(__file__).resolve().parent.parent +if str(REPO_ROOT) not in sys.path: + sys.path.append(str(REPO_ROOT)) + + +# --- Fake simulation binary ---------------------------------------------- +# +# Small Python script that plays the role of ExaConstit for testing. +# It reads a handful of key=value pairs from options.toml via regex +# (not a real TOML parser; we don't want a dependency for tests), +# writes a plausible-looking avg_stress.txt, and optionally fails on +# demand via environment variables. This mirrors what the demo script +# uses but is exposed as a fixture for test files. + +FAKE_BINARY_SRC = textwrap.dedent( + r""" + #!/usr/bin/env python3 + '''Test stand-in for ExaConstit. + + Reads options.toml for strain_rate and yield_stress (both simple + numeric fields). Writes results//avg_stress.txt and + avg_def_grad.txt with synthetic stress-strain data that follows + a saturating exponential (Voce-like) response. + + Environment variables the test suite uses to control behavior: + FAKE_FAIL=1 -> exit 7 with no output + FAKE_SLEEP= -> sleep between reading and writing + FAKE_TRUNCATE=1 -> write partial avg_stress.txt then exit 0 + (simulates a kill-after-write) + FAKE_MISSING= -> skip writing the named output file + ''' + import math + import os + import pathlib + import re + import sys + import time + + options = pathlib.Path("options.toml") + if not options.exists(): + print("options.toml missing", file=sys.stderr) + sys.exit(2) + text = options.read_text() + + def _num(key, default): + m = re.search(rf"{key}\s*=\s*([-\d.eE+]+)", text) + return float(m.group(1)) if m else default + + strain_rate = _num("strain_rate", 1e-3) + yield_stress = _num("yield_stress", 200.0) + hardening = _num("hardening", 2000.0) + basename = "options" + m_base = re.search(r'basename\s*=\s*"([^"]+)"', text) + if m_base: + basename = m_base.group(1) + + time.sleep(float(os.environ.get("FAKE_SLEEP", "0.01"))) + if os.environ.get("FAKE_FAIL") == "1": + print("fake_sim: forced failure", file=sys.stderr) + sys.exit(7) + + results = pathlib.Path("results") / basename + results.mkdir(parents=True, exist_ok=True) + + skip = os.environ.get("FAKE_MISSING", "") + truncate = os.environ.get("FAKE_TRUNCATE") == "1" + + # Generate 50 time steps of saturating response. + # sigma(eps) = yield_stress + hardening * (1 - exp(-50*eps)) + # eps(t) = strain_rate * t, uniaxial along z (matches ExaConstit's + # z-axis convention; StressStrainExtractor defaults to Szz/F33). + n = 50 + t_max = 1.0 + + # ExaConstit's volume-averaged output files start with an indented + # commented header line, followed by indented whitespace-separated + # data rows. Format matches + # ExaConstit/src/postprocessing/postprocessing_file_manager.hpp + # :: GetVolumeAverageHeader. The indentation is load-bearing: + # pandas' C engine mis-handles the "indented # on line 1 + indented + # data rows" shape, which is why the framework reader switches to + # the python engine whenever ``comment`` is set. The fake binary + # emits the exact shape so that path is exercised end-to-end. + stress_header = ( + " # Time Volume " + "Sxx Syy Szz " + "Sxy Sxz Syz" + ) + defgrad_header = ( + " # Time Volume " + "F11 F12 F13 " + "F21 F22 F23 " + "F31 F32 F33" + ) + + rows_stress = [stress_header] + rows_F = [defgrad_header] + for i in range(n): + t = t_max * i / (n - 1) + eps = strain_rate * t + # Load along z so Szz is the active component; Sxx = Syy = 0. + szz = yield_stress + hardening * (1.0 - math.exp(-50.0 * eps)) + volume = 1.0 # unit cube, held constant for simplicity + rows_stress.append( + f" {t:.8e} {volume:.8e} " + f"{0.0:.8e} {0.0:.8e} {szz:.8e} " + f"{0.0:.8e} {0.0:.8e} {0.0:.8e}" + ) + # Deformation gradient: axial stretch along z so F33 = 1+eps; + # transverse (F11, F22) set to 1-0.5*eps for a 0.5 nominal + # Poisson effect; all off-diagonals zero. + axial = 1.0 + eps + lateral = 1.0 - 0.5 * eps + rows_F.append( + f" {t:.8e} {volume:.8e} " + f"{lateral:.8e} {0.0:.8e} {0.0:.8e} " + f"{0.0:.8e} {lateral:.8e} {0.0:.8e} " + f"{0.0:.8e} {0.0:.8e} {axial:.8e}" + ) + + if truncate: + # Drop last 10 data rows to simulate a killed write. Keep the + # header so the file still parses (truncation is about partial + # data, not a corrupt file). + rows_stress = rows_stress[:-10] + + if skip != "avg_stress": + (results / "avg_stress.txt").write_text("\n".join(rows_stress) + "\n") + if skip != "avg_def_grad": + (results / "avg_def_grad.txt").write_text("\n".join(rows_F) + "\n") + """ +).lstrip() + + +@pytest.fixture +def workspace(tmp_path: Path) -> Path: + """A temporary working directory for one test. + + Wraps pytest's ``tmp_path`` so every test that needs a workspace + gets a fresh one, automatically cleaned up on teardown. + + Returns: + The workspace path. The directory exists and is empty. + """ + return tmp_path + + +@pytest.fixture +def fake_binary(workspace: Path) -> Path: + """Write the fake simulation script into the workspace and chmod +x. + + The script is written once per test. Tests that need to vary its + behavior control it through environment variables (``FAKE_FAIL``, + ``FAKE_SLEEP``, ``FAKE_TRUNCATE``, ``FAKE_MISSING``) rather than + by editing the script, so we never have to reason about source + interpolation inside a test. + + Args: + workspace: The per-test temporary directory. + + Returns: + Path to the executable script. + """ + path = workspace / "fake_sim.py" + path.write_text(FAKE_BINARY_SRC) + # Give everyone execute permission. The script lives under /tmp + # (or wherever pytest puts tmp_path) so permissive bits are fine. + path.chmod( + path.stat().st_mode + | stat.S_IEXEC + | stat.S_IXGRP + | stat.S_IXOTH + ) + return path + + +@pytest.fixture +def master_template(workspace: Path) -> Path: + """Write a minimal ExaConstit-like master template. + + The template exercises the ``%%key%%`` placeholder syntax and + includes fields the fake binary looks for (``strain_rate``, + ``yield_stress``, ``hardening``, ``basename``), plus one the + fake does not use (``temp_k``) so tests can verify that extra + keys pass through cleanly. + """ + content = textwrap.dedent( + """\ + [Problem] + name = "sim_%%gene%%_%%obj%%" + basename = "options" + strain_rate = %%strain_rate%% + yield_stress = %%yield_stress%% + hardening = %%hardening%% + temperature_k = %%temp_k%% + """ + ) + path = workspace / "master_options.toml" + path.write_text(content) + return path + + +@pytest.fixture(autouse=True) +def clean_fake_env(): + """Remove any FAKE_* env vars leaked in by prior tests. + + ``autouse=True`` means this runs before every test in the suite, + without requiring tests to ask for it. Prevents a test that sets + ``FAKE_FAIL`` from silently corrupting the next test if it + forgets to clean up. + """ + keys = [k for k in os.environ if k.startswith("FAKE_")] + for k in keys: + os.environ.pop(k, None) + yield + keys = [k for k in os.environ if k.startswith("FAKE_")] + for k in keys: + os.environ.pop(k, None) diff --git a/workflows/exaconstit-calibrate/tests/demo_workflow.py b/workflows/exaconstit-calibrate/tests/demo_workflow.py new file mode 100644 index 0000000..fd689e0 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/demo_workflow.py @@ -0,0 +1,536 @@ +""" +End-to-end smoke test / demo for workflow_common. + +Purpose +------- +This script exists to serve three audiences: + +1. **Developers testing changes to workflow_common**: the fastest way + to verify the plumbing still works after a modification is to run + this script on a desktop. It does not require flux, an HPC + allocation, or ExaConstit itself. +2. **New users learning the framework**: the code here is a concrete + minimal example of how the pieces (``TemplatePathResolver``, + ``SimJobSpec``, ``LocalBackend``, ``Manifest``, sentinels) fit + together. Read it alongside the module-level docstrings. +3. **CI**: the script's return code is 0 when everything worked, so + a continuous-integration job can run it as a regression check. + +What it does +------------ +The script creates a small temporary workspace under ``/tmp/wfc_demo`` +(or wherever ``--root`` points), writes a trivial "fake simulation" +Python script, and then runs a tiny 3-gene, 2-objective optimization +against that fake simulation. The fake sim reads its options.toml, +sleeps briefly, and writes an ``avg_stress.txt`` file with a made-up +stress-strain curve. No real physics, just enough plumbing to +exercise every part of the framework. + +Modes +----- +The ``--phase`` flag selects which scenario is exercised: + +* ``full``: happy path. All cases succeed. +* ``partial``: every case is poisoned to fail via the ``FAKE_FAIL`` + environment variable. Used to verify the failure path and the + manifest's FAILED state transitions. +* ``restart``: rerun after a ``partial`` phase. The restart logic + should see failed sentinels and either rerun or skip depending + on policy. (Current demo just rechecks sentinels; adjust to taste + when prototyping your own restart policy.) + +Usage +----- +Quickest verification:: + + python tests/demo_workflow.py --clean --phase full + +A restart cycle:: + + python tests/demo_workflow.py --clean --phase partial + python tests/demo_workflow.py --phase restart + +The ``--clean`` flag wipes the workspace before starting, so repeated +runs are deterministic. Omit it to inspect leftover state from a +previous run. + +Reading guide +------------- +If you are reading this file to learn the framework, start here and +follow the call chain in order: + +1. :func:`main` at the bottom - entry point. Shows how the pieces + are wired together: logging, path resolver, manifest, backend. +2. :func:`run_generation` - the heart of a real driver. Demonstrates + the sentinel-first-then-manifest ordering, the restart skip + check, and output validation. +3. :func:`build_specs_and_contexts` - how per-case input files get + rendered from a template and turned into executable specs. +4. The fake binary ``FAKE_BINARY_SRC`` - a stub "simulation" that + reads its options and writes a plausible output file. In a real + workflow this would be replaced by the actual mechanics code. + +Everything else in this file is plumbing to make the three functions +above self-contained and runnable on any machine. +""" +from __future__ import annotations + +import argparse +import os +import shutil +import stat +import sys +import textwrap +import time +from pathlib import Path + +# Make workflow_common importable without a package install, by putting +# the repository root on sys.path at runtime. This is the "script +# living next to the package" idiom and is fine for internal tools. +HERE = Path(__file__).resolve().parent +sys.path.insert(0, str(HERE.parent)) + +from workflow_common import ( # noqa: E402 + CaseContext, + CaseState, + LocalBackend, + Manifest, + ManifestEntry, + SimJobSpec, + TemplatePathResolver, + configure_logging, + get_logger, + render_template_file, + write_sentinel, +) +from workflow_common.backends.base import JobOutcome # noqa: E402 +from workflow_common.sentinel import ( # noqa: E402 + is_case_complete, + read_sentinel, + Sentinel, + validate_outputs, +) + +logger = get_logger("demo") + + +# --- Fixtures ------------------------------------------------------------- +# +# We generate both the fake simulation binary and the master template on +# the fly so the demo is fully self-contained - the repo does not need to +# carry these as extra data files. + +# Source code of the fake simulation. Written out to disk, chmod +x'd, +# and invoked like a real binary. Reads options.toml for a strain_rate, +# sleeps briefly to simulate work, and writes a dummy avg_stress.txt. +# Honors FAKE_FAIL=1 to force a nonzero rc, which is how the --phase +# partial mode exercises the failure path. +FAKE_BINARY_SRC = textwrap.dedent( + """\ + #!/usr/bin/env python3 + '''Fake simulation. Reads options.toml (we just grep for a value), + writes an avg_stress.txt, optionally fails depending on env vars.''' + import os, sys, time, pathlib, re + + opt_path = pathlib.Path("options.toml") + if not opt_path.exists(): + print("no options.toml", file=sys.stderr) + sys.exit(2) + + text = opt_path.read_text() + m = re.search(r'strain_rate\\s*=\\s*([-\\d.eE+]+)', text) + strain_rate = float(m.group(1)) if m else 1e-3 + + # Simulate a bit of work so parallel execution actually looks parallel. + time.sleep(float(os.environ.get('FAKE_SLEEP', '0.05'))) + + # Optional forced failure, controlled by the demo driver. + if os.environ.get('FAKE_FAIL') == '1': + print('simulated crash', file=sys.stderr) + sys.exit(7) + + out_dir = pathlib.Path('results/options') + out_dir.mkdir(parents=True, exist_ok=True) + rows = [] + for i in range(10): + t = 0.1 * i + s = 100.0 * (1 - 2 ** (-strain_rate * t * 10)) + rows.append(f'{s:.6f} {strain_rate*t:.6f}') + (out_dir / 'avg_stress.txt').write_text('\\n'.join(rows) + '\\n') + print('fake sim ok') + """ +) + + +# Master-template input "file". Uses the %%key%% placeholder syntax so +# the template renderer substitutes per-case values in. +MASTER_TEMPLATE = textwrap.dedent( + """\ + [Problem] + name = "case_%%gene%%_%%obj%%" + strain_rate = %%strain_rate%% + temperature_k = %%temp_k%% + """ +) + + +def prepare_fake_binary(root: Path) -> Path: + """Write the fake simulation script to disk and make it executable. + + Args: + root: Directory under which to write the script. Written as + ``/fake_sim.py``. + + Returns: + Path to the written script, ready to use as a + :attr:`SimJobSpec.binary`. + """ + bin_path = root / "fake_sim.py" + bin_path.write_text(FAKE_BINARY_SRC) + # chmod +x so the kernel can exec it via shebang. We preserve any + # existing mode bits and OR in the execute bits for all users; the + # script lives under /tmp so permissive perms are fine. + bin_path.chmod( + bin_path.stat().st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH + ) + return bin_path + + +# --- Core per-generation flow -------------------------------------------- + + +def build_specs_and_contexts( + genes, + generation: int, + resolver: TemplatePathResolver, + binary: Path, + master_template_path: Path, +): + """Render per-case inputs and build :class:`SimJobSpec` instances. + + For each gene and each objective, this function: + + 1. Builds a :class:`CaseContext` tagged with per-case parameters. + 2. Resolves the working directory via the supplied + :class:`TemplatePathResolver`. + 3. Renders the master template into an ``options.toml`` inside + that working directory. + 4. Constructs a :class:`SimJobSpec` pointing at the fake binary. + + The function returns pairs ``(ctx, spec)`` rather than just specs + because downstream code needs both: the context to drive the + manifest transitions, and the spec to hand to the backend. + + Args: + genes: A list of genes, where each gene is itself a list of + per-objective parameter dicts. For example, + ``genes[1][0]`` is the parameter dict for gene 1, + objective 0. + generation: The generation number for this batch. + resolver: Provides per-case working directories. + binary: Path to the simulation binary. + master_template_path: Template to render per case. + + Returns: + A list of ``(CaseContext, SimJobSpec)`` pairs, one per + objective of each gene. + """ + out = [] + for igene, gene in enumerate(genes): + for iobj, params in enumerate(gene): + ctx = CaseContext( + generation=generation, + gene=igene, + obj=iobj, + extra=dict(params), + ) + wd = resolver.working_dir(ctx) + wd.mkdir(parents=True, exist_ok=True) + + # Render options.toml with per-case parameter values. The + # values dict picks up the same per-case parameters used + # in the CaseContext.extra, plus the indices themselves. + values = dict(gene=igene, obj=iobj, **params) + render_template_file( + master_template_path, + wd / "options.toml", + values, + ) + + spec = SimJobSpec( + working_dir=wd, + binary=binary, + args=("-opt", "options.toml"), + num_nodes=1, + num_tasks=1, + duration_s=60, + stdout="stdout.log", + stderr="stderr.log", + tag=f"gen{generation}_g{igene}_o{iobj}", + ) + out.append((ctx, spec)) + return out + + +def run_generation( + *, + generation: int, + genes, + resolver: TemplatePathResolver, + binary: Path, + master_template_path: Path, + manifest: Manifest, + backend: LocalBackend, + required_outputs, +): + """Run one generation end-to-end with manifest and sentinel plumbing. + + This is the function that shows how a real driver should wire up + the machinery. Steps, in order: + + 1. Build all case contexts and specs for this generation. + 2. For each case: check if a sentinel already says it is complete. + If so, skip (and reflect that in the manifest). + 3. For each non-skipped case: record a SUBMITTED manifest entry, + then hand the spec to the backend. + 4. As results stream back, validate the outputs, write a sentinel + (sentinel first!), then record the terminal manifest entry. + + The sentinel-before-manifest ordering is deliberate: if a crash + happens between the two writes, the sentinel is already on disk, + so restart correctly treats the case as complete. The manifest + catches up on the next successful record. + + Args: + generation: Current generation number. + genes: Per-gene, per-objective parameter dicts. + resolver: Path resolver for this run. + binary: Simulation binary. + master_template_path: Input-file template. + manifest: The run's manifest (already loaded). + backend: The job backend to use. + required_outputs: List of output-file paths (relative to each + case's working directory) that must exist and be nonempty + for the case to be considered successful. + """ + all_items = build_specs_and_contexts( + genes, generation, resolver, binary, master_template_path + ) + + to_run = [] + skipped = 0 + for ctx, spec in all_items: + # Restart shortcut: a sentinel means this case was completed + # (successfully or not) on a previous run. We do not rerun it, + # but we do reflect the outcome in the manifest so the + # in-memory state matches disk. + if is_case_complete(spec.working_dir): + s = read_sentinel(spec.working_dir) + logger.info( + "skipping gen=%d gene=%d obj=%d (sentinel present, rc=%d)", + ctx.generation, ctx.gene, ctx.obj, s.rc if s else -1, + ) + manifest.record( + ManifestEntry( + generation=ctx.generation, + gene=ctx.gene, + obj=ctx.obj, + state=( + CaseState.COMPLETED + if (s and s.rc == 0) + else CaseState.FAILED + ), + rc=s.rc if s else None, + case_dir=str(spec.working_dir), + message="restored from sentinel", + ) + ) + skipped += 1 + continue + + # About to hand the case to the backend - record SUBMITTED + # before the handoff so restart has a record of "I was trying + # to run this when I died" even if the process is killed + # mid-submission. + manifest.record( + ManifestEntry( + generation=ctx.generation, + gene=ctx.gene, + obj=ctx.obj, + state=CaseState.SUBMITTED, + case_dir=str(spec.working_dir), + ) + ) + to_run.append((ctx, spec)) + + logger.info( + "gen %d: %d cases to run, %d restored from sentinel", + generation, len(to_run), skipped, + ) + + if not to_run: + return + + # We need to map results back to their contexts when they stream + # in from the backend. Using id(spec) is safe here because the + # spec objects persist for the entire loop and are never copied. + spec_to_ctx = {id(spec): ctx for ctx, spec in to_run} + specs = [spec for _, spec in to_run] + + for result in backend.stream_batch(specs): + ctx = spec_to_ctx[id(result.spec)] + + # Validate outputs regardless of rc. A zero rc with missing + # outputs is still a workflow-level failure - for example, a + # process that exited cleanly before writing its results. + ok, missing = validate_outputs(result.spec.working_dir, required_outputs) + if result.outcome == JobOutcome.OK and not ok: + logger.warning( + "gen=%d gene=%d obj=%d rc=0 but outputs missing: %s", + ctx.generation, ctx.gene, ctx.obj, missing, + ) + + terminal = ( + CaseState.COMPLETED + if (result.outcome == JobOutcome.OK and ok) + else CaseState.FAILED + ) + + # Sentinel first. If we crash between this write and the + # manifest record below, restart will see the sentinel and + # correctly treat the case as finished. + write_sentinel( + result.spec.working_dir, + Sentinel( + rc=result.rc, + wall_time_s=result.wall_time_s, + jobid=result.jobid, + output_files={p: str(p) for p in required_outputs}, + status="ok" if terminal == CaseState.COMPLETED else "bad", + message=result.error_message, + ), + ) + manifest.record( + ManifestEntry( + generation=ctx.generation, + gene=ctx.gene, + obj=ctx.obj, + state=terminal, + rc=result.rc, + jobid=result.jobid, + case_dir=str(result.spec.working_dir), + message=result.error_message, + ) + ) + logger.info( + "gen=%d gene=%d obj=%d -> %s rc=%d wall=%.2fs", + ctx.generation, ctx.gene, ctx.obj, + terminal.value, result.rc, result.wall_time_s, + ) + + +def main() -> int: + """Script entry point. + + Parses CLI arguments, prepares the workspace, runs one + generation, and exits with status 0 if every case completed + successfully or 1 if any case failed. Returns, rather than + directly calling ``sys.exit``, so tests can import and invoke + ``main()`` programmatically if desired. + + Returns: + 0 on success, 1 if any case failed. + """ + p = argparse.ArgumentParser(description="workflow_common demo") + p.add_argument("--root", type=Path, default=Path("/tmp/wfc_demo")) + p.add_argument("--clean", action="store_true", + help="wipe the workspace before starting") + p.add_argument("--workers", type=int, default=2, + help="number of concurrent simulations") + p.add_argument( + "--phase", + choices=("full", "partial", "restart"), + default="full", + help="'full' runs all cases normally; 'partial' poisons cases " + "to fail; 'restart' reruns and uses existing sentinels.", + ) + args = p.parse_args() + + if args.clean and args.root.exists(): + shutil.rmtree(args.root) + args.root.mkdir(parents=True, exist_ok=True) + + # stream=True (default) is helpful for demo output so you can see + # the progression live. On a real HPC run you would typically + # disable stream and write logs to a file. + configure_logging(level="info") + + # Layout setup: fake binary, master template, path resolver. + binary = prepare_fake_binary(args.root) + master_template_path = args.root / "master.toml" + master_template_path.write_text(MASTER_TEMPLATE) + + resolver = TemplatePathResolver( + working_dir_pattern="wf_files/gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={ + "avg_stress": "{working_dir}/results/options/avg_stress.txt", + }, + root=args.root, + ) + + # Load the manifest. On a fresh run this is a no-op; on restart + # it replays history and marks anything stuck in SUBMITTED as + # INTERRUPTED. The count is logged so the operator can see what + # the restart detected. + manifest = Manifest(args.root / "wf_files" / "manifest.jsonl") + manifest.load() + n_interrupted = manifest.mark_submitted_as_interrupted() + if n_interrupted: + logger.info("marked %d submitted-but-unknown cases as INTERRUPTED", + n_interrupted) + + backend = LocalBackend(max_workers=args.workers) + + # Tiny toy optimization: 3 genes, each with 2 objectives at + # different strain rates. Parameters are passed through the + # CaseContext.extra mapping and into the template. + genes = [ + [dict(strain_rate=1e-3, temp_k=298.0), dict(strain_rate=1e-1, temp_k=298.0)], + [dict(strain_rate=5e-4, temp_k=298.0), dict(strain_rate=5e-2, temp_k=298.0)], + [dict(strain_rate=2e-3, temp_k=298.0), dict(strain_rate=2e-1, temp_k=298.0)], + ] + + # 'partial' mode poisons the subprocess environment so every case + # fails. Use this to exercise the FAILED branch of the driver. + # The env var goes on os.environ so the child processes inherit it. + if args.phase == "partial": + os.environ["FAKE_FAIL"] = "1" + else: + os.environ.pop("FAKE_FAIL", None) + + required_outputs = ["results/options/avg_stress.txt"] + + run_generation( + generation=0, + genes=genes, + resolver=resolver, + binary=binary, + master_template_path=master_template_path, + manifest=manifest, + backend=backend, + required_outputs=required_outputs, + ) + + # Snapshot at end of generation so a later restart does not have + # to replay the entire JSONL log from the beginning. + manifest.snapshot() + + # Final tally, printed for human consumption. + n_ok = sum(1 for e in manifest.all_entries() if e.state == CaseState.COMPLETED) + n_bad = sum(1 for e in manifest.all_entries() if e.state == CaseState.FAILED) + logger.info("done. completed=%d failed=%d", n_ok, n_bad) + return 0 if n_bad == 0 else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/workflows/exaconstit-calibrate/tests/test_archive.py b/workflows/exaconstit-calibrate/tests/test_archive.py new file mode 100644 index 0000000..b08f0ba --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_archive.py @@ -0,0 +1,1215 @@ +""" +Unit tests for :mod:`workflow_common.archive`. + +What these prove +---------------- +1. **Round-trip correctness** — runs, generations, genes, and case + outputs written to the archive come back bit-identical via the + query methods. Pickled DataFrames round-trip exactly (dtype, + index, values). +2. **Key constraints** — duplicate run_ids raise, out-of-bound + gene records on record_generation raise at construction, + foreign-key cascades delete dependent rows. +3. **Resume discard** — ``discard_from_generation`` removes + exactly the rows with ``gen_idx >= K``, cascading into genes + and case_outputs. +4. **Read-only mode** — opening ``readonly=True`` rejects writes + and allows queries. +5. **WAL concurrent access** — a reader opened while a writer is + active sees committed generations without blocking the writer. +6. **Schema version check** — a DB stamped with a future schema + version raises a clear error at open. +""" +from __future__ import annotations + +import json +import sqlite3 +import threading +import time +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from workflow_common import ArchiveDB, GeneRecord +from workflow_common.archive import SCHEMA_VERSION +from workflow_common.paths import CaseContext +from workflow_common.results import CaseResultSet, TabularResult + + +# --- Helpers ------------------------------------------------------------ + + +def _make_case_result_set( + ctx: CaseContext, + *, + avg_stress: pd.DataFrame, + avg_def_grad: pd.DataFrame = None, +) -> CaseResultSet: + """Synthesize a CaseResultSet for archive tests.""" + tables = { + "avg_stress": TabularResult( + name="avg_stress", df=avg_stress, + source_path=Path("fake/avg_stress.txt"), + ), + } + if avg_def_grad is not None: + tables["avg_def_grad"] = TabularResult( + name="avg_def_grad", df=avg_def_grad, + source_path=Path("fake/avg_def_grad.txt"), + ) + return CaseResultSet(ctx=ctx, tables=tables) + + +def _voce_stress_df(y0=200.0, H=1900.0, k=48.0, n=20) -> pd.DataFrame: + t = np.linspace(0.0, 1.0, n) + strain = t + stress = y0 + H * (1 - np.exp(-k * strain)) + return pd.DataFrame({ + "Time": t, "Szz": stress, + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + + +def _example_gene_record(run_id, *, gen_idx, pop_idx, birth_gen, birth_gene, + fitness=(0.5,), rank=0): + return GeneRecord( + run_id=run_id, + gen_idx=gen_idx, + pop_idx=pop_idx, + birth_gen=birth_gen, + birth_gene=birth_gene, + gene_vector=np.array([1.0, 2.0, 3.0]), + fitness=fitness, + rank=rank, + ) + + +# --- Run-level round-trip --------------------------------------------- + + +def test_start_end_run_round_trip(tmp_path): + """A run written + ended comes back with matching fields.""" + db_path = tmp_path / "a.db" + with ArchiveDB(db_path) as a: + run_id = a.start_run( + seed=42, + param_names=["yield_stress", "hardening"], + objective_labels=["stress_rmse", "slope_rmse"], + config={"n_gens": 10}, + ) + assert run_id # non-empty + a.end_run(run_id) + + # Re-open and query. + with ArchiveDB(db_path, readonly=True) as a: + run = a.get_run(run_id) + assert run.seed == 42 + assert run.param_names == ["yield_stress", "hardening"] + assert run.objective_labels == ["stress_rmse", "slope_rmse"] + assert run.completed_at is not None + + +def test_start_run_generates_uuid_when_none(): + """No run_id -> UUID4 generated.""" + import tempfile + with tempfile.TemporaryDirectory() as d: + with ArchiveDB(Path(d) / "a.db") as a: + rid = a.start_run(seed=0, param_names=["x"]) + # Basic UUID shape: 5 hex groups separated by dashes. + assert rid.count("-") == 4 + + +def test_duplicate_run_id_raises(tmp_path): + with ArchiveDB(tmp_path / "a.db") as a: + a.start_run(run_id="explicit", seed=0, param_names=["x"]) + with pytest.raises(sqlite3.IntegrityError): + a.start_run(run_id="explicit", seed=1, param_names=["x"]) + + +def test_list_runs_order(tmp_path): + """list_runs returns runs oldest-first.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid1 = a.start_run(run_id="r1", seed=1, param_names=["x"]) + time.sleep(0.01) + rid2 = a.start_run(run_id="r2", seed=2, param_names=["x"]) + runs = a.list_runs() + assert [r.run_id for r in runs] == [rid1, rid2] + + +# --- Case-level round-trip -------------------------------------------- + + +def test_record_and_load_case_outputs(tmp_path): + """Pickled DataFrames round-trip bit-identical through the archive.""" + db = tmp_path / "a.db" + ctx = CaseContext(generation=0, gene=0, obj=0) + stress_df = _voce_stress_df() + dg_df = pd.DataFrame({"Time": [0.0, 1.0], "F33": [1.0, 1.5]}) + rs = _make_case_result_set(ctx, avg_stress=stress_df, avg_def_grad=dg_df) + + with ArchiveDB(db) as a: + rid = a.start_run(seed=0, param_names=["x", "y"]) + a.record_case_outputs( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, results=rs, + ) + + with ArchiveDB(db, readonly=True) as a: + loaded = a.load_case_outputs( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) + assert loaded is not None + pd.testing.assert_frame_equal( + loaded.df("avg_stress"), stress_df, + ) + pd.testing.assert_frame_equal( + loaded.df("avg_def_grad"), dg_df, + ) + + +def test_load_case_outputs_missing_returns_none(tmp_path): + """Unknown coordinates -> None (matches disk-based API).""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=0, param_names=["x"]) + got = a.load_case_outputs( + rid, birth_gen=99, birth_gene=99, sim_case_idx=0, + ) + assert got is None + + +def test_record_case_outputs_replaces_on_key_collision(tmp_path): + """INSERT OR REPLACE: re-running a case overwrites the old blob.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=0, param_names=["x"]) + ctx = CaseContext(0, 0, 0) + df1 = pd.DataFrame({"Time": [0.0], "Szz": [1.0]}) + df2 = pd.DataFrame({"Time": [0.0], "Szz": [99.0]}) + # Write once, then overwrite. + a.record_case_outputs( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + results=_make_case_result_set(ctx, avg_stress=df1), + ) + a.record_case_outputs( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + results=_make_case_result_set(ctx, avg_stress=df2), + ) + loaded = a.load_case_outputs( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) + assert loaded.df("avg_stress")["Szz"].tolist() == [99.0] + + +# --- Generation-level round-trip -------------------------------------- + + +def test_record_and_load_generation(tmp_path): + """Genes + stats round-trip via record_generation / load_genes.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=0, param_names=["x", "y", "z"]) + genes = [ + _example_gene_record( + rid, gen_idx=5, pop_idx=i, birth_gen=3, birth_gene=i, + fitness=(0.1 * i, 0.2 * i), + ) + for i in range(4) + ] + a.record_generation( + rid, gen_idx=5, genes=genes, stats={"min": [0.0, 0.0]}, + ) + + with ArchiveDB(tmp_path / "a.db", readonly=True) as a: + loaded = a.load_genes(rid, gen_idx=5) + assert len(loaded) == 4 + # Order preserved by pop_idx. + for i, g in enumerate(loaded): + assert g.pop_idx == i + assert g.birth_gen == 3 + assert g.birth_gene == i + np.testing.assert_array_equal( + g.gene_vector, [1.0, 2.0, 3.0], + ) + assert g.fitness == (0.1 * i, 0.2 * i) + + gens = a.list_generations(rid) + assert len(gens) == 1 + assert gens[0].gen_idx == 5 + assert gens[0].n_pop == 4 + assert gens[0].stats == {"min": [0.0, 0.0]} + + +def test_record_generation_rejects_mismatched_gen_idx(tmp_path): + """A gene record with gen_idx != outer must raise.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=0, param_names=["x"]) + bad = _example_gene_record( + rid, gen_idx=5, pop_idx=0, birth_gen=0, birth_gene=0, + ) + with pytest.raises(ValueError, match="gen_idx"): + a.record_generation(rid, gen_idx=6, genes=[bad]) + + +def test_record_generation_rejects_mismatched_run_id(tmp_path): + with ArchiveDB(tmp_path / "a.db") as a: + rid1 = a.start_run(run_id="r1", seed=0, param_names=["x"]) + rid2 = a.start_run(run_id="r2", seed=0, param_names=["x"]) + bad = _example_gene_record( + rid1, gen_idx=0, pop_idx=0, birth_gen=0, birth_gene=0, + ) + with pytest.raises(ValueError, match="run_id"): + a.record_generation(rid2, gen_idx=0, genes=[bad]) + + +def test_record_generation_replaces_partial_prior_write(tmp_path): + """A re-record of the same gen_idx replaces any partial rows. + + The specific scenario: a crash mid-generation left partial gene + rows in the archive. On resume the driver re-runs the + generation, calling record_generation with the full pop. The + stale partial rows must be gone afterwards. + """ + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=0, param_names=["x"]) + # Simulate a partial write: just 2 of 4 gene rows. + partial = [ + _example_gene_record( + rid, gen_idx=1, pop_idx=i, birth_gen=1, birth_gene=i, + ) + for i in range(2) + ] + a.record_generation(rid, gen_idx=1, genes=partial) + + # Full write on retry. + full = [ + _example_gene_record( + rid, gen_idx=1, pop_idx=i, birth_gen=1, birth_gene=i, + ) + for i in range(4) + ] + a.record_generation(rid, gen_idx=1, genes=full) + + loaded = a.load_genes(rid, gen_idx=1) + # Exactly 4; no leftover duplicates or missing rows. + assert [g.pop_idx for g in loaded] == [0, 1, 2, 3] + + +# --- Resume semantics ------------------------------------------------- + + +def test_discard_from_generation_cascades(tmp_path): + """discard_from_generation(K) removes gens K.. and cascades to genes+case_outputs.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=0, param_names=["x"]) + # Populate 3 generations of stuff. + for g in range(3): + genes = [ + _example_gene_record( + rid, gen_idx=g, pop_idx=0, birth_gen=g, birth_gene=0, + ), + ] + a.record_generation(rid, gen_idx=g, genes=genes) + # one case_output per gen too, keyed by birth_gen=g + ctx = CaseContext(generation=g, gene=0, obj=0) + a.record_case_outputs( + rid, birth_gen=g, birth_gene=0, sim_case_idx=0, + results=_make_case_result_set( + ctx, avg_stress=_voce_stress_df(y0=100 + g), + ), + ) + + # Discard gen 1.. + a.discard_from_generation(rid, gen_idx=1) + + # Gen 0 survives, gens 1..2 gone. load_genes returns []. + assert len(a.load_genes(rid, 0)) == 1 + assert len(a.load_genes(rid, 1)) == 0 + assert len(a.load_genes(rid, 2)) == 0 + # Cascaded case outputs: gen 0 still has its output, 1..2 don't. + assert a.load_case_outputs( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) is not None + assert a.load_case_outputs( + rid, birth_gen=1, birth_gene=0, sim_case_idx=0, + ) is None + assert a.load_case_outputs( + rid, birth_gen=2, birth_gene=0, sim_case_idx=0, + ) is None + + # generations table also cleaned. + assert [g.gen_idx for g in a.list_generations(rid)] == [0] + + +def test_delete_run_removes_run_and_all_dependents(tmp_path): + """delete_run wipes the target run entirely — row + generations + + genes + case_outputs — via schema CASCADE, leaving OTHER runs in + the same archive untouched. + + Exercises the "clean up the residue of a bad invocation" use + case: two runs in one archive, delete one, the other survives + in full. + """ + with ArchiveDB(tmp_path / "a.db") as a: + keep_id = a.start_run(seed=1, param_names=["x"]) + drop_id = a.start_run(seed=2, param_names=["x"]) + # Two generations in each run, plus one case_output per gene, + # so we can verify the cascade reaches every dependent table. + for rid in (keep_id, drop_id): + for g in range(2): + a.record_generation( + rid, gen_idx=g, + genes=[ + _example_gene_record( + rid, gen_idx=g, pop_idx=0, + birth_gen=g, birth_gene=0, + ), + ], + ) + ctx = CaseContext(generation=g, gene=0, obj=0) + a.record_case_outputs( + rid, birth_gen=g, birth_gene=0, sim_case_idx=0, + results=_make_case_result_set( + ctx, avg_stress=_voce_stress_df(y0=100 + g), + ), + ) + + # Delete one run. + n = a.delete_run(drop_id) + assert n == 1 # confirmation signal + + # drop_id must be gone from every table. + assert drop_id not in [r.run_id for r in a.list_runs()] + assert a.list_generations(drop_id) == [] + assert a.load_genes(drop_id, 0) == [] + assert a.load_case_outputs( + drop_id, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) is None + + # keep_id must be completely unaffected. + assert keep_id in [r.run_id for r in a.list_runs()] + kept_gens = [g.gen_idx for g in a.list_generations(keep_id)] + assert kept_gens == [0, 1] + assert len(a.load_genes(keep_id, 0)) == 1 + assert len(a.load_genes(keep_id, 1)) == 1 + assert a.load_case_outputs( + keep_id, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) is not None + + +def test_delete_run_raises_on_unknown_id(tmp_path): + """Typos and already-deleted IDs get a KeyError, not a silent no-op. + + Silent-no-op is a classic trap: caller thinks they cleaned up + and actually just misspelled the UUID. + """ + with ArchiveDB(tmp_path / "a.db") as a: + a.start_run(run_id="real-one", seed=0, param_names=["x"]) + with pytest.raises(KeyError, match="no run with run_id"): + a.delete_run("not-a-real-run-id") + # The real run is still there — we didn't partial-delete. + assert [r.run_id for r in a.list_runs()] == ["real-one"] + + +def test_delete_run_rejects_on_readonly(tmp_path): + """Read-only connections must refuse delete_run like every other mutation.""" + db = tmp_path / "a.db" + with ArchiveDB(db) as a: + a.start_run(run_id="only", seed=0, param_names=["x"]) + # Reopen read-only. + with ArchiveDB(db, readonly=True) as a: + with pytest.raises(RuntimeError, match="read-only"): + a.delete_run("only") + + +# --- prune_empty_runs -------------------------------------------------- + + +def test_prune_empty_runs_removes_completed_empty_runs(tmp_path): + """Runs that called end_run but never wrote a generation are always eligible.""" + with ArchiveDB(tmp_path / "a.db") as a: + # Three runs: the middle one is the only "real" one with + # a generation; the other two are empty-and-completed. + empty1 = a.start_run(seed=1, param_names=["x"]) + a.end_run(empty1) + + full = a.start_run(seed=2, param_names=["x"]) + a.record_generation( + full, gen_idx=0, + genes=[_example_gene_record( + full, gen_idx=0, pop_idx=0, birth_gen=0, birth_gene=0, + )], + ) + a.end_run(full) + + empty2 = a.start_run(seed=3, param_names=["x"]) + a.end_run(empty2) + + deleted = a.prune_empty_runs() + # Both empty runs, oldest first (started_at ASC). + assert deleted == [empty1, empty2] + + # Only the one real run survives. + survivors = [r.run_id for r in a.list_runs()] + assert survivors == [full] + + +def test_prune_empty_runs_preserves_young_empty_runs_by_default(tmp_path): + """A run that was just started and is empty must NOT be pruned — + it might be a live run still initializing. Default age gate of + 60 minutes protects it. + """ + with ArchiveDB(tmp_path / "a.db") as a: + # Young empty run — not completed, just started. + young = a.start_run(seed=1, param_names=["x"]) + # Another young run that's completed — eligible regardless of age. + completed = a.start_run(seed=2, param_names=["x"]) + a.end_run(completed) + + deleted = a.prune_empty_runs() + # Only the completed one got pruned; the live-looking young + # one stays. + assert deleted == [completed] + survivors = [r.run_id for r in a.list_runs()] + assert survivors == [young] + + +def test_prune_empty_runs_with_age_zero_prunes_young_runs_too(tmp_path): + """--age-minutes 0 disables the young-run protection. + + For the "I know my live run isn't running" case, the user can + override the safety default. + """ + with ArchiveDB(tmp_path / "a.db") as a: + young = a.start_run(seed=1, param_names=["x"]) + # Don't call end_run — simulates a crashed run that'll never + # flip completed_at. + + deleted = a.prune_empty_runs(min_age_minutes=0.0) + assert deleted == [young] + assert a.list_runs() == [] + + +def test_prune_empty_runs_dry_run_returns_list_without_deleting(tmp_path): + """dry_run=True shows what WOULD be deleted. Nothing is actually removed.""" + with ArchiveDB(tmp_path / "a.db") as a: + empty = a.start_run(seed=1, param_names=["x"]) + a.end_run(empty) + + preview = a.prune_empty_runs(dry_run=True) + assert preview == [empty] + # Run still there. + assert [r.run_id for r in a.list_runs()] == [empty] + + # Second dry-run gives same answer (no side effects). + assert a.prune_empty_runs(dry_run=True) == [empty] + + # Real run confirms the prediction. + assert a.prune_empty_runs() == [empty] + assert a.list_runs() == [] + + +def test_prune_empty_runs_preserves_run_with_case_outputs_only(tmp_path): + """A run with case_outputs but no generations represents a partial + crash mid-gen-0. Those case outputs are real simulation artifacts + — don't delete them even though ``generations`` is empty. + """ + from workflow_common.paths import CaseContext + + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["x"]) + # Case output written, but record_generation never called. + ctx = CaseContext(generation=0, gene=0, obj=0) + a.record_case_outputs( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + results=_make_case_result_set( + ctx, avg_stress=_voce_stress_df(y0=100), + ), + ) + a.end_run(rid) # mark completed to bypass age gate + + deleted = a.prune_empty_runs() + # Not pruned — the case_outputs row protects it. + assert deleted == [] + assert [r.run_id for r in a.list_runs()] == [rid] + + +def test_prune_empty_runs_noop_on_empty_archive(tmp_path): + """An archive with no runs returns an empty list, no errors.""" + with ArchiveDB(tmp_path / "a.db") as a: + assert a.prune_empty_runs() == [] + assert a.prune_empty_runs(dry_run=True) == [] + + +def test_prune_empty_runs_rejects_on_readonly(tmp_path): + """Read-only connections must refuse the prune operation.""" + db = tmp_path / "a.db" + with ArchiveDB(db) as a: + rid = a.start_run(run_id="empty", seed=0, param_names=["x"]) + a.end_run(rid) + with ArchiveDB(db, readonly=True) as a: + with pytest.raises(RuntimeError, match="read-only"): + a.prune_empty_runs() + + +# --- Read-only mode --------------------------------------------------- + + +def test_readonly_rejects_writes(tmp_path): + db = tmp_path / "a.db" + with ArchiveDB(db) as a: + a.start_run(run_id="ro", seed=0, param_names=["x"]) + # Re-open read-only. + with ArchiveDB(db, readonly=True) as a: + # Queries work. + assert a.get_run("ro").run_id == "ro" + # Writes do not. + with pytest.raises(RuntimeError, match="read-only"): + a.start_run(run_id="ro2", seed=0, param_names=["x"]) + + +# --- WAL concurrent reader -------------------------------------------- + + +def test_writer_and_reader_coexist(tmp_path): + """WAL mode: a reader opened while a writer is active sees committed data.""" + db = tmp_path / "a.db" + with ArchiveDB(db) as writer: + rid = writer.start_run(run_id="r", seed=0, param_names=["x"]) + + # Open a read-only connection while writer is alive. Both + # connections exist simultaneously. + reader = ArchiveDB(db, readonly=True) + reader.open() + try: + # Reader sees the existing run. + assert reader.get_run(rid).run_id == rid + + # Writer commits more data; reader sees it on next query. + writer.record_generation( + rid, gen_idx=0, + genes=[_example_gene_record( + rid, gen_idx=0, pop_idx=0, birth_gen=0, birth_gene=0, + )], + ) + # New connection-level query sees the just-committed row. + # (SQLite snapshot-at-txn-start semantics mean the same + # SELECT statement on a live cursor might not; a fresh + # execute does.) + assert len(reader.load_genes(rid, 0)) == 1 + finally: + reader.close() + + +# --- Schema version ---------------------------------------------------- + + +def test_schema_version_stamped(tmp_path): + """On first write, the schema_meta table records the current version.""" + db = tmp_path / "a.db" + with ArchiveDB(db) as a: + pass # just create + # Direct SQL read to verify the stamp. + conn = sqlite3.connect(db) + v = conn.execute( + "SELECT value FROM schema_meta WHERE key='schema_version'" + ).fetchone() + conn.close() + assert int(v[0]) == SCHEMA_VERSION + + +def test_future_schema_version_raises(tmp_path): + """Opening a DB stamped with a newer schema raises at open().""" + db = tmp_path / "a.db" + # Create a normal DB first. + with ArchiveDB(db) as a: + pass + # Manually bump the stamp to a future version. + conn = sqlite3.connect(db) + conn.execute( + "UPDATE schema_meta SET value=? WHERE key='schema_version'", + (str(SCHEMA_VERSION + 5),), + ) + conn.commit() + conn.close() + # Now opening should refuse. + with pytest.raises(RuntimeError, match="schema version"): + with ArchiveDB(db) as a: + pass + + +# --- Context-manager discipline --------------------------------------- + + +def test_query_without_open_raises(tmp_path): + a = ArchiveDB(tmp_path / "a.db") # not opened + with pytest.raises(RuntimeError, match="not open"): + a.list_runs() + + +def test_close_is_idempotent(tmp_path): + a = ArchiveDB(tmp_path / "a.db") + a.open() + a.close() + a.close() # second close: no-op + + +# --- Experimental data --------------------------------------------------- + + +def test_record_experiment_round_trips_dataframe(tmp_path): + """Experimental data round-trips faithfully (column names, dtypes, values).""" + db = tmp_path / "a.db" + df = pd.DataFrame({ + "strain": np.linspace(0.0, 0.1, 11), + "stress": np.linspace(200.0, 350.0, 11), + "extra_meta": ["a"] * 11, + }) + with ArchiveDB(db) as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_experiment(rid, sim_case_idx=0, df=df, label="exp1") + with ArchiveDB(db, readonly=True) as a: + result = a.load_experiment(rid, 0) + assert result is not None + label, got = result + assert label == "exp1" + assert list(got.columns) == ["strain", "stress", "extra_meta"] + pd.testing.assert_frame_equal(got, df) + + +def test_record_experiment_replaces_on_collision(tmp_path): + """Re-recording the same (run_id, sim_case_idx) overwrites — supports resume.""" + df1 = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + df2 = pd.DataFrame({"strain": [0.0, 0.2], "stress": [100.0, 250.0]}) + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_experiment(rid, sim_case_idx=0, df=df1, label="v1") + a.record_experiment(rid, sim_case_idx=0, df=df2, label="v2") + result = a.load_experiment(rid, 0) + assert result is not None + label, got = result + assert label == "v2" + pd.testing.assert_frame_equal(got, df2) + + +def test_load_experiment_returns_none_when_missing(tmp_path): + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + # No record_experiment call. + assert a.load_experiment(rid, 0) is None + assert a.load_experiment(rid, 99) is None + + +def test_list_experiments_returns_sim_case_index_label_pairs(tmp_path): + """list_experiments enumerates without loading the (potentially big) DataFrames.""" + df = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_experiment(rid, sim_case_idx=0, df=df, label="exp_quasi") + a.record_experiment(rid, sim_case_idx=2, df=df, label="exp_dyn") + # No data for sim_case_idx=1 — ordering should still be ascending, + # gaps preserved. + listing = a.list_experiments(rid) + assert listing == [(0, "exp_quasi"), (2, "exp_dyn")] + + +def test_record_experiment_cascades_on_run_delete(tmp_path): + """Deleting a run must remove its experiment rows too (FK cascade).""" + df = pd.DataFrame({"strain": [0.0], "stress": [100.0]}) + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_experiment(rid, sim_case_idx=0, df=df, label="exp") + a.delete_run(rid) + # Experiment row should be gone. + cur = a._conn.execute( + "SELECT COUNT(*) FROM experiments WHERE run_id=?", (rid,), + ) + assert cur.fetchone()[0] == 0 + + +def test_record_experiment_rejects_on_readonly(tmp_path): + db = tmp_path / "a.db" + df = pd.DataFrame({"strain": [0.0], "stress": [100.0]}) + with ArchiveDB(db) as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + with ArchiveDB(db, readonly=True) as a: + with pytest.raises(RuntimeError, match="read-only"): + a.record_experiment(rid, sim_case_idx=0, df=df, label="exp") + + +# --- Backward compat for archives lacking the experiments table --------- + + +def _build_pre_experiments_archive(db_path): + """Build an archive with the pre-experiments-table schema. + + Used to exercise the load_experiment / list_experiments + backward-compat path: those methods must return None / [] when + the table is missing rather than letting sqlite3.OperationalError + bubble up. + """ + import sqlite3 + con = sqlite3.connect(db_path) + con.executescript(""" + CREATE TABLE schema_meta (key TEXT PRIMARY KEY, value TEXT NOT NULL); + INSERT INTO schema_meta VALUES('schema_version', '1'); + CREATE TABLE runs ( + run_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, + completed_at TEXT, seed INTEGER, param_names TEXT NOT NULL, + objective_labels TEXT, config_json TEXT + ); + CREATE TABLE generations ( + run_id TEXT NOT NULL, gen_idx INTEGER NOT NULL, + recorded_at TEXT NOT NULL, n_pop INTEGER NOT NULL, + stats_json TEXT, PRIMARY KEY (run_id, gen_idx) + ); + CREATE TABLE genes ( + run_id TEXT NOT NULL, gen_idx INTEGER NOT NULL, + pop_idx INTEGER NOT NULL, birth_gen INTEGER NOT NULL, + birth_gene INTEGER NOT NULL, gene_vector TEXT NOT NULL, + fitness TEXT NOT NULL, rank INTEGER, + PRIMARY KEY (run_id, gen_idx, pop_idx) + ); + CREATE TABLE case_outputs ( + run_id TEXT NOT NULL, birth_gen INTEGER NOT NULL, + birth_gene INTEGER NOT NULL, sim_case_idx INTEGER NOT NULL, + output_name TEXT NOT NULL, data_blob BLOB NOT NULL, + PRIMARY KEY (run_id, birth_gen, birth_gene, sim_case_idx, output_name) + ); + INSERT INTO runs VALUES ('r1', '2025-01-01T00:00:00', NULL, 1, + '["p"]', '["o"]', NULL); + """) + con.commit() + con.close() + + +def test_load_experiment_returns_none_when_table_missing(tmp_path): + """An archive written before the experiments table existed must + still be readable. load_experiment returns None instead of + raising sqlite3.OperationalError. + + This is the bug Robert reported: a real-world archive on his + machine pre-dated this table, and the plotter crashed with + "no such table: experiments" before falling back to the user's + --experimental CSVs. + """ + db = tmp_path / "old.db" + _build_pre_experiments_archive(db) + with ArchiveDB(db, readonly=True) as a: + # Sanity: the table genuinely isn't there. + cur = a._conn.execute( + "SELECT name FROM sqlite_master WHERE type='table' AND name='experiments'" + ) + assert cur.fetchone() is None + # The contract: returns None, doesn't raise. + assert a.load_experiment("r1", 0) is None + assert a.load_experiment("r1", 99) is None + + +def test_list_experiments_returns_empty_when_table_missing(tmp_path): + """Same backward-compat as load_experiment, but for the + enumerate path used by tools that ask "what experiments exist?" + before deciding whether to fetch any. + """ + db = tmp_path / "old.db" + _build_pre_experiments_archive(db) + with ArchiveDB(db, readonly=True) as a: + assert a.list_experiments("r1") == [] + + +def test_writable_open_creates_missing_experiments_table(tmp_path): + """Opening a pre-experiments archive in WRITE mode must create + the table on the fly (via ``IF NOT EXISTS`` in the schema script) + so subsequent writes work without manual migration. Read-only + opens skip this and stay backward-compat via the load path's + None return. + """ + db = tmp_path / "old.db" + _build_pre_experiments_archive(db) + df = pd.DataFrame({"strain": [0.0, 0.5], "stress": [100.0, 200.0]}) + # Writable open — schema script runs, experiments table created. + with ArchiveDB(db) as a: + a.record_experiment("r1", sim_case_idx=0, df=df, label="x") + result = a.load_experiment("r1", 0) + assert result is not None + label, got = result + assert label == "x" + pd.testing.assert_frame_equal(got, df) + + +# --- minmax_strain on experiments --------------------------------------- + + +def test_record_experiment_stores_minmax_strain(tmp_path): + """record_experiment must persist the (lo, hi) window so plotters + can shade the optimized region against the full curve.""" + df = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_experiment(rid, sim_case_idx=0, df=df, label="exp", + minmax_strain=(0.005, 0.13)) + win = a.load_experiment_window(rid, 0) + assert win == (0.005, 0.13) + + +def test_record_experiment_window_supports_unbounded_sides(tmp_path): + """(None, hi) and (lo, None) round-trip — either side is optional.""" + df = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_experiment(rid, sim_case_idx=0, df=df, + minmax_strain=(None, 0.13)) + a.record_experiment(rid, sim_case_idx=1, df=df, + minmax_strain=(0.005, None)) + a.record_experiment(rid, sim_case_idx=2, df=df, + minmax_strain=None) + assert a.load_experiment_window(rid, 0) == (None, 0.13) + assert a.load_experiment_window(rid, 1) == (0.005, None) + # No window stored at all → None (NOT (None, None) — that + # sentinel would mean "stored but unbounded both sides," + # which the user didn't say). + assert a.load_experiment_window(rid, 2) is None + + +def test_load_experiment_window_returns_none_for_missing_record(tmp_path): + """Querying a (run_id, sim_case_idx) that was never recorded returns + None, not an exception. Plotter relies on this for graceful skip.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + assert a.load_experiment_window(rid, 99) is None + + +def test_load_experiment_window_returns_none_on_archive_without_column(tmp_path): + """An archive written before the minmax_strain column existed + should stay readable read-only — the load returns None and the + plotter falls through to "no shading" cleanly. + """ + import sqlite3, pickle + db = tmp_path / "old.db" + con = sqlite3.connect(db) + con.executescript(""" + CREATE TABLE schema_meta(key TEXT PRIMARY KEY, value TEXT NOT NULL); + INSERT INTO schema_meta VALUES('schema_version', '1'); + CREATE TABLE runs(run_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, + completed_at TEXT, seed INTEGER, param_names TEXT NOT NULL, + objective_labels TEXT, config_json TEXT); + CREATE TABLE experiments( + run_id TEXT NOT NULL, sim_case_idx INTEGER NOT NULL, + label TEXT, data_blob BLOB NOT NULL, + PRIMARY KEY (run_id, sim_case_idx)); + INSERT INTO runs VALUES('r1', '2025', NULL, 1, '["p"]', '["o"]', NULL); + """) + df = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + con.execute("INSERT INTO experiments VALUES('r1', 0, 'old', ?)", + (pickle.dumps(df),)) + con.commit(); con.close() + + with ArchiveDB(db, readonly=True) as a: + # The DataFrame still loads — only the new column is missing. + assert a.load_experiment("r1", 0)[0] == "old" + # And the window query degrades to None. + assert a.load_experiment_window("r1", 0) is None + + +def test_writable_open_migrates_minmax_strain_column(tmp_path): + """Opening an old archive in writable mode auto-adds the + minmax_strain column so subsequent record_experiment calls + can persist windows. No manual migration step required.""" + import sqlite3, pickle + db = tmp_path / "old.db" + con = sqlite3.connect(db) + con.executescript(""" + CREATE TABLE schema_meta(key TEXT PRIMARY KEY, value TEXT NOT NULL); + INSERT INTO schema_meta VALUES('schema_version', '1'); + CREATE TABLE runs(run_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, + completed_at TEXT, seed INTEGER, param_names TEXT NOT NULL, + objective_labels TEXT, config_json TEXT); + CREATE TABLE experiments( + run_id TEXT NOT NULL, sim_case_idx INTEGER NOT NULL, + label TEXT, data_blob BLOB NOT NULL, + PRIMARY KEY (run_id, sim_case_idx)); + INSERT INTO runs VALUES('r1', '2025', NULL, 1, '["p"]', '["o"]', NULL); + """) + con.commit(); con.close() + + # Writable open triggers _migrate_add_missing_columns. + df = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + with ArchiveDB(db) as a: + # Column is present — record_experiment with minmax_strain + # works without raising. + a.record_experiment("r1", sim_case_idx=0, df=df, label="new", + minmax_strain=(0.005, 0.10)) + assert a.load_experiment_window("r1", 0) == (0.005, 0.10) + + # Verify by hand: the column genuinely exists in the file. + con = sqlite3.connect(db) + cols = [r[1] for r in con.execute( + "PRAGMA table_info(experiments)" + ).fetchall()] + con.close() + assert "minmax_strain" in cols + + +def test_migration_idempotent_on_fresh_archive(tmp_path): + """Running the migration on a brand-new archive (where the column + was already created by the schema script) must be a no-op rather + than raising "duplicate column name.""" + db = tmp_path / "new.db" + # First open creates fresh schema with the column already present. + with ArchiveDB(db): + pass + # Second open re-runs the migration step. Must not raise. + with ArchiveDB(db): + pass + import sqlite3 + con = sqlite3.connect(db) + cols = [r[1] for r in con.execute( + "PRAGMA table_info(experiments)" + ).fetchall()] + con.close() + # Exactly one minmax_strain column, no duplicates. + assert cols.count("minmax_strain") == 1 + + +# --- extractor_config on experiments ----------------------------------- + + +def test_record_experiment_persists_extractor_config(tmp_path): + """Round-trip a full extractor config through record/load.""" + df = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + cfg = { + "stress_output": "avg_stress", + "stress_column": "Szz", + "strain_source": "time_rate", + "strain_rate": 1e-3, + "strain_source_output": "avg_def_grad", + "strain_source_column": "F33", + "time_column": "Time", + "window": [0.005, 0.13], + } + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_experiment(rid, sim_case_idx=0, df=df, + extractor_config=cfg) + loaded = a.load_extractor_config(rid, 0) + assert loaded == cfg + + +def test_load_extractor_config_returns_none_when_no_record(tmp_path): + """Querying an unrecorded (run, sim_case) returns None, not raise.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + assert a.load_extractor_config(rid, 99) is None + + +def test_load_extractor_config_returns_none_when_record_has_null_config(tmp_path): + """A recorded experiment with extractor_config=None reads back as None.""" + df = pd.DataFrame({"strain": [0.0], "stress": [0.0]}) + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + # No extractor_config supplied. + a.record_experiment(rid, sim_case_idx=0, df=df, label="bare") + assert a.load_extractor_config(rid, 0) is None + + +def test_load_extractor_config_returns_none_when_column_absent(tmp_path): + """Old archive without the extractor_config column → None on read, + no crash. Read-only opens skip the migration so the column stays + absent.""" + import sqlite3, pickle + db = tmp_path / "old.db" + con = sqlite3.connect(db) + con.executescript(""" + CREATE TABLE schema_meta(key TEXT PRIMARY KEY, value TEXT NOT NULL); + INSERT INTO schema_meta VALUES('schema_version', '1'); + CREATE TABLE runs(run_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, + completed_at TEXT, seed INTEGER, param_names TEXT NOT NULL, + objective_labels TEXT, config_json TEXT); + CREATE TABLE experiments( + run_id TEXT NOT NULL, sim_case_idx INTEGER NOT NULL, + label TEXT, data_blob BLOB NOT NULL, minmax_strain TEXT, + PRIMARY KEY (run_id, sim_case_idx)); + INSERT INTO runs VALUES('r1', '2025', NULL, 1, '["p"]', '["o"]', NULL); + """) + df = pd.DataFrame({"strain": [0.0], "stress": [0.0]}) + con.execute("INSERT INTO experiments VALUES('r1', 0, 'old', ?, NULL)", + (pickle.dumps(df),)) + con.commit(); con.close() + + with ArchiveDB(db, readonly=True) as a: + # The earlier-introduced fields still load. + assert a.load_experiment("r1", 0)[0] == "old" + # And the new column degrades to None gracefully. + assert a.load_extractor_config("r1", 0) is None + + +def test_writable_open_migrates_extractor_config_column(tmp_path): + """Opening an old archive in writable mode auto-adds the new + column (via _migrate_add_missing_columns), and subsequent + record_experiment with extractor_config persists correctly. + """ + import sqlite3, pickle + db = tmp_path / "old.db" + con = sqlite3.connect(db) + con.executescript(""" + CREATE TABLE schema_meta(key TEXT PRIMARY KEY, value TEXT NOT NULL); + INSERT INTO schema_meta VALUES('schema_version', '1'); + CREATE TABLE runs(run_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, + completed_at TEXT, seed INTEGER, param_names TEXT NOT NULL, + objective_labels TEXT, config_json TEXT); + CREATE TABLE experiments( + run_id TEXT NOT NULL, sim_case_idx INTEGER NOT NULL, + label TEXT, data_blob BLOB NOT NULL, + PRIMARY KEY (run_id, sim_case_idx)); + INSERT INTO runs VALUES('r1', '2025', NULL, 1, '["p"]', '["o"]', NULL); + """) + con.commit(); con.close() + + df = pd.DataFrame({"strain": [0.0, 0.1], "stress": [100.0, 200.0]}) + cfg = {"strain_source": "time_rate", "strain_rate": 1e-3} + with ArchiveDB(db) as a: + a.record_experiment("r1", sim_case_idx=0, df=df, + extractor_config=cfg) + loaded = a.load_extractor_config("r1", 0) + # Round-trip should match — the from_dict tolerance is for the + # plotter; the archive stores verbatim what was given. + assert loaded == cfg + + con = sqlite3.connect(db) + cols = [r[1] for r in con.execute( + "PRAGMA table_info(experiments)" + ).fetchall()] + con.close() + assert "extractor_config" in cols + + +# --- case_curves: extracted (independent, dependent) curves -------------- + + +def test_record_case_curve_round_trips_arrays(tmp_path): + """Stored independent/dependent arrays load back identically.""" + db = tmp_path / "a.db" + independent = np.linspace(0.0, 0.1, 50) + dependent = 200.0 + 1900.0 * (1 - np.exp(-48.0 * independent)) + with ArchiveDB(db) as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_case_curve( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + independent=independent, dependent=dependent, + independent_label="strain", dependent_label="stress", + ) + with ArchiveDB(db, readonly=True) as a: + result = a.load_case_curve( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) + assert result is not None + ind, dep, ind_label, dep_label = result + np.testing.assert_array_equal(ind, independent) + np.testing.assert_array_equal(dep, dependent) + assert ind_label == "strain" + assert dep_label == "stress" + + +def test_record_case_curve_validates_shapes(tmp_path): + """Mismatched lengths or non-1-D arrays raise.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + with pytest.raises(ValueError, match="same length"): + a.record_case_curve( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + independent=np.array([0.0, 0.1]), + dependent=np.array([1.0, 2.0, 3.0]), + ) + with pytest.raises(ValueError, match="1-D"): + a.record_case_curve( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + independent=np.zeros((3, 2)), + dependent=np.zeros((3, 2)), + ) + + +def test_load_case_curve_returns_none_for_missing(tmp_path): + """Unrecorded coordinates return None, not raise.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + assert a.load_case_curve( + rid, birth_gen=99, birth_gene=99, sim_case_idx=99, + ) is None + + +def test_load_case_curve_returns_none_when_table_missing(tmp_path): + """Old archive without case_curves table reads as None.""" + import sqlite3 + db = tmp_path / "old.db" + con = sqlite3.connect(db) + con.executescript(""" + CREATE TABLE schema_meta(key TEXT PRIMARY KEY, value TEXT NOT NULL); + INSERT INTO schema_meta VALUES('schema_version', '1'); + CREATE TABLE runs(run_id TEXT PRIMARY KEY, started_at TEXT NOT NULL, + completed_at TEXT, seed INTEGER, param_names TEXT NOT NULL, + objective_labels TEXT, config_json TEXT); + INSERT INTO runs VALUES('r1', '2025', NULL, 1, '["p"]', '["o"]', NULL); + """) + con.commit(); con.close() + with ArchiveDB(db, readonly=True) as a: + assert a.load_case_curve( + "r1", birth_gen=0, birth_gene=0, sim_case_idx=0, + ) is None + + +def test_record_case_curve_replaces_on_collision(tmp_path): + """Re-recording the same coords overwrites — supports retries.""" + a1 = np.array([0.0, 0.1]) + b1 = np.array([100.0, 200.0]) + a2 = np.array([0.0, 0.05, 0.1]) + b2 = np.array([100.0, 150.0, 200.0]) + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_case_curve(rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + independent=a1, dependent=b1) + a.record_case_curve(rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + independent=a2, dependent=b2) + ind, dep, _, _ = a.load_case_curve( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) + np.testing.assert_array_equal(ind, a2) + np.testing.assert_array_equal(dep, b2) + + +def test_case_curves_cascade_on_run_delete(tmp_path): + """delete_run propagates to case_curves via the FK cascade.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + a.record_case_curve( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + independent=np.array([0.0, 0.1]), + dependent=np.array([100.0, 200.0]), + ) + a.delete_run(rid) + cur = a._conn.execute( + "SELECT COUNT(*) FROM case_curves WHERE run_id=?", (rid,), + ) + assert cur.fetchone()[0] == 0 + + +def test_discard_from_generation_drops_case_curves(tmp_path): + """discard_from_generation must delete case_curves at gen >= K.""" + with ArchiveDB(tmp_path / "a.db") as a: + rid = a.start_run(seed=1, param_names=["p"], objective_labels=["o"]) + # Need a generation row first so the FK doesn't get angry. + a.record_generation(rid, gen_idx=0, genes=[], stats={}) + a.record_generation(rid, gen_idx=1, genes=[], stats={}) + for g in (0, 1): + a.record_case_curve( + rid, birth_gen=g, birth_gene=0, sim_case_idx=0, + independent=np.array([0.0, 0.1]), + dependent=np.array([100.0, 200.0]), + ) + a.discard_from_generation(rid, gen_idx=1) + # gen 0 survives, gen 1 is gone. + assert a.load_case_curve( + rid, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) is not None + assert a.load_case_curve( + rid, birth_gen=1, birth_gene=0, sim_case_idx=0, + ) is None diff --git a/workflows/exaconstit-calibrate/tests/test_case_setup.py b/workflows/exaconstit-calibrate/tests/test_case_setup.py new file mode 100644 index 0000000..367a6aa --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_case_setup.py @@ -0,0 +1,442 @@ +""" +Unit tests for :mod:`workflow_common.case_setup`. + +Covers :class:`CaseTemplater` and the two shipped +:class:`PropertyWriter` implementations. Each test writes a small +template fixture to the workspace, exercises the writer, and +asserts the resulting file has the expected content. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +from workflow_common import ( + CallablePropertyWriter, + CaseContext, + CaseLayout, + CaseTemplater, + DelimitedPropertyWriter, + PropertyWriter, + TemplatePathResolver, + TemplatePropertyWriter, + TemplateTarget, +) + + +def _make_layout(workspace: Path) -> CaseLayout: + resolver = TemplatePathResolver( + working_dir_pattern="wf/gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={}, + root=workspace, + ) + return CaseLayout( + ctx=CaseContext(generation=0, gene=3, obj=1), + resolver=resolver, + ) + + +# --- CaseTemplater ------------------------------------------------------- + + +def test_templater_renders_single_target(workspace: Path): + """One template, one output. Substitution works.""" + src = workspace / "master.toml" + src.write_text("rate = %%rate%%\nname = %%name%%\n") + templater = CaseTemplater([TemplateTarget(source=src, dest="options.toml")]) + + layout = _make_layout(workspace) + written = templater.render(layout, {"rate": 1e-3, "name": "hello"}) + + assert len(written) == 1 + content = written[0].read_text() + assert "rate = 0.001" in content + assert "name = hello" in content + assert "%%" not in content + + +def test_templater_renders_multiple_targets(workspace: Path): + """Multiple targets all rendered in one render() call.""" + src1 = workspace / "opts.toml" + src2 = workspace / "mesh.toml" + src1.write_text("rate = %%rate%%\n") + src2.write_text("mesh_size = %%mesh%%\n") + templater = CaseTemplater([ + TemplateTarget(source=src1, dest="options.toml"), + TemplateTarget(source=src2, dest="mesh.toml"), + ]) + + layout = _make_layout(workspace) + written = templater.render(layout, {"rate": 1e-3, "mesh": "fine"}) + + assert len(written) == 2 + assert "rate = 0.001" in written[0].read_text() + assert "mesh_size = fine" in written[1].read_text() + + +def test_templater_creates_parent_directories(workspace: Path): + """Destination with nested subpath creates parents.""" + src = workspace / "master.toml" + src.write_text("value = %%v%%\n") + templater = CaseTemplater([ + TemplateTarget(source=src, dest="cfg/sub/options.toml"), + ]) + + layout = _make_layout(workspace) + written = templater.render(layout, {"v": 42}) + + assert written[0].exists() + assert written[0].parent.is_dir() + + +def test_templater_passes_through_extras(workspace: Path): + """Values not used by the template are not an error.""" + src = workspace / "master.toml" + src.write_text("only_this = %%a%%\n") + templater = CaseTemplater([TemplateTarget(source=src, dest="out.toml")]) + + layout = _make_layout(workspace) + # "unused_b" is not referenced by the template; should not raise. + written = templater.render(layout, {"a": 1, "unused_b": 99}) + assert "only_this = 1" in written[0].read_text() + + +def test_templater_strict_raises_on_missing_key(workspace: Path): + """A strict target with an unresolved placeholder raises.""" + src = workspace / "master.toml" + src.write_text("value = %%missing%%\n") + templater = CaseTemplater([ + TemplateTarget(source=src, dest="out.toml", strict=True), + ]) + + layout = _make_layout(workspace) + with pytest.raises(KeyError): + templater.render(layout, {"other": 1}) + + +def test_templater_non_strict_passes_through_unknown(workspace: Path): + """A non-strict target leaves unknown placeholders intact.""" + src = workspace / "master.toml" + src.write_text("a = %%a%%, b = %%b%%\n") + templater = CaseTemplater([ + TemplateTarget(source=src, dest="out.toml", strict=False), + ]) + + layout = _make_layout(workspace) + written = templater.render(layout, {"a": 1}) + content = written[0].read_text() + assert "a = 1" in content + assert "%%b%%" in content + + +def test_templater_accepts_empty_targets(workspace: Path): + """Empty targets is a valid no-op: some workflows do all per-case + file writing inside a PropertyWriter and need no template + rendering at all. render() must succeed silently in that case. + """ + from workflow_common.paths import CaseContext + from workflow_common.results import CaseLayout + + templater = CaseTemplater([]) + assert templater.targets == () + resolver = TemplatePathResolver( + working_dir_pattern="gen_{generation}/gene_{gene}", + output_file_patterns={}, + root=workspace, + ) + layout = CaseLayout( + ctx=CaseContext(generation=0, gene=0, obj=0), + resolver=resolver, + ) + # render() should not raise even with no targets. + templater.render(layout, {"any": "values"}) + + +def test_templater_substitute_false_copies_bytes_verbatim(workspace: Path): + """substitute=False bypasses %%key%% substitution entirely so + binary or binary-safe files can be staged alongside rendered + text files. + """ + # A file whose bytes would be corrupted by a naive text-mode + # read/write round trip. The %%looks_like_a_placeholder%% text + # is included deliberately - substitute=False must NOT try to + # resolve it. + src = workspace / "binary_ish.ori" + raw_bytes = b"\x00\x01\x02%%looks_like_a_placeholder%%\xffquat\n" + src.write_bytes(raw_bytes) + + templater = CaseTemplater([ + TemplateTarget(source=src, dest="staged.ori", substitute=False), + ]) + layout = _make_layout(workspace) + written = templater.render(layout, values={}) + assert len(written) == 1 + # Bytes are preserved exactly - no decoding, no placeholder + # substitution, no trailing-newline fiddling. + assert written[0].read_bytes() == raw_bytes + + +def test_templater_substitute_false_ignores_missing_values(workspace: Path): + """A substitute=False target must not raise + UnresolvedPlaceholderError even if the source happens to contain + text that looks like a %%placeholder%%. The whole point of the + flag is to skip the placeholder pass. + """ + src = workspace / "raw.bin" + src.write_text("this has %%unresolved%% text in it\n") + + # strict=True would normally blow up on %%unresolved%% during a + # substituting render. With substitute=False, strict is ignored. + templater = CaseTemplater([ + TemplateTarget( + source=src, dest="out.bin", + substitute=False, strict=True, + ), + ]) + layout = _make_layout(workspace) + # Empty values mapping: proves we never consulted values at all. + written = templater.render(layout, values={}) + assert written[0].read_text() == "this has %%unresolved%% text in it\n" + + +def test_templater_mixed_substituting_and_raw_targets(workspace: Path): + """A single templater can render some targets and copy others + verbatim in one call. Order is preserved in the returned list. + """ + tmpl_src = workspace / "master.toml" + tmpl_src.write_text("rate = %%rate%%\n") + raw_src = workspace / "static.bin" + raw_src.write_bytes(b"\x00\x01static\n") + + templater = CaseTemplater([ + TemplateTarget(source=tmpl_src, dest="options.toml"), + TemplateTarget(source=raw_src, dest="static.bin", substitute=False), + ]) + layout = _make_layout(workspace) + written = templater.render(layout, values={"rate": 1.5}) + assert len(written) == 2 + assert written[0].read_text() == "rate = 1.5\n" + assert written[1].read_bytes() == b"\x00\x01static\n" + + +# --- TemplatePropertyWriter --------------------------------------------- + + +def test_template_property_writer_basic(workspace: Path): + """Gene values substituted into a template file.""" + tmpl = workspace / "master_props.toml" + tmpl.write_text( + "[Voce]\n" + "yield = %%yield%%\n" + "hardening = %%hardening%%\n" + ) + writer = TemplatePropertyWriter( + template_path=tmpl, dest="properties.toml", + ) + + layout = _make_layout(workspace) + path = writer.write( + layout, + gene=[200.0, 2000.0], + param_names=["yield", "hardening"], + ) + content = path.read_text() + assert "yield = 200" in content + assert "hardening = 2000" in content + + +def test_template_property_writer_mismatch_raises(workspace: Path): + """Gene and param_names must have same length.""" + tmpl = workspace / "p.toml" + tmpl.write_text("x = %%x%%\n") + writer = TemplatePropertyWriter(template_path=tmpl) + + layout = _make_layout(workspace) + with pytest.raises(ValueError, match="entries"): + writer.write(layout, gene=[1.0, 2.0], param_names=["x"]) + + +def test_template_property_writer_extra_values(workspace: Path): + """extra_values are applied, gene values win on collision.""" + tmpl = workspace / "p.toml" + tmpl.write_text( + "gene_val = %%yield%%\n" + "extra_val = %%constant%%\n" + ) + writer = TemplatePropertyWriter( + template_path=tmpl, + extra_values={"constant": "fixed", "yield": "should_be_overridden"}, + ) + layout = _make_layout(workspace) + path = writer.write( + layout, gene=[250.0], param_names=["yield"], + ) + content = path.read_text() + assert "gene_val = 250" in content # gene wins + assert "extra_val = fixed" in content # extra still applied + + +def test_template_property_writer_protocol_conformance(workspace: Path): + """TemplatePropertyWriter satisfies the PropertyWriter Protocol.""" + assert isinstance( + TemplatePropertyWriter(template_path=Path("x")), PropertyWriter + ) + + +# --- DelimitedPropertyWriter -------------------------------------------- + + +def test_delimited_writer_default_one_per_line(workspace: Path): + """Default newline delimiter produces one value per line.""" + writer = DelimitedPropertyWriter(dest="props.txt") + layout = _make_layout(workspace) + path = writer.write( + layout, gene=[200.0, 2000.0, 5.0], + param_names=["a", "b", "c"], + ) + text = path.read_text() + lines = text.rstrip("\n").split("\n") + assert lines == ["200", "2000", "5"] + + +def test_delimited_writer_csv_with_header(workspace: Path): + writer = DelimitedPropertyWriter( + dest="p.csv", delimiter=",", include_names=True, + ) + layout = _make_layout(workspace) + path = writer.write( + layout, gene=[1.0, 2.0], param_names=["a", "b"], + ) + lines = path.read_text().rstrip("\n").split("\n") + assert lines[0] == "a,b" + assert lines[1] == "1,2" + + +def test_delimited_writer_include_names_requires_alignment(workspace: Path): + writer = DelimitedPropertyWriter( + dest="p.txt", include_names=True, + ) + layout = _make_layout(workspace) + with pytest.raises(ValueError): + writer.write(layout, gene=[1.0, 2.0], param_names=["only_one"]) + + +def test_delimited_writer_custom_fmt(workspace: Path): + """fmt controls per-value formatting.""" + writer = DelimitedPropertyWriter(dest="p.txt", fmt="%.4e") + layout = _make_layout(workspace) + path = writer.write(layout, gene=[0.001234], param_names=["x"]) + assert path.read_text().rstrip("\n") == "1.2340e-03" + + +def test_delimited_writer_protocol_conformance(): + """DelimitedPropertyWriter satisfies the PropertyWriter Protocol.""" + assert isinstance(DelimitedPropertyWriter(), PropertyWriter) + + +# --- CallablePropertyWriter -------------------------------------------- + + +def test_callable_property_writer_invokes_user_function(workspace: Path): + """The user callable receives (case_dir, gene, names, sim_case) and + its returned path is echoed back to the framework. + """ + captured = {} + + def write_props(case_dir, gene, names, sim_case): + captured["case_dir"] = case_dir + captured["gene"] = list(gene) + captured["names"] = list(names) + captured["sim_case"] = sim_case + path = case_dir / "mat.props" + case_dir.mkdir(parents=True, exist_ok=True) + path.write_text(f"# gene={list(gene)}\n") + return path + + writer = CallablePropertyWriter(func=write_props) + layout = _make_layout(workspace) + out = writer.write( + layout, gene=[1.5, 2.5], param_names=["a", "b"], + ) + assert out == layout.working_dir / "mat.props" + assert out.read_text() == "# gene=[1.5, 2.5]\n" + assert captured["case_dir"] == layout.working_dir + assert captured["gene"] == [1.5, 2.5] + assert captured["names"] == ["a", "b"] + # No Problem dispatched this, so ctx.sim_case stays None. + assert captured["sim_case"] is None + + +def test_callable_property_writer_receives_sim_case(workspace: Path): + """When ctx.sim_case is set (as Problem does in production), + the callable gets the SimCase object and can reach its + case_data / label for per-experiment logic. + """ + from workflow_common import SimCase + + captured_sim_case = {} + + def write_props(case_dir, gene, names, sim_case): + captured_sim_case["obj"] = sim_case + p = case_dir / "p.txt" + case_dir.mkdir(parents=True, exist_ok=True) + p.write_text("ok\n") + return p + + writer = CallablePropertyWriter(func=write_props) + + sc = SimCase( + case_data={"strain_rate": 1e-3, "ori_file": "grains.ori"}, + label="quasi_static", + ) + resolver = TemplatePathResolver( + working_dir_pattern="wf/gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={}, + root=workspace, + ) + ctx = CaseContext(generation=0, gene=0, obj=0, sim_case=sc) + layout = CaseLayout(ctx=ctx, resolver=resolver) + writer.write(layout, gene=[1.0], param_names=["x"]) + + assert captured_sim_case["obj"] is sc + assert captured_sim_case["obj"].case_data["ori_file"] == "grains.ori" + assert captured_sim_case["obj"].label == "quasi_static" + + +def test_callable_property_writer_rejects_non_path_return(workspace: Path): + """A callable that forgets to return a Path gets a clear TypeError.""" + def bad_writer(case_dir, gene, names, sim_case): + return "/tmp/some/str/path" # str instead of Path + + writer = CallablePropertyWriter(func=bad_writer) + layout = _make_layout(workspace) + with pytest.raises(TypeError, match="pathlib.Path"): + writer.write(layout, gene=[1.0], param_names=["x"]) + + +def test_callable_property_writer_propagates_exceptions(workspace: Path): + """User errors inside the callable surface unchanged so the + configuration-vs-sim-failure distinction stays clear. + """ + class _ConfigError(Exception): + pass + + def raise_config_error(case_dir, gene, names, sim_case): + raise _ConfigError("gene out of bounds") + + writer = CallablePropertyWriter(func=raise_config_error) + layout = _make_layout(workspace) + with pytest.raises(_ConfigError, match="out of bounds"): + writer.write(layout, gene=[1.0], param_names=["x"]) + + +def test_callable_property_writer_protocol_conformance(): + """Structurally satisfies PropertyWriter.""" + def noop(case_dir, gene, names, sim_case): + p = case_dir / "x" + case_dir.mkdir(parents=True, exist_ok=True) + p.touch() + return p + + assert isinstance(CallablePropertyWriter(func=noop), PropertyWriter) diff --git a/workflows/exaconstit-calibrate/tests/test_example_imports.py b/workflows/exaconstit-calibrate/tests/test_example_imports.py new file mode 100644 index 0000000..0fadbac --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_example_imports.py @@ -0,0 +1,69 @@ +""" +Guard against the example script rotting silently. + +This file does NOT run the example — running it would require a +real ``mechanics`` binary and experimental CSVs. It just proves the +module imports cleanly, which catches: + +* Framework imports that were renamed or moved. +* RunConfig / Bounds / Problem signatures that changed. +* The example's custom evaluator classes no longer conforming to + the framework's "evaluate(results, ctx) -> float" contract in + a way that would fail on instantiation. + +When the API evolves, this test fails at CI time rather than when +a user first tries to run the example. +""" +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import pytest + + +REPO_ROOT = Path(__file__).resolve().parents[1] +EXAMPLE_PATH = REPO_ROOT / "examples" / "nsga3_calibration.py" + + +@pytest.mark.skipif( + not EXAMPLE_PATH.is_file(), + reason="examples/nsga3_calibration.py not present in source tree", +) +def test_example_imports_cleanly(): + """Import the example module without running its main().""" + spec = importlib.util.spec_from_file_location( + "nsga3_calibration_example", EXAMPLE_PATH, + ) + assert spec is not None + assert spec.loader is not None + module = importlib.util.module_from_spec(spec) + # Register before exec so any circular-ish intra-module refs work + sys.modules["nsga3_calibration_example"] = module + try: + spec.loader.exec_module(module) + + # Sanity-check that the public surface users copy is there. + assert callable(module.main), "main() missing from example" + assert hasattr(module, "_StdNormalizedStressEvaluator") + assert hasattr(module, "_StdNormalizedSlopeEvaluator") + + # Custom evaluators must be constructable with the documented + # signature - catches drift in the argument list. + import numpy as np + import pandas as pd + from workflow_common import StressStrainExtractor + df = pd.DataFrame({"strain": [0.0, 0.05, 0.10], + "stress": [0.0, 100.0, 150.0]}) + extractor = StressStrainExtractor( + stress_column="s33", + strain_source="time_rate", strain_rate=1e-3, + ) + ev = module._StdNormalizedStressEvaluator( + experimental=df, extractor=extractor, + ) + assert hasattr(ev, "evaluate"), \ + "_StdNormalizedStressEvaluator must expose evaluate()" + finally: + sys.modules.pop("nsga3_calibration_example", None) diff --git a/workflows/exaconstit-calibrate/tests/test_fs.py b/workflows/exaconstit-calibrate/tests/test_fs.py new file mode 100644 index 0000000..cd4cccb --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_fs.py @@ -0,0 +1,112 @@ +""" +Unit tests for :mod:`workflow_common._fs`. + +These cover the low-level filesystem helpers: the ``cd`` context +manager and :func:`atomic_write_text`. Focus is on the invariants +the rest of the framework relies on - exception safety for ``cd``, +and crash-resilience for ``atomic_write_text``. +""" +from __future__ import annotations + +import os +from pathlib import Path + +import pytest + +from workflow_common._fs import ( + atomic_replace, + atomic_write_text, + cd, + ensure_dir, +) + + +def test_cd_changes_and_restores(workspace: Path): + """``cd`` should enter the directory and restore cwd on exit.""" + original = Path.cwd() + target = workspace / "subdir" + target.mkdir() + with cd(target) as resolved: + assert Path.cwd() == target + assert resolved == target + assert Path.cwd() == original + + +def test_cd_restores_even_on_exception(workspace: Path): + """Exceptions inside the ``with`` block must not leak cwd changes.""" + original = Path.cwd() + target = workspace / "subdir" + target.mkdir() + with pytest.raises(RuntimeError, match="boom"): + with cd(target): + raise RuntimeError("boom") + assert Path.cwd() == original + + +def test_cd_raises_when_target_missing(workspace: Path): + """A nonexistent target should raise FileNotFoundError.""" + missing = workspace / "no_such_dir" + with pytest.raises(FileNotFoundError): + with cd(missing): + pass + + +def test_atomic_write_text_basic(workspace: Path): + """Happy path: file appears with exactly the requested contents.""" + target = workspace / "out.txt" + atomic_write_text(target, "hello world") + assert target.read_text() == "hello world" + + +def test_atomic_write_text_creates_parents(workspace: Path): + """Parent directories are created if missing (``mkdir -p`` semantics).""" + target = workspace / "nested" / "deeply" / "out.txt" + atomic_write_text(target, "ok") + assert target.read_text() == "ok" + + +def test_atomic_write_text_overwrites_existing(workspace: Path): + """Writes are fully replacing, not appending.""" + target = workspace / "out.txt" + target.write_text("old contents with more bytes") + atomic_write_text(target, "new") + assert target.read_text() == "new" + + +def test_atomic_write_text_leaves_no_tempfile_on_success(workspace: Path): + """The tempfile-plus-rename scheme must not leak tempfiles.""" + target = workspace / "out.txt" + atomic_write_text(target, "x") + # After a successful write, only the target should remain; no + # leftover ".out.txt..tmp" should be visible. + leftovers = [p for p in workspace.iterdir() if p.name.startswith(".out.txt.")] + assert leftovers == [], f"unexpected tempfiles: {leftovers}" + + +def test_atomic_write_text_unicode(workspace: Path): + """Default UTF-8 encoding handles unicode correctly.""" + target = workspace / "out.txt" + atomic_write_text(target, "resumé café 你好") + assert target.read_text(encoding="utf-8") == "resumé café 你好" + + +def test_atomic_replace(workspace: Path): + """atomic_replace should move src over dst, replacing any existing file.""" + src = workspace / "src.txt" + dst = workspace / "dst.txt" + src.write_text("NEW") + dst.write_text("OLD") + atomic_replace(src, dst) + assert not src.exists() + assert dst.read_text() == "NEW" + + +def test_ensure_dir_creates_and_idempotent(workspace: Path): + """ensure_dir creates missing dirs and no-ops on existing ones.""" + target = workspace / "a" / "b" / "c" + out = ensure_dir(target) + assert target.is_dir() + assert out == target + # Second call must not raise + ensure_dir(target) + assert target.is_dir() diff --git a/workflows/exaconstit-calibrate/tests/test_inspect_archive.py b/workflows/exaconstit-calibrate/tests/test_inspect_archive.py new file mode 100644 index 0000000..d4ce558 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_inspect_archive.py @@ -0,0 +1,1202 @@ +"""Tests for the :mod:`workflows.optimization.inspect_archive` CLI. + +Exercises each view (--runs, --gens, --genes) and each output format +against a small fake archive built by hand. The tests check that the +CLI produces output in the right SHAPE (header + rows, CSV with the +right column count, parseable JSON) rather than exact bytes, because +column widths and formatting nuances shouldn't pin the test. +""" +from __future__ import annotations + +import csv +import io +import json +from pathlib import Path + +import numpy as np +import pytest + +from workflow_common.archive import ArchiveDB, GeneRecord +from workflows.optimization import inspect_archive + + +@pytest.fixture +def tiny_archive(tmp_path: Path) -> Path: + """A three-generation, four-individual archive with known ranks. + + Two runs so --run selection can be tested. ``run-old`` has one + gen and earlier timestamp; ``run-new`` has three gens and is + what --runs defaults to when ``--run`` is omitted. + """ + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + # Old run first so its started_at is earlier. + old_id = a.start_run( + run_id="run-old", seed=1, + param_names=["p1"], objective_labels=["obj"], + ) + a.record_generation( + old_id, gen_idx=0, + genes=[GeneRecord( + run_id=old_id, gen_idx=0, pop_idx=0, + birth_gen=0, birth_gene=0, + gene_vector=np.array([1.0]), + fitness=(0.1,), rank=0, + )], + stats={"avg": [0.1]}, + ) + a.end_run(old_id) + + new_id = a.start_run( + run_id="run-new", seed=2, + param_names=["yield_stress", "hardening"], + objective_labels=["rmse"], + ) + for gen_idx in range(3): + genes = [ + GeneRecord( + run_id=new_id, gen_idx=gen_idx, pop_idx=i, + birth_gen=gen_idx, birth_gene=i, + gene_vector=np.array([200.0 + i, 2000.0 + 10 * i]), + fitness=(1.5 - 0.1 * gen_idx + 0.01 * i,), + # First two individuals per gen are rank 0. + rank=0 if i < 2 else 1, + ) + for i in range(4) + ] + a.record_generation( + new_id, gen_idx=gen_idx, genes=genes, + stats={ + "avg": [1.5 - 0.1 * gen_idx], + "min": [1.4 - 0.1 * gen_idx], + "max": [1.6 - 0.1 * gen_idx], + }, + ) + a.end_run(new_id) + return tmp_path + + +def test_inspect_runs_lists_every_run( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """--runs dumps one row per run; both runs appear; headers present.""" + rc = inspect_archive.main([str(tiny_archive), "--runs"]) + assert rc == 0 + out = capsys.readouterr().out + assert "run_id" in out # header present + assert "run-old" in out + assert "run-new" in out + + +def test_inspect_gens_defaults_to_latest_run( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """--gens (no --run) picks the most recent run automatically.""" + rc = inspect_archive.main([str(tiny_archive), "--gens"]) + assert rc == 0 + out = capsys.readouterr().out + # Three gens in the new run, so three body lines. + body = [ + line for line in out.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + assert len(body) == 3 + + +def test_inspect_gens_respects_explicit_run( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """Passing --run forces that run even when newer ones exist.""" + rc = inspect_archive.main( + [str(tiny_archive), "--gens", "--run", "run-old"], + ) + assert rc == 0 + out = capsys.readouterr().out + body = [ + line for line in out.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + assert len(body) == 1 # only one gen in run-old + + +def test_inspect_gens_unknown_run_errors( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """Unknown run_id fails cleanly with exit code 1.""" + with pytest.raises(SystemExit) as excinfo: + inspect_archive.main( + [str(tiny_archive), "--gens", "--run", "nope"], + ) + assert excinfo.value.code == 1 + err = capsys.readouterr().err + assert "nope" in err + + +def test_inspect_genes_at_specific_gen( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """--genes --gen 2 yields NPOP rows with real parameter names.""" + rc = inspect_archive.main( + [str(tiny_archive), "--genes", "--gen", "2"], + ) + assert rc == 0 + out = capsys.readouterr().out + # Header has the real param names (not 'p0', 'p1'). + assert "yield_stress" in out + assert "hardening" in out + assert "rmse" in out + body = [ + line for line in out.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + assert len(body) == 4 + + +def test_inspect_genes_pareto_only( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """--pareto-only filters to rank-0 individuals.""" + rc = inspect_archive.main( + [str(tiny_archive), "--genes", "--gen", "2", "--pareto-only"], + ) + assert rc == 0 + out = capsys.readouterr().out + body = [ + line for line in out.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + # Fixture put two rank-0 individuals per gen. + assert len(body) == 2 + + +def test_inspect_genes_csv_is_parseable( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """--format csv produces a CSV that csv.DictReader can parse back.""" + rc = inspect_archive.main( + [str(tiny_archive), "--genes", "--gen", "2", "--format", "csv"], + ) + assert rc == 0 + out = capsys.readouterr().out + rows = list(csv.DictReader(io.StringIO(out))) + assert len(rows) == 4 + # Each row has the parameter columns filled with numeric-looking + # strings. + for r in rows: + assert r["yield_stress"] + assert r["hardening"] + assert r["rmse"] + + +def test_inspect_json_is_valid_json( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """--format json produces a valid JSON array of objects.""" + rc = inspect_archive.main( + [str(tiny_archive), "--genes", "--gen", "2", "--format", "json"], + ) + assert rc == 0 + out = capsys.readouterr().out + parsed = json.loads(out) + assert isinstance(parsed, list) + assert len(parsed) == 4 + assert all("yield_stress" in item for item in parsed) + + +def test_inspect_no_header( + tiny_archive: Path, capsys: pytest.CaptureFixture, +): + """--no-header suppresses the header row in CSV output.""" + rc = inspect_archive.main( + [str(tiny_archive), "--genes", "--gen", "2", + "--format", "csv", "--no-header"], + ) + assert rc == 0 + out = capsys.readouterr().out + # First line shouldn't contain the word "yield_stress" (which + # would imply the header printed). + first_line = out.splitlines()[0] + assert "yield_stress" not in first_line + + +def test_inspect_missing_archive_errors( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Pointing at a directory with no *.db files at all errors cleanly. + + Used to rely on the old ``--archive-name`` path; now routes + through ``_resolve_archive_path`` which raises ``SystemExit(1)`` + — argparse convention. The error message names the directory + and the default filename so the user knows what was looked for. + """ + with pytest.raises(SystemExit) as excinfo: + inspect_archive.main([str(tmp_path), "--runs"]) + assert excinfo.value.code == 1 + err = capsys.readouterr().err + assert "archive.db" in err + assert "no " in err + + +# --- Path-resolution behaviors (direct file / auto-pick / prompt) ------- + + +def test_resolve_accepts_file_path_directly(tmp_path: Path): + """Behavior 1: positional arg is a ``.db`` file, used as-is. + + The user reported `./calibration_run/calibration.db` failing + because the old CLI blindly appended `archive.db` to anything. + The fix is: if the path IS a file, return it verbatim. + """ + from workflow_common.archive import ArchiveDB + from workflows.optimization.inspect_archive import _resolve_archive_path + + # Custom filename — not 'archive.db'. + custom = tmp_path / "calibration.db" + with ArchiveDB(custom) as a: + a.start_run( + run_id="r0", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.end_run("r0") + + resolved = _resolve_archive_path(custom, "archive.db") + assert resolved == custom + + +def test_resolve_prefers_default_name_in_directory(tmp_path: Path): + """Behavior 2: directory contains ``archive.db``; that wins.""" + from workflow_common.archive import ArchiveDB + from workflows.optimization.inspect_archive import _resolve_archive_path + + default = tmp_path / "archive.db" + other = tmp_path / "other.db" + for p in (default, other): + with ArchiveDB(p) as a: + a.start_run( + run_id="r", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.end_run("r") + + # Even though there's also "other.db", the default wins silently. + resolved = _resolve_archive_path(tmp_path, "archive.db") + assert resolved == default + + +def test_resolve_auto_picks_sole_db_file_and_warns( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Behavior 3a: no archive.db, exactly one *.db → use it, warn on stderr. + + The stderr notice is the audit trail: the user sees which file + was inferred so they can't be surprised later. + """ + from workflow_common.archive import ArchiveDB + from workflows.optimization.inspect_archive import _resolve_archive_path + + sole = tmp_path / "calibration.db" + with ArchiveDB(sole) as a: + a.start_run( + run_id="r", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.end_run("r") + + resolved = _resolve_archive_path(tmp_path, "archive.db") + assert resolved == sole + err = capsys.readouterr().err + assert "calibration.db" in err + + +def test_resolve_prompts_when_multiple_db_files( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Behavior 3b: multiple *.db files, interactive → prompt for a pick. + + Uses injected ``input_fn`` and ``isatty_fn`` to drive the + prompt without touching real stdin. Verifies the user can + pick one of the candidates by 1-based index. + """ + from workflow_common.archive import ArchiveDB + from workflows.optimization.inspect_archive import _resolve_archive_path + + a_path = tmp_path / "a.db" + b_path = tmp_path / "b.db" + for p in (a_path, b_path): + with ArchiveDB(p) as arch: + arch.start_run( + run_id="r", seed=1, + param_names=["p"], objective_labels=["o"], + ) + arch.end_run("r") + + resolved = _resolve_archive_path( + tmp_path, "archive.db", + input_fn=lambda _prompt: "2", # pick the 2nd (glob is sorted) + isatty_fn=lambda: True, # pretend we have a tty + ) + # Sorted candidates: [a.db, b.db]; index 2 = b.db. + assert resolved == b_path + + +def test_resolve_prompt_rejects_bad_input_then_accepts( + tmp_path: Path, +): + """Prompt loops on garbage input rather than bailing on first error. + + User types 'banana', then '99', then '1'. Resolver loops + until it gets a valid index. + """ + from workflow_common.archive import ArchiveDB + from workflows.optimization.inspect_archive import _resolve_archive_path + + (tmp_path / "one.db").touch() + (tmp_path / "two.db").touch() + with ArchiveDB(tmp_path / "one.db") as a: + a.start_run(run_id="r", seed=1, param_names=["p"], objective_labels=["o"]) + a.end_run("r") + with ArchiveDB(tmp_path / "two.db") as a: + a.start_run(run_id="r", seed=1, param_names=["p"], objective_labels=["o"]) + a.end_run("r") + + replies = iter(["banana", "99", "1"]) + resolved = _resolve_archive_path( + tmp_path, "archive.db", + input_fn=lambda _p: next(replies), + isatty_fn=lambda: True, + ) + assert resolved == tmp_path / "one.db" + + +def test_resolve_prompt_quit_exits_nonzero(tmp_path: Path): + """User typing 'q' at the prompt should exit cleanly with rc=1.""" + from workflow_common.archive import ArchiveDB + from workflows.optimization.inspect_archive import _resolve_archive_path + + for name in ("a.db", "b.db"): + (tmp_path / name).touch() + with ArchiveDB(tmp_path / name) as a: + a.start_run( + run_id="r", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.end_run("r") + + with pytest.raises(SystemExit) as excinfo: + _resolve_archive_path( + tmp_path, "archive.db", + input_fn=lambda _p: "q", + isatty_fn=lambda: True, + ) + assert excinfo.value.code == 1 + + +def test_resolve_refuses_to_prompt_in_non_interactive_context( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Behavior 3c: multiple *.db and no tty → error, don't hang. + + This is the CI-friendly path. Without this guard, a CI job + piping data to the tool would deadlock at the prompt. + """ + from workflow_common.archive import ArchiveDB + from workflows.optimization.inspect_archive import _resolve_archive_path + + for name in ("a.db", "b.db"): + with ArchiveDB(tmp_path / name) as a: + a.start_run( + run_id="r", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.end_run("r") + + with pytest.raises(SystemExit) as excinfo: + _resolve_archive_path( + tmp_path, "archive.db", + # input_fn would hang if called; verify it isn't. + input_fn=lambda _p: (_ for _ in ()).throw( + AssertionError("input_fn must NOT be called on non-tty") + ), + isatty_fn=lambda: False, + ) + assert excinfo.value.code == 1 + err = capsys.readouterr().err + assert "multiple" in err + assert "a.db" in err and "b.db" in err + + +def test_resolve_reports_nonexistent_path( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Bad path arg (typo, missing dir) exits 1 with a clear message.""" + from workflows.optimization.inspect_archive import _resolve_archive_path + + with pytest.raises(SystemExit) as excinfo: + _resolve_archive_path( + tmp_path / "does_not_exist", + "archive.db", + ) + assert excinfo.value.code == 1 + err = capsys.readouterr().err + assert "does not exist" in err + + +def test_inspect_accepts_file_path_end_to_end( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Full-CLI round trip: pass a non-default filename directly. + + Regression for the original user report: + python -m ... inspect_archive ./calibration_run/calibration.db --runs + used to fail with "archive not found at calibration_run/calibration.db/archive.db". + """ + from workflow_common.archive import ArchiveDB + + custom = tmp_path / "calibration.db" + with ArchiveDB(custom) as a: + a.start_run( + run_id="calib-run-0", seed=42, + param_names=["yield_stress"], + objective_labels=["rmse"], + ) + a.end_run("calib-run-0") + + rc = inspect_archive.main([str(custom), "--runs"]) + assert rc == 0 + out = capsys.readouterr().out + assert "calib-run-0" in out + + +# --- Cross-generation --pareto-only ------------------------------------ + + +@pytest.fixture +def cross_gen_archive(tmp_path: Path) -> Path: + """Archive where each category's winner lives in a different gen. + + Designed so the cross-gen view can be verified by exact + identity: gen 1 pop 0 is the L2 champion, gen 3 pop 0 is the + stress_rmse champion, gen 5 pop 0 is the slope_rmse champion. + Other individuals cluster mid-fitness so they shouldn't appear + in any top-3 list. + """ + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="cross-gen-test", + seed=42, + param_names=["yield_stress", "hardening"], + objective_labels=["stress_rmse", "slope_rmse"], + ) + for gen_idx in range(6): + genes = [] + for pop_idx in range(5): + # Three "champion" genes scattered across gens: + if gen_idx == 1 and pop_idx == 0: + gv, fit = np.array([250.0, 2000.0]), (0.10, 0.15) + elif gen_idx == 3 and pop_idx == 0: + gv, fit = np.array([260.0, 2200.0]), (0.05, 0.60) + elif gen_idx == 5 and pop_idx == 0: + gv, fit = np.array([230.0, 1800.0]), (0.70, 0.08) + else: + # Mid-pack filler, different gene per slot so + # dedup doesn't collapse them. + gv = np.array([ + 200.0 + gen_idx * 5 + pop_idx, + 2000.0 + gen_idx * 20 + pop_idx * 10, + ]) + fit = (0.3 + 0.01 * pop_idx, 0.4 + 0.01 * pop_idx) + genes.append(GeneRecord( + run_id=rid, gen_idx=gen_idx, pop_idx=pop_idx, + birth_gen=gen_idx, birth_gene=pop_idx, + gene_vector=gv, fitness=fit, rank=pop_idx, + )) + a.record_generation(rid, gen_idx=gen_idx, genes=genes, stats={}) + a.end_run(rid) + return tmp_path + + +def test_pareto_only_shows_cross_generation_winners( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """--pareto-only alone (no --gen) surfaces the historical bests. + + Each of the three category winners in the fixture should + appear as row 1 of its respective category: L2 champion at + the top of the l2 block, stress_rmse winner at the top of its + block, slope_rmse winner at the top of its. + """ + rc = inspect_archive.main( + [str(cross_gen_archive), "--genes", "--pareto-only", "--format", "csv"], + ) + assert rc == 0 + out = capsys.readouterr().out + rows = list(csv.DictReader(io.StringIO(out))) + + l2_rows = [r for r in rows if r["category"] == "l2"] + stress_rows = [r for r in rows if r["category"] == "obj:stress_rmse"] + slope_rows = [r for r in rows if r["category"] == "obj:slope_rmse"] + + # Default --top is 3. + assert len(l2_rows) == 3 + assert len(stress_rows) == 3 + assert len(slope_rows) == 3 + + # Champion identities (verified by birth_gen — the fixture + # places each champion at a known generation). + assert l2_rows[0]["birth_gen"] == "1" + assert stress_rows[0]["birth_gen"] == "3" + assert slope_rows[0]["birth_gen"] == "5" + + +def test_pareto_only_respects_top_flag( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """--top 5 shows five rows per category instead of three.""" + rc = inspect_archive.main( + [str(cross_gen_archive), "--genes", "--pareto-only", + "--top", "5", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + # 5 per category × (1 L2 + 2 objectives) = 15 rows. + assert len(rows) == 15 + + +def test_pareto_only_with_gen_keeps_old_single_gen_behavior( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """--pareto-only --gen N preserves the pre-cross-gen behavior. + + Users who were scripting against the old "rank-0 of one + generation" semantics need that mode to still exist. Binding + it to "pareto-only AND explicit gen" is the compatibility + story. + """ + rc = inspect_archive.main( + [str(cross_gen_archive), "--genes", "--gen", "3", + "--pareto-only", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + # Fixture gave pop_idx 0 rank 0; only that row comes back. + assert len(rows) == 1 + assert rows[0]["gen_idx"] == "3" + assert rows[0]["rank"] == "0" + # And the columns are the OLD layout, not the cross-gen one. + assert "gen_idx" in rows[0] + assert "category" not in rows[0] + + +def test_pareto_only_dedups_identical_genes_across_gens( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Elitism copies good genes across gens; dedup keeps each unique + gene once, attributed to its FIRST-seen (birth) generation. + Without dedup the top-3 table would be ``[champion, champion, + champion]`` — all the same individual surviving three + generations — which tells the user nothing. + """ + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="elitist", + seed=1, + param_names=["p"], + objective_labels=["o"], + ) + # Same champion at gens 0, 1, 2 — elitism. Filler genes + # differ per gen so they dedup to three unique records. + for gen_idx in range(3): + champion = GeneRecord( + run_id=rid, gen_idx=gen_idx, pop_idx=0, + birth_gen=0, birth_gene=0, # birth stays at gen 0 + gene_vector=np.array([42.0]), + fitness=(0.01,), rank=0, + ) + filler = GeneRecord( + run_id=rid, gen_idx=gen_idx, pop_idx=1, + birth_gen=gen_idx, birth_gene=1, + gene_vector=np.array([100.0 + gen_idx]), + fitness=(1.0 + 0.1 * gen_idx,), rank=1, + ) + a.record_generation(rid, gen_idx=gen_idx, + genes=[champion, filler], stats={}) + a.end_run(rid) + + rc = inspect_archive.main( + [str(tmp_path), "--genes", "--pareto-only", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + l2_rows = [r for r in rows if r["category"] == "l2"] + # Champion should appear exactly ONCE despite showing up in + # every generation. + champion_appearances = [r for r in l2_rows if r["p"] == "42"] + assert len(champion_appearances) == 1 + + +def test_pareto_only_skips_nonfinite_fitness( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Failed genes (fitness=inf or nan) must NOT appear in the + rankings — they'd either dominate the "worst" slot meaninglessly + or crash sort comparisons depending on which way the + implementation bent. + """ + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="with-failures", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.record_generation( + rid, gen_idx=0, + genes=[ + GeneRecord(run_id=rid, gen_idx=0, pop_idx=0, + birth_gen=0, birth_gene=0, + gene_vector=np.array([1.0]), + fitness=(0.5,), rank=0), + GeneRecord(run_id=rid, gen_idx=0, pop_idx=1, + birth_gen=0, birth_gene=1, + gene_vector=np.array([2.0]), + # Simulate a failure-handler penalty. + fitness=(float("inf"),), rank=1), + GeneRecord(run_id=rid, gen_idx=0, pop_idx=2, + birth_gen=0, birth_gene=2, + gene_vector=np.array([3.0]), + fitness=(0.3,), rank=0), + ], + stats={}, + ) + a.end_run(rid) + + rc = inspect_archive.main( + [str(tmp_path), "--genes", "--pareto-only", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + # Only 2 finite individuals — top-3 request returns what's available. + l2_rows = [r for r in rows if r["category"] == "l2"] + assert len(l2_rows) == 2 + # The inf-fitness gene (p=2) must not appear. + assert not any(r["p"] == "2" for r in l2_rows) + + +# --- --limit cap on raw --genes output --------------------------------- + + +def test_limit_caps_raw_genes_output( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Default --limit 200 truncates a 500-row generation with a notice.""" + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="big", seed=1, + param_names=["p"], objective_labels=["o"], + ) + genes = [ + GeneRecord(run_id=rid, gen_idx=0, pop_idx=i, + birth_gen=0, birth_gene=i, + gene_vector=np.array([float(i)]), + fitness=(float(i),), rank=0) + for i in range(500) + ] + a.record_generation(rid, gen_idx=0, genes=genes, stats={}) + a.end_run(rid) + + rc = inspect_archive.main( + [str(tmp_path), "--genes", "--format", "csv"], + ) + assert rc == 0 + captured = capsys.readouterr() + rows = list(csv.DictReader(io.StringIO(captured.out))) + # Default cap is 200. + assert len(rows) == 200 + # User notified via stderr about the truncation. + assert "truncated" in captured.err + assert "500" in captured.err + + +def test_limit_zero_disables_cap( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """--limit 0 returns every row with no truncation notice.""" + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="big", seed=1, + param_names=["p"], objective_labels=["o"], + ) + genes = [ + GeneRecord(run_id=rid, gen_idx=0, pop_idx=i, + birth_gen=0, birth_gene=i, + gene_vector=np.array([float(i)]), + fitness=(float(i),), rank=0) + for i in range(350) + ] + a.record_generation(rid, gen_idx=0, genes=genes, stats={}) + a.end_run(rid) + + rc = inspect_archive.main( + [str(tmp_path), "--genes", "--limit", "0", "--format", "csv"], + ) + assert rc == 0 + captured = capsys.readouterr() + rows = list(csv.DictReader(io.StringIO(captured.out))) + assert len(rows) == 350 + assert "truncated" not in captured.err + + +def test_limit_ignored_for_pareto_only( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """--pareto-only output is already bounded; --limit doesn't touch it. + + A 600-gene cross-gen view at top 3 returns at most 3*(1+M) rows + per run. Setting --limit 1 must not truncate this further. + """ + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="p", seed=1, + param_names=["p"], objective_labels=["oa", "ob"], + ) + for gen_idx in range(3): + genes = [ + GeneRecord(run_id=rid, gen_idx=gen_idx, pop_idx=i, + birth_gen=gen_idx, birth_gene=i, + gene_vector=np.array([float(10 * gen_idx + i)]), + fitness=(float(i), float(i) + 1.0), rank=0) + for i in range(5) + ] + a.record_generation(rid, gen_idx=gen_idx, genes=genes, stats={}) + a.end_run(rid) + + rc = inspect_archive.main( + [str(tmp_path), "--genes", "--pareto-only", + "--limit", "1", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + # Top 3 × (l2 + obj:oa + obj:ob) = 9 rows; --limit 1 must NOT + # shrink this to 1. + assert len(rows) == 9 + + +# --- --gens-best convergence view -------------------------------------- + + +def test_gens_best_tracks_running_minimums( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Per-generation best-ever fitness monotonically improves (or holds).""" + from workflow_common.archive import ArchiveDB, GeneRecord + + # Fixture designed with a visible plateau: + # gen 0: best (0.5, 0.5) + # gen 2: new champion (0.2, 0.3) + # gens 3-6: no one beats the gen-2 champion + # gen 7: new champion (0.15, 0.25) + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="conv", seed=1, + param_names=["p"], objective_labels=["a", "b"], + ) + for gen_idx in range(10): + if gen_idx == 2: + fit = (0.2, 0.3) + elif gen_idx == 7: + fit = (0.15, 0.25) + else: + fit = (0.5, 0.5) + a.record_generation( + rid, gen_idx=gen_idx, + genes=[GeneRecord( + run_id=rid, gen_idx=gen_idx, pop_idx=0, + birth_gen=gen_idx, birth_gene=0, + gene_vector=np.array([float(gen_idx)]), + fitness=fit, rank=0, + )], + stats={}, + ) + a.end_run(rid) + + rc = inspect_archive.main( + [str(tmp_path), "--gens-best", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + assert len(rows) == 10 + + # Running minimums: non-increasing from one generation to the next. + a_best = [float(r["a_best"]) for r in rows] + b_best = [float(r["b_best"]) for r in rows] + for prev, cur in zip(a_best, a_best[1:]): + assert cur <= prev + 1e-12 + for prev, cur in zip(b_best, b_best[1:]): + assert cur <= prev + 1e-12 + + # Champion-birth plateau: gens 2-6 all pinned to birth_gen 2. + plateau = [int(r["champion_birth_gen"]) for r in rows[2:7]] + assert plateau == [2, 2, 2, 2, 2] + # Gen 7 shows the new champion taking over. + assert int(rows[7]["champion_birth_gen"]) == 7 + + +def test_gens_best_skips_nonfinite_fitness( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Failed/penalty genes must not peg the running minimums to inf.""" + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="with-nan", seed=1, + param_names=["p"], objective_labels=["a"], + ) + a.record_generation( + rid, gen_idx=0, + genes=[ + GeneRecord(run_id=rid, gen_idx=0, pop_idx=0, + birth_gen=0, birth_gene=0, + gene_vector=np.array([1.0]), + fitness=(float("inf"),), rank=1), + GeneRecord(run_id=rid, gen_idx=0, pop_idx=1, + birth_gen=0, birth_gene=1, + gene_vector=np.array([2.0]), + fitness=(0.4,), rank=0), + ], + stats={}, + ) + a.end_run(rid) + + rc = inspect_archive.main( + [str(tmp_path), "--gens-best", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + assert len(rows) == 1 + # a_best must reflect the 0.4 gene, not inf. + assert float(rows[0]["a_best"]) == 0.4 + # champion_birth_gen should also name the non-inf gene. + assert int(rows[0]["champion_birth_gen"]) == 0 + + +def test_gens_best_empty_run_returns_no_rows( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """A run with zero generations errors out via _emit's empty-row path. + + Same contract as the other views: an empty result yields a + non-zero exit code so CI scripts don't silently continue when + their expected data isn't there. + """ + from workflow_common.archive import ArchiveDB + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + a.start_run( + run_id="empty", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.end_run("empty") + + with pytest.raises(SystemExit) as excinfo: + inspect_archive.main([str(tmp_path), "--gens-best"]) + # _emit raises SystemExit(2) on "no rows matched the query". + assert excinfo.value.code == 2 + + +# --- --pareto-only implies --genes ------------------------------------ + + +def test_pareto_only_alone_implies_genes_view( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """Typing just --pareto-only (without --genes) must produce the + cross-generation top-N view. + + Before the fix, the view-selector fallback silently promoted + to --gens, and --pareto-only was dropped. The output was + per-generation summary stats — completely different from what + the user asked for. + """ + rc = inspect_archive.main( + [str(cross_gen_archive), "--pareto-only", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + # Cross-gen view has a "category" column; --gens would have + # "gen_idx"/"recorded_at"/... and no "category". + assert rows, "expected cross-gen rows, got none" + assert "category" in rows[0] + # Must contain the L2 + per-objective blocks. + categories = {r["category"] for r in rows} + assert "l2" in categories + assert any(c.startswith("obj:") for c in categories) + + +def test_pareto_only_combined_with_gens_errors( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """--pareto-only together with a non-gene view is nonsense; + the tool exits 1 with a clear message rather than silently + dropping the flag. + """ + rc = inspect_archive.main( + [str(cross_gen_archive), "--gens", "--pareto-only"], + ) + assert rc == 1 + err = capsys.readouterr().err + assert "--pareto-only" in err + assert "--genes" in err + + +def test_pareto_only_combined_with_runs_errors( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """Same rule for --runs + --pareto-only.""" + rc = inspect_archive.main( + [str(cross_gen_archive), "--runs", "--pareto-only"], + ) + assert rc == 1 + + +def test_pareto_only_combined_with_gens_best_errors( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """And --gens-best.""" + rc = inspect_archive.main( + [str(cross_gen_archive), "--gens-best", "--pareto-only"], + ) + assert rc == 1 + + +# --- l2_norm column visible in cross-gen view ------------------------- + + +def test_cross_gen_view_includes_l2_norm_column( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """Every row in the --pareto-only output carries an l2_norm value. + + The fixture's L2 champion (gen 1 pop 0, fitness (0.10, 0.15)) + has norm sqrt(0.01 + 0.0225) == 0.18027..., which must appear + as the first L2-category row's l2_norm value. + """ + rc = inspect_archive.main( + [str(cross_gen_archive), "--genes", "--pareto-only", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + + # Every row has the column populated with a float. + for r in rows: + assert "l2_norm" in r + val = float(r["l2_norm"]) + assert val >= 0.0 # norms are non-negative + + # The fixture's L2 champion should have the smallest norm in the + # whole table. And the top-L2 row's norm matches the computed + # value for (0.10, 0.15) -> 0.18027... + l2_block = [r for r in rows if r["category"] == "l2"] + assert l2_block, "no L2-category rows" + top_l2 = float(l2_block[0]["l2_norm"]) + # CSV formatting uses "%g" (6 significant figures) so the + # round-trip loses sub-microscopic precision. 1e-5 tolerance + # easily distinguishes the champion's norm from any other + # in the fixture (next-smallest L2 is 0.5). + assert abs(top_l2 - float(np.hypot(0.10, 0.15))) < 1e-5 + + +def test_l2_column_norms_sort_ascending_within_l2_block( + cross_gen_archive: Path, capsys: pytest.CaptureFixture, +): + """The L2 category is ranked by l2_norm; the column must reflect that. + + Scanning down the L2 block, l2_norm values should be + non-decreasing. The column is the same quantity the ranking + uses, so anything else would be a sign the column and the + sort key got out of sync. + """ + rc = inspect_archive.main( + [str(cross_gen_archive), "--genes", "--pareto-only", "--format", "csv"], + ) + assert rc == 0 + rows = list(csv.DictReader(io.StringIO(capsys.readouterr().out))) + l2_norms = [ + float(r["l2_norm"]) for r in rows if r["category"] == "l2" + ] + assert l2_norms, "no L2-category rows" + for a, b in zip(l2_norms, l2_norms[1:]): + assert a <= b + 1e-12 + + +# --- --clean-empty-runs CLI flag --------------------------------------- + + +def _seed_archive_with_mixed_runs(tmp_path: Path) -> tuple[str, list[str]]: + """Build an archive with one populated + two empty-completed runs. + + Returns (full_run_id, [empty_ids]) for the tests to assert against. + """ + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + empty_ids = [] + with ArchiveDB(db) as a: + full_rid = a.start_run( + run_id="full-run", seed=1, + param_names=["p"], objective_labels=["o"], + ) + a.record_generation( + full_rid, gen_idx=0, + genes=[GeneRecord( + run_id=full_rid, gen_idx=0, pop_idx=0, + birth_gen=0, birth_gene=0, + gene_vector=np.array([1.0]), + fitness=(0.1,), rank=0, + )], + stats={}, + ) + a.end_run(full_rid) + + for i in range(2): + rid = a.start_run( + run_id=f"empty-{i}", seed=10 + i, + param_names=["p"], objective_labels=["o"], + ) + a.end_run(rid) + empty_ids.append(rid) + return full_rid, empty_ids + + +def test_clean_empty_runs_deletes_empty_and_keeps_full( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Normal mode: removes every completed-empty run, leaves populated ones.""" + full_rid, empty_ids = _seed_archive_with_mixed_runs(tmp_path) + + rc = inspect_archive.main([str(tmp_path), "--clean-empty-runs"]) + assert rc == 0 + err = capsys.readouterr().err + # stderr reports the deletion count and names the victims. + assert "deleted 2 empty run(s)" in err + for rid in empty_ids: + assert rid in err + + # Verify: only full-run remains in the archive. + from workflow_common.archive import ArchiveDB + with ArchiveDB(tmp_path / "archive.db", readonly=True) as a: + survivors = [r.run_id for r in a.list_runs()] + assert survivors == [full_rid] + + +def test_clean_empty_runs_dry_run_lists_without_deleting( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Dry run shows candidates on stderr but doesn't touch the DB.""" + full_rid, empty_ids = _seed_archive_with_mixed_runs(tmp_path) + + rc = inspect_archive.main( + [str(tmp_path), "--clean-empty-runs", "--dry-run"], + ) + assert rc == 0 + err = capsys.readouterr().err + assert "would delete 2 empty run(s)" in err + assert "dry run: no changes were made" in err + + # DB state unchanged. + from workflow_common.archive import ArchiveDB + with ArchiveDB(tmp_path / "archive.db", readonly=True) as a: + survivors = sorted(r.run_id for r in a.list_runs()) + assert survivors == sorted([full_rid] + empty_ids) + + +def test_clean_empty_runs_nothing_to_do( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """Empty-run-free archive yields a helpful 'nothing to do' message.""" + from workflow_common.archive import ArchiveDB, GeneRecord + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + rid = a.start_run( + run_id="only-real", seed=0, + param_names=["p"], objective_labels=["o"], + ) + a.record_generation( + rid, gen_idx=0, + genes=[GeneRecord( + run_id=rid, gen_idx=0, pop_idx=0, + birth_gen=0, birth_gene=0, + gene_vector=np.array([1.0]), + fitness=(0.1,), rank=0, + )], + stats={}, + ) + a.end_run(rid) + + rc = inspect_archive.main([str(tmp_path), "--clean-empty-runs"]) + assert rc == 0 + err = capsys.readouterr().err + assert "no empty runs eligible" in err + # Helper line pointing at --age-minutes 0 — discoverable UX. + assert "--age-minutes 0" in err + + +def test_clean_empty_runs_respects_age_minutes( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """A young, never-ended empty run is protected unless --age-minutes 0.""" + from workflow_common.archive import ArchiveDB + + db = tmp_path / "archive.db" + with ArchiveDB(db) as a: + # Young empty run — not completed, just started. + a.start_run(run_id="young-empty", seed=1, + param_names=["p"], objective_labels=["o"]) + + # Default age gate preserves it. + rc = inspect_archive.main([str(tmp_path), "--clean-empty-runs"]) + assert rc == 0 + err = capsys.readouterr().err + assert "no empty runs eligible" in err + + # --age-minutes 0 catches it. + rc = inspect_archive.main( + [str(tmp_path), "--clean-empty-runs", "--age-minutes", "0"], + ) + assert rc == 0 + err = capsys.readouterr().err + assert "deleted 1 empty run(s)" in err + assert "young-empty" in err + + +def test_clean_empty_runs_rejects_combination_with_pareto_only( + tmp_path: Path, capsys: pytest.CaptureFixture, +): + """--pareto-only + --clean-empty-runs is nonsense; error out.""" + _seed_archive_with_mixed_runs(tmp_path) + rc = inspect_archive.main( + [str(tmp_path), "--clean-empty-runs", "--pareto-only"], + ) + assert rc == 1 + err = capsys.readouterr().err + assert "--pareto-only" in err diff --git a/workflows/exaconstit-calibrate/tests/test_integration.py b/workflows/exaconstit-calibrate/tests/test_integration.py new file mode 100644 index 0000000..4ca168f --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_integration.py @@ -0,0 +1,513 @@ +""" +End-to-end integration tests for the workflow_common framework. + +Why these exist alongside the unit tests +---------------------------------------- +Unit tests cover each module in isolation. Integration tests here +cover the *interactions* between modules - the handoffs that are +easiest to get wrong and hardest to notice until a real run: + +* Does the PathResolver actually agree with the templates about where + options.toml should live? +* Does the manifest's SUBMITTED -> COMPLETED transition happen in the + right order relative to the sentinel write? +* Does the ResultReader read back data that the backend just finished + writing? +* Does a failed case get correctly recorded as FAILED in the manifest + AND have a sentinel that reflects the failure? + +Each test in this file runs the full chain from "I have a parameter +dict" to "I have a DataFrame of stress-strain data". They are slower +than unit tests (one subprocess per case) but still fast enough - +tens of milliseconds each - to run every commit. +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import List, Tuple + +import pandas as pd +import pytest + +from workflow_common import ( + CaseContext, + CaseLayout, + CaseState, + LocalBackend, + Manifest, + ManifestEntry, + Sentinel, + SimJobSpec, + TemplatePathResolver, + TextTableReader, + TextTableSpec, + configure_logging, + render_template_file, + write_sentinel, +) +from workflow_common.backends.base import JobOutcome +from workflow_common.sentinel import ( + is_case_complete, + read_sentinel, + validate_outputs, +) + + +# --- Test helpers the integration tests share ---------------------------- + + +def _build_resolver(workspace: Path) -> TemplatePathResolver: + """The resolver used by the integration tests. + + Matches the ExaConstit default output layout convention: + ``/wf/gen_N/gene_N_obj_N/results/options/...``. + """ + return TemplatePathResolver( + working_dir_pattern="wf/gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={ + "avg_stress": "{working_dir}/results/options/avg_stress.txt", + "avg_def_grad": "{working_dir}/results/options/avg_def_grad.txt", + }, + root=workspace, + ) + + +def _build_reader() -> TextTableReader: + """Reader matching the fake binary's output schema.""" + return TextTableReader({ + "avg_stress": TextTableSpec( + columns=["Time", "Volume", "Sxx", "Syy", "Szz", "Sxy", "Sxz", "Syz"], + required=True, + ), + "avg_def_grad": TextTableSpec( + columns=[ + "Time", "Volume", "F11", "F12", "F13", "F21", "F22", "F23", "F31", "F32", "F33", + ], + required=False, + ), + }) + + +def _prepare_case( + resolver: TemplatePathResolver, + master_template: Path, + ctx: CaseContext, + params: dict, +) -> Tuple[CaseLayout, SimJobSpec, Path]: + """Render per-case input and build a SimJobSpec. + + Returns a tuple of (layout, spec, working_dir) so tests have + direct handles to the working dir without re-resolving. + """ + wd = resolver.working_dir(ctx) + wd.mkdir(parents=True, exist_ok=True) + # Values fed into the template. The template uses %%key%% placeholders + # so the keys must match the placeholder names. + values = { + "gene": ctx.gene, + "obj": ctx.obj, + "strain_rate": params.get("strain_rate", 1e-3), + "yield_stress": params.get("yield_stress", 200.0), + "hardening": params.get("hardening", 2000.0), + "temp_k": params.get("temp_k", 298.0), + } + render_template_file(master_template, wd / "options.toml", values) + layout = CaseLayout(ctx=ctx, resolver=resolver) + return layout, None, wd # spec built per-test below + + +def _run_full_case( + *, + ctx: CaseContext, + params: dict, + workspace: Path, + fake_binary: Path, + master_template: Path, + resolver: TemplatePathResolver, + manifest: Manifest, + backend: LocalBackend, + reader: TextTableReader, + required_outputs: List[str], +) -> Tuple[JobOutcome, pd.DataFrame | None]: + """Run one case end-to-end and return (outcome, stress DataFrame). + + This function is the minimum responsible driver - it does every + step a production driver would: + + 1. Render input files. + 2. Record SUBMITTED in the manifest. + 3. Run the simulation. + 4. Validate outputs. + 5. Write sentinel (atomic). + 6. Record terminal manifest entry. + 7. Read results. + """ + layout, _, wd = _prepare_case(resolver, master_template, ctx, params) + spec = SimJobSpec( + working_dir=wd, + binary=fake_binary, + args=(), + num_tasks=1, + duration_s=30, + stdout="run.out", + stderr="run.err", + tag=f"g{ctx.gene}o{ctx.obj}", + ) + + # Manifest: SUBMITTED before the backend gets the spec, so a crash + # during submission is recoverable. + manifest.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(wd), + )) + + # Backend: run the subprocess. + result = backend.submit_one(spec) + + # Output validation: catches rc=0 with missing files (not an issue + # with the fake binary but would be with a killed real one). + ok, bad = validate_outputs(wd, required_outputs) + terminal = ( + CaseState.COMPLETED + if (result.outcome == JobOutcome.OK and ok) + else CaseState.FAILED + ) + + # Sentinel FIRST, manifest SECOND. See the architecture doc for why. + write_sentinel(wd, Sentinel( + rc=result.rc, + wall_time_s=result.wall_time_s, + jobid=result.jobid, + output_files={n: str(layout.output_file(n)) for n in reader.specs}, + status="ok" if terminal == CaseState.COMPLETED else "bad", + message=result.error_message if result.error_message else None, + )) + manifest.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=terminal, rc=result.rc, jobid=result.jobid, + case_dir=str(wd), + )) + + # Results: read what the simulation produced. Only attempt if the + # case terminated successfully; failed cases may have missing or + # truncated files which the reader will reject. + df = None + if terminal == CaseState.COMPLETED: + rs = reader.read(layout) + df = rs.df("avg_stress") + return terminal, df + + +# --- Integration tests --------------------------------------------------- + + +def test_happy_path_full_pipeline( + workspace: Path, fake_binary: Path, master_template: Path +): + """Render -> run -> validate -> sentinel -> manifest -> read, all clean.""" + configure_logging(level="warning", stream=False) + resolver = _build_resolver(workspace) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + backend = LocalBackend(max_workers=1) + reader = _build_reader() + + terminal, df = _run_full_case( + ctx=CaseContext(generation=0, gene=0, obj=0), + params={"strain_rate": 1e-3, "yield_stress": 200.0, "hardening": 2000.0}, + workspace=workspace, fake_binary=fake_binary, + master_template=master_template, resolver=resolver, + manifest=manifest, backend=backend, reader=reader, + required_outputs=["results/options/avg_stress.txt"], + ) + + assert terminal == CaseState.COMPLETED + assert df is not None + # Fake binary writes 50 rows of saturating Voce-like response. + # At t=1 with strain_rate=1e-3, eps_final = 1e-3 and the argument of + # the exponential is 50*1e-3 = 0.05, so 1-exp(-0.05) ~= 0.0488. + # s11_final ~= yield + hardening * 0.0488 = 200 + 2000*0.0488 ~= 297.6. + assert len(df) == 50 + assert df["Szz"].iloc[0] == pytest.approx(200.0, abs=1.0) + # Monotonically increasing (hardening response). + assert (df["Szz"].diff().dropna() > 0).all() + # Final value in the expected range. + assert 290.0 < df["Szz"].iloc[-1] < 310.0 + + +def test_failed_case_recorded_as_failed( + workspace: Path, fake_binary: Path, master_template: Path +): + """A simulation that exits nonzero -> FAILED in manifest + sentinel.""" + configure_logging(level="warning", stream=False) + resolver = _build_resolver(workspace) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + backend = LocalBackend(max_workers=1) + reader = _build_reader() + + os.environ["FAKE_FAIL"] = "1" + try: + terminal, df = _run_full_case( + ctx=CaseContext(generation=0, gene=0, obj=0), + params={}, + workspace=workspace, fake_binary=fake_binary, + master_template=master_template, resolver=resolver, + manifest=manifest, backend=backend, reader=reader, + required_outputs=["results/options/avg_stress.txt"], + ) + finally: + os.environ.pop("FAKE_FAIL", None) + + assert terminal == CaseState.FAILED + assert df is None # reader never called for failed cases + + # The manifest should report FAILED. + entry = manifest.get(0, 0, 0) + assert entry is not None + assert entry.state == CaseState.FAILED + assert entry.rc == 7 + + # The sentinel should exist but indicate failure. + wd = resolver.working_dir(CaseContext(0, 0, 0)) + sen = read_sentinel(wd) + assert sen is not None + assert sen.rc == 7 + assert sen.status == "bad" + + +def test_truncated_output_caught_by_validator( + workspace: Path, fake_binary: Path, master_template: Path +): + """rc=0 with a truncated avg_stress still marked FAILED. + + The fake binary honors FAKE_TRUNCATE=1 to drop the last 10 rows. + The validator only checks size-not-zero, so a truncated-but-nonempty + file slips through validate_outputs. This test then demonstrates + the NEXT line of defense: the TextTableReader's column-count check + catches format drift, and a stricter validator (the caller's + responsibility) could count rows. + """ + configure_logging(level="warning", stream=False) + resolver = _build_resolver(workspace) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + backend = LocalBackend(max_workers=1) + reader = _build_reader() + + os.environ["FAKE_TRUNCATE"] = "1" + try: + terminal, df = _run_full_case( + ctx=CaseContext(generation=0, gene=0, obj=0), + params={}, + workspace=workspace, fake_binary=fake_binary, + master_template=master_template, resolver=resolver, + manifest=manifest, backend=backend, reader=reader, + required_outputs=["results/options/avg_stress.txt"], + ) + finally: + os.environ.pop("FAKE_TRUNCATE", None) + + # Size > 0 so validator passes. Reader then gets the short data. + assert terminal == CaseState.COMPLETED + assert df is not None + # Truncated: should have 40 rows instead of 50. + assert len(df) == 40 + + +def test_missing_optional_output_reported( + workspace: Path, fake_binary: Path, master_template: Path +): + """An absent optional output is collected but does not fail the run.""" + configure_logging(level="warning", stream=False) + resolver = _build_resolver(workspace) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + backend = LocalBackend(max_workers=1) + reader = _build_reader() + + os.environ["FAKE_MISSING"] = "avg_def_grad" + try: + layout_ctx = CaseContext(generation=0, gene=0, obj=0) + terminal, df = _run_full_case( + ctx=layout_ctx, params={}, + workspace=workspace, fake_binary=fake_binary, + master_template=master_template, resolver=resolver, + manifest=manifest, backend=backend, reader=reader, + required_outputs=["results/options/avg_stress.txt"], + ) + finally: + os.environ.pop("FAKE_MISSING", None) + + # The required output is present so the case is COMPLETED. + assert terminal == CaseState.COMPLETED + # A re-read shows avg_def_grad in the missing list. + layout = CaseLayout(ctx=layout_ctx, resolver=resolver) + rs = reader.read(layout) + assert "avg_stress" in rs + assert "avg_def_grad" in rs.missing + assert not bool(rs) + + +def test_parallel_run_produces_independent_outputs( + workspace: Path, fake_binary: Path, master_template: Path +): + """Two parallel cases write to distinct dirs without cross-contamination.""" + configure_logging(level="warning", stream=False) + resolver = _build_resolver(workspace) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + backend = LocalBackend(max_workers=3) + reader = _build_reader() + + # Build 6 cases with different strain rates. + contexts = [ + CaseContext(generation=0, gene=g, obj=o) + for g in range(3) for o in range(2) + ] + specs: List[SimJobSpec] = [] + for ctx in contexts: + layout, _, wd = _prepare_case( + resolver, master_template, ctx, + {"strain_rate": 10 ** (-3 - ctx.obj)}, + ) + specs.append(SimJobSpec( + working_dir=wd, binary=fake_binary, args=(), + duration_s=30, stdout="run.out", stderr="run.err", + )) + manifest.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(wd), + )) + + # Submit everything. + results = backend.submit_batch(specs) + assert len(results) == 6 + for r in results: + assert r.outcome == JobOutcome.OK + + # Sentinel and manifest for each. + for ctx, spec in zip(contexts, specs): + write_sentinel(spec.working_dir, Sentinel(rc=0, wall_time_s=1.0)) + manifest.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.COMPLETED, rc=0, case_dir=str(spec.working_dir), + )) + + # Each case should have its own output with the expected strain rate. + for ctx in contexts: + layout = CaseLayout(ctx=ctx, resolver=resolver) + rs = reader.read(layout) + assert "avg_stress" in rs + # Fake binary uses strain_rate=1e-(3+obj); at t=1, the + # exponent argument is 50*strain_rate which determines saturation. + # Just sanity-check shape and nonzero values. + df = rs.df("avg_stress") + assert len(df) == 50 + assert df["Szz"].iloc[-1] > df["Szz"].iloc[0] + + +def test_integration_manifest_snapshot_and_reload( + workspace: Path, fake_binary: Path, master_template: Path +): + """Snapshot + reload round trip preserves all state from a real run.""" + configure_logging(level="warning", stream=False) + resolver = _build_resolver(workspace) + manifest_path = workspace / "manifest.jsonl" + manifest = Manifest(manifest_path) + manifest.load() + backend = LocalBackend(max_workers=2) + reader = _build_reader() + + contexts = [CaseContext(generation=0, gene=g, obj=0) for g in range(4)] + for ctx in contexts: + _run_full_case( + ctx=ctx, params={}, + workspace=workspace, fake_binary=fake_binary, + master_template=master_template, resolver=resolver, + manifest=manifest, backend=backend, reader=reader, + required_outputs=["results/options/avg_stress.txt"], + ) + + manifest.snapshot() + + # Load from scratch in a new Manifest. + m2 = Manifest(manifest_path) + m2.load() + # All four cases should be COMPLETED in the reloaded manifest. + for ctx in contexts: + e = m2.get(ctx.generation, ctx.gene, ctx.obj) + assert e is not None + assert e.state == CaseState.COMPLETED + + +# --- Relative-workspace coverage (audit Finding 7) ----------------------- + + +@pytest.mark.parametrize("workspace_style", ["absolute", "relative"]) +def test_happy_path_works_with_absolute_and_relative_workspace( + tmp_path: Path, + fake_binary: Path, + master_template: Path, + monkeypatch: pytest.MonkeyPatch, + workspace_style: str, +): + """Regression coverage for the "relative WORKSPACE" bug class. + + Users naturally write ``WORKSPACE = Path("./calibration_run")`` in + their drivers, but every existing integration test uses the + absolute ``tmp_path`` fixture. That coverage gap is exactly how + the double-join bug from earlier in the audit reached production: + the join chain ``working_dir/required_output`` produced a + relative path, and ``validate_outputs`` joined against + ``working_dir`` a second time, giving ``//...``. Tests + on absolute-tmpdir workspaces never saw this because the first + join produced an absolute path and the second was a no-op. + + This parameterized test runs the happy-path pipeline under both + workspace styles. The absolute case is redundant with + ``test_happy_path_full_pipeline`` above; we keep it anyway so + the two branches sit right next to each other and the test + serves as a clear reference for the bug class. + """ + configure_logging(level="warning", stream=False) + + # Build the workspace according to the requested style. + if workspace_style == "absolute": + workspace = tmp_path / "wf_abs" + workspace.mkdir() + else: + # Run from tmp_path so the relative path is scoped to this + # test and doesn't leak into the repo. + monkeypatch.chdir(tmp_path) + workspace = Path("wf_rel") + workspace.mkdir() + assert not workspace.is_absolute(), ( + "sanity: workspace must be relative for the relative branch" + ) + + resolver = _build_resolver(workspace) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + backend = LocalBackend(max_workers=1) + reader = _build_reader() + + terminal, df = _run_full_case( + ctx=CaseContext(generation=0, gene=0, obj=0), + params={"strain_rate": 1e-3, "yield_stress": 200.0, "hardening": 2000.0}, + workspace=workspace, fake_binary=fake_binary, + master_template=master_template, resolver=resolver, + manifest=manifest, backend=backend, reader=reader, + required_outputs=["results/options/avg_stress.txt"], + ) + + # Full pipeline must succeed regardless of workspace shape. + assert terminal == CaseState.COMPLETED + assert df is not None + assert len(df) == 50 + # Load direction is z; Szz is the active component. Same physics + # assertions as the absolute-only happy_path test. + assert df["Szz"].iloc[0] == pytest.approx(200.0, abs=1.0) + assert (df["Szz"].diff().dropna() > 0).all() + assert 290.0 < df["Szz"].iloc[-1] < 310.0 diff --git a/workflows/exaconstit-calibrate/tests/test_integration_restart.py b/workflows/exaconstit-calibrate/tests/test_integration_restart.py new file mode 100644 index 0000000..7a22005 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_integration_restart.py @@ -0,0 +1,292 @@ +""" +Restart-scenario integration tests. + +These simulate the workflow the framework is specifically designed +for: a long-running optimization where the HPC allocation dies in +the middle, and the user restarts the driver. The restart must: + +* Skip cases that completed successfully (sentinel present). +* Rerun cases that were in-flight when the driver died (SUBMITTED + with no terminal transition -> INTERRUPTED on reload). +* Not re-attempt cases that are terminal FAILED by policy (unless + the caller's policy says to - here we demonstrate the default + "leave failed cases alone"). + +Each test below emulates a crash by simply abandoning a Manifest +object partway through a run, then spinning up a new Manifest and +driving it forward. No actual process kill is necessary - the +crash-safety guarantees live entirely in the on-disk state (JSONL +manifest + per-case sentinels). +""" +from __future__ import annotations + +from pathlib import Path +from typing import List + +import pytest + +from workflow_common import ( + CaseContext, + CaseLayout, + CaseState, + LocalBackend, + Manifest, + ManifestEntry, + Sentinel, + SimJobSpec, + TemplatePathResolver, + TextTableReader, + TextTableSpec, + render_template_file, + write_sentinel, +) +from workflow_common.backends.base import JobOutcome +from workflow_common.sentinel import is_case_complete, read_sentinel + + +def _setup(workspace: Path): + """Build the resolver, reader, and manifest path used across tests.""" + resolver = TemplatePathResolver( + working_dir_pattern="wf/gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={ + "avg_stress": "{working_dir}/results/options/avg_stress.txt", + }, + root=workspace, + ) + reader = TextTableReader({ + "avg_stress": TextTableSpec( + columns=["Time", "Volume", "Sxx", "Syy", "Szz", "Sxy", "Sxz", "Syz"], + required=True, + ), + }) + manifest_path = workspace / "manifest.jsonl" + return resolver, reader, manifest_path + + +def _render_and_spec( + ctx: CaseContext, + resolver: TemplatePathResolver, + master_template: Path, + fake_binary: Path, +) -> SimJobSpec: + wd = resolver.working_dir(ctx) + wd.mkdir(parents=True, exist_ok=True) + render_template_file( + master_template, wd / "options.toml", + {"gene": ctx.gene, "obj": ctx.obj, + "strain_rate": 1e-3, "yield_stress": 200.0, + "hardening": 2000.0, "temp_k": 298.0}, + ) + return SimJobSpec( + working_dir=wd, binary=fake_binary, args=(), + duration_s=30, stdout="run.out", stderr="run.err", + tag=f"g{ctx.gene}o{ctx.obj}", + ) + + +def test_restart_skips_completed_cases( + workspace: Path, fake_binary: Path, master_template: Path +): + """Cases with a valid sentinel on disk are skipped on restart.""" + resolver, reader, manifest_path = _setup(workspace) + backend = LocalBackend(max_workers=2) + + # --- First "allocation": complete two cases, then "crash". --------- + m1 = Manifest(manifest_path) + m1.load() + + contexts = [CaseContext(generation=0, gene=i, obj=0) for i in range(4)] + # Run cases 0 and 1 fully through. + for ctx in contexts[:2]: + spec = _render_and_spec(ctx, resolver, master_template, fake_binary) + m1.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(spec.working_dir), + )) + res = backend.submit_one(spec) + assert res.outcome == JobOutcome.OK + write_sentinel(spec.working_dir, Sentinel(rc=0, wall_time_s=res.wall_time_s)) + m1.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.COMPLETED, rc=0, case_dir=str(spec.working_dir), + )) + + # Case 2 is submitted but "dies" before producing a sentinel. We + # simulate this by recording SUBMITTED but not actually running. + ctx = contexts[2] + spec = _render_and_spec(ctx, resolver, master_template, fake_binary) + m1.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(spec.working_dir), + )) + # Case 3 was never even submitted. + + # --- "Allocation dies." Drop m1, spin up m2 fresh. ----------------- + m2 = Manifest(manifest_path) + m2.load() + n_interrupted = m2.mark_submitted_as_interrupted() + # Exactly one case was stuck in SUBMITTED (case 2). + assert n_interrupted == 1 + + # --- Restart logic: for each context, skip if sentinel present. ---- + skipped: List[int] = [] + rerun: List[int] = [] + for ctx in contexts: + wd = resolver.working_dir(ctx) + if is_case_complete(wd): + skipped.append(ctx.gene) + continue + rerun.append(ctx.gene) + spec = _render_and_spec(ctx, resolver, master_template, fake_binary) + m2.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(spec.working_dir), + )) + res = backend.submit_one(spec) + assert res.outcome == JobOutcome.OK + write_sentinel(spec.working_dir, Sentinel(rc=0, wall_time_s=res.wall_time_s)) + m2.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.COMPLETED, rc=0, case_dir=str(spec.working_dir), + )) + + # Cases 0 and 1 were completed; cases 2 and 3 needed to run now. + assert skipped == [0, 1] + assert rerun == [2, 3] + + # Final manifest: every case should be COMPLETED. + for ctx in contexts: + e = m2.get(ctx.generation, ctx.gene, ctx.obj) + assert e is not None + assert e.state == CaseState.COMPLETED + + +def test_crash_between_sentinel_and_manifest_is_safe( + workspace: Path, fake_binary: Path, master_template: Path +): + """The sentinel-first-then-manifest ordering is crash-safe. + + Simulate: case finishes, sentinel is written, BUT the driver dies + before it can record the terminal manifest entry. On restart, + the sentinel is the authoritative signal - the case is treated + as complete even though the manifest still says SUBMITTED. + """ + resolver, reader, manifest_path = _setup(workspace) + backend = LocalBackend(max_workers=1) + + ctx = CaseContext(generation=0, gene=0, obj=0) + spec = _render_and_spec(ctx, resolver, master_template, fake_binary) + + m1 = Manifest(manifest_path) + m1.load() + m1.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(spec.working_dir), + )) + res = backend.submit_one(spec) + assert res.outcome == JobOutcome.OK + + # Write sentinel, but "die" before recording COMPLETED. + write_sentinel(spec.working_dir, Sentinel(rc=0, wall_time_s=res.wall_time_s)) + + # --- Restart: sentinel present, manifest says SUBMITTED. ---------- + m2 = Manifest(manifest_path) + m2.load() + n = m2.mark_submitted_as_interrupted() + # Because we did NOT record COMPLETED, the manifest entry is still + # SUBMITTED, so it gets promoted to INTERRUPTED. But the sentinel + # overrides this - our skip logic relies on sentinel presence, not + # manifest state. + assert n == 1 + assert m2.get(0, 0, 0).state == CaseState.INTERRUPTED + + # The correct restart policy: trust the sentinel. + assert is_case_complete(spec.working_dir) + sen = read_sentinel(spec.working_dir) + assert sen.rc == 0 + + +def test_interrupted_case_can_be_rerun_cleanly( + workspace: Path, fake_binary: Path, master_template: Path +): + """A case marked INTERRUPTED can be cleared and rerun from scratch.""" + resolver, reader, manifest_path = _setup(workspace) + backend = LocalBackend(max_workers=1) + + ctx = CaseContext(generation=0, gene=0, obj=0) + spec = _render_and_spec(ctx, resolver, master_template, fake_binary) + layout = CaseLayout(ctx=ctx, resolver=resolver) + + # Simulate a partial run: the SUBMITTED entry exists but no sentinel. + m1 = Manifest(manifest_path) + m1.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(spec.working_dir), + )) + + # Restart: promote to INTERRUPTED. + m2 = Manifest(manifest_path) + m2.load() + m2.mark_submitted_as_interrupted() + assert m2.get(0, 0, 0).state == CaseState.INTERRUPTED + + # Re-run logic: clear any partial outputs, then submit fresh. + layout.clear_outputs() # idempotent even if none exist + m2.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(spec.working_dir), + )) + res = backend.submit_one(spec) + assert res.outcome == JobOutcome.OK + write_sentinel(spec.working_dir, Sentinel(rc=0, wall_time_s=res.wall_time_s)) + m2.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.COMPLETED, rc=0, case_dir=str(spec.working_dir), + )) + + # Now the case is complete. Read its outputs to verify. + rs = reader.read(layout) + assert "avg_stress" in rs + assert len(rs.df("avg_stress")) == 50 + + +def test_snapshot_survives_simulated_crash( + workspace: Path, fake_binary: Path, master_template: Path +): + """A snapshot written before the crash is honored on reload.""" + resolver, reader, manifest_path = _setup(workspace) + backend = LocalBackend(max_workers=1) + + m1 = Manifest(manifest_path) + m1.load() + contexts = [CaseContext(generation=0, gene=i, obj=0) for i in range(3)] + + # Complete every case, take snapshot. + for ctx in contexts: + spec = _render_and_spec(ctx, resolver, master_template, fake_binary) + m1.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(spec.working_dir), + )) + res = backend.submit_one(spec) + write_sentinel(spec.working_dir, Sentinel(rc=0, wall_time_s=res.wall_time_s)) + m1.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.COMPLETED, rc=0, case_dir=str(spec.working_dir), + )) + m1.snapshot() + + # Simulate a torn-write on the JSONL log by appending garbage. + # This is what a kill-mid-write would produce. + with open(manifest_path, "a") as f: + f.write('{"generation": 0, "gene": 99, "obj": 0, "state": "sub') # truncated + + # Reload: the snapshot is authoritative, the torn line is dropped. + m2 = Manifest(manifest_path) + m2.load() + for ctx in contexts: + e = m2.get(ctx.generation, ctx.gene, ctx.obj) + assert e is not None + assert e.state == CaseState.COMPLETED + # The garbage entry did not materialize into a phantom case. + assert m2.get(0, 99, 0) is None diff --git a/workflows/exaconstit-calibrate/tests/test_integration_smoothing.py b/workflows/exaconstit-calibrate/tests/test_integration_smoothing.py new file mode 100644 index 0000000..b52bb30 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_integration_smoothing.py @@ -0,0 +1,410 @@ +""" +Integration tests that exercise the smoothing layer alongside the +rest of the framework. + +Why these tests matter +---------------------- +The smoothing layer is the last piece before error-metric computation +in a real ExaConstit optimization. These integration tests verify +that the output of the :class:`TextTableReader` can be fed directly +into a :class:`Smoother` and produce sensible, comparable curves. +They also exercise the "normalize scales using experimental ranges" +pattern that is essential for arc-length comparisons between sim +and exp. + +What a production ObjectiveEvaluator (step 6) will do internally +----------------------------------------------------------------- +The pattern demonstrated here is exactly what an ObjectiveEvaluator +in step 6 needs to do internally:: + + 1. Load experimental data once at the top of the run. + 2. For each simulated case: + a. Read simulation output via TextTableReader. + b. Extract strain and stress columns from the DataFrame. + c. Choose the right smoother (auto_smoother, or + PchipSmoother for monotonic sweep-style runs). + d. Smooth both sim and exp onto a common grid. + e. Compute an RMSE or similar error. + 3. Return errors keyed by (gene, obj). + +The tests below walk through each of those steps with real +subprocesses and real output files, so any regression in the +handoffs between modules surfaces immediately. +""" +from __future__ import annotations + +import os +from pathlib import Path +from typing import Tuple + +import numpy as np +import pandas as pd +import pytest + +from workflow_common import ( + ArcLengthSmoother, + CaseContext, + CaseLayout, + LocalBackend, + Manifest, + ManifestEntry, + CaseState, + PchipSmoother, + Sentinel, + SimJobSpec, + TemplatePathResolver, + TextTableReader, + TextTableSpec, + auto_smoother, + is_monotonic, + load_experimental_csv, + render_template_file, + write_sentinel, +) +from workflow_common.backends.base import JobOutcome + + +# --- Fixtures specific to the smoothing integration tests ---------------- + + +@pytest.fixture +def experimental_stress_strain(workspace: Path) -> pd.DataFrame: + """Generate a synthetic experimental reference dataset. + + Saturating Voce-law response with the same character as what the + fake binary produces, but slightly different parameters - so + there is a nonzero error the smoothing pipeline should be able + to quantify. Stored in the workspace as a CSV so the test + exercises ``load_experimental_csv`` too. + + Note: the saturation constant (50) matches the fake binary's so + sim and exp curves have the same shape; only the yield stress, + hardening modulus, and saturation rate differ slightly. That + produces a realistic small-ish RMSE on aligned curves. + """ + strain = np.linspace(0.0, 1.0, 40) + # Reference "truth" params slightly different from what the fake + # binary will be told to use - so sim and exp don't exactly match. + yield_stress = 210.0 + hardening = 1900.0 + # Saturation constant 48 (vs the binary's 50) -> curves are + # the same shape with slightly offset saturation rate. + stress = yield_stress + hardening * (1 - np.exp(-48.0 * strain)) + df = pd.DataFrame({"strain": strain, "stress": stress}) + # Round-trip through disk to mimic a real workflow. + path = workspace / "exp_reference.csv" + df.to_csv(path, index=False) + return load_experimental_csv(path, delimiter=",") + + +def _setup_common(workspace: Path): + """Shared setup shared with other integration tests.""" + resolver = TemplatePathResolver( + working_dir_pattern="wf/gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={ + "avg_stress": "{working_dir}/results/options/avg_stress.txt", + "avg_def_grad": "{working_dir}/results/options/avg_def_grad.txt", + }, + root=workspace, + ) + reader = TextTableReader({ + "avg_stress": TextTableSpec( + columns=["Time", "Volume", "Sxx", "Syy", "Szz", "Sxy", "Sxz", "Syz"], + required=True, + ), + "avg_def_grad": TextTableSpec( + columns=[ + "Time", "Volume", "F11", "F12", "F13", "F21", "F22", "F23", "F31", "F32", "F33", + ], + required=False, + ), + }) + return resolver, reader + + +def _run_case( + workspace: Path, + resolver: TemplatePathResolver, + master_template: Path, + fake_binary: Path, + ctx: CaseContext, + params: dict, + backend: LocalBackend, + manifest: Manifest, +) -> Path: + """Render, submit, and finalize one case. Returns the working directory.""" + wd = resolver.working_dir(ctx) + wd.mkdir(parents=True, exist_ok=True) + render_template_file( + master_template, wd / "options.toml", + {"gene": ctx.gene, "obj": ctx.obj, **params}, + ) + spec = SimJobSpec( + working_dir=wd, binary=fake_binary, args=(), + duration_s=30, stdout="run.out", stderr="run.err", + ) + manifest.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.SUBMITTED, case_dir=str(wd), + )) + res = backend.submit_one(spec) + assert res.outcome == JobOutcome.OK, f"sim failed: {res.error_message}" + write_sentinel(wd, Sentinel(rc=0, wall_time_s=res.wall_time_s)) + manifest.record(ManifestEntry( + generation=ctx.generation, gene=ctx.gene, obj=ctx.obj, + state=CaseState.COMPLETED, rc=0, case_dir=str(wd), + )) + return wd + + +# --- Integration tests --------------------------------------------------- + + +def test_pchip_smooths_sim_output_onto_exp_grid( + workspace: Path, fake_binary: Path, master_template: Path, + experimental_stress_strain: pd.DataFrame, +): + """Full pipeline: sim -> read -> smooth onto experimental strain grid.""" + resolver, reader = _setup_common(workspace) + backend = LocalBackend(max_workers=1) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + + ctx = CaseContext(generation=0, gene=0, obj=0) + _run_case( + workspace, resolver, master_template, fake_binary, ctx, + # strain_rate=1.0 so sim_strain = 1.0 * time ranges over [0, 1], + # matching the experimental strain range. In a real ExaConstit + # run the user aligns these by choosing t_final appropriately; + # here we control both the binary's strain-rate input and the + # experimental data, so we pick matching values directly. + {"strain_rate": 1.0, "yield_stress": 200.0, + "hardening": 2000.0, "temp_k": 298.0}, + backend, manifest, + ) + + # Step 1: read sim output. + layout = CaseLayout(ctx=ctx, resolver=resolver) + rs = reader.read(layout) + sim_df = rs.df("avg_stress") + + # Step 2: derive strain from time + strain_rate so we have a + # common axis with the experiment. In a real workflow this + # would come from the avg_def_grad file via F11 - 1. + sim_strain = 1.0 * sim_df["Time"].to_numpy() + sim_stress = sim_df["Szz"].to_numpy() + + # Sanity: sim is monotonic. + assert is_monotonic(sim_strain) + + # Step 3: smooth sim onto the experimental strain grid using PCHIP. + # We sample exactly at the experimental strain values so errors + # can be computed pointwise. + smoother = PchipSmoother(n_samples=200) + exp_strain = experimental_stress_strain["strain"].to_numpy() + exp_stress = experimental_stress_strain["stress"].to_numpy() + sim_on_exp = smoother.sample_at(sim_strain, sim_stress, exp_strain) + + # Pointwise aligned now: same length, same x. + assert sim_on_exp.x.shape == exp_strain.shape + assert np.allclose(sim_on_exp.x, exp_strain) + + # Step 4: RMSE is a small nonzero value (sim and exp intentionally + # have slightly different yield/hardening/saturation parameters). + # The bound is generous because the fake binary runs only 50 steps + # on a sharp-saturation curve, so transient-regime differences + # produce larger pointwise errors than a well-resolved real run would. + residual = sim_on_exp.y - exp_stress + rmse = float(np.sqrt(np.mean(residual ** 2))) + assert 0.0 < rmse < 200.0, f"rmse {rmse} outside sanity range" + + +def test_sim_and_exp_smoothed_onto_common_grid( + workspace: Path, fake_binary: Path, master_template: Path, + experimental_stress_strain: pd.DataFrame, +): + """A more realistic pattern: both curves smoothed, then compared. + + In real optimizations, the experimental curve is typically noisy + and should be smoothed just like the sim before comparison. + """ + resolver, reader = _setup_common(workspace) + backend = LocalBackend(max_workers=1) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + + ctx = CaseContext(generation=0, gene=0, obj=0) + _run_case( + workspace, resolver, master_template, fake_binary, ctx, + {"strain_rate": 1.0, "yield_stress": 200.0, + "hardening": 2000.0, "temp_k": 298.0}, + backend, manifest, + ) + + layout = CaseLayout(ctx=ctx, resolver=resolver) + rs = reader.read(layout) + sim_strain = 1.0 * rs.df("avg_stress")["Time"].to_numpy() + sim_stress = rs.df("avg_stress")["Szz"].to_numpy() + + exp_strain = experimental_stress_strain["strain"].to_numpy() + exp_stress = experimental_stress_strain["stress"].to_numpy() + + # Smooth both onto a common uniform strain grid. The sim-range is + # the authoritative upper bound since the experiment typically + # has more strain data than the sim does. + common_strain = np.linspace( + max(sim_strain.min(), exp_strain.min()), + min(sim_strain.max(), exp_strain.max()), + 100, + ) + smoother = PchipSmoother(n_samples=100) + sim_smooth = smoother.sample_at(sim_strain, sim_stress, common_strain) + exp_smooth = smoother.sample_at(exp_strain, exp_stress, common_strain) + + # Aligned and comparable. + assert np.allclose(sim_smooth.x, exp_smooth.x) + rmse = float(np.sqrt(np.mean((sim_smooth.y - exp_smooth.y) ** 2))) + assert rmse > 0 + # Smoothed residual should be bounded by the signal range. + sig_range = float(max(exp_stress.max(), sim_stress.max()) - + min(exp_stress.min(), sim_stress.min())) + assert rmse < sig_range + + +def test_arc_length_with_shared_scales_matches_arc_sample_indices( + workspace: Path, fake_binary: Path, master_template: Path, + experimental_stress_strain: pd.DataFrame, +): + """Sim and exp arc-length smoothers using the SAME scales agree index-wise. + + This is the subtle detail that the architecture doc calls out: + when comparing sim to exp by arc length, both smoothers must use + the SAME normalization scales (typically the experimental ranges) + so that the i-th arc-length sample of each curve corresponds to + the same fraction of the same reference path length. + """ + resolver, reader = _setup_common(workspace) + backend = LocalBackend(max_workers=1) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + + ctx = CaseContext(generation=0, gene=0, obj=0) + _run_case( + workspace, resolver, master_template, fake_binary, ctx, + {"strain_rate": 1.0, "yield_stress": 200.0, + "hardening": 2000.0, "temp_k": 298.0}, + backend, manifest, + ) + + layout = CaseLayout(ctx=ctx, resolver=resolver) + rs = reader.read(layout) + sim_strain = 1.0 * rs.df("avg_stress")["Time"].to_numpy() + sim_stress = rs.df("avg_stress")["Szz"].to_numpy() + exp_strain = experimental_stress_strain["strain"].to_numpy() + exp_stress = experimental_stress_strain["stress"].to_numpy() + + # Use experimental range as the common normalization. + x_scale = float(exp_strain.max() - exp_strain.min()) + y_scale = float(exp_stress.max() - exp_stress.min()) + + smoother = ArcLengthSmoother(n_samples=100, x_scale=x_scale, y_scale=y_scale) + sim_smooth = smoother.smooth(sim_strain, sim_stress) + exp_smooth = smoother.smooth(exp_strain, exp_stress) + + # Both curves have 100 samples. + assert len(sim_smooth.x) == 100 + assert len(exp_smooth.x) == 100 + + # The arc-length totals should be close (both curves trace similar + # paths in the normalized (strain, stress) box; the slight Voce + # parameter difference means they aren't identical). + ratio = sim_smooth.s[-1] / exp_smooth.s[-1] + assert 0.8 < ratio < 1.2, f"arc-length total ratio {ratio} out of range" + + +def test_auto_smoother_in_pipeline( + workspace: Path, fake_binary: Path, master_template: Path, +): + """auto_smoother picks PCHIP for our monotonic fake data.""" + resolver, reader = _setup_common(workspace) + backend = LocalBackend(max_workers=1) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + + ctx = CaseContext(generation=0, gene=0, obj=0) + _run_case( + workspace, resolver, master_template, fake_binary, ctx, + {"strain_rate": 1.0, "yield_stress": 200.0, + "hardening": 2000.0, "temp_k": 298.0}, + backend, manifest, + ) + + layout = CaseLayout(ctx=ctx, resolver=resolver) + rs = reader.read(layout) + sim_strain = 1.0 * rs.df("avg_stress")["Time"].to_numpy() + sim_stress = rs.df("avg_stress")["Szz"].to_numpy() + + # Let the framework pick. Fake data is monotonic -> PCHIP. + smoother = auto_smoother(sim_strain, sim_stress, n_samples=50) + # We don't assert the concrete class name here - the point of + # auto_smoother is to Just Work without the caller caring. But + # we do verify the output shape. + out = smoother.smooth(sim_strain, sim_stress) + assert len(out.x) == 50 + assert len(out.y) == 50 + + +def test_legacy_vs_pchip_on_real_sim_output( + workspace: Path, fake_binary: Path, master_template: Path, +): + """Migration check: legacy and PCHIP are close on real simulation output. + + If this test were to produce large differences, it would indicate + that switching to PCHIP midway through the refactor would change + objective values in a way a user might mistake for a real bug. + Verifying that they agree closely on monotonic data lets us + migrate the smoother in a separate step with confidence. + """ + from workflow_common import LegacyLinearSmoother + + resolver, reader = _setup_common(workspace) + backend = LocalBackend(max_workers=1) + manifest = Manifest(workspace / "manifest.jsonl") + manifest.load() + + ctx = CaseContext(generation=0, gene=0, obj=0) + _run_case( + workspace, resolver, master_template, fake_binary, ctx, + {"strain_rate": 1.0, "yield_stress": 200.0, + "hardening": 2000.0, "temp_k": 298.0}, + backend, manifest, + ) + + layout = CaseLayout(ctx=ctx, resolver=resolver) + rs = reader.read(layout) + x = 1e-3 * rs.df("avg_stress")["Time"].to_numpy() + y = rs.df("avg_stress")["Szz"].to_numpy() + + legacy = LegacyLinearSmoother(n_samples=200).smooth(x, y) + pchip = PchipSmoother(n_samples=200).smooth(x, y) + + # Grids should be identical. + assert np.allclose(legacy.x, pchip.x) + # Stress values should be close - but not identical, since PCHIP's + # cubic segments curve differently from the legacy linear + # connect-the-dots. The exact bound depends on how densely the + # source data samples the curve's knee; our fake binary runs only + # 50 steps on a sharp-saturation response, which is near the + # worst-case shape for legacy-vs-PCHIP divergence. Production + # ExaConstit runs typically use hundreds of steps and show + # sub-percent differences. + signal_range = float(y.max() - y.min()) + max_abs_diff = float(np.max(np.abs(legacy.y - pchip.y))) + rel_diff = max_abs_diff / signal_range + # This bound is deliberately loose. Tighten if you increase the + # fake binary's step count or use a gentler saturation rate. + assert rel_diff < 0.10, ( + f"legacy vs PCHIP disagreement {rel_diff:.4f} exceeds 10% of " + "signal range on a 50-point sharp-knee curve - surprising; " + "investigate before switching the default smoother." + ) diff --git a/workflows/exaconstit-calibrate/tests/test_local_backend.py b/workflows/exaconstit-calibrate/tests/test_local_backend.py new file mode 100644 index 0000000..1084936 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_local_backend.py @@ -0,0 +1,289 @@ +""" +Unit tests for :class:`workflow_common.backends.local.LocalBackend`. + +These use the ``fake_binary`` fixture from conftest to exercise the +real subprocess code path. We deliberately do NOT mock out +``subprocess.Popen`` because the subtleties the backend is trying to +handle - stdio redirection, timeouts, bad-binary-paths - all live +inside the subprocess module and are only visible with a real +invocation. +""" +from __future__ import annotations + +import os +import time +from pathlib import Path +from typing import List + +import pytest + +from workflow_common.backends.base import JobOutcome, JobResult, SimJobSpec +from workflow_common.backends.local import LocalBackend + + +def _spec( + workspace: Path, + fake_binary: Path, + *, + tag: str = "t", + duration_s: int = 30, + capture: bool = True, +) -> SimJobSpec: + """Build a minimal SimJobSpec aimed at the fake binary.""" + wd = workspace / f"case_{tag}" + wd.mkdir() + # The fake binary reads options.toml; give it a minimal one. + (wd / "options.toml").write_text( + 'basename = "options"\n' + "strain_rate = 1e-3\n" + "yield_stress = 200.0\n" + "hardening = 2000.0\n" + ) + return SimJobSpec( + working_dir=wd, + binary=Path(fake_binary), + args=(), + num_tasks=1, + duration_s=duration_s, + stdout="run.out" if capture else None, + stderr="run.err" if capture else None, + tag=tag, + ) + + +def test_local_serial_happy_path(workspace: Path, fake_binary: Path): + """max_workers=1 runs cases strictly in order and succeeds.""" + specs = [_spec(workspace, fake_binary, tag=f"{i}") for i in range(3)] + backend = LocalBackend(max_workers=1) + results = backend.submit_batch(specs) + assert len(results) == 3 + for r in results: + assert r.outcome == JobOutcome.OK + assert r.rc == 0 + # Output files should be present inside each working dir. + for spec in specs: + assert (spec.working_dir / "results" / "options" / "avg_stress.txt").exists() + + +def test_local_parallel_happy_path(workspace: Path, fake_binary: Path): + """max_workers>1 completes all cases and preserves submission-order output.""" + specs = [_spec(workspace, fake_binary, tag=f"{i}") for i in range(5)] + backend = LocalBackend(max_workers=3) + results = backend.submit_batch(specs) + assert len(results) == 5 + # submit_batch must return results in submission order even though + # they complete in arbitrary order internally. + for spec, result in zip(specs, results): + assert result.spec is spec + assert result.outcome == JobOutcome.OK + + +def test_local_stream_batch_yields_as_completed(workspace: Path, fake_binary: Path): + """stream_batch yields one JobResult per spec; each points to its input.""" + specs = [_spec(workspace, fake_binary, tag=f"{i}") for i in range(3)] + backend = LocalBackend(max_workers=2) + seen: List[JobResult] = list(backend.stream_batch(specs)) + assert len(seen) == 3 + # The set of returned specs must equal the set submitted. + assert {id(r.spec) for r in seen} == {id(s) for s in specs} + + +def test_local_failing_case_reports_failed(workspace: Path, fake_binary: Path): + """rc=7 from the fake binary -> JobOutcome.FAILED, rc preserved.""" + spec = _spec(workspace, fake_binary, tag="fail") + os.environ["FAKE_FAIL"] = "1" + try: + backend = LocalBackend(max_workers=1) + result = backend.submit_one(spec) + finally: + os.environ.pop("FAKE_FAIL", None) + assert result.outcome == JobOutcome.FAILED + assert result.rc == 7 + + +def test_local_bad_binary_reports_submit_error(workspace: Path): + """Nonexistent binary path -> SUBMIT_ERROR, not FAILED.""" + wd = workspace / "case" + wd.mkdir() + # Fake options.toml so the working directory has something. + (wd / "options.toml").write_text("") + spec = SimJobSpec( + working_dir=wd, + binary=Path("/no/such/binary"), + args=(), + duration_s=10, + ) + backend = LocalBackend(max_workers=1) + result = backend.submit_one(spec) + assert result.outcome == JobOutcome.SUBMIT_ERROR + assert result.error_message is not None + + +def test_local_timeout_kills_process(workspace: Path, fake_binary: Path): + """A spec with duration_s shorter than FAKE_SLEEP yields TIMEOUT.""" + spec = _spec(workspace, fake_binary, tag="timeout", duration_s=1) + os.environ["FAKE_SLEEP"] = "5" + try: + t0 = time.monotonic() + backend = LocalBackend(max_workers=1, kill_on_timeout=True) + result = backend.submit_one(spec) + elapsed = time.monotonic() - t0 + finally: + os.environ.pop("FAKE_SLEEP", None) + + assert result.outcome == JobOutcome.TIMEOUT + # Kill-on-timeout should interrupt well before the full 5s sleep. + # Give it a generous upper bound to avoid flakiness on slow CI. + assert elapsed < 4.5 + + +def test_local_stdio_files_are_written(workspace: Path, fake_binary: Path): + """stdout/stderr files requested in the spec actually get written.""" + spec = _spec(workspace, fake_binary, tag="stdio") + backend = LocalBackend(max_workers=1) + result = backend.submit_one(spec) + assert result.outcome == JobOutcome.OK + # The fake binary prints "fake sim ok" to stdout on success via our + # default. It doesn't actually do that currently; the stdout file + # should at least exist (possibly empty). + assert (spec.working_dir / "run.out").exists() + assert (spec.working_dir / "run.err").exists() + + +def test_local_rejects_invalid_workers(): + """max_workers < 1 is nonsensical and must raise.""" + with pytest.raises(ValueError): + LocalBackend(max_workers=0) + + +def test_local_empty_batch_is_noop(workspace: Path): + """submit_batch([]) returns [] without spinning up a pool.""" + backend = LocalBackend(max_workers=4) + assert backend.submit_batch([]) == [] + assert list(backend.stream_batch([])) == [] + + +def test_local_refuses_multi_rank_without_launcher(workspace: Path, fake_binary: Path): + """Requesting num_tasks > 1 with no MPI launcher is a user bug + (would silently drop ranks); the backend refuses at submit time + so the problem surfaces at the first case instead of after a + multi-day run produces single-rank results. + """ + backend = LocalBackend(max_workers=1) # no mpi_launcher + spec = SimJobSpec( + working_dir=workspace / "c", + binary=fake_binary, + args=(), + num_tasks=4, + duration_s=30, + ) + with pytest.raises(ValueError, match="num_tasks=4"): + backend.submit_one(spec) + + +def test_local_ranks_silent_allows_dropped_ranks(workspace: Path, fake_binary: Path): + """ranks_silent=True opt-in preserves the old behavior for users + who legitimately want to run a serial binary despite num_tasks > 1 + (e.g. using num_tasks only for bookkeeping). + """ + backend = LocalBackend(max_workers=1, ranks_silent=True) + spec = _spec(workspace, fake_binary, tag="serial_with_tasks") + # Override to request multi-rank while leaving everything else alone. + spec = SimJobSpec( + working_dir=spec.working_dir, + binary=spec.binary, + args=spec.args, + num_tasks=4, # would normally raise + duration_s=spec.duration_s, + stdout=spec.stdout, + stderr=spec.stderr, + tag=spec.tag, + ) + # Should not raise; the binary runs single-rank. + result = backend.submit_one(spec) + assert result.outcome == JobOutcome.OK + + +def test_local_uses_mpi_launcher_for_multi_rank(workspace: Path, tmp_path): + """When mpi_launcher is configured, the launcher is prepended + with the configured ntasks flag and the task count. + """ + backend = LocalBackend(mpi_launcher="mpirun") + spec = SimJobSpec( + working_dir=workspace / "c", + binary=Path("/usr/bin/echo"), + args=("hello",), + num_tasks=4, + ) + cmd = backend._build_cmd(spec) + assert cmd[:3] == ["mpirun", "-n", "4"] + assert cmd[3] == "/usr/bin/echo" + + +def test_local_custom_ntasks_flag(workspace: Path): + """mpi_launcher_ntasks_flag overrides the default '-n' for + launchers that need different syntax (jsrun '--nrs', lrun '-T'). + """ + backend = LocalBackend( + mpi_launcher="jsrun", mpi_launcher_ntasks_flag="--nrs", + ) + spec = SimJobSpec( + working_dir=workspace / "c", + binary=Path("/bin/true"), + args=(), + num_tasks=8, + ) + cmd = backend._build_cmd(spec) + assert cmd[:3] == ["jsrun", "--nrs", "8"] + + +def test_local_poll_stats_zero_before_any_jobs(workspace: Path): + """Fresh backend with no work: zero running, cores_total from OS.""" + backend = LocalBackend(max_workers=4) + stats = backend.poll_stats() + assert stats.running_jobs == 0 + assert stats.max_concurrent == 4 + assert stats.cores_in_use == 0 + # cores_total may be 0 on exotic kernels; >= 1 everywhere a test runs. + assert stats.cores_total >= 1 + + +def test_local_poll_stats_reports_running_jobs(workspace: Path, fake_binary: Path): + """poll_stats reflects in-flight jobs while stream_batch is pumping.""" + import threading + backend = LocalBackend(max_workers=2) + # Build four serial specs, each with a small sleep so poll_stats + # has a real window to observe in-flight work. + base_specs = [_spec(workspace, fake_binary, tag=f"p{i}") for i in range(4)] + specs = [ + SimJobSpec( + working_dir=s.working_dir, + binary=s.binary, + args=s.args, + env={"FAKE_SLEEP": "0.3"}, + num_tasks=1, + duration_s=s.duration_s, + stdout=s.stdout, + stderr=s.stderr, + tag=s.tag, + ) + for s in base_specs + ] + + seen_running = [] + + def poll(): + # Poll a few times during the batch run. + for _ in range(6): + seen_running.append(backend.poll_stats().running_jobs) + import time as _t; _t.sleep(0.1) + + t = threading.Thread(target=poll) + t.start() + results = list(backend.stream_batch(specs)) + t.join() + assert len(results) == 4 + # At some point during the run, at least one job was in flight. + assert max(seen_running) >= 1 + # After completion, everything is cleared. + assert backend.poll_stats().running_jobs == 0 diff --git a/workflows/exaconstit-calibrate/tests/test_manifest.py b/workflows/exaconstit-calibrate/tests/test_manifest.py new file mode 100644 index 0000000..9e42531 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_manifest.py @@ -0,0 +1,241 @@ +""" +Unit tests for :mod:`workflow_common.manifest` and +:mod:`workflow_common.sentinel`. + +These exercise the crash-safe state tracking that underpins restart. +Key scenarios: + +* Round-tripping state through the JSONL log. +* Snapshot compaction and partial replay. +* Survival of torn (malformed) trailing lines. +* Correct promotion of SUBMITTED -> INTERRUPTED on restart. +* Sentinel read/write/validation. + +""" +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from workflow_common.manifest import ( + CaseState, + Manifest, + ManifestEntry, +) +from workflow_common.sentinel import ( + Sentinel, + clear_sentinel, + is_case_complete, + read_sentinel, + sentinel_path, + validate_outputs, + write_sentinel, +) + + +# --- Manifest ------------------------------------------------------------- + + +def test_manifest_record_and_get(workspace: Path): + """Recording an entry puts it in memory and on disk.""" + m = Manifest(workspace / "m.jsonl") + e = ManifestEntry( + generation=0, gene=1, obj=0, state=CaseState.SUBMITTED + ) + m.record(e) + retrieved = m.get(0, 1, 0) + assert retrieved is not None + assert retrieved.state == CaseState.SUBMITTED + + +def test_manifest_persists_across_reload(workspace: Path): + """A fresh Manifest instance loads the prior one's state.""" + path = workspace / "m.jsonl" + m = Manifest(path) + m.record(ManifestEntry(generation=0, gene=0, obj=0, state=CaseState.SUBMITTED)) + m.record(ManifestEntry(generation=0, gene=0, obj=0, state=CaseState.COMPLETED)) + + m2 = Manifest(path) + m2.load() + got = m2.get(0, 0, 0) + assert got is not None + assert got.state == CaseState.COMPLETED # latest entry wins + + +def test_manifest_snapshot_plus_new_entries(workspace: Path): + """Snapshot + subsequent appends reload correctly after restart.""" + path = workspace / "m.jsonl" + m = Manifest(path) + # Pre-snapshot work: + m.record(ManifestEntry(generation=0, gene=0, obj=0, state=CaseState.COMPLETED)) + m.record(ManifestEntry(generation=0, gene=1, obj=0, state=CaseState.COMPLETED)) + m.snapshot() + # Post-snapshot work: + m.record(ManifestEntry(generation=1, gene=0, obj=0, state=CaseState.SUBMITTED)) + + m2 = Manifest(path) + m2.load() + # All three cases should be present, snapshot entries plus post-snapshot. + assert m2.get(0, 0, 0).state == CaseState.COMPLETED + assert m2.get(0, 1, 0).state == CaseState.COMPLETED + assert m2.get(1, 0, 0).state == CaseState.SUBMITTED + + +def test_manifest_tolerates_torn_trailing_line(workspace: Path): + """A malformed final line should be dropped, not crash the load.""" + path = workspace / "m.jsonl" + m = Manifest(path) + m.record(ManifestEntry(generation=0, gene=0, obj=0, state=CaseState.COMPLETED)) + # Append a half-written line, mimicking a kill mid-write. + with open(path, "a") as f: + f.write('{"generation": 0, "gene": 1, "obj": 0, "sta') # no newline, truncated + + m2 = Manifest(path) + m2.load() + # The complete earlier entry must still be there. + assert m2.get(0, 0, 0).state == CaseState.COMPLETED + # The torn entry must be absent. + assert m2.get(0, 1, 0) is None + + +def test_mark_submitted_as_interrupted(workspace: Path): + """SUBMITTED entries without a terminal transition become INTERRUPTED.""" + m = Manifest(workspace / "m.jsonl") + m.record(ManifestEntry(generation=0, gene=0, obj=0, state=CaseState.SUBMITTED)) + m.record(ManifestEntry(generation=0, gene=1, obj=0, state=CaseState.COMPLETED)) + + # New Manifest -> load -> mark: the submitted-but-no-terminal gets + # promoted; the completed one is left alone. + m2 = Manifest(workspace / "m.jsonl") + m2.load() + n = m2.mark_submitted_as_interrupted() + assert n == 1 + assert m2.get(0, 0, 0).state == CaseState.INTERRUPTED + assert m2.get(0, 1, 0).state == CaseState.COMPLETED + + +def test_mark_submitted_as_interrupted_idempotent(workspace: Path): + """Calling mark_submitted_as_interrupted twice finds nothing new the 2nd time.""" + m = Manifest(workspace / "m.jsonl") + m.record(ManifestEntry(generation=0, gene=0, obj=0, state=CaseState.SUBMITTED)) + m.load() # Not strictly needed here but mirrors restart + assert m.mark_submitted_as_interrupted() == 1 + # Second call: no SUBMITTED entries remain. + assert m.mark_submitted_as_interrupted() == 0 + + +def test_manifest_filter_state(workspace: Path): + """filter_state yields only entries matching the requested state.""" + m = Manifest(workspace / "m.jsonl") + m.record(ManifestEntry(generation=0, gene=0, obj=0, state=CaseState.COMPLETED)) + m.record(ManifestEntry(generation=0, gene=1, obj=0, state=CaseState.FAILED)) + m.record(ManifestEntry(generation=0, gene=2, obj=0, state=CaseState.COMPLETED)) + + completed = list(m.filter_state(CaseState.COMPLETED)) + failed = list(m.filter_state(CaseState.FAILED)) + assert len(completed) == 2 + assert len(failed) == 1 + + +def test_case_state_terminal_property(): + """is_terminal should be True only for COMPLETED and FAILED.""" + assert CaseState.COMPLETED.is_terminal + assert CaseState.FAILED.is_terminal + assert not CaseState.PENDING.is_terminal + assert not CaseState.SUBMITTED.is_terminal + assert not CaseState.INTERRUPTED.is_terminal + + +def test_manifest_entry_json_round_trip(): + """to_json_line followed by from_dict(json.loads(...)) is an identity.""" + e = ManifestEntry( + generation=2, gene=3, obj=1, + state=CaseState.COMPLETED, + rc=0, jobid="f1A2B3", message="ok", case_dir="/tmp/x", + ) + serialized = e.to_json_line() + e2 = ManifestEntry.from_dict(json.loads(serialized)) + assert e2.generation == e.generation + assert e2.gene == e.gene + assert e2.obj == e.obj + assert e2.state == e.state + assert e2.rc == e.rc + assert e2.jobid == e.jobid + + +# --- Sentinel ------------------------------------------------------------- + + +def test_sentinel_write_read_round_trip(workspace: Path): + """Writing then reading should recover every field.""" + case_dir = workspace / "case" + case_dir.mkdir() + original = Sentinel( + rc=0, wall_time_s=3.14, jobid="abc", + output_files={"avg_stress": "results/options/avg_stress.txt"}, + status="ok", + ) + write_sentinel(case_dir, original) + got = read_sentinel(case_dir) + assert got is not None + assert got.rc == 0 + assert got.wall_time_s == 3.14 + assert got.jobid == "abc" + assert got.status == "ok" + assert got.output_files == {"avg_stress": "results/options/avg_stress.txt"} + + +def test_sentinel_missing_returns_none(workspace: Path): + """No sentinel -> read_sentinel returns None, is_case_complete returns False.""" + assert read_sentinel(workspace) is None + assert not is_case_complete(workspace) + + +def test_sentinel_malformed_returns_none(workspace: Path): + """Garbage in the sentinel file should be treated as no sentinel at all.""" + (workspace / ".done").write_text("{not valid json") + assert read_sentinel(workspace) is None + assert not is_case_complete(workspace) + + +def test_sentinel_clear(workspace: Path): + """clear_sentinel removes the file if present and no-ops otherwise.""" + case_dir = workspace / "case" + case_dir.mkdir() + write_sentinel(case_dir, Sentinel(rc=0, wall_time_s=1.0)) + assert is_case_complete(case_dir) + clear_sentinel(case_dir) + assert not is_case_complete(case_dir) + # Second call must not raise on missing file. + clear_sentinel(case_dir) + + +def test_validate_outputs_happy_path(workspace: Path): + """All required files present and nonempty -> ok=True, bad=[].""" + case_dir = workspace / "case" + case_dir.mkdir() + (case_dir / "a.txt").write_text("xxx") + (case_dir / "b.txt").write_text("yy") + ok, bad = validate_outputs(case_dir, ["a.txt", "b.txt"]) + assert ok + assert bad == [] + + +def test_validate_outputs_flags_missing_and_empty(workspace: Path): + """Missing files and empty files both show up in ``bad``.""" + case_dir = workspace / "case" + case_dir.mkdir() + (case_dir / "empty.txt").write_text("") + (case_dir / "full.txt").write_text("x") + ok, bad = validate_outputs(case_dir, ["empty.txt", "full.txt", "missing.txt"]) + assert not ok + bad_names = [Path(b).name for b in bad] + assert set(bad_names) == {"empty.txt", "missing.txt"} + + +def test_sentinel_path_convention(workspace: Path): + """The sentinel file lives at ``/.done``.""" + assert sentinel_path(workspace).name == ".done" + assert sentinel_path(workspace).parent == workspace diff --git a/workflows/exaconstit-calibrate/tests/test_nsga3_driver.py b/workflows/exaconstit-calibrate/tests/test_nsga3_driver.py new file mode 100644 index 0000000..592a874 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_nsga3_driver.py @@ -0,0 +1,2130 @@ +""" +End-to-end tests for the NSGA-III driver built on DEAP's rcarson3 fork. + +What these prove +---------------- +1. **Driver smoke test** — a small (U-)NSGA-III run produces the + expected final population size, pareto subset, and DEAP logbook + records. +2. **Determinism** — same seed in two separate workspaces produces + bit-identical per-generation gene matrices and objective + matrices. This is the single most important property for + reproducible scientific pipelines and one that depends on + ``random.seed(...)`` being respected throughout DEAP's + variation operators. +3. **Shared-SimCase parity** — N objectives scored against one + shared SimCase produces the same initial-population objective + values as the same N objectives scored against N separate + SimCases. Confirms that the framework's sim-sharing + optimization does not change what DEAP sees. +4. **Checkpoint / resume parity** — a run that completes N + generations matches a run that runs N/2 generations, pickles a + checkpoint, and resumes for N/2 more from that pickle. +5. **UNSGA3 off / on** — both paths run without errors; UNSGA3 is + the fork-only feature that required keeping DEAP. +6. **Reference points + NPOP derivation** — the population size + computed from ``ref_dirs_partitions`` matches the NSGA-III + paper's recipe. +7. **Fail-retry on sim failure** — when the framework emits inf + on sim failure, the driver replaces the failing individual and + retries; fail_limit terminates the run. +8. **Bounds / shape guardrails** — misconfigured bounds or + objective counts raise at construction time, not during + evaluation. + +Heavyweight notes +----------------- +These tests run real subprocesses through LocalBackend and DEAP's +full selection/crossover/mutation pipeline. They are slower than +the pure-unit tests — a few seconds each, not milliseconds. +""" +from __future__ import annotations + +import os +import pickle +import textwrap +import time +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +# Import the driver via its proper package path. conftest.py has +# already put the repo root on sys.path for source-checkout runs; +# when the package is pip-installed the import resolves via +# site-packages. The underscore-prefixed helpers are internal but +# we test them explicitly so importing by name is intentional. +from workflows.optimization.nsga3_driver import ( # noqa: E402 + Bounds, + RunConfig, + RunResult, + run_nsga3, + _build_reference_points, + _derive_population_size, + _is_failed, +) +from workflow_common import ( # noqa: E402 + CaseTemplater, + ConstantPenaltyFailureHandler, + InfinityFailureHandler, + LocalBackend, + Manifest, + ObjectiveSpec, + PchipSmoother, + Problem, + ProblemConfig, + SimCase, + StressStrainExtractor, + StressStrainObjective, + TemplatePathResolver, + TemplatePropertyWriter, + TemplateTarget, + TextTableReader, + TextTableSpec, + load_experimental_csv, + rmse, +) + + +# --- Shared helpers ----------------------------------------------------- + + +def _build_problem( + workspace: Path, + fake_binary: Path, + *, + sim_cases, + objective_specs, + failure_penalty: float = 1e8, + manifest_name: str = "manifest.jsonl", + max_workers: int = 4, + archive=None, + archive_run_id=None, +): + """Build a Problem against the standard test plumbing. + + Same pattern as test_problem.py's fixtures. Kept inline here so + this file is self-contained (no cross-test imports). + """ + options_tmpl = workspace / "master_options.toml" + if not options_tmpl.exists(): + options_tmpl.write_text(textwrap.dedent("""\ + [Problem] + name = "case_fixed" + basename = "options" + strain_rate = %%strain_rate%% + yield_stress = %%yield_stress%% + hardening = %%hardening%% + temperature_k = %%temp_k%% + """)) + placeholder = workspace / "placeholder.txt" + if not placeholder.exists(): + placeholder.write_text("# placeholder\n") + + templater = CaseTemplater([ + TemplateTarget(source=placeholder, dest=".placeholder"), + ]) + writer = TemplatePropertyWriter( + template_path=options_tmpl, + dest="options.toml", + extra_values={"strain_rate": 1.0, "temp_k": 298.0}, + ) + resolver = TemplatePathResolver( + working_dir_pattern="wf/gen_{generation}/gene_{gene}_sc_{obj}", + output_file_patterns={ + "avg_stress": "{working_dir}/results/options/avg_stress.txt", + "avg_def_grad": "{working_dir}/results/options/avg_def_grad.txt", + }, + root=workspace, + ) + reader = TextTableReader({ + "avg_stress": TextTableSpec( + columns=["Time", "Volume", "Sxx", "Syy", "Szz", "Sxy", "Sxz", "Syz"], + ), + "avg_def_grad": TextTableSpec( + columns=["Time", "Volume", "F11", "F12", "F13", "F21", "F22", "F23", "F31", "F32", "F33"], + required=False, + ), + }) + return Problem( + config=ProblemConfig( + binary=fake_binary, binary_args=(), + num_tasks=1, duration_s=30, + stdout="stdout.log", stderr="stderr.log", + required_outputs=("results/options/avg_stress.txt",), + ), + param_names=["yield_stress", "hardening"], + sim_cases=sim_cases, + objective_specs=objective_specs, + templater=templater, + property_writer=writer, + resolver=resolver, + backend=LocalBackend(max_workers=max_workers), + reader=reader, + failure_handler=ConstantPenaltyFailureHandler(penalty=failure_penalty), + manifest=Manifest(workspace / manifest_name), + archive=archive, + archive_run_id=archive_run_id, + ) + + +@pytest.fixture +def exp_df(): + """Reference Voce-law curve matching the fake binary's saturation constant.""" + strain = np.linspace(0.0, 1.0, 40) + stress = 210.0 + 1900.0 * (1 - np.exp(-48.0 * strain)) + return pd.DataFrame({"strain": strain, "stress": stress}) + + +@pytest.fixture +def stress_evaluator(exp_df): + return StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor( + strain_source="time_rate", strain_rate=1.0, + ), + ) + + +def _slope_evaluator(stress_eval): + """Slope-matching evaluator sharing extractor + exp DataFrame.""" + exp_df = stress_eval.experimental + exp_strain = exp_df[stress_eval.experimental_strain_col].to_numpy() + exp_stress = exp_df[stress_eval.experimental_stress_col].to_numpy() + exp_slope = np.gradient(exp_stress, exp_strain) + exp_slope_df = pd.DataFrame({ + stress_eval.experimental_strain_col: exp_strain, + stress_eval.experimental_stress_col: exp_slope, + }) + + class _SlopeEvaluator: + extractor = stress_eval.extractor + experimental = exp_slope_df + experimental_strain_col = stress_eval.experimental_strain_col + experimental_stress_col = stress_eval.experimental_stress_col + + def evaluate(self, results, ctx): + strain, stress = stress_eval.extractor.extract(results) + slope = np.gradient(stress, strain) + lo = max(strain.min(), exp_strain.min()) + hi = min(strain.max(), exp_strain.max()) + if lo >= hi: + raise ValueError("no strain overlap") + common = np.linspace(lo, hi, 100) + sm = PchipSmoother(n_samples=100) + sim_y = sm.sample_at(strain, slope, common).y + exp_y = sm.sample_at(exp_strain, exp_slope, common).y + return rmse(sim_y, exp_y) + + return _SlopeEvaluator() + + +# --- 1. Smoke test ----------------------------------------------------- + + +def test_nsga3_single_objective_runs(workspace, fake_binary, stress_evaluator): + """Small single-objective run produces the right shape outputs.""" + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + result = run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + ), + ) + assert isinstance(result, RunResult) + assert len(result.final_pop) == 4 + assert len(result.pop_library) == 3 # gen 0, gen 1, gen 2 + # Logbook stats has one record per generation (0, 1, 2). + assert len(result.logbook_stats) == 3 + # Every individual has real fitness values. + for ind in result.final_pop: + assert np.isfinite(ind.fitness.values[0]) + + +def test_nsga3_multi_objective_runs( + workspace, fake_binary, stress_evaluator, +): + """2-objective run: fork's UNSGA3 + HV tracking + ND counting.""" + slope = _slope_evaluator(stress_evaluator) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0, label="stress"), + ObjectiveSpec(slope, sim_case=0, label="slope"), + ], + ) + result = run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=True, + ), + ) + assert len(result.final_pop) == 4 + # Post-gen-1, HV should be a finite number. + hv_gen_2 = result.logbook_stats[-1]["HV"] + assert np.isfinite(hv_gen_2) + # ND should be a sensible count. + nd = result.logbook_stats[-1]["ND"] + assert 1 <= nd <= 4 + + +# --- 2. Determinism ---------------------------------------------------- + + +def test_nsga3_same_seed_produces_same_trajectory( + fake_binary, stress_evaluator, tmp_path_factory, +): + """Two runs, same seed, separate workspaces -> identical trajectories.""" + ws_a = tmp_path_factory.mktemp("det_a") + ws_b = tmp_path_factory.mktemp("det_b") + + def run(ws): + p = _build_problem( + ws, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + return run_nsga3( + p, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=1234, track_hypervolume=False, + ), + ) + + r1 = run(ws_a) + r2 = run(ws_b) + + assert len(r1.pop_library) == len(r2.pop_library) + for gen, (p1, p2) in enumerate(zip(r1.pop_library, r2.pop_library)): + g1 = np.asarray([list(ind) for ind in p1]) + g2 = np.asarray([list(ind) for ind in p2]) + np.testing.assert_allclose( + g1, g2, rtol=0, atol=0, + err_msg=f"gene matrix differs at gen {gen}", + ) + f1 = np.asarray([ind.fitness.values for ind in p1]) + f2 = np.asarray([ind.fitness.values for ind in p2]) + np.testing.assert_allclose( + f1, f2, rtol=0, atol=0, + err_msg=f"fitness matrix differs at gen {gen}", + ) + + +# --- 3. Shared-vs-separate sim parity ---------------------------------- + + +def test_shared_vs_separate_sim_same_initial_stress( + fake_binary, stress_evaluator, tmp_path_factory, +): + """Shared-SimCase and separate-SimCase topologies: initial stress matches. + + Gen 0 of DEAP is a uniform-random sample whose seed is the only + thing affecting the draw. Both topologies use the same bounds + + same seed, so the initial genes match bit-for-bit. The simulation + is deterministic, so the STRESS RMSE (same evaluator in both + topologies) must also match bit-for-bit. + + The slope objective is not compared across topologies because a + fresh slope evaluator in each run may produce slightly different + numerical paths through the smoother - topology B has two + slope evaluators, one per sim case, both getting the same sim + output. Comparing only stress isolates the parity claim. + """ + ws_a = tmp_path_factory.mktemp("topo_a") + ws_b = tmp_path_factory.mktemp("topo_b") + + slope_a = _slope_evaluator(stress_evaluator) + slope_b = _slope_evaluator(stress_evaluator) + + pa = _build_problem( + ws_a, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0), + ObjectiveSpec(slope_a, sim_case=0), + ], + ) + pb = _build_problem( + ws_b, fake_binary, + sim_cases=[SimCase(label="quasi_a"), SimCase(label="quasi_b")], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0), + ObjectiveSpec(slope_b, sim_case=1), + ], + ) + + bounds = Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ) + cfg = RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=7, track_hypervolume=False, + ) + + ra = run_nsga3(pa, bounds, cfg) + rb = run_nsga3(pb, bounds, cfg) + + # Gen-0 genes must match. + ga = np.asarray([list(ind) for ind in ra.pop_library[0]]) + gb = np.asarray([list(ind) for ind in rb.pop_library[0]]) + np.testing.assert_allclose( + ga, gb, rtol=0, atol=0, + err_msg="gen 0 genes differ between topologies", + ) + # Stress RMSE (first objective) must match. + stress_a = np.asarray([ + ind.fitness.values[0] for ind in ra.pop_library[0] + ]) + stress_b = np.asarray([ + ind.fitness.values[0] for ind in rb.pop_library[0] + ]) + np.testing.assert_allclose( + stress_a, stress_b, rtol=1e-12, atol=1e-9, + err_msg="stress RMSE differs between topologies", + ) + + +# --- 4. UNSGA3 on / off ------------------------------------------------ + + +def test_unsga3_off_runs_but_differs_from_on( + fake_binary, stress_evaluator, tmp_path_factory, +): + """UNSGA3 flag changes the trajectory but both paths run cleanly. + + With 2 objectives both should work; UNSGA3 adds a niching step + before variation, so the second-generation population should + differ. This also acts as a smoke test for the non-UNSGA path. + """ + slope = _slope_evaluator(stress_evaluator) + + def run(ws, unsga3): + p = _build_problem( + ws, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0), + ObjectiveSpec(slope, sim_case=0), + ], + ) + return run_nsga3( + p, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=unsga3, ref_dirs_partitions=(4, 0), + seed=11, track_hypervolume=False, + ), + ) + + r_on = run(tmp_path_factory.mktemp("u_on"), True) + r_off = run(tmp_path_factory.mktemp("u_off"), False) + # Both succeed. + assert len(r_on.final_pop) == 4 + assert len(r_off.final_pop) == 4 + # Gen 0 is deterministic (random.seed only) so it should match + # regardless of UNSGA3 (UNSGA3 only runs from gen 1 onward). + g0_on = np.asarray([list(ind) for ind in r_on.pop_library[0]]) + g0_off = np.asarray([list(ind) for ind in r_off.pop_library[0]]) + np.testing.assert_allclose(g0_on, g0_off, rtol=0, atol=0) + # Gen 1+ can differ because UNSGA3 reorders via niching. We don't + # *require* them to differ (tiny pop may coincide), but if they + # match exactly we've not really tested anything. Just ensure + # both paths finished. + + +# --- 5. Checkpoint / resume -------------------------------------------- + + +def test_nsga3_checkpoint_resume_produces_same_final_pop( + fake_binary, stress_evaluator, tmp_path_factory, +): + """Full N-generation run == (first-half + resume second-half). + + Writes a checkpoint at gen 1, resumes from it, runs to gen 3, + then compares against a clean 3-generation run. They should + match bit-for-bit. + + This also serves as the "parity with old driver" test: the + checkpoint format uses the same pickle keys the old driver + uses, so an old checkpoint can be loaded by this driver for a + mid-run framework upgrade. + """ + ws_full = tmp_path_factory.mktemp("ckpt_full") + ws_split = tmp_path_factory.mktemp("ckpt_split") + ckpt_dir = ws_split / "checkpoint_files" + + bounds = Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ) + + # Full run, no checkpoint. + pa = _build_problem( + ws_full, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + r_full = run_nsga3( + pa, bounds, + RunConfig( + n_generations=3, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=321, track_hypervolume=False, + ), + ) + + # Split run part 1: gens 0..1, checkpoint at every gen. + pb1 = _build_problem( + ws_split, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + r_part1 = run_nsga3( + pb1, bounds, + RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=321, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + ), + ) + assert (ckpt_dir / "checkpoint_gen_1.pkl").exists() + + # Split run part 2: resume from gen 1, run through gen 3. + pb2 = _build_problem( + ws_split, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + manifest_name="manifest.jsonl", # same as part 1 + ) + r_part2 = run_nsga3( + pb2, bounds, + RunConfig( + n_generations=3, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=321, # ignored on resume (state comes from pickle) + track_hypervolume=False, + resume_from=ckpt_dir / "checkpoint_gen_1.pkl", + ), + ) + + # Final pops must match. + assert len(r_full.pop_library) == len(r_part2.pop_library) + for gen, (pf, ps) in enumerate(zip( + r_full.pop_library, r_part2.pop_library, + )): + gf = np.asarray([list(ind) for ind in pf]) + gs = np.asarray([list(ind) for ind in ps]) + np.testing.assert_allclose( + gf, gs, rtol=0, atol=0, + err_msg=f"gen {gen} genes differ between full and resumed", + ) + ff = np.asarray([ind.fitness.values for ind in pf]) + fs = np.asarray([ind.fitness.values for ind in ps]) + np.testing.assert_allclose( + ff, fs, rtol=0, atol=0, + err_msg=f"gen {gen} fitness differs between full and resumed", + ) + + +def test_checkpoint_file_has_expected_keys( + workspace, fake_binary, stress_evaluator, +): + """Checkpoint format matches the pre-refactor driver's exact keys. + + This guarantees that old ExaConstit_NSGA3.py checkpoints can + be loaded by the new driver - and vice versa. + """ + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + ckpt_dir = workspace / "ck" + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=0, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + ), + ) + with (ckpt_dir / "checkpoint_gen_1.pkl").open("rb") as f: + ckp = pickle.load(f) + # Must contain every key the pre-refactor driver used. We permit + # one additional key (archive_run_id) that the new driver added + # for archive-aware resume; old drivers can still read these + # pickles because pickle ignores extra keys. + required_keys = { + "pop_library", "iter_tot", "generation", + "fail_count", "stop_count", + "logbook1", "logbook2", "rndstate", + } + assert required_keys.issubset(set(ckp.keys())) + # The new key is always present but may be None when no + # archive was configured for the run. + assert "archive_run_id" in ckp + + +# --- 6. Population sizing ---------------------------------------------- + + +@pytest.mark.parametrize("n_obj,partitions,expected_h,expected_npop", [ + # From the NSGA-III paper: H = C(n_obj + P - 1, P). + (2, (4, 0), 5, 8), # C(5, 4) = 5, round up to 8 + (2, (10, 0), 11, 12), # C(11,10) = 11, round up to 12 + (3, (4, 0), 15, 16), # C(6, 4) = 15, round up to 16 + (4, (4, 0), 35, 36), # C(7, 4) = 35, round up to 36 +]) +def test_reference_points_and_npop(n_obj, partitions, expected_h, expected_npop): + """Das-Dennis count and NPOP-from-H formula match the paper.""" + ref, h = _build_reference_points(n_obj, partitions, (1.0, 0.0)) + assert ref.shape[0] == expected_h + assert ref.shape[1] == n_obj + assert h == expected_h + assert _derive_population_size(n_obj, h) == expected_npop + + +def test_single_obj_reference_points(): + """Single-obj trivially returns one reference direction.""" + ref, h = _build_reference_points(1, (10, 0), (1.0, 0.0)) + assert ref.shape == (1, 1) + assert h == 10 + # NPOP derivation uses h=10 → round up to 12. + assert _derive_population_size(1, h) == 12 + + +# --- 7. Failure detection ---------------------------------------------- + + +def test_is_failed_detects_inf(): + """_is_failed correctly flags infinite fitness values.""" + assert _is_failed((float("inf"),), float("inf")) + assert _is_failed((1.0, float("inf")), float("inf")) + assert not _is_failed((1.0, 2.0), float("inf")) + + +def test_is_failed_detects_nan(): + """NaN also counts as failure (np.isfinite returns False).""" + assert _is_failed((float("nan"),), float("inf")) + + +def test_is_failed_respects_threshold(): + """A custom threshold catches below-inf penalties too.""" + assert _is_failed((1e9,), 1e8) + assert not _is_failed((1.0,), 1e8) + + +def test_fail_limit_raises_runtime_error( + workspace, fake_binary, stress_evaluator, +): + """If all sims fail, fail_limit triggers a RuntimeError. + + Forces every sim to fail via the FAKE_FAIL env var and verifies + that the driver stops with a clear error message instead of + silently continuing. + """ + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + # Override the failure handler to emit inf so fail detection + # triggers (default is ConstantPenaltyFailureHandler(1e8)). + problem.failure_handler = InfinityFailureHandler() + + os.environ["FAKE_FAIL"] = "1" + try: + with pytest.raises(RuntimeError, match="fail_limit"): + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=0, track_hypervolume=False, + fail_limit=3, # short limit so test is fast + ), + ) + finally: + os.environ.pop("FAKE_FAIL", None) + + +# --- 8. Guard rails ---------------------------------------------------- + + +def test_bounds_rejects_upper_le_lower(): + with pytest.raises(ValueError, match="strictly greater"): + Bounds( + lower=np.array([0.0, 1.0]), + upper=np.array([1.0, 1.0]), + ) + + +def test_bounds_rejects_shape_mismatch(): + with pytest.raises(ValueError, match="shape"): + Bounds( + lower=np.array([0.0, 0.0]), + upper=np.array([1.0, 1.0, 1.0]), + ) + + +def test_nsga3_rejects_mismatched_bounds( + workspace, fake_binary, stress_evaluator, +): + """bounds.n_params must match problem.param_names.""" + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + with pytest.raises(ValueError, match="param_names"): + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([0.0, 0.0, 0.0]), # 3 params + upper=np.array([1.0, 1.0, 1.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=0, track_hypervolume=False, + ), + ) + + +# --- 9. End-to-end post-processing -------------------------------------- + + +def test_driver_run_postprocess_pipeline( + workspace, fake_binary, stress_evaluator, +): + """Full pipeline: run → checkpoint → load → extract → read case → plot. + + Proves the workflow_common.postprocess module integrates cleanly + with a real checkpoint from the new driver. Exercises the + entire post-analysis path a real user would take. + """ + from workflow_common.postprocess import ( + best_solution_eudist, + extract_gene_results, + load_case_results, + load_checkpoint, + ) + + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + ckpt_dir = workspace / "ck" + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + ), + ) + + # Step 1: load checkpoint from disk. + ckp = load_checkpoint(ckpt_dir / "checkpoint_gen_1.pkl") + assert ckp.n_pop == 4 + assert ckp.n_dim == 2 + + # Step 2: extract GeneResult records. + all_results = extract_gene_results(ckp.pop_library) + assert len(all_results) == 2 # gen 0, gen 1 + last_gen_results = all_results[-1] + assert len(last_gen_results) == 4 + + # Step 3: pick the best via EUDIST. + fits = np.array([r.fitness for r in last_gen_results]) + best_idx = best_solution_eudist(fits, nsmallest=1) + assert len(best_idx) == 1 + best_result = last_gen_results[best_idx[0]] + + # Step 4: re-read the sim output from disk for that gene. + # This is the replacement for the old ind.stress attribute. + case_results = load_case_results( + best_result, sim_case_idx=0, + resolver=problem.resolver, reader=problem.reader, + ) + assert case_results is not None + stress_df = case_results.df("avg_stress") + assert len(stress_df) > 0 + + # Step 5 (smoke): hand it to the plot function. + # We don't assert anything about the figure content - just that + # the data flows through the plotting helper without errors. + pytest.importorskip("matplotlib") + import matplotlib + + matplotlib.use("Agg") + from workflow_common.postprocess import plot_stress_strain_overlay + + exp_strain = np.linspace(0, 1, 20) + exp_stress = 210.0 + 1900.0 * (1 - np.exp(-48.0 * exp_strain)) + fig, ax = plot_stress_strain_overlay( + sim_strain=stress_df["Time"].to_numpy(), + sim_stress=stress_df["Szz"].to_numpy(), + exp_strain=exp_strain, + exp_stress=exp_stress, + title="best gene, final generation", + ) + assert fig is not None + + + + +# --- 10. Archive + rolling cleanup -------------------------------------- + + +def test_archive_captures_every_case_and_every_generation( + workspace, fake_binary, stress_evaluator, +): + """After a 2-gen run with archive, every case output + every gene + stats + is retrievable from the SQLite DB. + + This is the "archive actually captures what ran" test: without + any cleanup happening, just prove the archive side-effect + works end-to-end through the driver. + """ + from workflow_common import ArchiveDB + + db_path = workspace / "opt.db" + archive = ArchiveDB(db_path) + with archive: + # On a fresh run, the caller creates the archive row + # up front via start_run and passes the UUID into Problem. + # On resume, the driver fills in the UUID from the pickle + # instead — see test_archive_resume_adopts_pickled_run_id. + run_id = archive.start_run( + seed=11, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive, + archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=11, track_hypervolume=False, + ), + ) + + # Query the DB with a fresh read-only connection to prove the + # archive persisted properly (and is independent of the live + # connection used during the run). + with ArchiveDB(db_path, readonly=True) as a: + # Run was ended -> completed_at populated. + run = a.get_run(run_id) + assert run.completed_at is not None + assert run.seed == 11 + + # Three generations: gen 0 (init), gen 1, gen 2. + gens = a.list_generations(run_id) + assert [g.gen_idx for g in gens] == [0, 1, 2] + for g in gens: + assert g.n_pop == 4 + + # Case outputs: at minimum, every (gen, gene) that was + # actually run has its sim output in the DB. The number of + # distinct case rows depends on how many individuals were + # re-evaluated vs. survived selection. We check the floor: + # the initial pop (gen 0) has 4 new sims. + genes_g0 = a.load_genes(run_id, 0) + assert len(genes_g0) == 4 + for g in genes_g0: + rs = a.load_case_outputs( + run_id, + birth_gen=g.birth_gen, birth_gene=g.birth_gene, + sim_case_idx=0, + ) + assert rs is not None + # avg_stress is the required output - must be there. + assert "avg_stress" in rs.tables + assert len(rs.df("avg_stress")) > 0 + + +def test_rolling_cleanup_deletes_older_gen_preserves_recent( + workspace, fake_binary, stress_evaluator, +): + """cleanup_keep_generations=2: after gen N, delete gen N-2. + + Runs for 4 generations. Asserts: + - Gen 0 and gen 1 case dirs are DELETED from disk. + - Gen 2 and gen 3 case dirs REMAIN on disk. + - Archive still has the case outputs for gen 0 and gen 1 + (we can pull them back from pickled BLOBs). + """ + from workflow_common import ArchiveDB + from workflow_common.postprocess import load_case_results_from_archive + + db_path = workspace / "opt.db" + archive = ArchiveDB(db_path) + with archive: + run_id = archive.start_run( + seed=7, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive, + archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=3, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=7, track_hypervolume=False, + cleanup_keep_generations=2, + ), + ) + + # Disk state: dirs under wf/gen_N/gene_X_sc_0. + wf_root = workspace / "wf" + # Gen 0 cleanup triggers at end-of-gen-2 (gen >= keep=2). Gen 1 + # cleanup triggers at end-of-gen-3. Both are gone; gen 2 and + # gen 3 remain (gen 3 is current, gen 2 is the safety margin). + def _has_case_dirs(gen_dir: Path) -> bool: + return gen_dir.exists() and any(gen_dir.iterdir()) + + assert not _has_case_dirs(wf_root / "gen_0"), ( + "gen_0 directory still has case dirs after cleanup" + ) + assert not _has_case_dirs(wf_root / "gen_1"), ( + "gen_1 directory still has case dirs after cleanup" + ) + assert _has_case_dirs(wf_root / "gen_2"), ( + "gen_2 (keep-1 safety margin) was cleaned up prematurely" + ) + assert _has_case_dirs(wf_root / "gen_3"), ( + "gen_3 (current) was cleaned up - wrong!" + ) + + # Archive side: the gen 0 sim outputs are gone from disk but the + # DB still has them. Pull one back and prove it's a real DataFrame. + with ArchiveDB(db_path, readonly=True) as a: + genes_g0 = a.load_genes(run_id, 0) + assert genes_g0 + sample = next(g for g in genes_g0 if g.birth_gen == 0) + # Use a duck-typed stand-in for GeneResult to avoid building + # full postprocess objects here; the function only reads + # .generation and .gene. + gr = type("R", (), { + "generation": sample.birth_gen, + "gene": sample.birth_gene, + })() + rs = load_case_results_from_archive( + a, gr, sim_case_idx=0, run_id=run_id, + ) + assert rs is not None, "gen 0 case output not recoverable from archive" + df = rs.df("avg_stress") + assert len(df) > 0 + assert "Szz" in df.columns + + +def test_cleanup_without_archive_raises_at_run_start( + workspace, fake_binary, stress_evaluator, +): + """cleanup_keep_generations without an archive is a misconfiguration. + + The driver must refuse to start rather than silently destroy + the user's only copy of the simulation output. Failing AT RUN + START (not mid-run) keeps the error localized and obvious. + """ + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + # No archive. + ) + with pytest.raises(ValueError, match="archive"): + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=0, track_hypervolume=False, + cleanup_keep_generations=2, + ), + ) + + +def test_cleanup_keep_generations_zero_rejected( + workspace, fake_binary, stress_evaluator, +): + """cleanup_keep_generations=0 would delete the current gen's dirs; reject it.""" + from workflow_common import ArchiveDB + + with ArchiveDB(workspace / "a.db") as archive: + rid = archive.start_run( + seed=0, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive, archive_run_id=rid, + ) + with pytest.raises(ValueError, match="cleanup_keep_generations"): + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=0, track_hypervolume=False, + cleanup_keep_generations=0, + ), + ) + + +def test_archive_resume_discards_stale_generations( + workspace, fake_binary, stress_evaluator, +): + """Resume from gen 1 pickle: archive rows for gen 2+ get discarded. + + Simulates a mid-run crash. We set up the mid-run state by + running 2 gens fully, then resume from the gen 1 pickle and + run 2 more gens. The archive at the end must contain gens 0..3 + of the RESUMED run, with no leftover gen 2 entries from the + pre-crash sequence (cascade delete via discard_from_generation + cleans them before the phase-2 write). + """ + from workflow_common import ArchiveDB + + db_path = workspace / "opt.db" + ckpt_dir = workspace / "ck" + + # Phase 1: run 2 gens, get a checkpoint at gen 1. + archive = ArchiveDB(db_path) + with archive: + run_id = archive.start_run( + seed=21, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=21, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + ), + ) + + # Confirm phase 1 captured gens 0, 1, 2. + with ArchiveDB(db_path, readonly=True) as a: + phase1_gens = [g.gen_idx for g in a.list_generations(run_id)] + assert phase1_gens == [0, 1, 2] + + # Phase 2: resume from gen 1 pickle, run through gen 3. The + # resume path must discard the phase-1 gen 2 archive rows and + # rewrite them fresh during the resumed run. + archive = ArchiveDB(db_path) + with archive: + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=3, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=21, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + resume_from=ckpt_dir / "checkpoint_gen_1.pkl", + ), + ) + + # Archive should now have gens 0..3, all belonging to the same + # run_id, with no duplicate or stale gen 2 rows. Since genes has + # (run_id, gen_idx, pop_idx) as PRIMARY KEY, stale duplicates + # would have raised IntegrityError during phase 2 - they were + # discarded cleanly, so the write succeeded. + with ArchiveDB(db_path, readonly=True) as a: + final_gens = [g.gen_idx for g in a.list_generations(run_id)] + assert final_gens == [0, 1, 2, 3] + for g in final_gens: + loaded = a.load_genes(run_id, g) + assert len(loaded) == 4 + + +def test_archive_resume_rejects_conflicting_run_id_with_helpful_error( + workspace, fake_binary, stress_evaluator, +): + """Resuming with a Problem whose archive_run_id differs from the + pickled one fails with a message that names the fix. + + This is the error path Robert hit in production: the example + driver called ``archive.start_run()`` unconditionally in main(), + generating a fresh UUID. The library raises rather than silently + continuing with the wrong run_id (which would cross-contaminate + archive rows). The message must name the fix — "only start_run + when not resuming" — so next time the error is diagnosable. + """ + from workflow_common import ArchiveDB + + db_path = workspace / "opt.db" + ckpt_dir = workspace / "ck" + + # Phase 1: one quick run, pickle one checkpoint. + archive = ArchiveDB(db_path) + with archive: + run_id_a = archive.start_run( + seed=42, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive, archive_run_id=run_id_a, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + ), + ) + + # Phase 2: resume, but simulate the driver bug — start a NEW + # archive run and set its fresh UUID on the Problem before + # handing to run_nsga3. The library must refuse. + archive2 = ArchiveDB(db_path) + with archive2: + run_id_b = archive2.start_run( + seed=42, param_names=["yield_stress", "hardening"], + ) + assert run_id_a != run_id_b # different UUIDs + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive2, archive_run_id=run_id_b, + ) + with pytest.raises(ValueError) as excinfo: + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + resume_from=ckpt_dir / "checkpoint_gen_0.pkl", + ), + ) + msg = str(excinfo.value) + # Must name both UUIDs so the user can tell which is which, + # and must name the fix so the next person hitting it doesn't + # need to spelunk the source. + assert run_id_a in msg + assert run_id_b in msg + assert "start_run" in msg + assert "resume" in msg.lower() + + +def test_archive_resume_adopts_pickled_run_id_when_problem_has_none( + workspace, fake_binary, stress_evaluator, +): + """Resume path: Problem built with archive=..., archive_run_id=None + picks up the pickled run_id and continues writing to that row. + + This is the path Robert's example driver takes after the fix: + on resume, the driver does NOT call archive.start_run() — it + leaves archive_run_id=None on the Problem. The library reads + the UUID out of the checkpoint and assigns it before any + writes happen. + + Verifies end-to-end: + * Problem construction accepts the orphan case + (archive set but archive_run_id=None), which used to + be rejected by __init__. + * After resume, wf_problem.archive_run_id holds the + pickled UUID. + * Archive rows are written under the pickled run, not a + new one — list_runs() still shows exactly one run. + """ + from workflow_common import ArchiveDB + + db_path = workspace / "opt.db" + ckpt_dir = workspace / "ck" + + # Phase 1: full-setup fresh run that produces a checkpoint. + archive = ArchiveDB(db_path) + with archive: + pickled_run_id = archive.start_run( + seed=99, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive, archive_run_id=pickled_run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=99, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + ), + ) + + # Phase 2: resume with archive_run_id=None — this is what + # the example driver does on resume. The library fills it in + # from the pickle. + archive2 = ArchiveDB(db_path) + with archive2: + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=archive2, + archive_run_id=None, # <-- the fix in action + ) + assert problem.archive_run_id is None # pre-resume state + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=99, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + resume_from=ckpt_dir / "checkpoint_gen_0.pkl", + ), + ) + # After run_nsga3 the Problem should now carry the + # pickled UUID — library's resume-adoption worked. + assert problem.archive_run_id == pickled_run_id + + # No new run was created — we continued writing to the original. + with ArchiveDB(db_path, readonly=True) as a: + runs = a.list_runs() + assert len(runs) == 1 + assert runs[0].run_id == pickled_run_id + # All generations landed under the one run. + gens = [g.gen_idx for g in a.list_generations(pickled_run_id)] + assert gens == [0, 1, 2] + + +def test_problem_rejects_run_id_without_archive(workspace, fake_binary, stress_evaluator): + """The inverse orphan — run_id without archive — is still a user + error: there's nowhere to write. Caught at construction. + """ + import pytest + with pytest.raises(ValueError, match="archive_run_id=... requires archive"): + _build_problem( + workspace, fake_binary, + sim_cases=[SimCase()], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + archive=None, + archive_run_id="some-orphan-uuid", + ) + + +# --- Logbook .log file writing ------------------------------------------ + + +def test_logbook_files_written_per_generation( + workspace, fake_binary, stress_evaluator, tmp_path, +): + """logbook1_stats.log and logbook2_solutions.log exist and contain + one header + per-gen entries after a run. + + Matches the pre-refactor driver's output. Downstream team tooling + parses these as tab-delimited text; check the shape rather than + exact byte layout so a DEAP formatter tweak doesn't break the + test. + """ + log_dir = tmp_path / "logs" + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + log_dir=log_dir, + ), + ) + + stats_path = log_dir / "logbook1_stats.log" + solutions_path = log_dir / "logbook2_solutions.log" + + # Both files exist. + assert stats_path.is_file(), "logbook1_stats.log not written" + assert solutions_path.is_file(), "logbook2_solutions.log not written" + + # Stats file: one header line + one body line per generation. + # gen_0 + gen_1 + gen_2 = three body lines. The exact header + # text is DEAP's; we just check it's there. + stats_text = stats_path.read_text() + assert "gen" in stats_text + assert "avg" in stats_text and "std" in stats_text + # Count lines that begin with a digit — the body lines. There + # may be a header wrap too, but body lines are the robust count. + body_lines = [ + line for line in stats_text.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + assert len(body_lines) == 3, ( + f"expected 3 gen body lines (0, 1, 2) in stats log, got " + f"{len(body_lines)}:\n{stats_text}" + ) + + # Solutions file: NPOP rows per gen × 3 gens = 12 body rows. + solutions_text = solutions_path.read_text() + solutions_body = [ + line for line in solutions_text.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + assert len(solutions_body) == 12, ( + f"expected 12 individual entries (4 pop × 3 gens), got " + f"{len(solutions_body)}" + ) + + +def test_logbook_files_disabled_by_write_logbook_files_false( + workspace, fake_binary, stress_evaluator, tmp_path, +): + """Setting write_logbook_files=False skips the .log files entirely. + + In-memory logbooks are still populated (so downstream tools that + reach into RunResult still work); only the on-disk artifacts are + suppressed. The opt-out exists for tests and for users running + completely silent / pipe-driven workflows. + """ + log_dir = tmp_path / "logs" + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + log_dir=log_dir, + write_logbook_files=False, + ), + ) + # log_dir may not even exist if the writer wasn't constructed. + assert not (log_dir / "logbook1_stats.log").exists() + assert not (log_dir / "logbook2_solutions.log").exists() + + +def test_logbook_files_rewritten_cleanly_on_resume( + workspace, fake_binary, stress_evaluator, tmp_path, +): + """On resume-from-checkpoint, both .log files are rewritten from the + logbooks in the pickle, then continue to grow incrementally. + + The before-resume run + the after-resume run must collectively + contain exactly one record per generation at the end — no gaps, + no duplicates. Gaps would happen if the writer appended blindly + (it would skip gens already in the pickle); duplicates would + happen if we didn't truncate before replaying. + """ + ckpt_dir = tmp_path / "ckpt" + log_dir = tmp_path / "logs" + + # Initial run: 2 generations, checkpoints each gen. + problem1 = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + run_nsga3( + problem1, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + log_dir=log_dir, + ), + ) + + # Capture the pre-resume state of the two files. + stats_path = log_dir / "logbook1_stats.log" + stats_pre = stats_path.read_text() + stats_body_pre = [ + line for line in stats_pre.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + assert len(stats_body_pre) == 3 # gens 0, 1, 2 + + # Resume from gen 1 (forces gen 2 to be re-run). After the + # resume completes, the .log file should still have exactly 3 + # body lines (0, 1, 2) — the first rewrite from the pickle, + # followed by an incremental append for the re-run gen 2. + problem2 = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi")], + objective_specs=[ObjectiveSpec(stress_evaluator, sim_case=0)], + ) + run_nsga3( + problem2, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=2, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + checkpoint_dir=ckpt_dir, checkpoint_freq=1, + resume_from=ckpt_dir / "checkpoint_gen_1.pkl", + log_dir=log_dir, + ), + ) + + stats_post = stats_path.read_text() + stats_body_post = [ + line for line in stats_post.splitlines() + if line.strip() and line.strip().split()[0].isdigit() + ] + # Exactly 3 body lines still — no duplicates, no gaps. + assert len(stats_body_post) == 3, ( + f"expected 3 body lines after resume, got {len(stats_body_post)}:\n" + f"{stats_post}" + ) + + +# --- Experimental data archiving ---------------------------------------- + + +def test_run_archives_experimental_data_per_sim_case( + workspace, fake_binary, stress_evaluator, exp_df, +): + """The driver must store evaluator.experimental into the archive + once archive_run_id is known. Plotting tools rely on this so users + don't have to re-supply CSV paths post-run. + """ + from workflow_common import ArchiveDB + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=99, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi"), SimCase(label="dynamic")], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0, label="stress_0"), + ObjectiveSpec(stress_evaluator, sim_case=1, label="stress_1"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=99, track_hypervolume=False, + ), + ) + + # Archive must now have experimental data for both SimCases. + with ArchiveDB(db, readonly=True) as a: + listing = a.list_experiments(run_id) + assert listing == [(0, "quasi"), (1, "dynamic")] + result0 = a.load_experiment(run_id, 0) + assert result0 is not None + label0, df0 = result0 + assert label0 == "quasi" + # Round-trip the original experimental DataFrame faithfully. + pd.testing.assert_frame_equal(df0, exp_df) + + +def test_run_skips_archive_for_evaluators_without_experimental_data( + workspace, fake_binary, +): + """An evaluator that doesn't expose .experimental shouldn't crash + the archiving step. The SimCase just won't have experimental data + recorded, and the plotter falls back gracefully later. + """ + from workflow_common import ArchiveDB + from workflow_common.objectives import ObjectiveEvaluator + + class _NoExpEvaluator(ObjectiveEvaluator): + # Custom evaluator with no experimental DataFrame. + # Returns a value that varies with the gene so NSGA-III's + # niching has something to work with (a constant returner + # would cause divide-by-zero in the reference-point step). + def evaluate(self, results, ctx): + # Use the case context's gene index as a stand-in + # variation source. + return 0.5 + 0.01 * ctx.gene + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=1, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="custom")], + objective_specs=[ + ObjectiveSpec(_NoExpEvaluator(), sim_case=0, label="custom"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=1, track_hypervolume=False, + ), + ) + + # No experimental data was recorded, but the run still succeeded. + with ArchiveDB(db, readonly=True) as a: + assert a.list_experiments(run_id) == [] + + +def test_run_archives_minmax_strain_from_case_data( + workspace, fake_binary, stress_evaluator, +): + """case_data['minmax_strain'] must flow through the driver into + the archive's experiments.minmax_strain column. This is the + plumbing that lets the plotter shade the optimization window + without the user re-supplying it post-run. + """ + from workflow_common import ArchiveDB + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=42, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[ + SimCase(label="windowed", + case_data={"minmax_strain": (0.005, 0.13)}), + SimCase(label="upper_only", + case_data={"minmax_strain": (None, 0.10)}), + SimCase(label="no_window", case_data={}), + ], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0, label="a"), + ObjectiveSpec(stress_evaluator, sim_case=1, label="b"), + ObjectiveSpec(stress_evaluator, sim_case=2, label="c"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + ), + ) + + with ArchiveDB(db, readonly=True) as a: + assert a.load_experiment_window(run_id, 0) == (0.005, 0.13) + assert a.load_experiment_window(run_id, 1) == (None, 0.10) + # SimCase 2 didn't supply a window — None, not (None, None). + assert a.load_experiment_window(run_id, 2) is None + + +def test_run_handles_malformed_minmax_strain_gracefully( + workspace, fake_binary, stress_evaluator, caplog, +): + """A misformatted minmax_strain (e.g. a string instead of a tuple) + shouldn't abort the run. The driver should log a warning, drop + the window, and still record the experiment so the rest of the + plotter pipeline keeps working. + """ + import logging + from workflow_common import ArchiveDB + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=7, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[ + SimCase(label="bad", + case_data={"minmax_strain": "not a tuple"}), + ], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0, label="a"), + ], + archive=archive, archive_run_id=run_id, + ) + with caplog.at_level(logging.WARNING): + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=7, track_hypervolume=False, + ), + ) + + # Run completed; experiment recorded but window dropped. + with ArchiveDB(db, readonly=True) as a: + assert a.load_experiment(run_id, 0) is not None + assert a.load_experiment_window(run_id, 0) is None + # Warning was logged so user can find the misconfiguration. + assert any( + "minmax_strain" in rec.message + for rec in caplog.records + ) + + +def test_run_archives_extractor_config_from_evaluator( + workspace, fake_binary, stress_evaluator, +): + """``evaluator.extractor.to_dict()`` must flow through the driver + to ``archive.load_extractor_config``. Without this, post-run + plotters fall back to default extractor settings that may not + match what the optimizer actually used — silently producing + different curves than the run scored against (Robert's + 'only 1 of 2 SimCases plotted' bug). + """ + from workflow_common import ArchiveDB + from workflow_common.objectives import StressStrainExtractor + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=11, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi"), SimCase(label="dynamic")], + objective_specs=[ + ObjectiveSpec(stress_evaluator, sim_case=0, label="a"), + ObjectiveSpec(stress_evaluator, sim_case=1, label="b"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=11, track_hypervolume=False, + ), + ) + + # The fixture's stress_evaluator uses time_rate strain with + # strain_rate=1.0 — those settings should round-trip into the + # archive verbatim. Both SimCases share the same evaluator, + # so both archived configs should match. + with ArchiveDB(db, readonly=True) as a: + for sc_idx in (0, 1): + cfg = a.load_extractor_config(run_id, sc_idx) + assert cfg is not None, ( + f"sim_case {sc_idx} has no archived extractor config" + ) + assert cfg["strain_source"] == "time_rate" + assert cfg["strain_rate"] == 1.0 + # Reconstruct should match the original exactly. + rebuilt = StressStrainExtractor.from_dict(cfg) + assert rebuilt == stress_evaluator.extractor + + +def test_run_skips_extractor_config_for_evaluators_without_one( + workspace, fake_binary, +): + """An evaluator without an ``.extractor`` attribute (custom + user-defined) shouldn't crash the archiving step. The + experiment is still recorded; extractor_config column for + that SimCase stays NULL; the plotter falls back to its + default extractor with a warning. + """ + from workflow_common import ArchiveDB + from workflow_common.objectives import ObjectiveEvaluator + + class _CustomEvaluator(ObjectiveEvaluator): + # No .extractor and no .experimental — but this evaluator + # type is fine to optimize against, just doesn't auto-archive. + # We give it an experimental DataFrame so the experiment + # still gets stored, just without an extractor config. + def __init__(self): + self.experimental = pd.DataFrame({ + "strain": np.linspace(0, 0.1, 5), + "stress": np.linspace(100, 200, 5), + }) + + def evaluate(self, results, ctx): + return 0.5 + 0.01 * ctx.gene + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=2, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="custom")], + objective_specs=[ + ObjectiveSpec(_CustomEvaluator(), sim_case=0, label="x"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=2, track_hypervolume=False, + ), + ) + + # Experiment recorded; extractor_config absent. + with ArchiveDB(db, readonly=True) as a: + assert a.load_experiment(run_id, 0) is not None + assert a.load_extractor_config(run_id, 0) is None + + +def test_run_archives_full_range_curves_per_gene_and_simcase( + workspace, fake_binary, stress_evaluator, +): + """Every successfully-evaluated (gene, sim_case) pair must yield a + case_curve row covering the FULL extraction range. The window + (when supplied via the extractor) is stripped before extraction + so re-analysis with different metrics or windows can use the + archived curve directly. + + This pins Robert's ask: 'we should always make sure to save off + the independent and dependent variables related to our objective + functions for the full simulation range.' + """ + from workflow_common import ArchiveDB + from workflow_common.objectives import StressStrainExtractor, StressStrainObjective + + # Use an evaluator whose extractor has a window so the + # window-stripping logic gets exercised. The optimizer scores + # against the windowed curve; the archive stores the full one. + exp_df = stress_evaluator.experimental + windowed_extractor = StressStrainExtractor( + strain_source="time_rate", strain_rate=1.0, + window=(0.05, 0.5), + ) + windowed_evaluator = StressStrainObjective( + experimental=exp_df, + extractor=windowed_extractor, + ) + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=42, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="quasi"), SimCase(label="dynamic")], + objective_specs=[ + ObjectiveSpec(windowed_evaluator, sim_case=0, label="a"), + ObjectiveSpec(windowed_evaluator, sim_case=1, label="b"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=42, track_hypervolume=False, + ), + ) + + with ArchiveDB(db, readonly=True) as a: + # 4 genes × 2 SimCases × 2 generations (gen 0 + 1 transition, + # per n_generations semantics) = 16 rows expected. + cur = a._conn.execute( + "SELECT COUNT(*) FROM case_curves WHERE run_id=?", (run_id,), + ) + n_rows = cur.fetchone()[0] + assert n_rows == 16, f"expected 16 case_curves rows, got {n_rows}" + + # Pick one and confirm the strain range exceeds the window. + # Window was (0.05, 0.5); time_rate strain at rate=1.0 with + # the fake binary's t in [0,1] gives strain in [0,1]. So the + # archived curve should span (or come close to) [0,1], not + # be clipped to [0.05, 0.5]. + result = a.load_case_curve( + run_id, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) + assert result is not None + ind, dep, ind_label, dep_label = result + # Strain min should be <= 0.05 (i.e., extends below the + # window's lower bound — proof window was stripped). + assert ind.min() <= 0.05 + 1e-6, ( + f"archived strain min={ind.min()} exceeds window lo=0.05; " + f"window may not have been stripped before extraction" + ) + # Strain max should be >= 0.5 (extends above window upper). + assert ind.max() >= 0.5 - 1e-6, ( + f"archived strain max={ind.max()} below window hi=0.5" + ) + assert ind_label == "strain" + assert dep_label == "stress" + + +def test_run_skips_curve_archive_for_evaluators_without_extractor( + workspace, fake_binary, +): + """Custom evaluators without an ``.extractor`` attribute don't + crash the run — they just don't contribute case_curves rows.""" + from workflow_common import ArchiveDB + from workflow_common.objectives import ObjectiveEvaluator + + class _CustomEvaluator(ObjectiveEvaluator): + def __init__(self): + self.experimental = pd.DataFrame({ + "strain": [0.0, 0.1], "stress": [100.0, 200.0], + }) + + def evaluate(self, results, ctx): + return 0.5 + 0.01 * ctx.gene + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=2, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="custom")], + objective_specs=[ + ObjectiveSpec(_CustomEvaluator(), sim_case=0, label="x"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=2, track_hypervolume=False, + ), + ) + + with ArchiveDB(db, readonly=True) as a: + # No case_curves rows — extractor unavailable. + cur = a._conn.execute( + "SELECT COUNT(*) FROM case_curves WHERE run_id=?", (run_id,), + ) + assert cur.fetchone()[0] == 0 + + +def test_run_archives_curves_with_sign_matched_to_experimental( + workspace, fake_binary, +): + """When the extractor produces positive-signed sim curves but + the evaluator's ``.experimental`` is compression-shaped, the + framework should sign-match the simulated curve before archiving. + + Pins Robert's ask: a defensive check at save-off so the archive + holds curves consistent with the experimental data. + """ + from workflow_common import ArchiveDB + from workflow_common.objectives import ( + ObjectiveEvaluator, StressStrainExtractor, + ) + + # Stub evaluator that exposes both ``.extractor`` and + # ``.experimental`` (so Problem's curve-archiving logic finds + # them and pairs them) but has an ``evaluate`` method that + # returns finite values regardless of any sim/exp mismatch — + # avoids DEAP crashing on all-infinite fitnesses while still + # exercising the sign-correction path. + class _StubEvaluator(ObjectiveEvaluator): + def __init__(self): + self.extractor = StressStrainExtractor( + strain_source="time_rate", strain_rate=1.0, + ) + # Compression-shaped reference: negative strain, + # negative stress. + exp_strain = np.linspace(0.0, -1.0, 25) + exp_stress = -210.0 - 1900.0 * ( + 1 - np.exp(48.0 * exp_strain) + ) + self.experimental = pd.DataFrame({ + "strain": exp_strain, "stress": exp_stress, + }) + self.experimental_strain_col = "strain" + self.experimental_stress_col = "stress" + + def evaluate(self, results, ctx): + return 0.5 + 0.01 * ctx.gene + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=7, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="compression")], + objective_specs=[ + ObjectiveSpec(_StubEvaluator(), sim_case=0, label="x"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=7, track_hypervolume=False, + ), + ) + + with ArchiveDB(db, readonly=True) as a: + # Pull one archived curve and check signs. + result = a.load_case_curve( + run_id, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) + assert result is not None + ind, dep, _, _ = result + # Both axes should have non-positive dominant signs + # (matching exp), even though the raw simulation produced + # positive values for each. + assert ind[int(np.argmax(np.abs(ind)))] <= 0, ( + f"strain not sign-corrected; dominant value=" + f"{ind[int(np.argmax(np.abs(ind)))]}" + ) + assert dep[int(np.argmax(np.abs(dep)))] <= 0, ( + f"stress not sign-corrected; dominant value=" + f"{dep[int(np.argmax(np.abs(dep)))]}" + ) + + +def test_run_archives_curves_unchanged_when_no_experimental_data( + workspace, fake_binary, +): + """An evaluator without experimental data leaves the sim curve + untouched (no reference to align against). The raw extractor + output is what gets archived. + """ + from workflow_common import ArchiveDB + from workflow_common.objectives import ( + ObjectiveEvaluator, StressStrainExtractor, + ) + + # Custom evaluator with .extractor but no .experimental. + class _NoExpEvaluator(ObjectiveEvaluator): + def __init__(self): + self.extractor = StressStrainExtractor( + strain_source="time_rate", strain_rate=1.0, + ) + # No .experimental attribute. + + def evaluate(self, results, ctx): + return 0.5 + 0.01 * ctx.gene + + db = workspace / "opt.db" + archive = ArchiveDB(db) + with archive: + run_id = archive.start_run( + seed=8, param_names=["yield_stress", "hardening"], + ) + problem = _build_problem( + workspace, fake_binary, + sim_cases=[SimCase(label="ne")], + objective_specs=[ + ObjectiveSpec(_NoExpEvaluator(), sim_case=0, label="x"), + ], + archive=archive, archive_run_id=run_id, + ) + run_nsga3( + problem, + bounds=Bounds( + lower=np.array([150.0, 1500.0]), + upper=np.array([300.0, 2500.0]), + ), + config=RunConfig( + n_generations=1, population_size=4, + unsga3=True, ref_dirs_partitions=(4, 0), + seed=8, track_hypervolume=False, + ), + ) + + with ArchiveDB(db, readonly=True) as a: + result = a.load_case_curve( + run_id, birth_gen=0, birth_gene=0, sim_case_idx=0, + ) + assert result is not None + ind, dep, _, _ = result + # fake_binary produces positive Szz and positive strain, + # so without sign-matching both should remain positive. + assert ind[int(np.argmax(np.abs(ind)))] >= 0 + assert dep[int(np.argmax(np.abs(dep)))] >= 0 diff --git a/workflows/exaconstit-calibrate/tests/test_objectives.py b/workflows/exaconstit-calibrate/tests/test_objectives.py new file mode 100644 index 0000000..d9639f3 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_objectives.py @@ -0,0 +1,718 @@ +""" +Unit tests for :mod:`workflow_common.objectives`. + +Covers the extractor (with its three strain-derivation modes), the +shipped :class:`StressStrainObjective` evaluator, each error metric, +and the three :class:`FailureHandler` policies. +""" +from __future__ import annotations + +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from workflow_common import ( + CaseContext, + CaseResultSet, + ConstantPenaltyFailureHandler, + ERROR_METRICS, + FailureHandler, + InfinityFailureHandler, + LegacyLinearSmoother, + ObjectiveEvaluator, + PartialProgressFailureHandler, + PchipSmoother, + StressStrainExtractor, + StressStrainObjective, + TabularResult, + mae, + max_abs_error, + rmse, +) + + +# --- helpers ------------------------------------------------------------- + + +def _make_result_set( + stress_df: pd.DataFrame, + def_grad_df: pd.DataFrame = None, + *, + strain_output_name: str = "avg_def_grad", +) -> CaseResultSet: + """Build a CaseResultSet in memory for a test. + + The ``strain_output_name`` knob lets tests build a result set + where the strain-source table is registered under a non-default + name (e.g. ``"avg_lagrangian_strain"`` for a direct-strain test). + """ + ctx = CaseContext(generation=0, gene=0, obj=0) + tables = { + "avg_stress": TabularResult( + name="avg_stress", df=stress_df, source_path=Path("fake/avg_stress.txt"), + ), + } + if def_grad_df is not None: + tables[strain_output_name] = TabularResult( + name=strain_output_name, df=def_grad_df, + source_path=Path(f"fake/{strain_output_name}.txt"), + ) + return CaseResultSet(ctx=ctx, tables=tables) + + +def _voce_stress(strain: np.ndarray, y0: float, H: float, k: float) -> np.ndarray: + """Voce-law saturating stress response.""" + return y0 + H * (1 - np.exp(-k * strain)) + + +# --- Error metrics ------------------------------------------------------- + + +def test_rmse_zero_for_identical(): + a = np.array([1.0, 2.0, 3.0]) + assert rmse(a, a) == 0.0 + + +def test_rmse_known_value(): + """Sanity on a hand-computable example.""" + sim = np.array([1.0, 2.0, 3.0]) + exp = np.array([2.0, 2.0, 2.0]) + # residual: [-1, 0, 1], sqrt(mean(1, 0, 1)) = sqrt(2/3) + assert rmse(sim, exp) == pytest.approx(np.sqrt(2 / 3)) + + +def test_mae_and_max_abs(): + sim = np.array([1.0, 2.0, 3.0]) + exp = np.array([2.0, 2.0, 0.0]) + # residual: [-1, 0, 3], abs: [1, 0, 3] + assert mae(sim, exp) == pytest.approx(4 / 3) + assert max_abs_error(sim, exp) == pytest.approx(3.0) + + +def test_error_metrics_table_keys(): + """The ERROR_METRICS table should expose at least the three defaults.""" + assert {"rmse", "mae", "max_abs"}.issubset(ERROR_METRICS.keys()) + + +# --- StressStrainExtractor ----------------------------------------------- + + +def test_extractor_biot_strain(): + """Biot strain: strain = X - 1, where X is the axial stretch.""" + t = np.linspace(0, 1, 10) + stress_df = pd.DataFrame({ + "Time": t, + "Szz": _voce_stress(t * 0.01, 200, 2000, 50), + "Sxx": np.zeros_like(t), + "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), + "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + F33 = 1.0 + 0.01 * t + dg_df = pd.DataFrame({"Time": t, "F33": F33}) + + extractor = StressStrainExtractor() # default: biot + strain, stress = extractor.extract(_make_result_set(stress_df, dg_df)) + assert np.allclose(strain, 0.01 * t) + assert len(stress) == len(t) + + +def test_extractor_log_strain(): + """Hencky / log strain: strain = log(F33).""" + t = np.linspace(0, 1, 10) + stress_df = pd.DataFrame({ + "Time": t, + "Szz": np.ones_like(t), + "Sxx": np.zeros_like(t), + "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), + "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + F33 = 1.0 + 0.5 * t + dg_df = pd.DataFrame({"Time": t, "F33": F33}) + + extractor = StressStrainExtractor(strain_source="log") + strain, _ = extractor.extract(_make_result_set(stress_df, dg_df)) + assert np.allclose(strain, np.log(F33)) + + +def test_extractor_time_rate(): + """Constant-rate: strain = rate * time. No def-grad needed.""" + t = np.linspace(0, 1, 10) + stress_df = pd.DataFrame({ + "Time": t, "Szz": np.ones_like(t), "Sxx": np.zeros_like(t), + "Syy": np.zeros_like(t), "Sxy": np.zeros_like(t), + "Syz": np.zeros_like(t), "Sxz": np.zeros_like(t), + }) + extractor = StressStrainExtractor( + strain_source="time_rate", strain_rate=2e-3, + ) + strain, _ = extractor.extract(_make_result_set(stress_df)) + assert np.allclose(strain, 2e-3 * t) + + +def test_extractor_time_rate_requires_strain_rate(): + t = np.linspace(0, 1, 5) + stress_df = pd.DataFrame({ + "Time": t, "Szz": np.zeros_like(t), "Sxx": np.zeros_like(t), + "Syy": np.zeros_like(t), "Sxy": np.zeros_like(t), + "Syz": np.zeros_like(t), "Sxz": np.zeros_like(t), + }) + extractor = StressStrainExtractor(strain_source="time_rate") # no rate + with pytest.raises(ValueError, match="strain_rate"): + extractor.extract(_make_result_set(stress_df)) + + +def test_extractor_log_strain_rejects_nonpositive(): + """``log`` strain requires the axial stretch > 0 everywhere; zero or negative raises.""" + t = np.linspace(0, 1, 5) + stress_df = pd.DataFrame({ + "Time": t, "Szz": np.zeros_like(t), "Sxx": np.zeros_like(t), + "Syy": np.zeros_like(t), "Sxy": np.zeros_like(t), + "Syz": np.zeros_like(t), "Sxz": np.zeros_like(t), + }) + dg_df = pd.DataFrame({"Time": t, "F33": np.array([1, 0.5, 0, 1, 1])}) + extractor = StressStrainExtractor(strain_source="log") + with pytest.raises(ValueError, match="F33 > 0"): + extractor.extract(_make_result_set(stress_df, dg_df)) + + +def test_extractor_missing_def_grad_raises(): + """Biot/log strain need the strain-source file; without it we get a clear KeyError.""" + t = np.linspace(0, 1, 5) + stress_df = pd.DataFrame({ + "Time": t, "Szz": np.zeros_like(t), "Sxx": np.zeros_like(t), + "Syy": np.zeros_like(t), "Sxy": np.zeros_like(t), + "Syz": np.zeros_like(t), "Sxz": np.zeros_like(t), + }) + # no dg passed in + extractor = StressStrainExtractor() + with pytest.raises(KeyError, match="avg_def_grad"): + extractor.extract(_make_result_set(stress_df)) + + +def test_extractor_detects_nan_in_output(): + """Non-finite values in sim output are a red flag; raise.""" + t = np.linspace(0, 1, 5) + stress = np.array([100.0, 200.0, np.nan, 400.0, 500.0]) + stress_df = pd.DataFrame({ + "Time": t, "Szz": stress, "Sxx": np.zeros_like(t), + "Syy": np.zeros_like(t), "Sxy": np.zeros_like(t), + "Syz": np.zeros_like(t), "Sxz": np.zeros_like(t), + }) + dg_df = pd.DataFrame({"Time": t, "F33": 1.0 + 0.01 * t}) + extractor = StressStrainExtractor() + with pytest.raises(ValueError, match="NaN"): + extractor.extract(_make_result_set(stress_df, dg_df)) + + +# --- StressStrainObjective ----------------------------------------------- + + +def _exp_df() -> pd.DataFrame: + """Simple exp reference with matching Voce parameters.""" + strain = np.linspace(0, 0.01, 30) + stress = _voce_stress(strain, 200, 2000, 50) + return pd.DataFrame({"strain": strain, "stress": stress}) + + +def _sim_result(y0=200, H=2000, k=50) -> CaseResultSet: + """Sim result for Voce params y0, H, k over strain 0..0.01.""" + t = np.linspace(0, 1, 50) + strain = 0.01 * t + stress = _voce_stress(strain, y0, H, k) + stress_df = pd.DataFrame({ + "Time": t, "Szz": stress, + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + dg_df = pd.DataFrame({"Time": t, "F33": 1.0 + strain}) + return _make_result_set(stress_df, dg_df) + + +def test_objective_zero_when_sim_matches_exp(): + """Identical Voce params => near-zero RMSE (up to smoothing error).""" + exp_df = _exp_df() + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + ) + err = evaluator.evaluate(_sim_result(), CaseContext(0, 0, 0)) + # Won't be exactly 0 because PCHIP resamples on a different grid. + assert err < 1.0, f"expected near-zero, got {err}" + + +def test_objective_positive_when_sim_differs(): + """Different Voce params produce a measurable error.""" + exp_df = _exp_df() + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + ) + # Shift yield stress by 50 MPa. + err = evaluator.evaluate(_sim_result(y0=250), CaseContext(0, 0, 0)) + assert err > 10.0 + + +def test_objective_respects_custom_metric(): + """A callable metric is honored.""" + exp_df = _exp_df() + + def constant(sim, exp): + return 42.0 + + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + metric=constant, + ) + assert evaluator.evaluate(_sim_result(), CaseContext(0, 0, 0)) == 42.0 + + +def test_objective_respects_named_metric(): + """String names 'rmse', 'mae', 'max_abs' resolve.""" + exp_df = _exp_df() + for name in ("rmse", "mae", "max_abs"): + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + metric=name, + ) + err = evaluator.evaluate(_sim_result(y0=250), CaseContext(0, 0, 0)) + assert err > 0 + + +def test_objective_unknown_metric_raises(): + exp_df = _exp_df() + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + metric="nonexistent", + ) + with pytest.raises(ValueError, match="unknown metric"): + evaluator.evaluate(_sim_result(), CaseContext(0, 0, 0)) + + +def test_objective_no_strain_overlap_raises(): + """Sim and exp with disjoint strain ranges should raise, not silently zero.""" + exp_df = pd.DataFrame({ + "strain": np.linspace(5.0, 10.0, 20), + "stress": np.linspace(100.0, 200.0, 20), + }) + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + ) + with pytest.raises(ValueError, match="overlap"): + evaluator.evaluate(_sim_result(), CaseContext(0, 0, 0)) + + +def test_objective_protocol_conformance(): + exp_df = _exp_df() + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + ) + assert isinstance(evaluator, ObjectiveEvaluator) + + +def test_objective_alternate_smoother(): + """LegacyLinearSmoother is an acceptable drop-in.""" + exp_df = _exp_df() + evaluator = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + smoother=LegacyLinearSmoother(n_samples=200), + ) + err = evaluator.evaluate(_sim_result(y0=250), CaseContext(0, 0, 0)) + assert err > 0 + + +# --- FailureHandlers ----------------------------------------------------- + + +def test_infinity_handler(): + h = InfinityFailureHandler() + v = h.on_failure(CaseContext(0, 0, 0), "timeout", None) + assert v == float("inf") + + +def test_infinity_handler_custom_value(): + h = InfinityFailureHandler(value=1e18) + v = h.on_failure(CaseContext(0, 0, 0), "timeout", None) + assert v == 1e18 + + +def test_constant_penalty_handler(): + h = ConstantPenaltyFailureHandler(penalty=999.0) + assert h.on_failure(CaseContext(0, 0, 0), "rc=7", None) == 999.0 + + +def test_partial_progress_no_results_returns_base_penalty(): + exp_df = _exp_df() + inner = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + ) + h = PartialProgressFailureHandler( + inner_evaluator=inner, base_penalty=1e6, + ) + v = h.on_failure(CaseContext(0, 0, 0), "no output", None) + assert v == 1e6 + + +def test_partial_progress_half_way(): + """A sim that got halfway through should get roughly half the penalty.""" + exp_df = _exp_df() + inner = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + ) + h = PartialProgressFailureHandler( + inner_evaluator=inner, + base_penalty=1e6, + progress_weight=1.0, + strain_target=0.01, + ) + # Partial sim: only first 25 rows (strain 0..0.005, half of 0.01). + t = np.linspace(0, 0.5, 25) + strain = 0.01 * t + stress = _voce_stress(strain, 200, 2000, 50) + stress_df = pd.DataFrame({ + "Time": t, "Szz": stress, + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + dg_df = pd.DataFrame({"Time": t, "F33": 1.0 + strain}) + partial = _make_result_set(stress_df, dg_df) + + v = h.on_failure(CaseContext(0, 0, 0), "timeout", partial) + # Roughly 50% of base_penalty since progress_fraction ~ 0.5. + # Plus a small contribution from the inner error (close to 0 for + # matching parameters). + assert 4e5 < v < 6e5 + + +def test_failure_handlers_protocol_conformance(): + exp_df = _exp_df() + inner = StressStrainObjective( + experimental=exp_df, + extractor=StressStrainExtractor(), + ) + assert isinstance(InfinityFailureHandler(), FailureHandler) + assert isinstance(ConstantPenaltyFailureHandler(), FailureHandler) + assert isinstance( + PartialProgressFailureHandler(inner_evaluator=inner), FailureHandler + ) + + +def test_extractor_supports_non_exaconstit_column_convention(): + """The framework lives in ExaConstit and defaults to ExaConstit's + column convention (Szz / F33 / Time). Users calibrating a different + FEM code whose output uses a different convention (s11 / F11 / time, + or anything else) can still drive the extractor by supplying + explicit column names. This test verifies that override path stays + working — the framework provides ExaConstit defaults but doesn't + hard-code them anywhere in the logic. + """ + t = np.linspace(0, 1, 10) + # Fake a non-ExaConstit code's output: lowercase "time", "s11" as + # the axial stress, no Volume column. + stress_df = pd.DataFrame({ + "time": t, + "s11": _voce_stress(t * 0.01, 200, 2000, 50), + }) + dg_df = pd.DataFrame({"time": t, "F11": 1.0 + 0.01 * t}) + + extractor = StressStrainExtractor( + stress_column="s11", + strain_source_column="F11", + time_column="time", + # strain_source left at default ("biot") — applies the X-1 + # formula to whatever strain_source_column names. + ) + strain, stress = extractor.extract(_make_result_set(stress_df, dg_df)) + assert np.allclose(strain, 0.01 * t) + assert len(stress) == len(t) + # Sanity: default-construction against the same non-ExaConstit + # DataFrame fails loudly (ExaConstit default Szz not in columns). + default_extractor = StressStrainExtractor() + with pytest.raises(KeyError, match="Szz"): + default_extractor.extract(_make_result_set(stress_df, dg_df)) + + + + + +def test_extractor_direct_strain_reads_column_verbatim(): + """``strain_source='direct'`` reads a strain measure already on disk. + + Use case: the simulation outputs Lagrange/Euler/Biot strain in + its own file, and the user wants to consume it directly rather + than recomputing from F. The extractor's job becomes "pull the + column" with no math applied. + """ + t = np.linspace(0, 1, 10) + stress_df = pd.DataFrame({ + "Time": t, "Szz": _voce_stress(t * 0.05, 200, 2000, 50), + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + # Simulate a Lagrange-strain output file with E33 already + # computed by the sim. + e33 = 0.5 * ((1.0 + 0.05 * t) ** 2 - 1.0) + strain_df = pd.DataFrame({"Time": t, "E33": e33}) + extractor = StressStrainExtractor( + strain_source="direct", + strain_source_output="avg_lagrangian_strain", + strain_source_column="E33", + ) + rs = _make_result_set(stress_df, strain_df, + strain_output_name="avg_lagrangian_strain") + strain, stress = extractor.extract(rs) + # No transformation — strain comes straight from the column. + assert np.allclose(strain, e33) + assert len(stress) == len(t) + + +def test_extractor_window_crops_to_strain_interval(): + """``window=(lo, hi)`` keeps only points where lo <= |strain| <= hi. + + This is the optimization-restriction feature: skip the elastic + regime, ignore the elastic-plastic transition, focus the + optimizer on the plastic portion. + """ + t = np.linspace(0, 1, 21) + stress_df = pd.DataFrame({ + "Time": t, "Szz": _voce_stress(t * 0.1, 200, 2000, 50), + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + # Strain ramps 0 -> 0.1 (Biot). + dg_df = pd.DataFrame({"Time": t, "F33": 1.0 + 0.1 * t}) + extractor = StressStrainExtractor(window=(0.02, 0.08)) + strain, stress = extractor.extract(_make_result_set(stress_df, dg_df)) + # Every kept point lies in the interval. + assert np.all(strain >= 0.02 - 1e-12) + assert np.all(strain <= 0.08 + 1e-12) + # Same length on both sides. + assert len(strain) == len(stress) + # Original had 21 points; cropping to [0.02, 0.08] keeps roughly + # the middle 13 points (indices 4..16 inclusive on a uniform grid). + assert 10 <= len(strain) <= 14 + + +def test_extractor_window_works_for_compression_via_abs(): + """Window comparison uses ``|strain|`` so a compression run with + negative strain values gets the same windowing as tension. + """ + t = np.linspace(0, 1, 11) + stress_df = pd.DataFrame({ + "Time": t, "Szz": -1.0 * _voce_stress(t * 0.1, 200, 2000, 50), + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + # Compression: F33 < 1, strain < 0. + dg_df = pd.DataFrame({"Time": t, "F33": 1.0 - 0.1 * t}) + extractor = StressStrainExtractor(window=(0.02, 0.08)) + strain, _ = extractor.extract(_make_result_set(stress_df, dg_df)) + # Returned strain is still negative (window doesn't change sign); + # only the |strain| satisfies the bounds. + assert np.all(strain <= 0.0) + assert np.all(np.abs(strain) >= 0.02 - 1e-12) + assert np.all(np.abs(strain) <= 0.08 + 1e-12) + + +def test_extractor_window_excludes_all_raises(): + """A window with no overlap with the actual strain range fails loudly.""" + t = np.linspace(0, 1, 5) + stress_df = pd.DataFrame({ + "Time": t, "Szz": np.zeros_like(t), + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + dg_df = pd.DataFrame({"Time": t, "F33": 1.0 + 0.01 * t}) + # Strain range is [0, 0.01]; window at [10, 100] is way out. + extractor = StressStrainExtractor(window=(10.0, 100.0)) + with pytest.raises(ValueError, match="excluded every data point"): + extractor.extract(_make_result_set(stress_df, dg_df)) + + +def test_extractor_window_bad_bounds_raises(): + """``window=(hi, lo)`` is operator error; flag at extract time.""" + t = np.linspace(0, 1, 5) + stress_df = pd.DataFrame({ + "Time": t, "Szz": np.zeros_like(t), + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Sxy": np.zeros_like(t), "Syz": np.zeros_like(t), + "Sxz": np.zeros_like(t), + }) + dg_df = pd.DataFrame({"Time": t, "F33": 1.0 + 0.01 * t}) + extractor = StressStrainExtractor(window=(0.05, 0.01)) # inverted + with pytest.raises(ValueError, match="lower bound .* exceeds"): + extractor.extract(_make_result_set(stress_df, dg_df)) + + +# --- StressStrainExtractor JSON serialization --------------------------- + + +def test_extractor_to_dict_round_trips_through_json(): + """to_dict -> json.dumps -> json.loads -> from_dict is identity. + + Used by the archive to persist run-time extractor configs so + plotters reconstruct exactly what the optimizer scored against. + Tuples MUST round-trip back to tuples (not lists) since the + dataclass declares window: Tuple. + """ + import json + e = StressStrainExtractor( + stress_column="Sxx", + strain_source="time_rate", + strain_rate=1.5e-3, + time_column="t", + window=(0.005, 0.13), + ) + d = e.to_dict() + serialized = json.dumps(d) + restored = StressStrainExtractor.from_dict(json.loads(serialized)) + assert restored == e + # Window must be a tuple, not a list — dataclass type contract. + assert isinstance(restored.window, tuple) + + +def test_extractor_from_dict_tolerates_missing_keys(): + """An older config (lacking newer fields) should still load by + falling back to dataclass defaults, so a forward-compat plotter + can read older archives.""" + # Just stress info — most of the dataclass uses defaults. + e = StressStrainExtractor.from_dict({ + "strain_source": "time_rate", + "strain_rate": 1e-3, + }) + assert e.strain_source == "time_rate" + assert e.strain_rate == 1e-3 + # Defaulted fields. + assert e.stress_column == "Szz" + assert e.window is None + + +def test_extractor_from_dict_ignores_unknown_keys(): + """Forward-compat the other way: a newer archive with extra + fields should still load on older code that doesn't know about + them.""" + e = StressStrainExtractor.from_dict({ + "strain_source": "biot", + "strain_source_column": "F33", + "future_field_not_yet_invented": "ignored", + }) + assert e.strain_source == "biot" + assert e.strain_source_column == "F33" + + +def test_extractor_to_dict_round_trips_window_none(): + """window=None must JSON-serialize as null and round-trip back to None.""" + import json + e = StressStrainExtractor(window=None) + d = e.to_dict() + assert d["window"] is None + assert json.loads(json.dumps(d))["window"] is None + assert StressStrainExtractor.from_dict(d).window is None + + + + +# --- Example evaluator alignment direction ------------------------------ + + +def test_std_normalized_stress_evaluator_aligns_exp_to_sim_via_pchip(): + """The example's stress evaluator interpolates EXPERIMENTAL data + onto the SIMULATION's strain grid via PchipSmoother — the + direction the original ExaConstit code used. Verify the score + matches what we'd compute by hand using PchipSmoother.sample_at, + NOT what np.interp(sim_at_exp_grid) would produce. + + Pins Robert's correction: 'It was interpolating the experimental + data to the simulation data. It's the entire reason you have + workflow/smoothing.py.' + """ + import sys + from pathlib import Path + sys.path.insert(0, str(Path(__file__).parent.parent / "examples")) + import nsga3_calibration + + from workflow_common.smoothing import PchipSmoother + + # Sim and exp on different strain grids. + sim_strain = np.linspace(0.0, 0.1, 50) + sim_stress = 200.0 + 1900.0 * (1 - np.exp(-48.0 * sim_strain)) + exp_strain = np.linspace(0.0, 0.1, 25) + exp_stress = 210.0 + 1900.0 * (1 - np.exp(-48.0 * exp_strain)) + exp_df = pd.DataFrame({"strain": exp_strain, "stress": exp_stress}) + + class _FakeExtractor: + def extract(self, results): + return sim_strain, sim_stress + + ev = nsga3_calibration._StdNormalizedStressEvaluator( + experimental=exp_df, + extractor=_FakeExtractor(), + ) + score = ev.evaluate(None, None) + + # Hand-compute the expected score: align exp onto sim's strain + # grid via PchipSmoother (same tool the evaluator uses), then + # RMSE / std on that grid. + smoother = PchipSmoother(strict_monotonic=False) + exp_at_sim = smoother.sample_at( + exp_strain, exp_stress, sim_strain, + ).y + residual = sim_stress - exp_at_sim + expected = float(np.sqrt(np.mean(residual ** 2)) / np.std(exp_at_sim)) + assert score == pytest.approx(expected, rel=1e-9) + + +def test_std_normalized_slope_evaluator_aligns_exp_to_sim_via_pchip(): + """Slope evaluator's analogue of the stress test.""" + import sys + from pathlib import Path + sys.path.insert(0, str(Path(__file__).parent.parent / "examples")) + import nsga3_calibration + + from workflow_common.smoothing import PchipSmoother + + sim_strain = np.linspace(0.0, 0.1, 50) + sim_stress = 200.0 + 1900.0 * (1 - np.exp(-48.0 * sim_strain)) + exp_strain = np.linspace(0.0, 0.1, 25) + exp_stress = 210.0 + 1900.0 * (1 - np.exp(-48.0 * exp_strain)) + exp_df = pd.DataFrame({"strain": exp_strain, "stress": exp_stress}) + + class _FakeExtractor: + def extract(self, results): + return sim_strain, sim_stress + + ev = nsga3_calibration._StdNormalizedSlopeEvaluator( + experimental=exp_df, + extractor=_FakeExtractor(), + ) + score = ev.evaluate(None, None) + + smoother = PchipSmoother(strict_monotonic=False) + exp_at_sim = smoother.sample_at( + exp_strain, exp_stress, sim_strain, + ).y + diff_strain = np.diff(sim_strain) + sim_slope = np.diff(sim_stress) / diff_strain + exp_slope = np.diff(exp_at_sim) / diff_strain + residual = sim_slope - exp_slope + expected = float(np.sqrt(np.mean(residual ** 2)) / np.std(exp_slope)) + assert score == pytest.approx(expected, rel=1e-9) diff --git a/workflows/exaconstit-calibrate/tests/test_paths.py b/workflows/exaconstit-calibrate/tests/test_paths.py new file mode 100644 index 0000000..e01ece2 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_paths.py @@ -0,0 +1,201 @@ +""" +Unit tests for :mod:`workflow_common.paths`. + +Covers the :class:`TemplatePathResolver` - the default +:class:`PathResolver` implementation. Exercises the format-string +substitution for working directories and output files, the +``{working_dir}`` special key, missing-key error reporting, and the +``defaults`` / ``extra`` precedence rules. +""" +from __future__ import annotations + +from pathlib import Path + +import pytest + +from workflow_common.paths import ( + CaseContext, + PathResolver, + TemplatePathResolver, +) + + +def test_case_context_as_format_mapping_basic(): + """Top-level fields and the gen alias both appear in the mapping.""" + ctx = CaseContext(generation=4, gene=7, obj=1) + m = ctx.as_format_mapping() + assert m["generation"] == 4 + assert m["gen"] == 4 # alias + assert m["gene"] == 7 + assert m["obj"] == 1 + + +def test_case_context_extra_merged(): + """Entries in ``extra`` appear in the format mapping too.""" + ctx = CaseContext( + generation=0, gene=0, obj=0, extra={"rve_name": "grain_32"} + ) + assert ctx.as_format_mapping()["rve_name"] == "grain_32" + + +def test_case_context_extra_cannot_shadow_core_fields(): + """Top-level fields win over collisions in ``extra``.""" + # Users sometimes pass "generation" or "gen" in extra by mistake; + # the top-level value must take precedence. + ctx = CaseContext( + generation=99, gene=0, obj=0, extra={"generation": 1, "gen": 2} + ) + m = ctx.as_format_mapping() + assert m["generation"] == 99 + assert m["gen"] == 99 + + +def test_working_dir_basic(workspace: Path): + """Simple pattern produces the expected path.""" + resolver = TemplatePathResolver( + working_dir_pattern="wf/gen_{generation}/gene_{gene}", + output_file_patterns={}, + root=workspace, + ) + ctx = CaseContext(generation=2, gene=5, obj=0) + assert resolver.working_dir(ctx) == workspace / "wf" / "gen_2" / "gene_5" + + +def test_working_dir_absolute_pattern_ignores_root(workspace: Path): + """Absolute patterns bypass the root prefix.""" + resolver = TemplatePathResolver( + working_dir_pattern="/absolute/path/gen_{generation}", + output_file_patterns={}, + root=workspace, + ) + ctx = CaseContext(generation=3, gene=0, obj=0) + assert resolver.working_dir(ctx) == Path("/absolute/path/gen_3") + + +def test_working_dir_missing_key_raises_helpfully(workspace: Path): + """Missing placeholder -> KeyError whose message lists available keys.""" + resolver = TemplatePathResolver( + working_dir_pattern="{missing_key}/gen_{generation}", + output_file_patterns={}, + root=workspace, + ) + ctx = CaseContext(generation=0, gene=0, obj=0) + with pytest.raises(KeyError) as excinfo: + resolver.working_dir(ctx) + # The error must mention the key name AND the available fallbacks, + # so typos are easy to debug. + assert "missing_key" in str(excinfo.value) + assert "generation" in str(excinfo.value) + + +def test_output_file_with_working_dir_placeholder(workspace: Path): + """``{working_dir}`` in an output pattern expands to the case's working dir.""" + resolver = TemplatePathResolver( + working_dir_pattern="gen_{generation}/gene_{gene}", + output_file_patterns={ + "stress": "{working_dir}/results/{basename}/avg_stress.txt", + }, + defaults={"basename": "options"}, + root=workspace, + ) + ctx = CaseContext(generation=0, gene=1, obj=0) + expected = workspace / "gen_0" / "gene_1" / "results" / "options" / "avg_stress.txt" + assert resolver.output_file("stress", ctx) == expected + + +def test_output_file_unknown_logical_name_raises(workspace: Path): + """Unknown output names get a descriptive KeyError.""" + resolver = TemplatePathResolver( + working_dir_pattern="x", + output_file_patterns={"known": "{working_dir}/a.txt"}, + root=workspace, + ) + ctx = CaseContext(generation=0, gene=0, obj=0) + with pytest.raises(KeyError) as excinfo: + resolver.output_file("not_a_thing", ctx) + assert "not_a_thing" in str(excinfo.value) + assert "known" in str(excinfo.value) # lists the valid ones + + +def test_defaults_are_overridden_by_extra(workspace: Path): + """``ctx.extra`` entries override the resolver's defaults on name collision.""" + resolver = TemplatePathResolver( + working_dir_pattern="{basename}/gene_{gene}", + output_file_patterns={}, + defaults={"basename": "options"}, + root=workspace, + ) + ctx = CaseContext( + generation=0, gene=0, obj=0, extra={"basename": "override"} + ) + assert resolver.working_dir(ctx) == workspace / "override" / "gene_0" + + +def test_known_outputs_returns_registered_names(workspace: Path): + """known_outputs reflects the patterns passed at construction.""" + resolver = TemplatePathResolver( + working_dir_pattern="x", + output_file_patterns={"a": "x/a", "b": "x/b", "c": "x/c"}, + root=workspace, + ) + assert set(resolver.known_outputs()) == {"a", "b", "c"} + + +def test_template_resolver_satisfies_protocol(workspace: Path): + """TemplatePathResolver should pass runtime_checkable isinstance check.""" + resolver = TemplatePathResolver( + working_dir_pattern="x", + output_file_patterns={"a": "x/a"}, + root=workspace, + ) + assert isinstance(resolver, PathResolver) + + +def test_output_file_auto_prepends_working_dir_for_bare_relative_pattern(): + """Regression: output patterns WITHOUT an explicit ``{working_dir}`` + marker used to return bare relative paths, which then resolved + against the driver's cwd at read-time and silently missed the case + dir. Now they auto-prepend working_dir, matching how + ``working_dir_pattern`` auto-prepends ``root``. + """ + from pathlib import Path + + from workflow_common import TemplatePathResolver, CaseContext + + r = TemplatePathResolver( + working_dir_pattern="gen_{generation}/gene_{gene}_obj_{obj}", + output_file_patterns={ + "bare": "results/options/avg_stress.txt", + "with_marker": "{working_dir}/results/options/avg_stress.txt", + "abs": "/tmp/external.txt", + }, + root=Path("calibration_run"), + ) + ctx = CaseContext(generation=0, gene=3, obj=1) + wd = r.working_dir(ctx) + + # Bare relative: auto-prepended working_dir. + assert r.output_file("bare", ctx) == wd / "results/options/avg_stress.txt" + # Explicit marker: unchanged — user opted in, we don't double up. + assert r.output_file("with_marker", ctx) == wd / "results/options/avg_stress.txt" + # Absolute: honored exactly as-is, no prepend. + assert r.output_file("abs", ctx) == Path("/tmp/external.txt") + + +def test_output_file_bare_relative_matches_working_dir_regardless_of_root_type(tmp_path): + """The auto-prepend must work for both absolute and relative + workspace roots — relative was the case that hurt in production. + """ + from pathlib import Path + + from workflow_common import TemplatePathResolver, CaseContext + + for root in (Path("relative_workspace"), tmp_path / "absolute_workspace"): + r = TemplatePathResolver( + working_dir_pattern="gen_{generation}/gene_{gene}", + output_file_patterns={"s": "results/avg_stress.txt"}, + root=root, + ) + ctx = CaseContext(generation=0, gene=0, obj=0) + expected = r.working_dir(ctx) / "results/avg_stress.txt" + assert r.output_file("s", ctx) == expected diff --git a/workflows/exaconstit-calibrate/tests/test_plot_solutions.py b/workflows/exaconstit-calibrate/tests/test_plot_solutions.py new file mode 100644 index 0000000..4880ce4 --- /dev/null +++ b/workflows/exaconstit-calibrate/tests/test_plot_solutions.py @@ -0,0 +1,1957 @@ +"""Tests for the public selection API and the plot_solutions example. + +Two surfaces: + +1. :mod:`workflows.optimization.inspect_archive` public API — + ``select_top_genes``, ``pick_run``, ``dedup_on_gene_vector``, + ``collect_all_genes``, ``resolve_archive_path``. These were + promoted from internal helpers and need their library contracts + pinned independently of the CLI. + +2. :mod:`examples.plot_solutions` — the data-loading helpers + (skipping matplotlib UI which requires a display). Verifies the + example correctly composes the public API to produce ranked + ``RankedGene`` lists matching the user's request. + +Matplotlib plotting itself isn't tested at the rendering level; the +example uses matplotlib's standard ``Slider`` and ``pick_event`` +APIs, which are themselves well-tested upstream. +""" +from __future__ import annotations + +import importlib.util +import sys +from pathlib import Path + +import numpy as np +import pandas as pd +import pytest + +from workflow_common.archive import ArchiveDB, GeneRecord +from workflow_common.paths import CaseContext +from workflow_common.results import CaseResultSet, TabularResult +from workflows.optimization.inspect_archive import ( + RankedGene, + collect_all_genes, + dedup_on_gene_vector, + pick_run, + resolve_archive_path, + select_top_genes, +) + + +# --- Helpers ------------------------------------------------------------ + + +def _voce_stress_df(y0=200.0, H=1900.0, k=48.0, n=20) -> pd.DataFrame: + """Synthetic Voce-curve stress data shaped like ExaConstit avg_stress.""" + t = np.linspace(0.0, 1.0, n) + stress = y0 + H * (1 - np.exp(-k * t)) + return pd.DataFrame({ + "Time": t, "Volume": np.ones_like(t), + "Sxx": np.zeros_like(t), "Syy": np.zeros_like(t), + "Szz": stress, + "Sxy": np.zeros_like(t), "Sxz": np.zeros_like(t), + "Syz": np.zeros_like(t), + }) + + +def _voce_defgrad_df(rate=1.0, n=20) -> pd.DataFrame: + t = np.linspace(0.0, 1.0, n) + eps = rate * t + F33 = 1.0 + eps + return pd.DataFrame({ + "Time": t, "Volume": np.ones_like(t), + "F11": 1.0 - 0.5 * eps, "F12": np.zeros_like(t), + "F13": np.zeros_like(t), + "F21": np.zeros_like(t), "F22": 1.0 - 0.5 * eps, + "F23": np.zeros_like(t), + "F31": np.zeros_like(t), "F32": np.zeros_like(t), + "F33": F33, + }) + + +def _make_case_result_set( + ctx: CaseContext, y0: float = 200.0, H: float = 1900.0, +) -> CaseResultSet: + return CaseResultSet(ctx=ctx, tables={ + "avg_stress": TabularResult( + name="avg_stress", df=_voce_stress_df(y0=y0, H=H), + source_path=Path("fake/avg_stress.txt"), + ), + "avg_def_grad": TabularResult( + name="avg_def_grad", df=_voce_defgrad_df(), + source_path=Path("fake/avg_def_grad.txt"), + ), + }) + + +# --- select_top_genes -------------------------------------------------- + + +def _gene( + *, gen_idx, pop_idx, fitness, gene_vec=None, rank=0, +): + """Compact GeneRecord factory for ranking tests.""" + return GeneRecord( + run_id="r", gen_idx=gen_idx, pop_idx=pop_idx, + birth_gen=gen_idx, birth_gene=pop_idx, + gene_vector=np.asarray( + gene_vec if gene_vec is not None + else [float(gen_idx), float(pop_idx)], + dtype=float, + ), + fitness=tuple(fitness), rank=rank, + ) + + +def test_select_top_genes_l2_ranks_by_l2_norm_ascending(): + """The L2 category should be sorted ascending by sqrt(sum(f^2)).""" + genes = [ + _gene(gen_idx=0, pop_idx=0, fitness=[3.0, 4.0]), # L2=5 + _gene(gen_idx=0, pop_idx=1, fitness=[1.0, 1.0]), # L2~1.41 (best) + _gene(gen_idx=0, pop_idx=2, fitness=[6.0, 8.0]), # L2=10 + ] + out = select_top_genes( + genes, objective_labels=["a", "b"], top_n=3, + categories=("l2",), + ) + l2 = out["l2"] + assert [rg.gene.pop_idx for rg in l2] == [1, 0, 2] + assert [rg.rank for rg in l2] == [0, 1, 2] + assert [rg.category for rg in l2] == ["l2"] * 3 + # Score == L2 norm exactly for the L2 category. + assert l2[0].score == pytest.approx(np.sqrt(2.0)) + assert l2[1].score == pytest.approx(5.0) + + +def test_select_top_genes_per_objective_ranks_each_axis_separately(): + """obj: