diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..78011f0ba --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,195 @@ +# Changelog + +All notable changes to simcoon are listed here. The format follows +[Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and simcoon uses +[semantic versioning](https://semver.org/). Each pull request adds its entry +under **Unreleased**; the section takes the version number at the release. + +## In progress + +Known gaps and planned work, not yet in a release. An item moves to +**Unreleased** with the pull request that closes it. + +- `ncp="min"` serves the mechanical EPICP, EPKCP and MODUL only: every + thermomechanical law (`_T`, `sim.umat_T`, thermomechanical blocks) is refused + by `check_ncp_serves` and keeps Fischer-Burmeister. +- `ncp="min"` is refused with Armstrong-Frederick back stresses under the + cutting plane (path dependent); whether the closest point, which restarts + from the start of the step, lifts this is not verified. +- `integrator="closest_point"` is provided with its algorithmic tangent only + (`tangent_mode="algorithmic"`): its continuum operator is not implemented. +- Other complementarity functions to measure against Fischer-Burmeister and + `min`, under both integrators: active set, scaled Fischer-Burmeister. +- The numerical options of the mean-field schemes (MIMTN/MISCN closed form, + rule, Anderson, tolerance) are still props slots: they are per phase (nested + schemes), so they need a named builder rather than a run option. +- fedoo does not pass `ncp` and `integrator` through yet. +- The external plugins (UMEXT, UMABA, UMANS) serve the small-strain route only: a finite-strain + entry in the plugin API (law fed the logarithmic strain, returning tau, as the internal + kernels) would let them run under NLGEOM. +- Migration of the laws to the modular engine (MODUL), state in + `benchmark/README.md`. Routed through the adapter (code 201): ELISO, ELIST, + ELORT, EPKCP, EPHIL, EPTRI, EPHAC, EPANI, EPDFA, EPCHG, EPHIN. To do: + - adapters for the hyperelastic laws whose MODUL equivalent exists (NEOHC, + MOORI, YEOHH, ISHAH, GETHH, SWANH, HOLZA; NEOHI to confirm); + - EPICP, EPCHA and PRONK keep their legacy kernel while it is faster; + - no MODUL equivalent yet: OGDEN, SNTVE, HYPOO, EPJCK, LLDM0, ZENER, ZENNK, + the SMA family, the micromechanics schemes; + - the thermomechanical dispatch (`umat_T`) has no adapter: every `_T` law + runs its legacy kernel. + +## Planned for 3.0 + +Removals and renames scheduled for the next major version (breaking, kept in 2.x). + +- The migration message for `tangent_mode=3` / `"closest_point"`: the check + `tangent_mode in ("closest_point", 3)` of `solve` (`simcoon/solver/core.py`) + and its C++ counterpart in `kernel_tangent_mode` (`umat_smart.cpp`). The + value then gets the generic error of an unknown tangent mode; the internal + kernel code 3 of the closest point is unchanged. +- Possibly rename `ZENER` / `ZENNK` after their rheology: they are the Poynting-Thomson + (Kelvin) form of the standard linear solid and its generalized Kelvin chain, not the Zener + (Maxwell) form; the 2.x names stay for compatibility. + +## [Unreleased] + +### Breaking changes + +- `copy_parameters`, `apply_parameters`, `copy_constants` and `apply_constants` + are removed. `KeyTemplate(src, params, consts).write(dst)` copies the + templates and substitutes the keys in one call, about 5 times faster + (templates read once, each working file written once). +- The C++ kernel `umat_damage_weibull` is removed: no UMAT name dispatched to it. The + Weibull damage function `damage_weibull` (Functions/damage.hpp, `sim.damage_weibull`) stays. +- The C++ kernel `umat_prony_Nfast_plastic` (no header, no UMAT name) is removed: Prony + viscoelasticity with J2 plasticity is a `ModularMaterial` (MODUL). +- `tangent_mode=3` / `"closest_point"` is refused with a `ValueError`: the local + integrator is now chosen apart, with `integrator="closest_point"`, and + `tangent_mode` only takes `"none"`, `"continuum"` or `"algorithmic"`. +- `integrator="closest_point"` is refused with a `ValueError` for a law without a + closest-point update instead of running its cutting plane silently: it serves + EPICP, EPCHA, EPJCK, MODUL and its adapters and the SMA kernels (mechanical), + EPICP and EPKCP (thermomechanical). A Python law (PYEXT) receives + `tangent_mode` 0, 1 or 2 only. +- The numerical option slots of the props are removed: EPICP `props[6]` + (min-NCP) and `props[7]` (closed-form tangent), EPKCP `props[7]`, and + `ModularMaterial(ncp=...)`. The complementarity function is the run option + `ncp`, and the EPICP closed-form consistent tangent is always used. +- `condense="approximate"` / `"exact"` are renamed `"spatial_face"` / + `"material_face"` (#145). +- The thermomechanical UMATs return the heat of the increment `R` (storage + included), not a rate (#140). + +### Added + +- The Ansys USERMAT plugin (`UMANS`, `umat_plugin_ans_api`) is dispatched on the small-strain + route, as UMABA, and tested by `TUMABA` on the same case. The example plugin is rebuilt on the + `fea_transfer` component maps (now public: `fea_components`, `ansys_components`, `fe2vec`, + `fe2mat`...): it read the tangent and wrote the rotation row-major (transposed), hid four Wm + slots in the USERMAT's statev, and sent condensed states as 3D. `TUMANS` checks the data layout + on a USERMAT with a non-symmetric tangent and a non-trivial rotation. The Fortran example did + not compile in fixed form. `sim.umat` names the plugins in its error (`is_external_plugin`). +- `KeyTemplate.write(dst, test=i)`: a constant holds one value per test + (`input_values`, e.g. the fibre angle of each specimen) and the value of test + `i` is substituted. A constant with a single value warns: it belongs in the + template, not in a key. +- `identification(..., method="least_squares")`: bounded trust-region + reflective least squares, parameters scaled to [0, 1] (logarithmically for + positive bounds), `n_starts` starting points, `n_threads` for the Jacobian + columns or the generations of differential evolution. `calc_residuals` + returns the residuals whose sum of squares is `calc_cost`. Differential + evolution remains the default. +- Run options `ncp="fischer_burmeister" | "min"` and + `integrator="cutting_plane" | "closest_point"` in `solve`, `sim.umat` and + `sim.umat_T`. `min` applies to EPICP, EPKCP and MODUL (also next to Prony and + damage mechanisms), not to Armstrong-Frederick back stresses. +- `simcoon.rotate_voigt`: batch rotation of Voigt stresses, strains and + stiffness or concentration tensors by one rotation matrix per point, without + the GIL. +- Native dS/dE of the finite-strain laws built on F, on demand. +- C++: `cubic_coefficients` and `is_isotropic(L, Tensor4Type)` (constitutive.hpp; the isotropy + relation follows the Voigt convention of the type), `spectral_congruence` (eig_sym3.hpp), + `add_linearop_eigsym` (contimech.hpp), replacing private copies. +- `isochoric_invariants` takes a batch, (3,3,N) tensors or (6,N) Voigt columns, as + `isochoric_pstretch` does; both share one implementation (parallel, without the GIL). +- Condensed stress states (`nshr`, plane stress, shells, beams, trusses) in + `sim.umat`, the solver and the Abaqus bridges, with `condensation_serves` + (#139, #141, #145). +- Finite strain (NLGEOM) and thermomechanical finite strain in the Abaqus + bridges and the solver, `tangent_output` for `umat_T` (#143). +- LLDM0 ply damage model (Ladevèze–Le Dantec) (#144). +- STATEV size checks and typed exceptions in the Abaqus UMATs: numerical + failures request a step cut, input errors abort (#146). +- `slice_view`: lock-free aliases of cube slices in parallel loops (#147). +- GOH 2006 tube-inflation example for HOLZA (#142). +- `benchmark/`: batch benchmarks and bit-for-bit dumps of the solver outputs. + +### Changed + +- EPKCP thermomechanical: the cutting plane evaluates the power-law hardening through + `PowerLawHardening` (C1 blend below p = 1e-6), as EPICP and its own closest point; for + m < 1 the first plastic increment no longer meets an infinite slope. +- Faster kernels, results bitwise unchanged unless stated: reuse of the + ModularUMAT instances, fixed-size 3x3 spectral solves and small dense LU, + Eshelby integration and cached homogenization, closed-form + Fischer-Burmeister for one and two mechanisms. +- The Chaboche, composite and MOORI identification examples use the least + squares; the JSON-keys and analytical hyperelastic examples keep + differential evolution. + +### Fixed + +- An invalid elastic convention ended the process with `exit(0)` (`M_iso`, `L_cubic`, `M_cubic`, + `L_ortho`, `M_ortho`, `L_isotrans`, `M_isotrans`; from Python the interpreter quit with a success + code) or, in `L_iso`, returned a zero stiffness: they now throw `std::invalid_argument` + (`ValueError` in Python). +- Small code defects without effect on results: `assert(Y_crit(n))` out of bounds in + `Fischer_Burmeister(_limits)` (a Debug build aborted), `variant::build` did not store `g`, + the `state_variables_T` default constructor left `Wm`/`Wm_start` to the Armadillo default, + an unreachable `HfR = 0.5*a2* -a3` branch of the thermomechanical unified_T closest point. +- Copies of `step_meca` / `step_thermomeca` lost their loading: the copy constructor set + `BC_mecas` from `mecas`, the assignment skipped `BC_mecas`, `BC_Ts`, `control_type` and + `tab_data`. The copy operations of `step`, `step_meca` and `step_thermomeca` are now the + memberwise defaults (test `Tstep`). Nothing in simcoon copied a step. +- Under finite strain the solver ran UMEXT as a no-op (unchanged, zero stress) and failed on + UMABA with "Unknown umat name": the plugins are now refused with an explicit error. +- `Eshelby_penny` set the in-plane shear S_2323 (Voigt 23) to 1 and S_1212 to 0: now + S_1212 = S_1313 = 1/2 (1 in engineering Voigt) and S_2323 = 0, the flat oblate limit. + No homogenization scheme called it. +- `d2lagrange_pow_1` placed `p0` wrongly (exact only for p0 = 1); its test now checks it + against a finite difference of `dlagrange_pow_1`. No law called it. +- Documentation: .cpp comments cut to short implementation notes, contracts (props, + statev, theory) moved to the header Doxygen, dead code and wrong `@file` banners removed; + the EPCHA header example read the back-strains as the backstresses (statev 20-31). +- `isochoric_pstretch` ignored `J` on a single point (it recomputed it from the + tensor), unlike its batch form. +- Race on the Eshelby quadrature tables between concurrent solves. +- MIHEN crash (null pointer) and MISCN failure on the second block of a + loading path. +- `n_threads` only took effect for 1 thread: it is now honoured under GCD and + OpenMP, and `Lt_convert` takes it too. + +## [2.2.0] - 2026-10-14 + +Tensor bases (#126, #127, #128), cached rotation operators (#130), Eshelby memo +and lazy fields (#129), closest-point return mapping (#131, #133), Johnson-Cook +(#85), analytic SMA tangents (#136), logarithmic U (#137). See the +[release notes](https://github.com/3MAH/simcoon/releases/tag/2.2.0). + +## [2.1.0] - 2026-10-01 + +See the [release notes](https://github.com/3MAH/simcoon/releases/tag/2.1.0). + +## [2.0.1] - 2026-09-09 + +See the [release notes](https://github.com/3MAH/simcoon/releases/tag/2.0.1). + +## [2.0.0] - 2026-09-07 + +See the [release notes](https://github.com/3MAH/simcoon/releases/tag/2.0.0). + +[Unreleased]: https://github.com/3MAH/simcoon/compare/2.2.0...HEAD +[2.2.0]: https://github.com/3MAH/simcoon/compare/2.1.0...2.2.0 +[2.1.0]: https://github.com/3MAH/simcoon/compare/2.0.1...2.1.0 +[2.0.1]: https://github.com/3MAH/simcoon/compare/2.0.0...2.0.1 +[2.0.0]: https://github.com/3MAH/simcoon/compare/2.0.0b1...2.0.0 diff --git a/CMakeLists.txt b/CMakeLists.txt index b5e81051c..4f245e5ae 100755 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -371,14 +371,29 @@ if(SIMCOON_BUILD_TESTS) if(NOT MSVC) # Fortran is only needed for Abaqus UMAT plugin tests enable_language(Fortran) - set_source_files_properties(testBin/Umats/UMABA/external/UMAT_ABAQUS_ELASTIC.f PROPERTIES Fortran_PREPROCESS OFF) - add_library(umat_plugin_aba SHARED testBin/Umats/UMABA/external/UMAT_ABAQUS_ELASTIC.f testBin/Umats/UMABA/external/umat_plugin_aba.cpp) + set_source_files_properties(external/UMAT_ABAQUS_ELASTIC.f PROPERTIES Fortran_PREPROCESS OFF) + add_library(umat_plugin_aba SHARED external/UMAT_ABAQUS_ELASTIC.f external/umat_plugin_aba.cpp) # Use default PREFIX (lib) and SUFFIX (.so on Linux, .dylib on macOS) # to match dylib::decorations::os_default() expectations set_target_properties(umat_plugin_aba PROPERTIES LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/testBin/Umats/UMABA/external ) target_link_libraries(umat_plugin_aba PRIVATE simcoon ${ARMADILLO_LIBRARIES}) + + # Ansys USERMAT plugin, built from the external/ example: TUMABA runs it on the same case + set_source_files_properties(external/USERMAT_ANSYS_ELASTIC.for PROPERTIES Fortran_PREPROCESS OFF) + add_library(umat_plugin_ans SHARED external/USERMAT_ANSYS_ELASTIC.for external/umat_plugin_ans.cpp) + set_target_properties(umat_plugin_ans PROPERTIES + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/testBin/Umats/UMABA/external + ) + target_link_libraries(umat_plugin_ans PRIVATE simcoon ${ARMADILLO_LIBRARIES}) + + # the same plugin on a USERMAT that exposes what it receives (TUMANS: data layout) + add_library(umat_plugin_ans_layout SHARED external/umat_plugin_ans.cpp test_extern/Umats/UMANS/usermat_layout.cpp) + set_target_properties(umat_plugin_ans_layout PROPERTIES OUTPUT_NAME umat_plugin_ans + LIBRARY_OUTPUT_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/testBin/Umats/UMANS/external + ) + target_link_libraries(umat_plugin_ans_layout PRIVATE simcoon ${ARMADILLO_LIBRARIES}) endif() add_library(umat_plugin_ext SHARED diff --git a/benchmark/README.md b/benchmark/README.md index b5c4b88d6..d5159703d 100644 --- a/benchmark/README.md +++ b/benchmark/README.md @@ -1,4 +1,25 @@ -# Solver benchmark +# Benchmarks and non-regression tools + +> **Temporary folder.** These scripts serve two ongoing efforts and will be +> removed once both are done; nothing in the library or the test suite uses them. +> +> 1. **Computation time.** Each optimization step is checked for its outputs +> (`dump_results.py`: bit-identical, or its worst differences published) and +> for its speed (`benchmark_batch.py`, `benchmark_solver.py`, interleaved A/B +> against the previous step). +> 2. **Moving the laws to the modular engine, one at a time.** A legacy name is +> routed to MODUL through a props adapter (code 201 in `umat_smart.cpp`) once +> `dump_results.py` shows the same response and `bench_legacy_vs_modular.py` +> shows no loss of speed. The table at the end tracks where each law stands. + +| Script | Measures | +|---|---| +| `benchmark_solver.py` | the 0D solver, in-memory route against the path-file route | +| `benchmark_batch.py` | the batch APIs (N points per call) and fedoo-like iteration sequences | +| `dump_results.py` | output arrays of the solver and of the batch `umat`, compared between two builds | +| `bench_legacy_vs_modular.py` | the legacy kernels against their MODUL twins | + +## Solver benchmark Times identical loading cases through the two shipped solver entry points and cross-checks that they produce the same response: @@ -21,29 +42,94 @@ Cases cover linear isotropic/anisotropic elasticity (ELISO, ELIST), isotropic and kinematic plasticity (EPICP, EPKCP) and finite-strain hyperelasticity (NEOHC), with props mirroring the validated `testBin` materials. -## Known optimization opportunities in the C++ engine - -Profiling done on the (superseded) PR #63 branch identified hotspots that are -still present in the current engine — verified against today's sources. The -speedup figures are the original (Feb 2026) profiling estimates, to be -re-measured with this benchmark before acting on any of them: - -1. **UMAT dispatch maps rebuilt on every call** — `std::map` literals - `list_umat` are function-local in each `select_umat_*` dispatcher - (`src/Continuum_mechanics/Umat/umat_smart.cpp`), reconstructed once per - increment per iteration (est. 2–3×on small models). `static const` maps - (or a shared registry) would build them once. -2. **`inv(K)` then multiply instead of `solve`** — the global Newton uses - `inv(invK, K); Delta = -invK * residual` (`src/Simulation/Solver/solver.cpp`). - `arma::solve(K, residual)` is cheaper and better conditioned (est. 2–4× on - the 6×6 solve itself). -3. **Allocations inside the Newton loop** — `zeros(6,6)` / temporary vectors - recreated per iteration rather than reused (est. 2–3×). -4. **Rotations applied for isotropic materials** — `global2local`/`local2global` - run regardless of whether the phase orientation is identity (est. 2–3× on - multiphase models). -5. **State-variable packing with copies** — `statev_2_phases`/`phases_2_statev` - copy subvectors per phase per call rather than aliasing (est. 2×). - -These numbers compound only in the per-increment hot path; measure with this -benchmark (memory route, high `ninc`) before and after any change. +## Batch API benchmark + +`benchmark_batch.py` times the batch Python APIs on N material points per call +(`sim.umat`, `sim.umat_T`, `objective_rate`, `Lt_convert`, `stress_convert`, +`Log_strain`, `Rotation`, `Tensor2`/`Tensor4`), and two sequences that mirror +one fedoo global iteration: `fedooSS_` (small strain, the `umat` call as +fedoo makes it) and `fedooUL_` (updated Lagrangian: `objective_rate` +log_R, frame transport, `umat` with the box tangent, two `Lt_convert`). + +```bash +python benchmark/benchmark_batch.py run -l master -o master.json +python benchmark/benchmark_batch.py run -l branch -o branch.json --N 1e4,1e5 +python benchmark/benchmark_batch.py summary master.json branch.json +``` + +Each point is the minimum wall time per call over adaptive repetitions. The +summary fits `t = a + b·N` on the serial runs: `a` is the fixed cost of a call, +`b` the cost of one point (the figure a kernel optimization should move); the +parallel runs (`n_threads=0`) are tabulated per point. For an A/B comparison, +interleave runs of the two builds and pass all the files to `summary`. + +## Non-regression dumps + +`dump_results.py` saves every output array of a fixed set of computations and +compares two such dumps: + +- `solver`: all result fields of the `benchmark_solver.py` cases, of each family + of `bench_legacy_vs_modular.py` (legacy name and MODUL twin), and of every + `solve` call of five gallery examples (EPCHA, MODUL closest point, SMA_T, + SMA_TR, thermomechanical SMA_T) — 1435 arrays; +- `batch`: `sim.umat` on 997 points over six loading/unloading increments, for + ELISO, EPICP, EPKCP, MODUL and EPCHG, serial and parallel — 240 arrays. + +```bash +python benchmark/dump_results.py solver ref.npz # reference build +python benchmark/dump_results.py solver new.npz # build under test +python benchmark/dump_results.py compare ref.npz new.npz # bit for bit +python benchmark/dump_results.py compare ref.npz new.npz --rtol 1e-12 +``` + +`compare` exits with status 1 on any difference beyond `--rtol` (default 0) and +lists the largest relative difference per case. A performance change that keeps +the floating-point operations (caching, layout, dead code) must compare +bit-identical; one that reorders them (another linear solver, a closed form) +publishes its worst differences. + +## Legacy kernels against their MODUL twins + +`bench_legacy_vs_modular.py` times a legacy UMAT against the equivalent +MODUL configuration (interleaved A/B, minimum over repeats). Only EPICP, EPCHA +and PRONK still run a legacy kernel: ELISO, ELIST, ELORT, EPKCP, EPHIL, EPHAC, +EPANI, EPDFA, EPCHG and EPHIN are routed to the modular engine by a props +adapter (code 201 in `umat_smart.cpp`), so for them the benchmark compares the +modular engine with itself. By default it runs the three true pairs; `--all` +adds the adapter families, flagged as such. + +## Hotspots of the February 2026 profiling: current state + +The (superseded) PR #63 profiling listed five hotspots of the C++ engine. Their +state was measured on 2.2.0 (October 2026, `benchmark_solver.py` cases and +per-component timings of an ELISO increment, about 3 µs in Python), then updated +with the optimization lots that followed: + +| # | Hotspot (February) | State | Measured cost (2.2.0) | +|---|---|---|---| +| 1 | UMAT dispatch maps rebuilt on every call (`select_umat_*`, `umat_smart.cpp`) | **fixed**: `static const` maps | 24 ns per lookup (~1 % of an increment); rebuilt, the map would cost ~1.9 µs | +| 2 | `inv(K)` then multiply instead of `solve` in the global Newton (`solver.cpp`) | **fixed** in the mixed-control Newton: `newton_correction`, a fixed-size LU on the stack (`small_lu<7>`), no LAPACK call; the RNL path (`solver_type` 1) still uses `inv(K)` | ~650 ns per iteration, 20 % of a mixed-control ELISO increment; `arma::solve` gains only 0–30 % on a 6×6 (LAPACK call overhead) | +| 3 | Allocations inside the Newton loop | **absent** on the solver side | 0 malloc per iteration; the allocations of an increment are in the constitutive kernels (fixed-size matrices to use there) | +| 4 | Rotations applied to isotropic materials (`global2local`/`local2global`) | products already skipped; **copies remain** | ~250 ns per UMAT call | +| 5 | State-variable packing with copies (`statev_2_phases`/`phases_2_statev`) | **outside the 0D path** (Abaqus interface and plugins only) | 0 on the measured loads | + +The costs that dominated a small-strain increment next have been removed as +well: the modular engine is no longer rebuilt at every call (a per-thread cache +of the `ModularUMAT` instance; it cost ~0.9 µs of an ELISO increment), the result +sink reserves its records up front, and `Kirchoff2PKII` is skipped when F = I +exactly. What remains is in the constitutive kernels: heap temporaries and the +LAPACK inverse of the algorithmic tangent. Measure with `benchmark_solver.py` (memory route, high `ninc`) and +`benchmark_batch.py` before and after any change, and check the outputs with +`dump_results.py`. + +## Migration of the laws to the modular engine + +State of the mechanical dispatch (`umat_smart.cpp`); the thermomechanical one +(`umat_T`) has no adapter yet, every law there runs its legacy kernel. + +| Status | Laws | +|---|---| +| Routed to MODUL (adapter, code 201) | ELISO, ELIST, ELORT, EPKCP, EPHIL, EPTRI, EPHAC, EPANI, EPDFA, EPCHG, EPHIN | +| MODUL equivalent exists, adapter to write | NEOHC, MOORI, YEOHH, ISHAH, GETHH, SWANH, HOLZA (`HYPER_INVARIANTS` block, same `HyperPotential`); NEOHI to confirm (neo-Hookean with the quadratic volumetric term, kappa = 2/D1) | +| MODUL equivalent exists, legacy kept while faster | EPICP, EPCHA, PRONK | +| No MODUL equivalent | OGDEN (principal stretches), SNTVE, HYPOO, EPJCK, LLDM0 (anisotropic damage), ZENER, ZENNK (Kelvin chains), SMA family, micromechanics (MIHEN, MIMTN, MISCN, MIPLN) | diff --git a/bench/bench_legacy_vs_modular.py b/benchmark/bench_legacy_vs_modular.py similarity index 85% rename from bench/bench_legacy_vs_modular.py rename to benchmark/bench_legacy_vs_modular.py index 542455c96..e71895bf8 100644 --- a/bench/bench_legacy_vs_modular.py +++ b/benchmark/bench_legacy_vs_modular.py @@ -1,5 +1,15 @@ """Benchmark legacy UMATs against their MODUL (modular UMAT) twin configurations. +Only three legacy names still run a legacy kernel: EPICP, EPCHA and PRONK. All the +others listed below (ELISO, ELIST, ELORT, EPKCP, EPHIL, EPHAC, EPANI, EPDFA, EPCHG, +EPHIN) are dispatched to the modular engine through a props adapter (code 201 in +``umat_smart.cpp``, ``Modular/legacy_adapters.cpp``): timing them against MODUL +compares the modular engine with itself, plus the adapter's props translation +(ratio ~1.00, deviation 0). Those families are therefore benchmarked only on +request (``--all`` or ``--families``) and flagged "adapter" in the table; they +remain in ``FAMILIES`` because their legacy-name and MODUL runs are regression +cases of ``benchmark/dump_results.py``. + ZENER/ZENNK are intentionally absent: Zener_fast/Zener_Nfast are generalized KELVIN chains (branch driving force sigma - L_i EV_i), a different rheological model from the modular Prony (generalized Maxwell) — no twin exists. @@ -15,8 +25,9 @@ share. Usage: - python bench/bench_legacy_vs_modular.py [--families ELISO,EPCHA,...] - [--repeats 8] + python benchmark/bench_legacy_vs_modular.py # EPICP, EPCHA, PRONK + python benchmark/bench_legacy_vs_modular.py --all # + the adapter families + python benchmark/bench_legacy_vs_modular.py --families EPCHA,EPHIL --repeats 8 Run from anywhere; paths resolve relative to the repo. Requires the simcoon python package (editable install). Results land in a temp dir and are removed. @@ -60,6 +71,10 @@ def _mat(elasticity, mechanisms=()): return ModularMaterial(elasticity=elasticity, mechanisms=list(mechanisms)) +# Legacy names that still run their own (non-modular) kernel. Any other name of +# FAMILIES is routed to the modular engine by a props adapter (code 201). +LEGACY_KERNELS = ("EPICP", "EPCHA", "PRONK") + # family -> (legacy_name, legacy_props, legacy_nstatev, path_file, ModularMaterial) FAMILIES = { "ELISO": ( @@ -405,21 +420,29 @@ def main(): ap = argparse.ArgumentParser() ap.add_argument( "--families", - default=",".join(FAMILIES), - help="comma-separated subset of: " + ",".join(FAMILIES), + default=",".join(LEGACY_KERNELS), + help="comma-separated subset of: " + ",".join(FAMILIES) + + " (default: the true legacy kernels " + ",".join(LEGACY_KERNELS) + ")", + ) + ap.add_argument( + "--all", + action="store_true", + help="all families, including those whose legacy name runs the modular engine", ) ap.add_argument("--repeats", type=int, default=8) args = ap.parse_args() + families = list(FAMILIES) if args.all else args.families.split(",") - print("| family | legacy [ms] | MODUL [ms] | ratio | max dev |") - print("|---|---|---|---|---|") - for key in args.families.split(","): + print("| family | legacy route | legacy [ms] | MODUL [ms] | ratio | max dev |") + print("|---|---|---|---|---|---|") + for key in families: key = key.strip() if key not in FAMILIES: - print(f"| {key} | (unknown family) | | | |") + print(f"| {key} | (unknown family) | | | | |") continue + route = "kernel" if key in LEGACY_KERNELS else "adapter (MODUL)" tl, tm, dev = bench_family(key, args.repeats) - print(f"| {key} | {tl:.1f} | {tm:.1f} | {tm / tl:.2f} | {dev:.2e} |") + print(f"| {key} | {route} | {tl:.1f} | {tm:.1f} | {tm / tl:.2f} | {dev:.2e} |") if __name__ == "__main__": diff --git a/benchmark/benchmark_batch.py b/benchmark/benchmark_batch.py new file mode 100644 index 000000000..3937cd58d --- /dev/null +++ b/benchmark/benchmark_batch.py @@ -0,0 +1,395 @@ +"""Benchmark: the batch (N material points per call) Python APIs of simcoon. + +Times the calls a finite-element code such as fedoo makes once per global +iteration on all its integration points: ``sim.umat``/``sim.umat_T`` on +(6, N) arrays, ``objective_rate``, ``Lt_convert``, ``stress_convert``, +``Log_strain``, the rotation helpers, and two composite sequences: + +- ``fedooSS_`` — small strain: the ``umat`` call alone, with the + arguments fedoo passes (``F0 = F1 = []``, ``DR = I``, ``temp``); +- ``fedooUL_`` — updated Lagrangian, one global iteration: + ``objective_rate("log_R")``, ``Rotation.from_matrix`` + ``apply_strain``/ + ``apply_stress``, ``umat`` with ``corate=3`` and the box tangent, then the two + ``Lt_convert`` (``DsigmaDe_2_DSDE``, ``DSDE_2_Dsigma_LieDD``); +- ``fedooUL2_`` — the same iteration with ``rotate_voigt`` instead of + ``Rotation``, ``umat`` called with positional arguments and ``n_threads`` + passed to every call (fedoo once it uses ``rotate_voigt``). + +Every (case, N, n_threads) is timed as the MINIMUM wall time per call over +adaptive repetitions (least-noise estimator on a loaded machine). The +``summary`` command fits ``t = a + b N`` on the serial points (relative +least squares): ``a`` is the fixed cost of one call (argument parsing, +allocation of the outputs), ``b`` the marginal cost of one point, which is what +an optimization of a kernel should move. Parallel rows (``n_threads=0``, all +cores) are reported per point. + +Usage:: + + python benchmark/benchmark_batch.py run -l master -o master.json + python benchmark/benchmark_batch.py run -l branch -o branch.json \\ + --cases fedooSS_ELISO,fedooUL_ELISO --N 1e4,1e5 + python benchmark/benchmark_batch.py summary master.json branch.json + +For an A/B comparison, interleave the two builds (run A, run B, run A, run B, +...) and give all the files to ``summary``: it keeps the minimum per label. +The simcoon module timed is whatever ``import simcoon`` resolves (installed +package, or a build directory put first on ``PYTHONPATH``). Set +``OMP_NUM_THREADS``/``VECLIB_MAXIMUM_THREADS`` explicitly when the machine is +shared. +""" + +import argparse +import json +import sys +import time +from collections import defaultdict + +import numpy as np + +import simcoon as sim + +F = np.asfortranarray + +ELISO = ([200000.0, 0.3, 0.0], 1) +EPICP = ([200000.0, 0.3, 0.0, 300.0, 1000.0, 0.5], 8) +EPKCP = ([200000.0, 0.3, 0.0, 300.0, 1000.0, 0.5, 10000.0], 14) + + +def stack_eye(N): + return F(np.repeat(np.eye(3)[:, :, None], N, axis=2)) + + +def rand_F(N, amp=0.05, seed=0): + """N deformation gradients I + amp * gaussian noise, shape (3, 3, N).""" + rng = np.random.default_rng(seed) + return F(np.eye(3)[:, :, None] + amp * rng.standard_normal((3, 3, N))) + + +def modular_props(): + """props/nstatev of a MODUL J2 material (isotropic elasticity + power-law hardening).""" + from simcoon.modular import (IsotropicElasticity, ModularMaterial, Plasticity, + PowerLawHardening, VonMisesYield) + m = ModularMaterial( + elasticity=IsotropicElasticity(C1=200000.0, C2=0.3, alpha=0.0), + mechanisms=[Plasticity(sigma_Y=300.0, yield_criterion=VonMisesYield(), + isotropic_hardening=PowerLawHardening(k=1000.0, m=0.5))]) + return np.asarray(m.props, float), int(m.nstatev) + + +def law(name): + return {"ELISO": ELISO, "EPICP": EPICP, "EPKCP": EPKCP, "MODUL": modular_props()}[name] + + +def umat_case(name, props, nstatev, N, regime, finite=False, n_threads=1, temp=False): + """One ``sim.umat`` call on N identical points; regime "el" stays elastic, + "pl" is far past yield (sigma_Y = 300, E = 2e5).""" + etot = F(np.zeros((6, N))) + Detot = np.zeros((6, N)) + if regime == "el": + Detot[0, :] = 1e-4 + else: + Detot[0, :] = 5e-3 + Detot[1, :] = -2.5e-3 + Detot[2, :] = -2.5e-3 + Detot = F(Detot) + sigma = F(np.zeros((6, N))) + DR = stack_eye(N) + statev = F(np.zeros((nstatev, N))) + Wm = F(np.zeros((4, N))) + props = F(np.asarray(props, float).reshape(-1, 1)) + if finite: + F0 = stack_eye(N) + F1 = F0.copy(order="F") + F1[0, 0, :] = 1.0 + Detot[0, 0] + F1[0, 1, :] = 0.01 + else: + F0 = F1 = np.array([]) + kw = dict(n_threads=n_threads, start=False) + if temp: + kw["temp"] = np.array([290.0]) + + def call(): + return sim.umat(name, etot, Detot, F0, F1, sigma, DR, props, statev, 1.0, 0.01, Wm, **kw) + return call + + +def umat_T_case(name, props, nstatev, N, regime, n_threads=1): + etot = F(np.zeros((6, N))) + Detot = np.zeros((6, N)) + Detot[0, :] = 1e-4 if regime == "el" else 5e-3 + Detot = F(Detot) + sigma = F(np.zeros((6, N))) + DR = stack_eye(N) + statev = F(np.zeros((nstatev, N))) + Wm = F(np.zeros((4, N))) + Wt = F(np.zeros((3, N))) + T = np.full(N, 290.0) + DT = np.zeros(N) + props = F(np.asarray(props, float).reshape(-1, 1)) + + def call(): + return sim.umat_T(name, etot, Detot, sigma, DR, props, statev, 1.0, 0.01, Wm, Wt, T, DT, + n_threads=n_threads, start=False) + return call + + +def fedoo_ul_case(name, N, n_threads): + """The simcoon calls of one fedoo global iteration in updated Lagrangian + (corate log_R, box tangent converted twice by Lt_convert).""" + props, nstatev = law(name) + props = F(np.asarray(props, float).reshape(-1, 1)) + F0 = rand_F(N, 0.02, seed=1) + F1 = F(F0 + 0.004 * np.eye(3)[:, :, None] * np.array([1.0, -0.5, -0.5])[:, None, None]) + e0 = F(np.zeros((6, N))) + s0 = F(np.zeros((6, N))) + statev = F(np.zeros((nstatev, N))) + Wm = F(np.zeros((4, N))) + + def call(): + de, _D, DR, _Omega = sim.objective_rate("log_R", F0, F1, 0.01, True, n_threads) + rot = sim.Rotation.from_matrix(DR.transpose(2, 0, 1)) + e = rot.apply_strain(e0) + s = rot.apply_stress(s0) + sig, _sv, _wm, Lt = sim.umat(name, e, de, F0, F1, s, DR, props, statev, 1.0, 0.01, Wm, + temp=np.array([290.0]), ndi=3, n_threads=n_threads, + tangent_mode=2, corate=3, work_correction=True, + tangent_output="box", start=False) + dsde = sim.Lt_convert(Lt, F1, sig, "DsigmaDe_2_DSDE") + return sim.Lt_convert(dsde, F1, sig, "DSDE_2_Dsigma_LieDD") + return call + + +def fedoo_ul2_case(name, N, n_threads): + """fedooUL_ as fedoo runs it with rotate_voigt: no scipy Rotation, umat called with + positional arguments, n_threads passed to every batch call. Falls back on the Rotation + path and the default Lt_convert threads where the simcoon timed lacks them.""" + props, nstatev = law(name) + props = F(np.asarray(props, float).reshape(-1, 1)) + F0 = rand_F(N, 0.02, seed=1) + F1 = F(F0 + 0.004 * np.eye(3)[:, :, None] * np.array([1.0, -0.5, -0.5])[:, None, None]) + e0 = F(np.zeros((6, N))) + s0 = F(np.zeros((6, N))) + statev = F(np.zeros((nstatev, N))) + Wm = F(np.zeros((4, N))) + temp = np.array([290.0]) + rotate = getattr(sim, "rotate_voigt", None) + lt_kw = {"n_threads": n_threads} if "n_threads" in (sim.Lt_convert.__doc__ or "") else {} + + def call(): + de, _D, DR, _Omega = sim.objective_rate("log_R", F0, F1, 0.01, True, n_threads) + if rotate is not None: + e = rotate(e0, DR, "strain", True, n_threads) + s = rotate(s0, DR, "stress", True, n_threads) + else: + rot = sim.Rotation.from_matrix(DR.transpose(2, 0, 1)) + e = rot.apply_strain(e0) + s = rot.apply_stress(s0) + # umat_name ... Wm, temp, ndi, nshr, n_threads, tangent_mode, corate, work_correction, + # tangent_output, start + sig, _sv, _wm, Lt = sim.umat(name, e, de, F0, F1, s, DR, props, statev, 1.0, 0.01, Wm, + temp, 3, None, n_threads, 2, 3, True, "box", False) + dsde = sim.Lt_convert(Lt, F1, sig, "DsigmaDe_2_DSDE", **lt_kw) + return sim.Lt_convert(dsde, F1, sig, "DSDE_2_Dsigma_LieDD", **lt_kw) + return call + + +def build(case, N, n_threads): + """Return a zero-argument callable timing *case* on N points.""" + if case == "umat_ELISO": + return umat_case("ELISO", *ELISO, N, "el", n_threads=n_threads) + if case == "umat_ELISO_T": # with temp, as fedoo passes it + return umat_case("ELISO", *ELISO, N, "el", n_threads=n_threads, temp=True) + if case in ("umat_EPICP_el", "umat_EPICP_pl"): + return umat_case("EPICP", *EPICP, N, case[-2:], n_threads=n_threads) + if case == "umat_EPKCP_pl": + return umat_case("EPKCP", *EPKCP, N, "pl", n_threads=n_threads) + if case in ("umat_MODUL_el", "umat_MODUL_pl"): + return umat_case("MODUL", *modular_props(), N, case[-2:], n_threads=n_threads) + if case == "umat_NEOHC": + return umat_case("NEOHC", [1000.0, 10000.0], 1, N, "el", finite=True, n_threads=n_threads) + if case == "umat_ELISO_finite": + return umat_case("ELISO", *ELISO, N, "el", finite=True, n_threads=n_threads) + if case == "umatT_ELISO": + return umat_T_case("ELISO", [7800.0, 500.0, 200000.0, 0.3, 0.0], 1, N, "el", + n_threads=n_threads) + if case == "umatT_EPICP_pl": + return umat_T_case("EPICP", [7800.0, 500.0, 200000.0, 0.3, 0.0, 300.0, 1000.0, 0.5], 8, + N, "pl", n_threads=n_threads) + if case.startswith("objrate_"): + rate = case.split("_", 1)[1] + F0, F1 = stack_eye(N), rand_F(N) + return lambda: sim.objective_rate(rate, F0, F1, 0.01, True, n_threads) + if case.startswith("Ltconv_"): # no n_threads argument + key = case.split("_", 1)[1] + Lt = F(np.repeat((np.eye(6) * 1000.0)[:, :, None], N, axis=2)) + F1, s = rand_F(N), F(np.ones((6, N))) + return lambda: sim.Lt_convert(Lt, F1, s, key) + if case.startswith("stressconv_"): + key = case.split("_", 1)[1] + F1, s = rand_F(N), F(np.ones((6, N))) + return lambda: sim.stress_convert(s, F1, key, copy=False) + if case == "Log_strain": + F1 = rand_F(N) + return lambda: sim.Log_strain(F1, True, False) + if case == "rot_from_matrix_apply_strain": # fedoo mechanical3d, finite strain + DR = np.ascontiguousarray(rand_F(N, 0.0).transpose(2, 0, 1)) + e = F(np.ones((6, N))) + + def call(): + r = sim.Rotation.from_matrix(DR) + return r.apply_strain(e), r.apply_stress(e) + return call + if case == "rot_apply_stiffness": + from scipy.spatial.transform import Rotation as R + r = sim.Rotation.from_quat(R.random(max(N, 2), random_state=0).as_quat()) + L = F(np.repeat((np.eye(6) * 1000.0)[:, :, None], len(r), axis=2)) + return lambda: r.apply_stiffness(L) + if case == "tensor2_rotate": + from scipy.spatial.transform import Rotation as R + t = sim.Tensor2.stress(np.ones((N, 6))) + r = sim.Rotation.from_matrix(R.random(N, random_state=0).as_matrix()) + return lambda: t.rotate(r) + if case == "tensor4_push_forward": + Fm = np.ascontiguousarray(rand_F(N).transpose(2, 0, 1)) + L = sim.Tensor4.stiffness(np.repeat((np.eye(6) * 1000.0)[None], N, axis=0)) + return lambda: L.push_forward(Fm) + if case.startswith("fedooUL_"): + return fedoo_ul_case(case.split("_", 1)[1], N, n_threads) + if case.startswith("fedooUL2_"): + return fedoo_ul2_case(case.split("_", 1)[1], N, n_threads) + if case == "rotate_voigt_strain_stress": # the rotate_voigt form of rot_from_matrix_apply_strain + DR = rand_F(N, 0.0) + e = F(np.ones((6, N))) + return lambda: (sim.rotate_voigt(e, DR, "strain", True, n_threads), + sim.rotate_voigt(e, DR, "stress", True, n_threads)) + if case == "rotate_voigt_stiffness": + from scipy.spatial.transform import Rotation as R + DR = F(R.random(N, random_state=0).as_matrix().reshape(N, 3, 3).transpose(1, 2, 0)) + L = F(np.repeat((np.eye(6) * 1000.0)[:, :, None], N, axis=2)) + return lambda: sim.rotate_voigt(L, DR, "stiffness", True, n_threads) + if case.startswith("fedooSS_"): + name = case.split("_", 1)[1] + return umat_case(name, *law(name), N, "el", n_threads=n_threads, temp=True) + raise KeyError(f"unknown case {case!r}") + + +ALL_CASES = [ + "umat_ELISO", "umat_ELISO_T", "umat_EPICP_el", "umat_EPICP_pl", "umat_EPKCP_pl", + "umat_MODUL_el", "umat_MODUL_pl", "umat_NEOHC", "umat_ELISO_finite", "umatT_ELISO", + "umatT_EPICP_pl", "objrate_log_R", "objrate_jaumann", "objrate_log", + "Ltconv_DsigmaDe_2_DSDE", "Ltconv_DSDE_2_Dsigma_LieDD", "stressconv_Cauchy2PKII", + "Log_strain", "rot_from_matrix_apply_strain", "rot_apply_stiffness", "tensor2_rotate", + "tensor4_push_forward", + "fedooSS_ELISO", "fedooSS_EPICP", "fedooSS_MODUL", + "fedooUL_ELISO", "fedooUL_EPICP", "fedooUL_MODUL", + "fedooUL2_ELISO", "fedooUL2_EPICP", "fedooUL2_MODUL", +] + + +def time_call(call, budget=0.25, min_reps=5, max_reps=2000): + """Minimum wall time of call() in microseconds, over adaptive repetitions.""" + call() # warm-up (first-touch allocations, lazy imports) + best = float("inf") + t_start = time.perf_counter() + reps = 0 + while reps < min_reps or (time.perf_counter() - t_start < budget and reps < max_reps): + t0 = time.perf_counter_ns() + call() + best = min(best, time.perf_counter_ns() - t0) + reps += 1 + return best / 1e3, reps + + +def cmd_run(args): + cases = args.cases.split(",") if args.cases else ALL_CASES + Ns = [int(float(x)) for x in args.N.split(",")] + threads = [int(x) for x in args.threads.split(",")] + rows = [] + for case in cases: + for nt in threads: + for N in Ns: + if nt != 1 and N <= 100: + continue # the batch APIs run serially below 100 points anyway + try: + us, reps = time_call(build(case, N, nt), budget=args.budget) + except Exception as e: # one broken case must not void the run + print(f"{case} N={N} nt={nt}: ERROR {type(e).__name__}: {e}", file=sys.stderr) + continue + rows.append(dict(label=args.label, case=case, N=N, n_threads=nt, us=us, reps=reps)) + print(f"{args.label:8s} {case:30s} nt={nt} N={N:>7d} {us:12.1f} us " + f"{us / N:9.3f} us/pt", flush=True) + with open(args.output, "w") as f: + json.dump(rows, f, indent=1) + print(f"\nresults saved to {args.output}") + + +def fit_affine(N, t): + """t = a + b N by least squares on the relative residuals (each point weighs + the same whatever its magnitude, so N = 1 pins a and N = 1e5 pins b).""" + N, t = np.asarray(N, float), np.asarray(t, float) + A = np.vstack([np.ones_like(N), N]).T / t[:, None] + a, b = np.linalg.lstsq(A, np.ones_like(t), rcond=None)[0] + return a, b + + +def cmd_summary(args): + rows = [] + for p in args.files: + with open(p) as f: + rows += json.load(f) + best = {} + cases = [] + for r in rows: + k = (r["label"], r["case"], r["N"], r["n_threads"]) + best[k] = min(best.get(k, float("inf")), r["us"]) + if r["case"] not in cases: + cases.append(r["case"]) + labels = sorted({k[0] for k in best}, key=lambda l: [r["label"] for r in rows].index(l)) + + print("## serial (n_threads=1): t = a + b N [a in us/call, b in us/point]") + print(f"{'case':30s} " + " ".join(f"{l + ' a':>12s} {l + ' b':>12s}" for l in labels)) + for c in cases: + line = f"{c:30s} " + for l in labels: + pts = sorted((k[2], v) for k, v in best.items() if k[:2] == (l, c) and k[3] == 1) + if len(pts) < 3: + line += f"{'-':>12s} {'-':>12s} " + continue + a, b = fit_affine(*zip(*pts)) + line += f"{a:12.2f} {b:12.3f} " + print(line) + + Ns = sorted({k[2] for k in best}) + print("\n## time per point (us/pt) by N; n_threads=0 is the parallel run") + print(f"{'':30s} {'':6s} " + " ".join(f"{N:>9d}" for N in Ns)) + for c in cases: + for nt in sorted({k[3] for k in best}, reverse=True): + for l in labels: + vals = [best.get((l, c, N, nt)) for N in Ns] + if all(v is None for v in vals): + continue + cells = " ".join(f"{v / N:9.3f}" if v is not None else f"{'':9s}" + for v, N in zip(vals, Ns)) + print(f"{c:30s} nt={nt:<3d} {cells} {l}") + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + sub = parser.add_subparsers(dest="command", required=True) + run = sub.add_parser("run", help="time the cases and save the raw minima (JSON)") + run.add_argument("-l", "--label", default="current", help="build label stored in the rows") + run.add_argument("-o", "--output", default="benchmark_batch.json") + run.add_argument("--cases", default="", help="comma-separated subset of: " + ",".join(ALL_CASES)) + run.add_argument("--N", default="1,10,1000,10000,100000", help="points per call") + run.add_argument("--threads", default="1,0", help="n_threads values (0 = all cores)") + run.add_argument("--budget", type=float, default=0.25, help="seconds of repetitions per point") + run.set_defaults(func=cmd_run) + summ = sub.add_parser("summary", help="fit a + b N and tabulate one or more runs") + summ.add_argument("files", nargs="+") + summ.set_defaults(func=cmd_summary) + args = parser.parse_args() + args.func(args) + + +if __name__ == "__main__": + main() diff --git a/benchmark/dump_results.py b/benchmark/dump_results.py new file mode 100644 index 000000000..2e44bcd87 --- /dev/null +++ b/benchmark/dump_results.py @@ -0,0 +1,289 @@ +"""Non-regression dumps: run a fixed set of computations, save every output array, +and compare two dumps bit for bit (or within a tolerance). + +Two dumps: + +- ``solver`` — every result field (``scalar_data`` and ``field_data``) of: + + * the ``benchmark_solver.py`` cases (with ``record_tangent=True``); + * each family of ``benchmark/bench_legacy_vs_modular.py``, under its legacy + name and as its MODUL twin, on ``examples/data/MODUL_path.json``; + * every ``sim.solver.solve`` call made by a few gallery examples (EPCHA, + MODUL closest point, SMA_T, SMA_TR, thermomechanical SMA_T): the examples are + run as scripts with plotting disabled and each solve's result is recorded. + +- ``batch`` — ``sim.umat`` on (6, N) arrays (N = 997 random directions) over + six increments, loading then unloading, for ELISO, EPICP, EPKCP, MODUL and + EPCHG, serial (``n_threads=1``) and parallel (``n_threads=0``): sigma, statev, + Wm and Lt after each increment. + +Usage:: + + python benchmark/dump_results.py solver ref_solver.npz # with the reference build + python benchmark/dump_results.py batch ref_batch.npz + ... # switch builds + python benchmark/dump_results.py solver new_solver.npz + python benchmark/dump_results.py compare ref_solver.npz new_solver.npz + python benchmark/dump_results.py compare ref_batch.npz new_batch.npz --rtol 1e-12 + +``compare`` prints, per case, either BIT-IDENTICAL or the largest relative +difference ``max|a - b| / max|a|`` over its arrays, and exits with status 1 if an +array is missing, changes shape, or differs by more than ``--rtol`` (default 0: +bit-identical). A change that is meant to be bit-identical (memory layout, +caching, dead code) must pass with the default; a change of floating-point +operation order (other linear solver, closed forms) lists its worst differences +with ``--rtol``. + +The build compared is whatever ``import simcoon`` resolves: dump with one build, +switch ``PYTHONPATH`` (or reinstall), dump again. Keep the two runs on the same +machine: LAPACK/BLAS and the compiler's FMA contraction differ across platforms. +""" + +import argparse +import contextlib +import io +import os +import runpy +import sys +import warnings +from pathlib import Path + +import numpy as np + +REPO = Path(__file__).resolve().parents[1] +EXAMPLES = REPO / "examples" + +#: Gallery examples whose solver calls enter the ``solver`` dump (they cover the +#: legacy EPCHA kernel, the modular closest point return, and the SMA laws). +DEFAULT_EXAMPLES = ( + "mechanical/EPCHA.py", + "mechanical/MODUL_closest_point.py", + "mechanical/SMA_T.py", + "mechanical/SMA_TR.py", + "thermomechanical/SMA_T.py", +) + + +# --------------------------------------------------------------------------- solver + + +def _fields(res): + """All numeric result fields of a SolverResults, as float arrays.""" + out = {} + for k, v in list(res.scalar_data.items()) + list(res.field_data.items()): + try: + out[k] = np.asarray(v, dtype=float) + except (TypeError, ValueError): + pass + return out + + +def _run_example(path): + """Run one example script; return the results of its ``sim.solver.solve`` calls.""" + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + import simcoon as sim + import simcoon.solver as ssolver + from simcoon.solver import core as score + + results = [] + orig = score.solve + + def recording_solve(*args, **kwargs): + res = orig(*args, **kwargs) + results.append(res) + return res + + saved = (ssolver.solve, score.solve, plt.show, plt.savefig, os.getcwd()) + ssolver.solve = score.solve = sim.solver.solve = recording_solve + plt.show = plt.savefig = lambda *a, **k: None + os.chdir(path.parent) + try: + with contextlib.redirect_stdout(io.StringIO()), warnings.catch_warnings(): + warnings.simplefilter("ignore") + runpy.run_path(str(path), run_name="__main__") + except Exception as e: # a failure after the solves (plotting...) is not ours to judge + print(f" {path.name} raised {type(e).__name__}: {str(e)[:120]}") + finally: + ssolver.solve = score.solve = sim.solver.solve = saved[0] + plt.show, plt.savefig = saved[2], saved[3] + os.chdir(saved[4]) + plt.close("all") + return results + + +def solver_cases(): + """(name, thunk) pairs; each thunk returns a SolverResults.""" + import simcoon as sim + from simcoon.solver import Block, StepMeca + + sys.path.insert(0, str(REPO / "benchmark")) + import benchmark_solver as bs + import bench_legacy_vs_modular as blm + + cases = [] + for name, cfg in bs.BENCHMARK_CASES.items(): + value = np.zeros(6) + value[0] = cfg["strain_max"] + blk = Block(steps=[StepMeca(control=["strain"] + ["stress"] * 5, value=value, + ninc=cfg["ninc"], time=1.0)], + control_type=cfg["control_type"]) + cases.append((f"benchmark/{name}", lambda blk=blk, name=name, cfg=cfg: sim.solver.solve( + blk, name, cfg["props"], cfg["nstatev"], T_init=290.0, corate=2, + record_tangent=True))) + + blocks, T_init, _ = sim.solver.load_path_json(str(EXAMPLES / "data" / "MODUL_path.json")) + for fam, (lname, lprops, lnst, _path, mat) in blm.FAMILIES.items(): + cases.append((f"lvm/{fam}/legacy", lambda lname=lname, lprops=lprops, lnst=lnst: + sim.solver.solve(blocks, lname, lprops, lnst, T_init=T_init, + record_tangent=True))) + cases.append((f"lvm/{fam}/modul", lambda mat=mat: sim.solver.solve( + blocks, "MODUL", mat.props, mat.nstatev, T_init=T_init, record_tangent=True))) + return cases + + +def dump_solver(out, examples): + store = {} + + def keep(name, res): + for k, v in _fields(res).items(): + store[f"{name}|{k}"] = v + + for name, thunk in solver_cases(): + try: + keep(name, thunk()) + except Exception as e: + print(f"FAIL {name}: {type(e).__name__}: {str(e)[:120]}") + store[f"{name}|__error__"] = np.array([1.0]) + for rel in examples: + path = (EXAMPLES / rel).resolve() + tag = "_".join(Path(rel).with_suffix("").parts) + for i, res in enumerate(_run_example(path)): + keep(f"ex/{tag}/{i}", res) + np.savez(out, **store) + print(f"saved {len(store)} arrays to {out}") + + +# --------------------------------------------------------------------------- batch + + +def dump_batch(out, N=997, n_inc=6): + import simcoon as sim + sys.path.insert(0, str(REPO / "benchmark")) + from benchmark_batch import modular_props + + F = np.asfortranarray + laws = { + "ELISO": ([200000.0, 0.3, 1e-5], 1), + "EPICP": ([200000.0, 0.3, 1e-5, 300.0, 1000.0, 0.5], 8), + "EPKCP": ([200000.0, 0.3, 0.0, 300.0, 1000.0, 0.5, 10000.0], 14), + "MODUL": modular_props(), + "EPCHG": ([200000.0, 0.3, 80000.0, 1e-5, 300.0, 2, 2, 0, 100.0, 10.0, 50.0, 5.0, + 20000.0, 200.0, 5000.0, 50.0], 20), + } + rng = np.random.default_rng(3) + store = {} + for name, (props, nstatev) in laws.items(): + for nt in (1, 0): + props_a = F(np.asarray(props, float).reshape(-1, 1)) + etot = F(np.zeros((6, N))) + sigma = F(np.zeros((6, N))) + statev = F(np.zeros((nstatev, N))) + Wm = F(np.zeros((4, N))) + DR = F(np.repeat(np.eye(3)[:, :, None], N, axis=2)) + direction = rng.standard_normal((6, N)) + for inc in range(n_inc): + # 4 loading increments past yield, then unloading + Detot = F(2e-3 * direction * (1 if inc < 4 else -1)) + try: + sigma, statev, Wm, Lt = sim.umat( + name, etot, Detot, np.array([]), np.array([]), sigma, DR, props_a, + statev, inc * 0.1, 0.1, Wm, temp=np.array([290.0]), n_threads=nt, + start=(inc == 0)) + except Exception as e: + print(f"FAIL {name} nt={nt} inc {inc}: {type(e).__name__}: {str(e)[:100]}") + store[f"batch/{name}/nt{nt}/inc{inc}|__error__"] = np.array([1.0]) + break + etot = F(etot + Detot) + for k, v in (("sigma", sigma), ("statev", statev), ("Wm", Wm), ("Lt", Lt)): + store[f"batch/{name}/nt{nt}/inc{inc}|{k}"] = np.array(v) + np.savez(out, **store) + print(f"saved {len(store)} arrays to {out}") + + +# --------------------------------------------------------------------------- compare + + +def compare(path_a, path_b, rtol=0.0, verbose=False): + """Compare two dumps; return True when every array matches within rtol. + + Prints the cases that differ (all of them with *verbose*) and a summary line. + """ + a, b = np.load(path_a), np.load(path_b) + keys = sorted(set(a.files) | set(b.files)) + worst = {} # case -> (relative difference, field) + ok = True + n_exact = n_cmp = 0 + for k in keys: + case, _, field = k.rpartition("|") + if k not in a.files or k not in b.files: + print(f"MISSING in {'b' if k in a.files else 'a'}: {k}") + ok = False + continue + x, y = a[k], b[k] + if x.shape != y.shape: + print(f"SHAPE {k}: {x.shape} vs {y.shape}") + worst[case] = (np.inf, field) + ok = False + continue + n_cmp += 1 + if np.array_equal(x, y, equal_nan=True): + n_exact += 1 + worst.setdefault(case, (0.0, "")) + continue + scale = max(float(np.nanmax(np.abs(x))) if x.size else 0.0, 1e-300) + rel = float(np.nanmax(np.abs(x - y)) / scale) + if np.isnan(rel): # NaN on one side only + rel = np.inf + if rel > worst.get(case, (0.0, ""))[0]: + worst[case] = (rel, field) + if rel > rtol: + ok = False + for case, (rel, field) in sorted(worst.items()): + if rel == 0 and not verbose: + continue + status = "BIT-IDENTICAL" if rel == 0 else f"max rel diff {rel:.2e} ({field})" + print(f" {case:55s} {status}") + print(f"{n_exact}/{n_cmp} arrays bit-identical, {len(keys)} keys" + f"{'' if rtol == 0 else f', tolerance {rtol:g}'}: {'PASS' if ok else 'FAIL'}") + return ok + + +def main(): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + sub = parser.add_subparsers(dest="command", required=True) + p = sub.add_parser("solver", help="dump the 0D solver results") + p.add_argument("output") + p.add_argument("--examples", default=",".join(DEFAULT_EXAMPLES), + help="comma-separated example scripts, relative to examples/ ('' for none)") + p = sub.add_parser("batch", help="dump the batch sim.umat results") + p.add_argument("output") + p = sub.add_parser("compare", help="compare two dumps") + p.add_argument("a") + p.add_argument("b") + p.add_argument("--rtol", type=float, default=0.0, + help="largest relative difference accepted (default 0: bit-identical)") + p.add_argument("-v", "--verbose", action="store_true", help="also list identical cases") + args = parser.parse_args() + + if args.command == "solver": + dump_solver(args.output, [e for e in args.examples.split(",") if e]) + elif args.command == "batch": + dump_batch(args.output) + else: + sys.exit(0 if compare(args.a, args.b, args.rtol, args.verbose) else 1) + + +if __name__ == "__main__": + main() diff --git a/docs/continuum_mechanics/functions/doc_rotation.rst b/docs/continuum_mechanics/functions/doc_rotation.rst index 0c35531cc..bfb65d723 100644 --- a/docs/continuum_mechanics/functions/doc_rotation.rst +++ b/docs/continuum_mechanics/functions/doc_rotation.rst @@ -451,6 +451,23 @@ natively: # Get all 6x6 Voigt rotation matrices at once QS = rotations.as_voigt_stress_rotation() # (N, 6, 6) +**Without a Rotation object.** When the rotations already are matrices on simcoon's +``(3, 3, N)`` layout, as the ``DR`` of :func:`simcoon.objective_rate` in a finite-element +loop, ``rotate_voigt`` applies them directly: one C++ loop over the points, in parallel +and without the GIL, with no scipy conversion to quaternions. It returns what +``Rotation.from_matrix(DR.transpose(2, 0, 1)).apply_`` returns, up to round-off: + +.. code-block:: python + + stress_rotated = smc.rotate_voigt(stress, DR, "stress") # (6, N) + strain_rotated = smc.rotate_voigt(strain, DR, "strain") # (6, N) + L_rotated = smc.rotate_voigt(L, DR, "stiffness", n_threads=0) # (6, 6, N), all cores + + # one rotation for every tensor: DR of shape (3, 3) + L_rotated = smc.rotate_voigt(L, DR[:, :, 0], "stiffness") + +.. autofunction:: simcoon.rotate_voigt + Example 4: Averaging Orientations ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/cpp_api/continuum_mechanics_overview.md b/docs/cpp_api/continuum_mechanics_overview.md index f764057d8..dfc7755c7 100644 --- a/docs/cpp_api/continuum_mechanics_overview.md +++ b/docs/cpp_api/continuum_mechanics_overview.md @@ -73,15 +73,11 @@ User Material subroutines (UMAT) for finite element analysis, organized by strai **Damage:** - `damage_LLD_0` - Ladevèze-Le Dantec damage of the unidirectional ply -- `damage_weibull` - Weibull statistical damage model **Shape Memory Alloys (SMA):** - `SMA_mono` - Micromechanical monocrystal SMA model (supports SMAMO isotropic, SMAMC cubic symmetry) - `unified_T` - Unified thermomechanical SMA model -**Combined:** -- `Prony_Nfast_Plastic` - Viscoelastoplasticity - **External:** - `external_umat` - Plugin API for custom material models diff --git a/docs/external.rst b/docs/external.rst index 68b6cb86a..759e42b94 100755 --- a/docs/external.rst +++ b/docs/external.rst @@ -10,6 +10,10 @@ You can develop such an external plugin using the Simcoon API. Three plugin form Examples are provided in the ``external/`` folder. +The plugins serve the small-strain route of the solver: under finite strain (control ``green_lagrange``, +``logarithmic``, ``biot``, ``F`` or ``gradU``) the solver refuses them, since the plugin +API has no finite-strain entry. + UMEXT Plugin Format ------------------- @@ -363,7 +367,9 @@ The test data is located in ``testBin/Umats/UMEXT/``: **TUMABA Test** -Located in ``test_extern/Umats/UMABA/``, this test validates the Abaqus UMAT compatibility layer. +Located in ``test_extern/Umats/UMABA/``, this test validates the Abaqus UMAT and the Ansys USERMAT +compatibility layers: the same elastic case runs through both plugins (``UMABA``, and ``UMANS`` +built from ``external/umat_plugin_ans.cpp`` and ``external/USERMAT_ANSYS_ELASTIC.for``). The test structure is identical to TUMEXT with the ``UMABA`` name; its reference is in ``testBin/Umats/UMABA/comparison/``. .. note:: diff --git a/docs/simulation/identification.rst b/docs/simulation/identification.rst index d509ef3c5..0049ca61a 100644 --- a/docs/simulation/identification.rst +++ b/docs/simulation/identification.rst @@ -45,7 +45,7 @@ Parameter class .. code-block:: python - from simcoon.parameter import Parameter, read_parameters, copy_parameters, apply_parameters + from simcoon.parameter import Parameter, read_parameters # Create parameters programmatically params = [ @@ -78,7 +78,7 @@ A template is any text file, so the JSON inputs of the solver work as they are: ``keys/material.json`` with ``"props": [70000.0, 0.3, 1.0e-5, @sigmaY, @k, @m]`` is not valid JSON until the substitution and is valid after it. The ``identification/json_keys_identification.py`` example runs this chain end to -end (copy, apply, ``load_simulation_json``, ``solve``, ``calc_cost``, +end (``KeyTemplate``, ``load_simulation_json``, ``solve``, ``calc_cost``, ``identification``). Constant class @@ -88,23 +88,52 @@ Constants are fixed values (not optimized) that also use the key system: .. code-block:: python - from simcoon.constant import Constant, read_constants, copy_constants, apply_constants + from simcoon.constant import Constant, read_constants The ``Constant`` class is a ``NamedTuple`` with fields: ``number``, ``key``, ``input_values``, ``value``, ``sim_input_files``. -File operations -^^^^^^^^^^^^^^^ +A constant holds **one value per test** in ``input_values``, e.g. the fibre +angle of each specimen of a composite identification. ``read_constants(n, ...)`` +reads ``n`` values per constant from ``constants.inp``: + +.. code-block:: none + + #Number #key #test_0 #test_1 #test_2 #number_of_files #files + 0 @theta 0 45 90 1 ply.json + +``value`` is the first one; ``KeyTemplate.write(dst, test=i)`` (below) +substitutes the value of test ``i``. A constant with a single value is the same +in every test: it is not a constant of the identification but a fixed parameter +of the simulation, to write in the template rather than as a key, and +``write(..., test=i)`` warns about it. + +Writing the working files +^^^^^^^^^^^^^^^^^^^^^^^^^ + +A ``KeyTemplate`` reads the template files of ``keys/`` once and keeps them in +memory. Each ``write`` replaces the keys of the parameters, then those of the +constants, by their current values and writes every working file once: .. code-block:: python - # 1. Copy template files from keys/ to data/ - copy_parameters(params, src_path="keys", dst_path="data") + from simcoon.parameter import KeyTemplate + + template = KeyTemplate("keys", params, consts) # consts may be omitted + + def cost(x): + for p, v in zip(params, x): # values set by the optimizer + p.value = v + template.write("data") + ... + +With several tests, each one is written in its own directory with the +constants of that test: - # 2. Replace keys with current values - params[0].value = 200.0 # set by optimizer - params[1].value = 0.3 - apply_parameters(params, dst_path="data") +.. code-block:: python + + for i in range(3): # one fibre angle per specimen + template.write(f"data/test_{i}", test=i) This works with **any** file format — simcoon input files, FE meshes, JSON configs, etc. The key system is deliberately simple: it performs string @@ -136,8 +165,8 @@ identified values are written back to each ``Parameter.value``. ``job.inp`` here is a placeholder: a template input deck of an **external**, file-driven solver, supplied by the user in the ``keys`` folder (it is not part of simcoon). ``sim_input_files`` is the bridge to such solvers: the keys are -substituted into their decks before each evaluation (``copy_parameters`` + -``apply_parameters``, called from ``my_cost_function``). Simcoon's own models +substituted into their decks before each evaluation (``KeyTemplate.write``, +called from ``my_cost_function``). Simcoon's own models no longer need it — pass ``props``, and for mean-field models the ``phases``, straight to :func:`~simcoon.solver.solve`, and leave ``sim_input_files`` out. @@ -150,6 +179,52 @@ straight to :func:`~simcoon.solver.solve`, and leave ``sim_input_files`` out. **Returns:** ``scipy.optimize.OptimizeResult`` +Bounded least squares +^^^^^^^^^^^^^^^^^^^^^ + +Most identification costs are sums of squared differences between a model and +experiments. ``method="least_squares"`` minimizes them with the bounded +trust-region reflective algorithm of ``scipy.optimize.least_squares``, which +exploits that structure and typically needs 7 to 20 times fewer model +evaluations than differential evolution. The cost function then returns the +**vector of residuals**; ``calc_residuals`` builds it with the arguments of +``calc_cost`` (its sum of squares is the ``"mse"``, ``"nmse"`` or +``"nmse_per_response"`` cost): + +.. code-block:: python + + from simcoon.identify import identification, calc_residuals + + def residuals(x): + y_num = run_model(x) + return calc_residuals(y_exp, y_num, metric="nmse_per_response") + + result = identification(residuals, params, method="least_squares", + n_starts=3, seed=0) + cost = np.sum(result.fun ** 2) # the calc_cost value at the solution + +- The parameters are mapped onto [0, 1] (``scale="auto"``: logarithmically when + the lower bound is positive, linearly otherwise; or ``"linear"``/``"log"``, + per parameter if needed). The Jacobian is computed by finite differences in + these coordinates (``diff_step``, relative step, default ``sqrt(eps)``). +- The first start is ``Parameter.value`` where set, the middle of the scaled + range otherwise; ``n_starts - 1`` more are drawn by Latin hypercube sampling + (``seed``), and the best result is kept (``result.starts`` lists them all). +- A model failure is best returned as NaN residuals: the trial step is rejected + and the trust region shrunk. +- Other ``least_squares`` options (``xtol``, ``ftol``, ``gtol``, ``max_nfev``, + ``loss``…) are forwarded. + +Parallel evaluations +^^^^^^^^^^^^^^^^^^^^ + +``n_threads`` (default 1, serial) evaluates the cost function in a pool of +threads: the finite-difference columns of the Jacobian for +``"least_squares"`` (the result does not depend on the number of threads), a +whole generation for differential evolution (``updating="deferred"``). It +pays off because :func:`~simcoon.solver.solve` and ``L_eff`` release the GIL, +and requires a thread-safe cost function. + For more control (other optimizers, constraints, custom initialization), call ``scipy.optimize`` directly — the ``Parameter`` objects provide the bounds and values you need. @@ -231,10 +306,11 @@ Built-in metrics (numpy only, no extra dependency): - ``"mse"`` — Mean Squared Error (default) - ``"nmse"`` — Normalized MSE (divided by variance of all experimental data) - ``"nmse_per_response"`` — NMSE computed independently per response column, - then averaged. Each column is divided by its own ``sum(y_exp^2)``, - balancing responses of different magnitudes (e.g., force in N vs - displacement in mm). This is the recommended metric for multi-response - identification. + then averaged. Each column is divided by its own ``sum(y_exp^2)``, summed + over the tests, balancing responses of different magnitudes (e.g., force in + N vs displacement in mm). This is the recommended metric for multi-response + identification. The tests are pooled within a column, not normalised one by + one: a test with larger values weighs more (use ``w_test`` to rebalance). - ``"rmse"`` — Root Mean Squared Error - ``"mae"`` — Mean Absolute Error - ``"mape"`` — Mean Absolute Percentage Error (scikit-learn definition) @@ -260,18 +336,18 @@ The key system works with any simulation tool. For example, with fedoo .. code-block:: python import subprocess - from simcoon.parameter import Parameter, copy_parameters, apply_parameters + from simcoon.parameter import KeyTemplate, Parameter from simcoon.identify import identification, calc_cost params = [ Parameter(0, bounds=(100e3, 300e3), key="@E", sim_input_files=["material.json"]), ] + template = KeyTemplate("keys", params) def cost(x): params[0].value = x[0] - copy_parameters(params, "keys", "data") - apply_parameters(params, "data") + template.write("data") # Run external solver subprocess.run(["python", "run_fedoo_simulation.py"]) diff --git a/docs/simulation/python_solver.rst b/docs/simulation/python_solver.rst index 46e04bff3..71b62596a 100644 --- a/docs/simulation/python_solver.rst +++ b/docs/simulation/python_solver.rst @@ -133,8 +133,11 @@ The mean-field models (MIHEN, MIMTN, MISCN, MIPLN) take their sub-phases in memo as :class:`~simcoon.solver.micromechanics.Ellipsoid` or :class:`~simcoon.solver.micromechanics.Layer` objects passed to ``solve(phases=...)`` or ``sim.L_eff(..., phases=...)`` (see :doc:`solver`, ``phases``); their own ``props`` -carry only the scheme's settings, ``[mp, np, n_matrix]`` for Mori-Tanaka (see -:doc:`umat_catalog`). Every orientation +carry only the scheme's settings, ``[mp, np, n_matrix]`` for Mori-Tanaka, plus optional +numerical options (closed-form Eshelby tensors, half-sphere quadrature, Anderson +acceleration; see :doc:`umat_catalog`). For an orientation distribution, +:func:`~simcoon.solver.micromechanics.discretize_odf` with ``weights="fourier"`` gives the +exact Mori-Tanaka average (isotropic matrix) with a handful of phases. Every orientation in those objects is a :class:`simcoon.Rotation`: the material frame of a phase (``material_orientation``) and the geometry of an inclusion or a layer (``geometry_orientation``). Any of these forms is accepted and coerced: @@ -228,7 +231,8 @@ Solver parameters ----------------- ``solve()`` exposes the numeric controls of the adaptive Newton loop as -keyword arguments: ``precision`` (default 1e-6), ``maxiter``/``miniter``, +keyword arguments: ``precision`` (default 1e-6, absolute), ``precision_rel`` (default 0; +``> 0`` makes the tolerance ``max(precision, precision_rel * |tau|)``), ``maxiter``/``miniter``, ``div_tnew_dt``/``mul_tnew_dt`` (time-step cut/growth factors), ``inforce``, ``lambda_solver`` (penalty stiffness of strain-driven components), plus ``tangent_mode`` (``'none'``, ``'continuum'``, ``'algorithmic'`` — default) diff --git a/docs/simulation/python_umat.rst b/docs/simulation/python_umat.rst index 0b58362e3..9f64b9e26 100644 --- a/docs/simulation/python_umat.rst +++ b/docs/simulation/python_umat.rst @@ -137,7 +137,7 @@ The contract tells a caller (a finite-element code) whether that condensed point is validated for a law, under either ``condense`` option: the condition holds at convergence and the tangent is the Schur complement of the law's own, exact - whenever the 3D tangent is (``tangent_mode=3`` for ``EPHIL``, ``EPKCP``, + whenever the 3D tangent is (``integrator="closest_point"`` for ``EPHIL``, ``EPKCP``, ``EPCHA``). It is ``True`` for ``ndi = 3`` and ``False`` for a law not validated in that form; a caller selects its path on it rather than on the version number. diff --git a/docs/simulation/solver.rst b/docs/simulation/solver.rst index 94680a4cf..9e7e4ccf5 100755 --- a/docs/simulation/solver.rst +++ b/docs/simulation/solver.rst @@ -139,8 +139,14 @@ Solver parameters - string or int - Former name of ``kinematic_form``: the same value under either name * - tangent_mode - - int - - Tangent-operator mode (default 2): see below + - string or int + - Tangent operator (default ``"algorithmic"``): see below + * - integrator + - string + - Local integrator, ``"cutting_plane"`` (default) or ``"closest_point"``: see below + * - ncp + - string + - Complementarity function of the cutting plane, ``"fischer_burmeister"`` (default) or ``"min"`` (see the UMAT catalog) * - solver_type - int - Solver strategy (0: Newton-Raphson) @@ -159,8 +165,9 @@ Tangent-operator modes The ``tangent_mode`` keyword selects the tangent operator returned by the constitutive models (also exposed as named constants: -``sim.tangent_none``, ``sim.tangent_continuum``, ``sim.tangent_algorithmic``, -``sim.tangent_closest_point``): +``sim.tangent_none``, ``sim.tangent_continuum``, ``sim.tangent_algorithmic``). +It is the consistent operator of the local integrator that ran, chosen apart +by ``integrator`` (below): .. list-table:: :header-rows: 1 @@ -185,13 +192,15 @@ constitutive models (also exposed as named constants: viscoelastic and damage models and their ``MODUL`` compositions (see the note below); approximate otherwise (this was mode 1 before the 2.0 renumbering) - * - 3 - - closest-point - - Closest-point-projection integrator: the plastic strain uses the flow of - the converged state, so the consistent operator is the exact Jacobian - of the discrete update for every criterion and hardening law the - integrator supports (rolling out law by law in this version; a law - without it returns the algorithmic operator of mode 2) + +``integrator="closest_point"`` replaces the cutting-plane local loop (the +default ``"cutting_plane"``) by the closest-point projection: the plastic strain +uses the flow of the converged state, so the algorithmic operator becomes the +exact Jacobian of the discrete update for every criterion and hardening law the +integrator supports; a law or a row without it keeps the cutting plane. It takes +``tangent_mode="algorithmic"``. It used to be ``tangent_mode=3``, which is no +longer accepted (``ValueError``). See the UMAT catalog, integrator and tangent +operator. .. note:: **Scope of the algorithmic tangent (2.1).** The plastic return mapping is a @@ -203,9 +212,10 @@ constitutive models (also exposed as named constants: path, and no tangent can be its exact derivative: the algorithmic operator is then a close approximation (about :math:`10^{-4}` to :math:`10^{-2}` relative, more for Tresca), which slows the global Newton iteration but leaves the - converged response unchanged. Mode 3 replaces the cutting-plane scheme by a - closest-point projection, which makes the operator exact for the laws that - carry it. + converged response unchanged. ``integrator="closest_point"`` (formerly mode 3) + replaces the cutting-plane scheme by a closest-point projection, which makes + the operator exact for the laws that carry it (see the UMAT catalog, + integrator and tangent operator). .. note:: **2.0 renumbering.** Pre-2.0, ``tangent_mode 0`` meant *continuum* and diff --git a/docs/simulation/umat_catalog.rst b/docs/simulation/umat_catalog.rst index 52c5cff26..f1bd068f8 100644 --- a/docs/simulation/umat_catalog.rst +++ b/docs/simulation/umat_catalog.rst @@ -120,7 +120,7 @@ Small-strain mechanical models identical or inactive second surfaces); the modular engine handles multiple surfaces correctly, so N >= 2 is now functional. * - ZENER - - Generalized KELVIN chain, 1 branch (standard solid) + - Standard linear solid, Poynting-Thomson (Kelvin) form: spring in series with one Kelvin-Voigt unit - legacy (kept) - E0, nu0, alpha, E1, nu1, etaB1, etaS1 - No modular equivalent (Kelvin branches in series; the modular @@ -347,8 +347,29 @@ Unchanged dedicated implementations (out of the modular scope): MIPLN (``mp``, ``np``: integration points of the Eshelby integrals; ``n_matrix``: index of the matrix phase in the list; ``start``: first guess of the self-consistent iteration, 1 Mori-Tanaka (default) or 0 homogeneous strain with - ``n_matrix < 0``). The length is checked, so the pre-2.0 layout with its leading - ``nphases`` and file-number slots is refused rather than misread. + ``n_matrix < 0``). The length and the values are checked, so the pre-2.0 layout with + its leading ``nphases`` and file-number slots is refused rather than misread. + + Optional trailing slots set numerical options; all default to 0, the historical + behaviour (giving a slot gives the ones before it): + ``[mp, np, n_matrix, closed_form, rule]`` for MIMTN and + ``[mp, np, n_matrix, start, closed_form, rule, anderson, rtol]`` for MISCN. + + * ``closed_form = 1``: the Eshelby tensor is taken in closed form (sphere, prolate or + oblate spheroid of axis 1) when the reference medium is isotropic, and integrated + otherwise. Exact, and no integration: ``L_eff`` of a self-consistent composite of + spheres is 7 to 10 times faster (20 times with ``anderson = 2``). + * ``rule = 1``: half-sphere quadrature, ``(mp+1)/2`` Gauss-Legendre points in + ``cos(theta)`` and ``np`` trapezoidal points in ``phi`` (the integrand is centrally + symmetric and periodic in ``phi``): half the evaluations of the default + Gauss-Legendre rule for the same accuracy or better. + * ``anderson = m`` (MISCN, ``L_eff`` only): Anderson acceleration of depth ``m`` of the + self-consistent fixed point (``m = 2`` is a good choice: 65 iterations become about + 11 at a 50 % concentration). The solver does one self-consistent sweep per + localization iteration and ignores it. + * ``rtol > 0`` (MISCN, ``L_eff`` only): relative stopping criterion + ``||L - L_prev|| <= rtol ||L||`` of that fixed point instead of the absolute + ``1e-6`` (in the stiffness unit), so the iteration count no longer depends on the unit. - **Plugins**: UMEXT (external dylib), UMABA (Abaqus wrapper). - **Python**: PYEXT (registered Python law; ``props``/``nstatev`` from the object). @@ -584,13 +605,43 @@ selecting the rate that tangent is expressed in. The default is contract-preserv returns precisely the box the finite kernels used to produce unconditionally. Rescaling that tangent by :math:`1/J` at the boundary would break ``Lt_convert`` by exactly :math:`J`. -Tangent-operator mode -===================== +Integrator and tangent operator +=============================== + +A run makes three numerical choices for the laws with a return mapping, given to ``solve`` +and ``sim.umat`` by name: + +.. list-table:: + :header-rows: 1 -All models receive the solver's ``tangent_mode`` (named constants in -``parameter.hpp`` / ``sim.tangent_*``): 0 = none (Lt = elastic L, explicit -integration), 1 = continuum, 2 = algorithmic/Simo-Hughes (**default**), -3 = closest-point (exact operator of the closest-point integrator where the law has one, else the algorithmic operator). Pre-2.0 numbering was 0 = continuum, + * - Option + - Chooses + - Values + - Changes the converged answer + * - ``integrator`` + - the discrete update of the local step + - ``"cutting_plane"`` (default), ``"closest_point"`` (backward Euler) + - yes: two different schemes + * - ``ncp`` + - how the Kuhn-Tucker conditions are solved inside it + - ``"fischer_burmeister"`` (default), ``"min"`` + - no, to the local tolerance (see below) + * - ``tangent_mode`` + - the operator handed to the global Newton + - ``"none"``, ``"continuum"``, ``"algorithmic"`` (default) + - no: global convergence only + +``"algorithmic"`` is the consistent operator of the integrator that ran: Simo-Hughes after the +cutting plane, the exact operator of the closest-point map after the closest point. The closest +point is offered with that operator only (``integrator="closest_point"`` with ``"none"`` or +``"continuum"`` raises ``ValueError``). ``tangent_mode = 3`` (``"closest_point"``), the former way +to ask for the closest point, is no longer accepted: it raises ``ValueError``, pointing to +``integrator="closest_point"``. Inside, the kernels still read one code (``kernel_tangent_mode``, +``parameter.hpp``): 0 = none (Lt = elastic L, explicit integration), 1 = continuum, +2 = algorithmic/Simo-Hughes (**default**), 3 = the closest-point integrator with its exact +operator. Code 3 reaches only the laws that implement it (``check_integrator_serves``; closest-point +coverage below), so a +Python law (``PYEXT``) receives 0, 1 or 2. Pre-2.0 numbering was 0 = continuum, 1 = algorithmic — see :doc:`solver` for the migration note. The finite-strain hyperelastic models ignore the mode (their tangent is always the exact one of the hyperelastic law). So do the linear viscoelastic models (``ZENER``, @@ -602,7 +653,7 @@ von Mises criterion and isotropic hardening only; with kinematic hardening or a Drucker or Tresca criterion the cutting-plane update is path dependent and the operator is approximate (see the note on the scope of the algorithmic tangent in :doc:`solver`). -**Mode 3 coverage (2.2).** The closest-point integrator with its exact operator runs for +**Closest-point coverage (2.2).** The closest-point integrator with its exact operator runs for ``MODUL`` rows whose criterion has a flow Hessian (von Mises, Hill, DFA, Ani; with every hardening law and a hyperelastic block), for the dedicated kernels ``EPICP``, ``EPCHA``, ``EPICP`` / ``EPKCP`` thermomechanical, and for the SMA kernels ``SMADI`` / ``SMADC`` / @@ -613,10 +664,167 @@ up to 5x on a non-proportional path, for an operator exact to :math:`10^{-8}`). ``EPJCK`` (and the adapter-served von Mises + isotropic names) mode 3 is **identical to mode 2 by construction**: with a radial flow the cutting-plane update already is the closest-point projection and the algorithmic operator is the exact Jacobian of the discrete map, including -the rate term. Mode 3 falls back to the mode-2 operator for a Tresca or Drucker row, the -thermomechanical SMA kernel and ``SMA_mono``. A condensed stress state (plane stress, beam, +the rate term. Any other law, the thermomechanical SMA kernel and ``SMA_mono`` included, refuses +``integrator="closest_point"`` with ``ValueError`` rather than running its cutting plane +silently; inside ``MODUL``, a Tresca or Drucker row keeps the cutting plane and its mode-2 +operator. A condensed stress state (plane stress, beam, truss) keeps mode 3: the law runs 3D and the state is solved around it. +Numerical options of the local Newton +===================================== + +These options change *how* the local return mapping is solved, not the constitutive law. They +are settings of the run, not material parameters: they are given to ``solve`` or ``sim.umat`` +by name, and the ``props`` stay those of the law. + +``ncp``: a faster local solve for simple plasticity +--------------------------------------------------- + +``solve(..., ncp="min")`` or ``sim.umat(..., ncp="min")`` switches the complementarity function +of the cutting plane from Fischer-Burmeister (``ncp="fischer_burmeister"``, the default, the +historical results bit for bit) to the min function, the classic cutting-plane step. EPICP, +EPKCP and ``MODUL`` read it, in their cutting plane. Any other law, a thermomechanical block, a +mean-field model or the closest-point integrator raises ``ValueError`` for ``ncp="min"``. + +**Use it** for a single yield surface with isotropic or linear (Prager) kinematic hardening. +The return is the same to the local tolerance, and the local iterations drop from about 6 to 3. +In ``MODUL`` this includes a yield surface composed with viscoelastic (Prony) or damage +mechanisms: their rows are always satisfied and uncoupled, so the min step applies to the yield +surface next to them. With several coupled yield surfaces the option has no effect: +Fischer-Burmeister is kept. +If the min step makes the error grow, the solver switches back to Fischer-Burmeister for the +rest of the increment. + +**Do not use it** with Armstrong-Frederick (Chaboche) back stresses. ``MODUL`` refuses the +combination, and the Chaboche kernels do not take the option. Those back stresses are integrated +along the cutting-plane iterates, so the full min steps land on a different answer, without +any growth in the residual that the fallback could detect. On coarse increments of the +Chaboche identification programmes, this answer is up to 46 % off the closest-point update, +where Fischer-Burmeister stays within 4 %. For an accurate Chaboche return, use +``integrator="closest_point"``. + +How the two functions work +~~~~~~~~~~~~~~~~~~~~~~~~~~ + +At each increment the law seeks the plastic multiplier :math:`\Delta p` of each yield surface +that satisfies the Kuhn-Tucker conditions + +.. math:: + + \Phi \le 0, \qquad \Delta p \ge 0, \qquad \Phi\,\Delta p = 0, + +with :math:`\Phi` the yield function (for EPICP, +:math:`\Phi = \sigma_{Mises} - R(p) - \sigma_Y`): either the point stays elastic +(:math:`\Delta p = 0`, :math:`\Phi \le 0`) or it lies on the surface (:math:`\Phi = 0`, +:math:`\Delta p > 0`). The cutting plane solves them by Newton on :math:`\Delta p`, with the +linearisation :math:`\Phi(\Delta p + \delta p) \approx \Phi + B\,\delta p`, +:math:`B = \partial\Phi/\partial\Delta p` (for von Mises with isotropic hardening +:math:`B \approx -(3G + H)`, :math:`H` the hardening slope). A complementarity function turns +the three conditions into one equation. Both variants use the normalised multiplier +:math:`\Delta p^* = |B|\,\Delta p`, a stress like :math:`\Phi`. + +**Fischer-Burmeister** (``ncp="fischer_burmeister"``) solves + +.. math:: + + \sqrt{\Phi^2 + \Delta p^{*2}} + \Phi - \Delta p^* = 0 . + +The equation is smooth away from :math:`(0, 0)` and decides by itself whether the surface is +active: Newton never chooses a branch. It extends as is to a coupled system of several surfaces +(multi-mechanism ``MODUL``, the SMA kernels). Its first step is short: from +:math:`\Delta p = 0` on a violated surface (:math:`\Phi > 0`), the residual is :math:`2\Phi` +and its derivative :math:`2B - |B| = 3B`, so the step is :math:`-2\Phi/(3B)`, two thirds of the +full cutting-plane step :math:`-\Phi/B`. The return then needs about 6 local iterations. The +stopping criterion is the residual divided by :math:`\sigma_Y`, summed over the surfaces. + +**Min** (``ncp="min"``) solves + +.. math:: + + \min(\Delta p^*, -\Phi) = 0 + +by semi-smooth Newton (primal-dual active set, Hintermueller, Ito and Kunisch 2002), which +picks a branch at each iteration: + +* active surface (:math:`\Phi > 0` or :math:`-\Phi < \Delta p^*`): it solves + :math:`\Phi + B\,\delta p = 0`, the full step :math:`\delta p = -\Phi/B` of the classic cutting + plane (Ortiz and Simo 1986); +* inactive surface: the multiplier goes back to zero, :math:`\delta p = -\Delta p`. + +With linear hardening :math:`\Phi` is affine in :math:`\Delta p`, so the first step is the exact +return, and one more iteration confirms it. With a nonlinear hardening (power law) the return +takes about 3 iterations instead of 6. The stopping criterion is twice the residual of the chosen +branch over :math:`\sigma_Y`: on the first step this equals the Fischer-Burmeister residual, and +near the solution it is at least as strict. + +**Fallback.** Within an increment, the min step switches to Fischer-Burmeister for the rest of +the local solve as soon as its error grows from one iteration to the next (the full step +overshoots, or the branch choice oscillates) or :math:`B` vanishes. Coupled surfaces stay on +Fischer-Burmeister from the start, because the plain semi-smooth Newton can cycle there, the set +of active surfaces changing from one iteration to the next without converging. + +**Which rows count.** The min step applies row by row when the local Jacobian :math:`B` is +diagonal, i.e. when the rows are independent scalar problems. That is the case of a single yield +surface, and of a yield surface next to the rows of the viscoelastic and damage mechanisms of +``MODUL``: those are integrated outside the Newton (the Prony branches in closed form, the damage +by a fixed point), so their rows are always satisfied (:math:`\Phi = -Y^{crit}`), carry no +multiplier and no cross term, and stay at zero. Two yield surfaces share the stress, so their +rows are coupled (off-diagonal terms of :math:`B`) and keep Fischer-Burmeister. + +.. list-table:: What ``ncp="min"`` does, case by case + :header-rows: 1 + + * - Case + - Result + * - EPICP, linear isotropic hardening + - one exact step and one check; the Fischer-Burmeister return to :math:`10^{-10}` + * - EPICP, power-law isotropic hardening + - about 3 iterations instead of 6; the Fischer-Burmeister return to :math:`10^{-7}` + * - EPKCP, ``MODUL`` with isotropic and Prager hardening + - the Fischer-Burmeister return to the local tolerance (EPKCP and ``MODUL`` agree to + :math:`10^{-12}`) + * - ``MODUL``, one yield surface with viscoelastic (Prony) or damage mechanisms + - the min step on the yield surface, the other rows untouched; the Fischer-Burmeister + return to the local tolerance + * - ``MODUL`` with several yield surfaces (coupled) + - no effect: Fischer-Burmeister is kept + * - ``MODUL`` with an Armstrong-Frederick back stress + - refused (``ValueError``), see above + * - any other law, a thermomechanical block, a mean-field model + - refused (``ValueError``); the sub-phases of a mean-field model keep Fischer-Burmeister + * - ``integrator="closest_point"`` + - refused (``ValueError``): the closest-point integrator keeps Fischer-Burmeister + +On a batch of :math:`10^4` EPICP points (``sim.umat``, one thread), one increment takes 20 ms +with Fischer-Burmeister and 14 ms with ``ncp="min"``. On a cyclic uniaxial ``MODUL`` solve +(150 increments) the whole run drops by 17 % with one Prony branch next to the yield surface, +29 % with three, 23 % with a damage mechanism. + +EPICP consistent tangent +------------------------ + +With ``tangent_mode = 2``, EPICP builds the consistent tangent of the radial return in closed +form (Simo and Taylor 1985), +:math:`3K\,\mathbb{I}_{vol} + 2G\theta\,\mathbb{I}_{dev} - 2G\bar\theta\,\mathbf{N}\otimes\mathbf{N}`. +It is the operator of the generic assembly to round-off at the converged return, without its +6x6 inverse. + +Choosing the tangent and the integrator for EPCHA (0D uniaxial) +---------------------------------------------------------------- + +* ``tangent_mode = 1`` (continuum) gives the same iterates as the default mode 2 (to + :math:`10^{-14}` on the Chaboche identification programmes) for about 27 % less time. +* ``integrator="closest_point"`` is a different update, + backward Euler: 1 to 2 % off the cutting plane there, at about 2.3 times the cost. + +Global Newton tolerance +----------------------- + +The global Newton has its own option, ``solve(..., precision_rel=...)`` +(``solver_params::precision_rel``). The residual tolerance becomes +:math:`\max(\mathrm{precision}, \mathrm{precision\_rel}\,|\boldsymbol\tau|)`, see +:doc:`python_solver`. + Validation and performance ========================== @@ -635,6 +843,6 @@ Each adapter-served name is validated at two levels: elastic/power-law families, < 2e-3 for the Voce/Chaboche family (legacy incremental vs modular closed-form Voce integration). -A benchmark row per family lives in ``bench/bench_legacy_vs_modular.py``: +A benchmark row per family lives in ``benchmark/bench_legacy_vs_modular.py``: results show no measurable adapter overhead, and the modular engine runs at 0.8-1.2x the speed of the removed kernels on all families. diff --git a/examples/heterogeneous/composite_parameter_identification.py b/examples/heterogeneous/composite_parameter_identification.py index 52f55bbc5..3375b2479 100644 --- a/examples/heterogeneous/composite_parameter_identification.py +++ b/examples/heterogeneous/composite_parameter_identification.py @@ -13,14 +13,16 @@ - **Self-Consistent** (``MISCN``): better at high volume fractions (c > 0.3) **Forward model**: simcoon mean-field homogenization (``L_eff``) -**Optimization**: ``scipy.optimize.differential_evolution`` (global optimizer) +**Optimization**: :func:`simcoon.identification` with ``method="least_squares"`` +(bounded trust-region reflective least squares, three starting points) **Phases**: described in memory, so an evaluation touches no file at all """ import numpy as np import matplotlib.pyplot as plt -from scipy.optimize import differential_evolution import simcoon as sim +from simcoon.identify import identification +from simcoon.parameter import Parameter from simcoon.solver.micromechanics import Ellipsoid import os @@ -113,14 +115,15 @@ def compute_E_eff(E_f, nu_f, concentrations, umat_name, props_composite): return E_eff -def cost_function(params_opt, c_exp, E_exp, umat_name, props_composite): - """MSE cost function for reinforcement property identification.""" +def residuals(params_opt, c_exp, E_exp, umat_name, props_composite): + """Residuals E_pred - E_exp; the mean of their squares is the MSE.""" E_f, nu_f = params_opt try: E_pred = compute_E_eff(E_f, nu_f, c_exp, umat_name, props_composite) - return np.mean((E_pred - E_exp) ** 2) except Exception: - return 1e12 + # NaN residuals: the optimizer rejects the step and shrinks it + return np.full(len(c_exp), np.nan) + return E_pred - E_exp def identify_reinforcement( @@ -132,7 +135,11 @@ def identify_reinforcement( verbose=True, ): """ - Identify reinforcement properties using differential evolution. + Identify reinforcement properties by bounded least squares. + + Three starting points (the middle of the log-scaled bounds and two Latin + hypercube samples) guard against a local minimum; each converges in a few + tens of evaluations of the six effective moduli. Returns ------- @@ -144,31 +151,33 @@ def identify_reinforcement( print(f" Identification with {umat_name}") print(f"{'=' * 60}") - result = differential_evolution( - cost_function, - bounds=bounds, + params = [ + Parameter(0, bounds=bounds[0], key="@E_f"), + Parameter(1, bounds=bounds[1], key="@nu_f"), + ] + result = identification( + residuals, + params, + method="least_squares", args=(c_exp, E_exp, umat_name, props_composite), - strategy="best1bin", - maxiter=200, - popsize=15, - tol=1e-8, - mutation=(0.5, 1.0), - recombination=0.7, + n_starts=3, seed=42, - polish=True, - disp=verbose, + xtol=1e-10, + ftol=1e-10, + gtol=1e-10, ) E_f_opt, nu_f_opt = result.x + mse = np.mean(result.fun ** 2) if verbose: print(f"\n E_f = {E_f_opt:.0f} MPa") print(f" nu_f = {nu_f_opt:.3f}") - print(f" MSE = {result.fun:.2e} MPa^2") + print(f" MSE = {mse:.2e} MPa^2 ({result.nfev} evaluations)") return { "E_f": E_f_opt, "nu_f": nu_f_opt, - "mse": result.fun, + "mse": mse, "result": result, "umat_name": umat_name, } @@ -200,7 +209,7 @@ def identify_reinforcement( print("=" * 60) print(" COMPOSITE REINFORCEMENT IDENTIFICATION") - print(" Glass/Epoxy — Mean-field + Differential Evolution") + print(" Glass/Epoxy — Mean-field + bounded least squares") print("=" * 60) print(f"\nExperimental data (Wang 2003): {len(c_exp)} points") for c, E in zip(c_exp, E_exp): @@ -339,7 +348,7 @@ def identify_reinforcement( fig.suptitle( "Composite Reinforcement Identification — Glass/Epoxy\n" - "Mori-Tanaka vs Self-Consistent + Differential Evolution", + "Mori-Tanaka vs Self-Consistent + bounded least squares", fontsize=14, fontweight="bold", ) diff --git a/examples/hyperelasticity/hyperelastic_umat_identification.py b/examples/hyperelasticity/hyperelastic_umat_identification.py index 1226e75ab..db8743345 100644 --- a/examples/hyperelasticity/hyperelastic_umat_identification.py +++ b/examples/hyperelasticity/hyperelastic_umat_identification.py @@ -11,10 +11,12 @@ transverse-equilibrium condition (zero stress on the free faces) is handled *inside* the solver through mixed control, so there is no hand-written ``fsolve`` loop. -- **Optimizer**: :func:`simcoon.identification` (a thin wrapper around - ``scipy.optimize.differential_evolution``) reading bounds from +- **Optimizer**: :func:`simcoon.identification` with + ``method="least_squares"`` (bounded trust-region reflective least squares + from three starting points) reading bounds from :class:`simcoon.parameter.Parameter` objects. -- **Cost**: :func:`simcoon.calc_cost` with the ``"nmse_per_response"`` metric. +- **Cost**: :func:`simcoon.calc_residuals`, the residuals whose sum of squares + is :func:`simcoon.calc_cost` with the ``"nmse_per_response"`` metric. This is the library-native counterpart of ``examples/analysis/hyperelastic_parameter_identification.py``, which solves @@ -41,7 +43,7 @@ import simcoon as sim from simcoon.parameter import Parameter -from simcoon.identify import identification, calc_cost +from simcoon.identify import identification, calc_residuals ############################################################################### @@ -141,26 +143,29 @@ def model_pk1(lambda_exp, props, programme): ############################################################################### -# Cost function: ``sim.calc_cost`` with NMSE-per-response -# ------------------------------------------------------ +# Residuals: ``sim.calc_residuals`` with NMSE-per-response +# -------------------------------------------------------- # # The analytical example # (``examples/analysis/hyperelastic_parameter_identification.py``) builds its # combined cost *by hand*: for each loading case it takes the # ``mean_squared_error`` and divides by the variance of the experimental data -# (an NMSE), then sums the cases. That normalisation is exactly what -# :func:`simcoon.calc_cost` provides via ``metric="nmse_per_response"`` — each -# response (here the stress column of each test) is divided by its own sum of -# squares before averaging, so UT, PS and ET contribute on an equal footing -# despite their different stress magnitudes. We hand-rolled it there; we simply -# call the library here. +# (an NMSE), then sums the cases. :func:`simcoon.calc_cost` with +# ``metric="nmse_per_response"`` normalises per response *column* instead: the +# three tests share one stress column here, so the cost is the summed squared +# error of UT, PS and ET divided by their summed squared stresses. The tests are +# pooled, not normalised one by one: the case with the largest stresses weighs +# most (``w_test`` can rebalance them). # -# ``calc_cost`` expects one 2-D array of shape ``(n_points, n_responses)`` per -# test; with a single stress response per test the arrays are ``(n_points, 1)``. +# The least-squares optimizer works on the residuals themselves: +# :func:`simcoon.calc_residuals` returns, with the same arguments, the vector +# whose sum of squares is ``calc_cost``. Both expect one 2-D array of shape +# ``(n_points, n_responses)`` per test; with a single stress response per test +# the arrays are ``(n_points, 1)``. -def cost(x, jobs): - """NMSE-per-response cost over the loading cases in *jobs*. +def residuals(x, jobs): + """NMSE-per-response residuals over the loading cases in *jobs*. *jobs* is a list of ``(name, programme, lambda_exp, P_exp)`` tuples, ``programme`` the parsed ``(blocks, T_init)`` of the case. @@ -171,22 +176,22 @@ def cost(x, jobs): try: P_model = model_pk1(lam_exp, props, programme) except Exception: - return 1e12 + # NaN residuals: the optimizer rejects the step and shrinks it + return np.full(sum(len(job[3]) for job in jobs), np.nan) y_exp.append(P_exp.reshape(-1, 1)) y_num.append(P_model.reshape(-1, 1)) - return calc_cost(y_exp, y_num, metric="nmse_per_response") + return calc_residuals(y_exp, y_num, metric="nmse_per_response") -def identify(jobs, popsize=15, maxiter=25, seed=42): +def identify(jobs, n_starts=3, seed=42): """Run ``sim.identification`` over the given *jobs*; return ``([C10, C01], final_cost)``.""" params = make_params() result = identification( - cost, params, - args=(jobs,), - seed=seed, popsize=popsize, maxiter=maxiter, tol=1e-6, disp=False, + residuals, params, method="least_squares", + args=(jobs,), n_starts=n_starts, seed=seed, ) - return [p.value for p in params], result.fun + return [p.value for p in params], float(np.sum(result.fun ** 2)) ############################################################################### @@ -235,14 +240,14 @@ def main(): print("\n" + "-" * 66) print(" INDIVIDUAL FITS (one parameter set per loading case)") print("-" * 66) - print(f"{'Case':<6}{'C10':>10}{'C01':>10}{'C10 lit.':>12}{'C01 lit.':>12}") + print(f"{'Case':<6}{'C10':>10}{'C01':>10}{'C10 lit.':>12}{'C01 lit.':>12}{'NMSE':>12}") individual = {} for name, _pf, _lc, _pc in CASES: lam_exp, P_exp = exp[name] - (c10, c01), _fun = identify([(name, programmes[name], lam_exp, P_exp)]) + (c10, c01), cost_i = identify([(name, programmes[name], lam_exp, P_exp)]) individual[name] = (c10, c01) l10, l01 = LIT[name] - print(f"{name:<6}{c10:>10.4f}{c01:>10.4f}{l10:>12.4f}{l01:>12.4f}") + print(f"{name:<6}{c10:>10.4f}{c01:>10.4f}{l10:>12.4f}{l01:>12.4f}{cost_i:>12.4e}") # ----- Combined fit --------------------------------------------------- # print("\n" + "-" * 66) diff --git a/examples/identification/chaboche_cyclic_identification.py b/examples/identification/chaboche_cyclic_identification.py index 962cda2ce..a576898f3 100644 --- a/examples/identification/chaboche_cyclic_identification.py +++ b/examples/identification/chaboche_cyclic_identification.py @@ -30,6 +30,9 @@ 3. Block 3 (mode 3, tabular) — replay of the experimental table, read from the ``path_id_N_tab1.csv`` file the JSON references +Blocks 1 and 2 use 100 increments per step, with ``Dn_mini = 0.1`` so the +solver can still sub-increment for stiff trial parameters. + The ``path_id_N.json`` / ``path_id_N_tab1.csv`` pairs are provided in ``data/`` because they are tricky to construct manually (they were converted from the legacy ``path_id_N.txt`` + ``tab_file_N.txt`` pairs with ``scripts/legacy_to_json.py``). @@ -38,9 +41,13 @@ data. Forward model: :func:`simcoon.solver` (UMAT material-point integrator). -Optimization: :func:`simcoon.identification` (wraps ``differential_evolution``). -Cost: ``nmse_per_response`` — normalises each test's stress column by its own -sum of squares, balancing the three tests despite different stress magnitudes. +Optimization: :func:`simcoon.identification` with ``method="least_squares"`` +(bounded trust-region reflective least squares, parameters scaled +logarithmically, since most of them span two to three decades), on the +residuals of :func:`simcoon.calc_residuals`. +Cost: ``nmse_per_response`` — the three tests share one stress column, so the cost is +the summed squared error of the three tests divided by their summed squared stresses +(the tests are pooled, not normalised one by one: a test with larger stresses weighs more). """ import os @@ -48,7 +55,7 @@ import matplotlib.pyplot as plt import simcoon as sim from simcoon.parameter import Parameter -from simcoon.identify import identification, calc_cost +from simcoon.identify import identification, calc_residuals # --------------------------------------------------------------------------- @@ -84,6 +91,11 @@ ] PARAM_NAMES = ["sigmaY", "Q", "b", "C_1", "D_1", "C_2", "D_2"] +# The Jacobian of the least squares needs one simulation per parameter: they are +# independent and ``sim.solver.solve`` releases the GIL, so they run in threads +# (the result does not depend on the number of threads). +N_THREADS = 4 + def build_props(x): """Assemble the EPCHA props vector from the optimizer's parameter array.""" return np.array([E_FIXED, NU_FIXED, ALPHA_FIXED, *x]) @@ -111,17 +123,19 @@ def run_one_test(props, programme): return np.asarray(res["Stress"][0])[block == block.max()] -def cost(x, exp_stresses, programmes): - """NMSE-per-response cost across the three tests.""" +def residuals(x, exp_stresses, programmes): + """Residuals of the three tests; their sum of squares is the + ``nmse_per_response`` cost of :func:`simcoon.calc_cost`.""" props = build_props(x) y_num = [] for programme in programmes: try: sigma11 = run_one_test(props, programme) except Exception: - return 1e12 + # NaN residuals: the optimizer rejects the step and shrinks it + return np.full(sum(len(e) for e in exp_stresses), np.nan) y_num.append(sigma11.reshape(-1, 1)) - return calc_cost(exp_stresses, y_num, metric="nmse_per_response") + return calc_residuals(exp_stresses, y_num, metric="nmse_per_response") def main(): @@ -152,13 +166,13 @@ def main(): programmes = [sim.solver.load_path_json(os.path.join(path_data, pathfile))[:2] for _, pathfile, _ in TESTS] - # Gallery budget (~1-2 min). Bump popsize/maxiter for tighter fits. + # One start from the middle of the (log-scaled) bounds; raise n_starts to + # explore the valley further. diff_step: finite-difference step above the + # solver tolerance. result = identification( - cost, PARAMS, + residuals, PARAMS, method="least_squares", args=(exp_stresses, programmes), - seed=42, - popsize=15, maxiter=80, tol=1e-6, - disp=False, + n_starts=1, diff_step=1e-6, n_threads=N_THREADS, ) print() @@ -167,7 +181,8 @@ def main(): print("=" * 60) for n, p in zip(PARAM_NAMES, PARAMS): print(f" {n:8s} = {p.value:>12.3f} (bounds {p.bounds})") - print(f"\n Final cost (NMSE/response) = {result.fun:.4e}") + print(f"\n Final cost (NMSE/response) = {np.sum(result.fun ** 2):.4e}" + f" ({result.nfev} evaluations)") # All three tests on one plot — dashed = experiment, solid = identified fig, ax = plt.subplots(figsize=(9, 7)) diff --git a/examples/identification/data/path_id_1.json b/examples/identification/data/path_id_1.json index 5ed2f7999..3b06e9684 100644 --- a/examples/identification/data/path_id_1.json +++ b/examples/identification/data/path_id_1.json @@ -12,7 +12,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", @@ -37,7 +37,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", @@ -68,7 +68,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", diff --git a/examples/identification/data/path_id_2.json b/examples/identification/data/path_id_2.json index 386f78dbe..8a226dda5 100644 --- a/examples/identification/data/path_id_2.json +++ b/examples/identification/data/path_id_2.json @@ -12,7 +12,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", @@ -37,7 +37,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", @@ -68,7 +68,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", diff --git a/examples/identification/data/path_id_3.json b/examples/identification/data/path_id_3.json index ae71df1ac..1c49f1fe6 100644 --- a/examples/identification/data/path_id_3.json +++ b/examples/identification/data/path_id_3.json @@ -12,7 +12,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", @@ -37,7 +37,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", @@ -68,7 +68,7 @@ "time": 1.0, "ninc": 100, "Dn_init": 1.0, - "Dn_mini": 1.0, + "Dn_mini": 0.1, "control": [ "strain", "stress", diff --git a/examples/identification/json_keys_identification.py b/examples/identification/json_keys_identification.py index 9df383c06..8229e2d91 100644 --- a/examples/identification/json_keys_identification.py +++ b/examples/identification/json_keys_identification.py @@ -12,11 +12,11 @@ The chain at each evaluation: -1. ``copy_parameters`` copies ``keys/material.json`` to the working directory, -2. ``apply_parameters`` and ``apply_constants`` write the values in place of the keys, -3. ``load_simulation_json`` reads the material back with the loading path, -4. ``sim.solver.solve`` runs the tension test in memory, -5. ``calc_cost`` compares the stress history with the experiment. +1. a :class:`~simcoon.parameter.KeyTemplate`, which read ``keys/material.json`` + once, writes the working copy with the keys replaced by the current values; +2. ``load_simulation_json`` reads the material back with the loading path, +3. ``sim.solver.solve`` runs the tension test in memory, +4. ``calc_cost`` compares the stress history with the experiment. ``sim.identification`` wraps ``scipy.optimize.differential_evolution`` with the bounds of the :class:`~simcoon.parameter.Parameter` objects read from @@ -39,9 +39,9 @@ import matplotlib.pyplot as plt import simcoon as sim -from simcoon.constant import apply_constants, read_constants +from simcoon.constant import read_constants from simcoon.identify import calc_cost, identification -from simcoon.parameter import apply_parameters, copy_parameters, read_parameters +from simcoon.parameter import KeyTemplate, read_parameters from simcoon.solver import Block, StepMeca, load_simulation_json, save_path_json UMAT_NAME = "EPICP" @@ -57,11 +57,9 @@ def build_working_dir(script_dir): return work -def run_from_files(params, consts, keys_dir, work): +def run_from_files(template, work): """Substitute the keys, read the JSON pair back, run the test: sigma_11(t).""" - copy_parameters(params, src_path=keys_dir, dst_path=work) - apply_parameters(params, dst_path=work) - apply_constants(consts, dst_path=work) + template.write(work) kwargs = load_simulation_json(os.path.join(work, "material.json"), os.path.join(work, "path.json")) res = sim.solver.solve(**kwargs) @@ -77,11 +75,12 @@ def main(): params = read_parameters(os.path.join(script_dir, "data", "parameters.inp")) consts = read_constants(1, os.path.join(script_dir, "data", "constants.inp")) work = build_working_dir(script_dir) + template = KeyTemplate(keys_dir, params, consts) # keys/material.json, read once # Synthetic experiment: the reference values, plus 0.5 % multiplicative noise for p in params: p.value = REFERENCE[p.key] - strain, sigma_ref = run_from_files(params, consts, keys_dir, work) + strain, sigma_ref = run_from_files(template, work) rng = np.random.default_rng(0) sigma_exp = sigma_ref * (1.0 + 0.005 * rng.standard_normal(sigma_ref.size)) y_exp = [sigma_exp.reshape(-1, 1)] @@ -90,7 +89,7 @@ def cost(x): for p, value in zip(params, x): p.value = value try: - _, sigma = run_from_files(params, consts, keys_dir, work) + _, sigma = run_from_files(template, work) except Exception: return 1e12 return calc_cost(y_exp, [sigma.reshape(-1, 1)], metric="nmse_per_response") @@ -102,7 +101,7 @@ def cost(x): print(f" {p.key:8s} = {p.value:10.3f} (reference {REFERENCE[p.key]:g})") # The material file the last evaluation left behind is the identified one - _, sigma_id = run_from_files(params, consts, keys_dir, work) + _, sigma_id = run_from_files(template, work) with open(os.path.join(work, "material.json")) as f: print("identified material.json:\n" + f.read()) shutil.rmtree(work, ignore_errors=True) diff --git a/examples/mechanical/MODUL_closest_point.py b/examples/mechanical/MODUL_closest_point.py index 319e3b027..118a699e3 100644 --- a/examples/mechanical/MODUL_closest_point.py +++ b/examples/mechanical/MODUL_closest_point.py @@ -2,11 +2,11 @@ Closest-Point Projection — Non-Proportional Loading =================================================== -``tangent_mode=3`` replaces the cutting-plane local loop of the ``MODUL`` UMAT by a +``integrator="closest_point"`` replaces the cutting-plane local loop of the ``MODUL`` UMAT by a closest-point projection: the plastic strain uses the flow direction of the converged state, and the consistent tangent is the exact Jacobian of the discrete update for every -criterion with a flow Hessian and every hardening law. The cutting-plane loop (modes 1 and -2) accumulates the plastic strain along the direction of each Newton iterate; the two +criterion with a flow Hessian and every hardening law. The cutting-plane loop (the default +integrator) accumulates the plastic strain along the direction of each Newton iterate; the two integrators coincide only when that direction does not rotate within the increment — von Mises with isotropic hardening (radial return). @@ -16,7 +16,8 @@ components free, applied to a ladder of models of growing complexity: J2 + Voce (radial), J2 + Armstrong–Frederick, J2 + two-term Chaboche, Hill + Armstrong–Frederick. -Three things are measured, mode 2 against mode 3, and printed as a table: +Three things are measured, cutting plane (mode 2 below) against closest point (mode 3), and +printed as a table: 1. the error of the coarse-increment response against a fine reference — both integrators are first order in the increment; where the flow direction rotates, the closest-point @@ -99,11 +100,17 @@ def figure_eight(n_inc): return np.column_stack([t, a * np.sin(t), b * np.sin(2.0 * t)]) +def scheme(mode): + """Keyword arguments of the two integrators compared here: mode 2 is the cutting plane with + its algorithmic tangent, mode 3 the closest point with its exact one.""" + return {"integrator": "closest_point"} if mode == 3 else {"tangent_mode": mode} + + def run(mat, n_inc, mode): step = StepMeca(control=["strain", "zero", "zero", "strain", "zero", "zero"], mode="tabular", tabular=figure_eight(n_inc)) t0 = time.perf_counter() - res = solve(step, "MODUL", mat.props, mat.nstatev, T_init=293.0, tangent_mode=mode) + res = solve(step, "MODUL", mat.props, mat.nstatev, T_init=293.0, **scheme(mode)) return res, (time.perf_counter() - t0) / n_inc @@ -156,7 +163,7 @@ def umat_point(mat, eps, De, sig, sv, Wm, mode): I3 = np.eye(3).reshape(3, 3, 1).copy(order="F") s, sv_new, Wm_new, Lt = sim.umat("MODUL", col(eps), col(De), I3, I3, col(sig), I3, col(mat.props), col(sv), 1.0, 1.0, col(Wm), - temp=np.full(1, 293.0), n_threads=1, tangent_mode=mode, + temp=np.full(1, 293.0), n_threads=1, **scheme(mode), start=False) return s[:, 0], sv_new[:, 0], Wm_new[:, 0], Lt[:, :, 0] diff --git a/examples/mechanical/README.rst b/examples/mechanical/README.rst index b6f7491e6..8aa973c74 100644 --- a/examples/mechanical/README.rst +++ b/examples/mechanical/README.rst @@ -27,7 +27,7 @@ Below are examples illustrating Simcoon's mechanical constitutive laws library. - **MODUL** - Elasticity block + von Mises plasticity with Voce hardening - **MODUL_finite** - The same composition under finite strain (Hencky hyperelasto-plasticity) - **MODUL_hyper_visco** - Yeoh hyperelastic block + Prony branches (finite-strain viscoelasticity) -- **MODUL_closest_point** - Closest-point projection (``tangent_mode=3``) vs the cutting-plane loop under a non-proportional figure-eight: accuracy, Newton convergence, cost +- **MODUL_closest_point** - Closest-point projection (``integrator="closest_point"``) vs the cutting-plane loop under a non-proportional figure-eight: accuracy, Newton convergence, cost **Shape Memory Alloys:** diff --git a/external/USERMAT_ANSYS_ELASTIC.for b/external/USERMAT_ANSYS_ELASTIC.for index 4c171da79..f018dbbbb 100644 --- a/external/USERMAT_ANSYS_ELASTIC.for +++ b/external/USERMAT_ANSYS_ELASTIC.for @@ -59,6 +59,12 @@ c Material properties alpha = 0.0d0 endif c +c 3D only (ncomp = 6): the tangent below fills the 6x6 + if (ncomp .ne. 6) then + write(*,*) 'USERMAT_ANSYS_ELASTIC: 3D only (ncomp = 6)' + stop + endif +c c Lame constants lambda = E * nu / ((1.0d0 + nu) * (1.0d0 - 2.0d0 * nu)) mu = E / (2.0d0 * (1.0d0 + nu)) @@ -89,9 +95,9 @@ c Shear terms dsdde(5,5) = mu dsdde(6,6) = mu c -c Initialize reference temperature from state variable - if (nStatev .ge. 5) then - T_init = ustatev(5) +c Reference temperature: the first state variable + if (nStatev .ge. 1) then + T_init = ustatev(1) else T_init = Temp endif @@ -104,14 +110,15 @@ c First increment: initialize enddo sedEl = 0.0d0 sedPl = 0.0d0 - if (nStatev .ge. 5) then - ustatev(5) = T_init + if (nStatev .ge. 1) then + ustatev(1) = T_init endif endif c c Compute elastic strain (total strain minus thermal strain) do i = 1, 3 - Eel(i) = Strain(i) + dStrain(i) - alpha * (Temp + dTemp - T_init) + Eel(i) = Strain(i) + dStrain(i) + & - alpha * (Temp + dTemp - T_init) enddo do i = 4, ncomp Eel(i) = Strain(i) + dStrain(i) diff --git a/external/umat_plugin_ans.cpp b/external/umat_plugin_ans.cpp index 889bc8f0a..b2094d7e4 100644 --- a/external/umat_plugin_ans.cpp +++ b/external/umat_plugin_ans.cpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include #include @@ -32,6 +34,7 @@ #include #include #include +#include using namespace std; using namespace arma; @@ -84,197 +87,70 @@ extern "C" { ); } -///@brief Convert simcoon state to Ansys USERMAT format -///@details Transforms simcoon Armadillo arrays to C-style arrays for Ansys interface -void smart2ansys_M( - double *stress, double *dsdde, double *ustatev, - double *Strain, double *dStrain, double *Time, double *dTime, - double *Temp, double *dTemp, double *prop, double *rotateM, - int &nDirect, int &nShear, int &ncomp, int &nStatev, int &nProp, - const vec &sigma_smart, const mat &Lt_smart, - const vec &Etot_smart, const vec &DEtot_smart, - const double &T_smart, const double &DT_smart, - const double &Time_smart, const double &DTime_smart, - const vec &props_smart, const vec &Wm_smart, const vec &statev_smart, - const mat &DR_smart, const bool &start -) { - // Dimensions - nDirect = 3; - nShear = 3; - ncomp = 6; - nStatev = statev_smart.n_elem; - nProp = props_smart.n_elem; - - // Time - *Time = Time_smart; - *dTime = DTime_smart; - - // Temperature - *Temp = T_smart; - *dTemp = DT_smart; - - // Material properties - for (int i = 0; i < nProp; i++) { - prop[i] = props_smart(i); - } - - // Ansys uses Voigt notation: 11, 22, 33, 12, 23, 13 - // simcoon uses: 11, 22, 33, 12, 13, 23 - // Need to swap indices 4<->5 (13 and 23) - int ansys_map[6] = {0, 1, 2, 3, 5, 4}; - - // Stress (initialize if start) - if (start) { - for (int i = 0; i < 6; i++) { - stress[i] = 0.0; - } - } else { - for (int i = 0; i < 6; i++) { - stress[i] = sigma_smart(ansys_map[i]); - } - } - - // Strain and strain increment - for (int i = 0; i < 6; i++) { - Strain[i] = Etot_smart(ansys_map[i]); - dStrain[i] = DEtot_smart(ansys_map[i]); - } - - // Rotation matrix (row-major for Ansys) - for (int i = 0; i < 3; i++) { - for (int j = 0; j < 3; j++) { - rotateM[i * 3 + j] = DR_smart(i, j); - } - } - - // State variables (first 4 are Wm components, then user statev) - ustatev[0] = Wm_smart(0); // Wm - ustatev[1] = Wm_smart(1); // Wm_r - ustatev[2] = Wm_smart(2); // Wm_ir - ustatev[3] = Wm_smart(3); // Wm_d - for (int i = 0; i < nStatev; i++) { - ustatev[i + 4] = statev_smart(i); - } - - // Initialize tangent modulus - for (int i = 0; i < 36; i++) { - dsdde[i] = 0.0; - } -} - -///@brief Convert Ansys USERMAT output back to simcoon format -void ansys2smart_M( - const double *stress, const double *dsdde, - const int &nStatev, const double *ustatev, - const int &nDirect, const int &nShear, - vec &sigma_smart, mat &Lt_smart, vec &Wm_smart, vec &statev_smart -) { - // Ansys to simcoon index mapping - int ansys_map[6] = {0, 1, 2, 3, 5, 4}; - - // Stress - for (int i = 0; i < 6; i++) { - sigma_smart(ansys_map[i]) = stress[i]; - } - - // Tangent modulus with index reordering - for (int i = 0; i < 6; i++) { - for (int j = 0; j < 6; j++) { - Lt_smart(ansys_map[i], ansys_map[j]) = dsdde[i * 6 + j]; - } - } - - // State variables (extract Wm and user statev) - Wm_smart(0) = ustatev[0]; // Wm - Wm_smart(1) = ustatev[1]; // Wm_r - Wm_smart(2) = ustatev[2]; // Wm_ir - Wm_smart(3) = ustatev[3]; // Wm_d - for (int i = 0; i < nStatev; i++) { - statev_smart(i) = ustatev[i + 4]; - } -} - +// simcoon calls a foreign Ansys USERMAT: the components are those of Ansys (fea_transfer.hpp), +// the matrices column-major as Fortran, and the law's statev are its own (no simcoon header) class LIB_EXPORT umat_plugin_ans : public umat_plugin_ans_api { public: std::string name() const { return "umans"; } - + void umat_ansys(simcoon::phase_characteristics &rve, const arma::mat &DR, const double &Time, const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, double &tnew_dt) { - - // Get state variables pointer - auto rve_sv_M = std::dynamic_pointer_cast(rve.sptr_sv_local); - - // Ansys USERMAT variables - int matId = 1; - int elemId = 1; - int kDomIntPt = 1; - int kLayer = 1; - int kSectPt = 1; + (void)solver_type; + auto sv = std::dynamic_pointer_cast(rve.sptr_sv_local); + const fea_components K = ansys_components(ndi, nshr); + + int matId = 1, elemId = 1, kDomIntPt = 1, kLayer = 1, kSectPt = 1; int ldstep = 1; - int isubst = 1; + int isubst = start ? 1 : 2; // the USERMAT initialises its state on the first increment int keycut = 0; - int nDirect = 3; - int nShear = 3; - int ncomp = 6; - int nStatev = rve.sptr_sv_global->nstatev; - int nProp = rve.sptr_matprops->nprops; - - // Allocate arrays - double stress[6]; - double ustatev[nStatev + 4]; // +4 for Wm components - double dsdde[36]; - double sedEl = 0.0; - double sedPl = 0.0; - double epseq = 0.0; - double Strain[6]; - double dStrain[6]; - double epsPl[6] = {0.0}; - double prop[nProp]; - double coords[3] = {0.0, 0.0, 0.0}; + int nDirect = ndi, nShear = nshr, ncomp = K.ntens; + int nStatev = static_cast(sv->statev.n_elem); + int nProp = static_cast(rve.sptr_matprops->props.n_elem); + std::vector ustatev(sv->statev.begin(), sv->statev.end()); + std::vector prop(rve.sptr_matprops->props.begin(), rve.sptr_matprops->props.end()); + + if (start) sv->sigma.zeros(); + const vec sigma_start = sv->sigma; + double stress[6], Strain[6], dStrain[6], epsPl[6] = {0.}, dsdde[36] = {0.}; + vec2fe(sv->sigma, K, stress); + vec2fe(sv->Etot, K, Strain); + vec2fe(sv->DEtot, K, dStrain); double rotateM[9]; - double defGrad_t[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}; - double defGrad[9] = {1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0}; - double tsstif[2] = {0.0, 0.0}; - double epsZZ = 0.0; - double dTime_loc = DTime; - double Time_loc = Time; - double Temp_loc = rve_sv_M->T; - double dTemp_loc = rve_sv_M->DT; + std::copy(DR.memptr(), DR.memptr() + 9, rotateM); // column-major, as rotateM(3,3) + double sedEl = sv->Wm(1), sedPl = sv->Wm(3), epseq = 0.; + double Time_loc = Time, dTime_loc = DTime, Temp_loc = sv->T, dTemp_loc = sv->DT; + double coords[3] = {0., 0., 0.}; + double defGrad_t[9] = {1., 0., 0., 0., 1., 0., 0., 0., 1.}; + double defGrad[9] = {1., 0., 0., 0., 1., 0., 0., 0., 1.}; + double tsstif[2] = {0., 0.}; + double epsZZ = 0.; int var1 = 0, var2 = 0, var3 = 0, var4 = 0, var5 = 0; - - // Convert simcoon to Ansys format - smart2ansys_M(stress, dsdde, ustatev, Strain, dStrain, &Time_loc, &dTime_loc, - &Temp_loc, &dTemp_loc, prop, rotateM, - nDirect, nShear, ncomp, nStatev, nProp, - rve_sv_M->sigma, rve_sv_M->Lt, - rve_sv_M->Etot, rve_sv_M->DEtot, - rve_sv_M->T, rve_sv_M->DT, - Time, DTime, - rve.sptr_matprops->props, rve_sv_M->Wm, rve_sv_M->statev, - DR, start); - - // Call Ansys USERMAT + usermat_(&matId, &elemId, &kDomIntPt, &kLayer, &kSectPt, &ldstep, &isubst, &keycut, &nDirect, &nShear, &ncomp, &nStatev, &nProp, &Time_loc, &dTime_loc, &Temp_loc, &dTemp_loc, - stress, ustatev, dsdde, + stress, ustatev.data(), dsdde, &sedEl, &sedPl, &epseq, Strain, dStrain, epsPl, - prop, coords, rotateM, + prop.data(), coords, rotateM, defGrad_t, defGrad, tsstif, &epsZZ, &var1, &var2, &var3, &var4, &var5); - - // Convert Ansys output back to simcoon format - ansys2smart_M(stress, dsdde, nStatev, ustatev, nDirect, nShear, - rve_sv_M->sigma, rve_sv_M->Lt, rve_sv_M->Wm, rve_sv_M->statev); - - // Handle cutback request + + fe2vec(stress, K, sv->sigma); + fe2mat(dsdde, K, sv->Lt); + std::copy(ustatev.begin(), ustatev.end(), sv->statev.begin()); + // work by the trapezoidal rule; recoverable and dissipated parts from the USERMAT energies + sv->Wm(0) += 0.5*sum((sigma_start + sv->sigma) % sv->DEtot); + sv->Wm(1) = sedEl; + sv->Wm(3) = sedPl; + sv->Wm(2) = sv->Wm(0) - sedEl - sedPl; + if (keycut == 1) { - tnew_dt = 0.5; // Request smaller time step + tnew_dt = 0.5; } } diff --git a/include/simcoon/Continuum_mechanics/Functions/constitutive.hpp b/include/simcoon/Continuum_mechanics/Functions/constitutive.hpp index c3eba6a26..8a392387d 100755 --- a/include/simcoon/Continuum_mechanics/Functions/constitutive.hpp +++ b/include/simcoon/Continuum_mechanics/Functions/constitutive.hpp @@ -21,6 +21,8 @@ along with simcoon. If not, see . namespace simcoon{ +enum class Tensor4Type; // tensor.hpp + /** * @file constitutive.hpp * @author Yves Chemisky @@ -197,6 +199,7 @@ arma::vec Ir05(); double nu = 0.3; mat Liso = L_iso(E, nu, "Enu"); * @endcode + * @throw std::invalid_argument for an unknown convention */ arma::mat L_iso(const double &C1, const double &C2, const std::string &conv="Enu"); @@ -212,6 +215,7 @@ arma::mat L_iso(const double &C1, const double &C2, const std::string &conv="Enu double nu = 0.3; mat Miso = M_iso(E, nu, "Enu"); * @endcode + * @throw std::invalid_argument for an unknown convention */ arma::mat M_iso(const double &C1, const double &C2, const std::string& conv="Enu"); @@ -227,6 +231,7 @@ arma::mat M_iso(const double &C1, const double &C2, const std::string& conv="Enu double C44 = alead(10000., 100000.); mat Lcubic = L_cubic(C11, C12, C44, "Cii"); * @endcode + * @throw std::invalid_argument for an unknown convention */ arma::mat L_cubic(const double &C1, const double &C2, const double &C3, const std::string& conv="EnuG"); @@ -242,23 +247,68 @@ arma::mat L_cubic(const double &C1, const double &C2, const double &C3, const st double C44 = alead(10000., 100000.); mat Mcubic = M_cubic(C11,C12,C44); * @endcode + * @throw std::invalid_argument for an unknown convention */ arma::mat M_cubic(const double &C1, const double &C2, const double &C3, const std::string& conv = "EnuG"); +/** + * @brief Tests whether a 6x6 Voigt matrix has cubic symmetry in its own axes and returns its coefficients. + * + * Cubic structure: normal block \f$ a\,\mathbf{I} + b\,(\mathbf{1}-\mathbf{I}) \f$, shear diagonal + * \f$ c\,\mathbf{I} \f$, zero coupling between the two, with \f$ a = A_{11} \f$, \f$ b = A_{12} \f$, + * \f$ c = A_{44} \f$. Every entry is compared with that structure to \f$ \mathrm{tol}\,\max|A_{ij}| \f$; + * the default tol = 0 asks for the exact structure. A matrix with a NaN is never cubic. + * @param[in] A the 6x6 matrix + * @param[out] a, b, c its cubic coefficients (meaningful when true is returned) + * @param[in] tol relative tolerance (0: exact) + * @return true when @p A has that structure + */ +bool cubic_coefficients(const arma::mat &A, double &a, double &b, double &c, const double &tol = 0.); + +/** + * @brief Tests whether a 6x6 engineering-Voigt tensor of the given type is isotropic. + * + * Isotropy is the cubic structure (cubic_coefficients) whose Kelvin-Mandel shear coefficient equals + * the deviatoric eigenvalue of the normal block, \f$ c\,r_s c_s = a - b \f$, with \f$ r_s, c_s \f$ the + * engineering-to-Mandel shear factors of @p type (mandel_factors): \f$ c = (a-b)/2 \f$ for a stiffness + * (\f$ a = \lambda + 2\mu \f$, \f$ b = \lambda \f$, \f$ c = \mu \f$), \f$ c = 2(a-b) \f$ for a compliance, + * \f$ c = a-b \f$ for a strain or stress concentration. Each relation holds to + * \f$ \mathrm{tol}\,\max|L_{ij}| \f$. + * @param L the 6x6 tensor (engineering Voigt) + * @param type its Tensor4Type (generic is read as a stiffness, as in mandel_factors) + * @param tol relative tolerance, round-off by default + * @return true when @p L is isotropic + */ +bool is_isotropic(const arma::mat &L, const Tensor4Type &type, const double &tol = 1.E-14); + +/** + * @brief Inverse of a 6x6 stiffness or compliance (Voigt) with cubic symmetry in its own axes. + * + * The normal block \f$ a\,\mathbf{I} + b\,(\mathbf{1}-\mathbf{I}) \f$ has the eigenvalues \f$ a+2b \f$ + * (volumetric) and \f$ a-b \f$ (deviatoric) and the shear diagonal \f$ c \f$ inverts entrywise, so the + * inverse is in closed form (cubic_coefficients, exact); isotropy is the case \f$ c = (a-b)/2 \f$ + * (stiffness) or \f$ c = 2(a-b) \f$ (compliance). Typically the stiffness of a Reuss mixture of isotropic or cubic phases (the SMA laws), + * without a LAPACK call. A matrix without that exact structure goes through the general inverse. + * @param A the 6x6 matrix + * @return its inverse + * @throws exception_inv for a singular matrix + */ +arma::mat inv_cubic(const arma::mat &A); + /** * @brief Provides the elastic stiffness tensor for an orthotropic material. * @param C11 first stiffness coefficient (or Ex) * @param C12 second stiffness coefficient (or Ey) * @param C13 third stiffness coefficient (or Ez) * @param C22 fourth stiffness coefficient (or nu_xy) - * @param C23 fifth stiffness coefficient (or nu_yz) - * @param C33 sixth stiffness coefficient (or nu_xz) + * @param C23 fifth stiffness coefficient (or nu_xz) + * @param C33 sixth stiffness coefficient (or nu_yz) * @param C44 seventh stiffness coefficient (or G_xy) - * @param C55 eighth stiffness coefficient (or G_yz) - * @param C66 ninth stiffness coefficient (or G_xz) + * @param C55 eighth stiffness coefficient (or G_xz) + * @param C66 ninth stiffness coefficient (or G_yz) * @param conv convention string: "Cii" for stiffness coefficients or "EnuG" for material parameters * @return The 6x6 stiffness matrix considering a Voigt notation (arma::mat) - * @details Arguments could be all the stiffness coefficients (\f$ C_{ij} \f$) or the material parameter. In this last case for an orthotropic material the material parameters should be : \f$ E_x, E_y, E_z, \nu_{xy}, \nu_{yz}, \nu_{xz}, G_{xy}, G_{yz}, G_{xz} \f$. + * @details Arguments could be all the stiffness coefficients (\f$ C_{ij} \f$) or the material parameter. In this last case for an orthotropic material the material parameters should be : \f$ E_x, E_y, E_z, \nu_{xy}, \nu_{xz}, \nu_{yz}, G_{xy}, G_{xz}, G_{yz} \f$. * The last argument must be set to “Cii” if the inputs are the stiffness coefficients or to “EnuG” if the inputs are the material parameters. * @code double C11 = alead(10000., 100000.); @@ -272,6 +322,7 @@ arma::mat M_cubic(const double &C1, const double &C2, const double &C3, const st double C66 = alead(10000., 100000.); mat Lortho = L_ortho(C11, C12, C13, C22, C23, C33, C44, C55, C66,"Cii"); * @endcode + * @throw std::invalid_argument for an unknown convention */ arma::mat L_ortho(const double &C11, const double &C12, const double &C13, const double &C22, const double &C23, const double &C33, const double &C44, const double &C55, const double &C66, const std::string& conv="EnuG"); @@ -281,14 +332,14 @@ arma::mat L_ortho(const double &C11, const double &C12, const double &C13, const * @param C12 second stiffness coefficient (or Ey) * @param C13 third stiffness coefficient (or Ez) * @param C22 fourth stiffness coefficient (or nu_xy) - * @param C23 fifth stiffness coefficient (or nu_yz) - * @param C33 sixth stiffness coefficient (or nu_xz) + * @param C23 fifth stiffness coefficient (or nu_xz) + * @param C33 sixth stiffness coefficient (or nu_yz) * @param C44 seventh stiffness coefficient (or G_xy) - * @param C55 eighth stiffness coefficient (or G_yz) - * @param C66 ninth stiffness coefficient (or G_xz) + * @param C55 eighth stiffness coefficient (or G_xz) + * @param C66 ninth stiffness coefficient (or G_yz) * @param conv convention string: "Cii" for stiffness coefficients or "EnuG" for material parameters * @return The 6x6 compliance matrix considering a Voigt notation (arma::mat) - * @details Arguments could be all the stiffness coefficients (\f$ C_{ij} \f$) or the material parameter. In this last case for an orthotropic material the material parameters should be : \f$ E_x, E_y, E_z, \nu_{xy}, \nu_{yz}, \nu_{xz}, G_{xy}, G_{yz}, G_{xz} \f$. + * @details Arguments could be all the stiffness coefficients (\f$ C_{ij} \f$) or the material parameter. In this last case for an orthotropic material the material parameters should be : \f$ E_x, E_y, E_z, \nu_{xy}, \nu_{xz}, \nu_{yz}, G_{xy}, G_{xz}, G_{yz} \f$. * The last argument must be set to “Cii” if the inputs are the stiffness coefficients or to “EnuG” if the inputs are the material parameters. * @code double C11 = alead(10000., 100000.); @@ -302,6 +353,7 @@ arma::mat L_ortho(const double &C11, const double &C12, const double &C13, const double C66 = alead(10000., 100000.); mat Mortho = M_ortho(C11, C12, C13, C22, C23, C33, C44, C55, C66,"Cii"); * @endcode + * @throw std::invalid_argument for an unknown convention */ arma::mat M_ortho(const double &C11, const double &C12, const double &C13, const double &C22, const double &C23, const double &C33, const double &C44, const double &C55, const double &C66, const std::string& conv="EnuG"); @@ -319,6 +371,7 @@ arma::mat M_ortho(const double &C11, const double &C12, const double &C13, const double axis = 1; mat Lisotrans = L_isotrans(EL, ET, nuTL, nuTT, GLT, axis); * @endcode + * @throw std::invalid_argument for an axis other than 1, 2 or 3 */ arma::mat L_isotrans(const double &EL, const double &ET, const double &nuTL, const double &nuTT, const double &GLT, const int &axis); @@ -336,6 +389,7 @@ arma::mat L_isotrans(const double &EL, const double &ET, const double &nuTL, con double axis = 1; mat Misotrans = M_isotrans(EL, ET, nuTL, nuTT, GLT, axis); * @endcode + * @throw std::invalid_argument for an axis other than 1, 2 or 3 */ arma::mat M_isotrans(const double &EL, const double &ET, const double &nuTL, const double &nuTT, const double &GLT, const int &axis); diff --git a/include/simcoon/Continuum_mechanics/Functions/contimech.hpp b/include/simcoon/Continuum_mechanics/Functions/contimech.hpp index 6a5cb6672..6691e831e 100755 --- a/include/simcoon/Continuum_mechanics/Functions/contimech.hpp +++ b/include/simcoon/Continuum_mechanics/Functions/contimech.hpp @@ -629,6 +629,19 @@ arma::mat sym_dyadic_operator(const arma::mat &a, const arma::mat &b); */ arma::mat linearop_eigsym(const arma::vec &b_i, const arma::vec &b_j); +/** + * @brief Accumulates \f$ c\,\mathbb{G}^{ij} \f$ (see linearop_eigsym) into a 6x6 Voigt matrix, in place. + * + * The form of the spectral sums \f$ \sum_{i,j} c_{ij}\,\mathbb{G}^{ij} \f$ of the logarithmic rates + * and of the concentration tensors, built without a 6x6 temporary per term; + * linearop_eigsym(b_i, b_j) is this with \f$ c = 1 \f$ on a zero matrix. + * @param[in,out] acc the 6x6 accumulator + * @param[in] c the coefficient of the term + * @param[in] b_i the first eigenvector + * @param[in] b_j the second eigenvector + */ +void add_linearop_eigsym(arma::mat::fixed<6,6> &acc, const double &c, const arma::vec::fixed<3> &b_i, const arma::vec::fixed<3> &b_j); + /** @} */ // end of contimech group } //namespace simcoon diff --git a/include/simcoon/Continuum_mechanics/Functions/kinematics.hpp b/include/simcoon/Continuum_mechanics/Functions/kinematics.hpp index 33015938e..7bd8a1a03 100755 --- a/include/simcoon/Continuum_mechanics/Functions/kinematics.hpp +++ b/include/simcoon/Continuum_mechanics/Functions/kinematics.hpp @@ -175,6 +175,24 @@ arma::mat L_Cauchy_Green(const arma::mat &F); */ void RU_decomposition(arma::mat &R, arma::mat &U, const arma::mat &F); +/** + * @brief Right polar decomposition together with the spectral decomposition it is built on. + * + * One eigen-solve of \f$\mathbf{C} = \mathbf{F}^T \cdot \mathbf{F} = \mathbf{N}\,\mathrm{diag}(\mathbf{c})\,\mathbf{N}^T\f$ + * (eig_sym_3x3) gives \f$\mathbf{U} = \mathbf{N}\,\mathrm{diag}(\sqrt{\mathbf{c}})\,\mathbf{N}^T\f$ and + * \f$\mathbf{R} = \mathbf{F} \cdot \mathbf{N}\,\mathrm{diag}(1/\sqrt{\mathbf{c}})\,\mathbf{N}^T\f$; the left + * Cauchy-Green tensor \f$\mathbf{B} = \mathbf{F} \cdot \mathbf{F}^T\f$ has the same eigenvalues and the + * eigenvectors \f$\mathbf{R} \cdot \mathbf{N}\f$. For the callers that need R and a spectral basis + * (log_R kinematics, logarithmic strains) without a second decomposition. + * Same failure modes as RU_decomposition (exception_sqrtmat_sympd, exception_inv). + * + * @param[out] R the rotation \f$\mathbf{R}\f$ + * @param[out] c eigenvalues of \f$\mathbf{C}\f$ (ascending) + * @param[out] N eigenvectors of \f$\mathbf{C}\f$ (columns) + * @param[in] F 3x3 transformation gradient + */ +void RU_spectral(arma::mat::fixed<3,3> &R, arma::vec::fixed<3> &c, arma::mat::fixed<3,3> &N, const arma::mat &F); + /** * @brief Provides the VR decomposition of the transformation gradient. * @@ -434,7 +452,8 @@ void finite_rotation(const arma::mat &F0, const arma::mat &F1, const double &DTi * \mathbf{\Delta Q} = \left( \mathbf{I} + \frac{1}{2} \Delta t \, \mathbf{\Omega}_0 \right) \cdot \left( \mathbf{I} - \frac{1}{2} \Delta t \, \mathbf{\Omega}_1 \right)^{-1} * \f] * - * where \f$\mathbf{I}\f$ is the 3x3 identity matrix. + * where \f$\mathbf{I}\f$ is the 3x3 identity matrix. With \f$\mathbf{\Omega}_0 = \mathbf{\Omega}_1\f$ + * the two Cayley factors commute and finite_DQ(W, W, dt) equals Hughes_Winget(W, dt) exactly. * * @param Omega0 spin/velocity at time \f$t_0\f$ * @param Omega1 spin/velocity at time \f$t_1\f$ diff --git a/include/simcoon/Continuum_mechanics/Functions/objective_rates.hpp b/include/simcoon/Continuum_mechanics/Functions/objective_rates.hpp index bb46bd7b1..18ea640b0 100755 --- a/include/simcoon/Continuum_mechanics/Functions/objective_rates.hpp +++ b/include/simcoon/Continuum_mechanics/Functions/objective_rates.hpp @@ -397,6 +397,8 @@ arma::mat Delta_log_strain_F(const arma::mat &D, const arma::mat &L, const doubl * - **3** log_R: \f$ \mathbf{D}_e=\mathbf{A}^{R}\!:\!\mathbf{D} \f$, the R-corotational (Green-Naghdi) * rate of \f$ \ln V \f$ integrated over the orthogonal frame; \f$ \mathbf{A}^{R} \f$ is PD and * \f$ \epsilon\to\ln V \f$, so the F-reconstruction stays well posed. + * The midpoint transport uses @p DR, the frame increment corate_kinematics() returns (the exact + * polar rotation, of which @p Omega is the inverse Cayley spin). * - **5** log_F: \f$ \mathbf{D}_e=\mathbf{A}^{F}\!:\!\mathbf{D} \f$, the convected (Oldroyd) rate of * \f$ \ln V \f$ integrated over the F-frame (\f$ \mathbf{DF} \f$ built from the velocity gradient * L, carried in @p Omega). \f$ \mathbf{A}^{F} \f$ now recovers \f$ \ln V \f$ like \f$ \mathbf{A}^{R} \f$ @@ -473,6 +475,17 @@ double Delta_work_conjugacy(const arma::vec &tau_start, const arma::vec &tau_sta */ void corate_kinematics(const int &corate_type, arma::mat &DR, arma::mat &D, arma::mat &Omega, const arma::mat &F0, const arma::mat &F1, const double &DTime); +/** + * @brief corate_kinematics() followed by Delta_log_strain_corate(), sharing the spectral work. + * + * Same @p DR, @p D, @p Omega as corate_kinematics(), and @p De the logarithmic-strain increment + * (tensor) Delta_log_strain_corate() returns for them. Under log_R (3) one eigen-solve of + * \f$ \mathbf{C}_1 \f$ gives \f$ \mathbf{R}_1 \f$, the eigenbasis \f$ \mathbf{R}_1\mathbf{N}_1 \f$ of + * \f$ \mathbf{B}_1 \f$ and \f$ \mathbf{A}^{R}\!:\!\mathbf{D} \f$, instead of a second decomposition of + * \f$ \mathbf{B}_1 \f$; the other rates simply chain the two calls. + */ +void corate_kinematics(const int &corate_type, arma::mat &DR, arma::mat &D, arma::mat &Omega, arma::mat &De, const arma::mat &F0, const arma::mat &F1, const double &DTime); + /** * @brief Computes the tangent modulus that links the Piola-Kirchoff II stress \f$ \mathbf{S} \f$ to the Green-Lagrange stress \f$ \mathbf{E} \f$ from the tangent modulus that links the Kirchoff stress tensor \f$ \mathbf{\tau} \f$ and logarithmic strain \f$ \mathbf{e} \f$ integrated using the logarithmic spin * @@ -821,8 +834,20 @@ arma::mat DSDE_2_Dsigma_logarithmicDD(const arma::mat &DSDE, const arma::mat &F, * equal ones. \f$ \mathbf{T} \f$ is recovered from @p tau by * Kirchoff2LogStressU(). @c DSDE_2_DtauDe_corate maps * \f$ \mathrm{d}\mathbf{S}/\mathrm{d}\mathbf{E}\to\mathbf{L}_t \f$; @c DtauDe_corate_2_DSDE is its inverse. + * corate_type_material_tangent (a tangent form, not a rate): both are the identity. */ arma::mat DSDE_2_DtauDe_corate(const arma::mat &DSDE, const int &corate_type, const arma::mat &F, const arma::mat &tau); + +/** + * @brief Box tangent of rate @p corate_type to material tangent: the exact inverse of + * DSDE_2_DtauDe_corate() (same per-corate transports, see there). + * + * @param[in] Lt (6x6 arma::mat) box tangent \f$ \partial\hat{\boldsymbol\tau}/\partial\mathbf{D}_e \f$ (Kirchhoff measure, no \f$ 1/J \f$) + * @param[in] corate_type (int) objective rate of the box (0-5, corate_type_total, corate_type_material_tangent) + * @param[in] F (3x3 arma::mat) transformation gradient \f$ \mathbf{F} \f$ + * @param[in] tau (3x3 arma::mat) Kirchhoff stress \f$ \boldsymbol\tau \f$ + * @return (6x6 arma::mat) \f$ \mathrm{d}\mathbf{S}/\mathrm{d}\mathbf{E} \f$ + */ arma::mat DtauDe_corate_2_DSDE(const arma::mat &Lt, const int &corate_type, const arma::mat &F, const arma::mat &tau); /** @@ -875,6 +900,8 @@ arma::vec box_covector_2_dE(const arma::vec &d_box, const arma::mat &F, const in * with the stress carried by \f$ \mathrm{sym}(\Delta\mathbf{F}\,\boldsymbol\tau\,\Delta\mathbf{F}^{-1}) \f$: * \f$ \mathbb{C}^J : (\mathbb{A}^F)^{-1} \f$, finite-difference verified, equal to the log box for * isotropic laws. corate_type_total goes through \f$ \mathrm{d}\mathbf{S}/\mathrm{d}\mathbf{E} \f$. + * corate_type_material_tangent returns \f$ \mathrm{d}\mathbf{S}/\mathrm{d}\mathbf{E} \f$ itself, the + * pull-back alone (Dtau_LieDD_2_DSDE). * * @param Dtau_LieDD the spatial Kirchhoff-Lie tangent \f$ \partial(\mathcal{L}_v\boldsymbol\tau)/\partial\mathbf{D} \f$ * @param corate_type the solver's corate (see corate_kinematics) @@ -884,6 +911,31 @@ arma::vec box_covector_2_dE(const arma::vec &d_box, const arma::mat &F, const in */ arma::mat Dtau_LieDD_2_DtauDe_corate(const arma::mat &Dtau_LieDD, const int &corate_type, const arma::mat &F, const arma::mat &tau); +/** + * @brief Box tangent \f$ \partial\boldsymbol\tau/\partial\mathbf{e} \f$ at a pure stretch + * \f$ \mathbf{F} = \mathbf{V} = \exp(\mathbf{e}) \f$, in closed form from the spatial (Lie) tangent of an + * ISOTROPIC response. + * + * With \f$ \boldsymbol\tau(\mathbf{b}) \f$ isotropic and \f$ \mathbf{b} = \exp(2\mathbf{e}) \f$: + * \f$ J\boldsymbol{\mathsf{c}}:\mathbf{d} = \partial_{\mathbf{b}}\boldsymbol\tau:(\mathbf{d}\mathbf{b}+\mathbf{b}\mathbf{d}) + * - (\mathbf{d}\boldsymbol\tau+\boldsymbol\tau\mathbf{d}) \f$, and the Daleckii-Krein derivative of + * \f$ \exp(2\mathbf{e}) \f$ on the eigenbasis \f$ \mathbf{N} \f$ of \f$ \mathbf{e} \f$ (eigenvalues \f$ e_a \f$) gives + * \f[ \mathbf{L}_t:\delta\mathbf{e} = J\boldsymbol{\mathsf{c}}:\tilde{\mathbf{d}} + \tilde{\mathbf{d}}\boldsymbol\tau + \boldsymbol\tau\tilde{\mathbf{d}}, + * \qquad \tilde{\mathbf{d}} = \mathbf{N}\left(H \circ \mathbf{N}^T\delta\mathbf{e}\,\mathbf{N}\right)\mathbf{N}^T, + * \qquad H_{ab} = \frac{\tanh(e_a - e_b)}{e_a - e_b} \f] + * (Miehe and Lambrecht, 2001). The same tangent as Dtau_LieDD_2_DtauDe_corate() with corate 3 and + * \f$ \mathbf{F} = \mathbf{V} \f$ (pull-back, then the exact spectral map; \f$ \mathbf{R} = \mathbf{I} \f$), with no + * second spectral decomposition and no pull-back. Not valid for an anisotropic response + * (\f$ \boldsymbol\tau \f$ not an isotropic function of \f$ \mathbf{b} \f$): use the general map. + * + * @param Dtau_LieDD the spatial Kirchhoff-Lie tangent \f$ J\boldsymbol{\mathsf{c}} \f$ (6x6 Voigt) + * @param e eigenvalues of the logarithmic strain \f$ \mathbf{e} = \ln\mathbf{V} \f$ + * @param N its eigenvectors (columns) + * @param tau Kirchhoff stress (3x3) + * @return the box tangent (6x6 Voigt, engineering strain) + */ +arma::mat Dtau_LieDD_2_DtauDe_stretch(const arma::mat &Dtau_LieDD, const arma::vec::fixed<3> &e, const arma::mat::fixed<3,3> &N, const arma::mat &tau); + /** * @brief Assemble the canonical box tangent * \f$ \mathbf{L}_t=\partial\hat{\boldsymbol\tau}/\partial\mathbf{D}_e \f$ (Kirchhoff, no-J, XBM/log rate) diff --git a/include/simcoon/Continuum_mechanics/Functions/tensor.hpp b/include/simcoon/Continuum_mechanics/Functions/tensor.hpp index ab34b1807..a0dc82e83 100644 --- a/include/simcoon/Continuum_mechanics/Functions/tensor.hpp +++ b/include/simcoon/Continuum_mechanics/Functions/tensor.hpp @@ -608,8 +608,10 @@ tensor4 auto_sym_dyadic(const tensor2 &a); // rot/F with n_slices==1 are broadcast to all N points. /// Batch rotate N tensor2 objects. voigt:(6,N), rot:(3,3,N_r). +/// @p n_threads caps the threads of the loop (0 = runtime default, 1 = serial; see parallel.hpp). arma::mat batch_rotate(const arma::mat &voigt, Tensor2Type vtype, - const arma::cube &rot_matrices, bool active = true); + const arma::cube &rot_matrices, bool active = true, + unsigned int n_threads = 0); /// Batch push-forward N tensor2. voigt:(6,N), F:(3,3,N_f). arma::mat batch_push_forward(const arma::mat &voigt, Tensor2Type vtype, @@ -633,7 +635,8 @@ arma::mat batch_contract(const arma::cube &t4, Tensor4Type t4type, /// Batch rotate N tensor4 (overload of the tensor2 version). t4:(6,6,N), rot:(3,3,N_r). arma::cube batch_rotate(const arma::cube &t4, Tensor4Type t4type, - const arma::cube &rot_matrices, bool active = true); + const arma::cube &rot_matrices, bool active = true, + unsigned int n_threads = 0); /// Batch push-forward N tensor4 (overload of the tensor2 version). t4:(6,6,N), F:(3,3,N_f). arma::cube batch_push_forward(const arma::cube &t4, Tensor4Type t4type, diff --git a/include/simcoon/Continuum_mechanics/Homogenization/ellipsoid_multi.hpp b/include/simcoon/Continuum_mechanics/Homogenization/ellipsoid_multi.hpp index 2d9eccb4d..d4b3f99ec 100755 --- a/include/simcoon/Continuum_mechanics/Homogenization/ellipsoid_multi.hpp +++ b/include/simcoon/Continuum_mechanics/Homogenization/ellipsoid_multi.hpp @@ -27,6 +27,7 @@ #include #include #include +#include namespace simcoon{ @@ -164,30 +165,19 @@ class ellipsoid_multi : public phase_multi /// successive increments and Newton iterations) reuse its own Eshelby tensor. eshelby_memo S_loc_memo; - /** @brief Number of integration points in polar direction for numerical integration */ - static int mp; - - /** @brief Number of integration points in azimuthal direction */ - static int np; - - /** - * @brief Gauss points for polar integration - * - * The four Gauss vectors are computed once (costly) and shared by every - * ellipsoid. They are references to heap vectors that are never destroyed: - * a static armadillo object's destructor would run at DLL unload, which is - * unsafe on Windows. - */ - static arma::vec& x; - - /** @brief Gauss weights for polar integration */ - static arma::vec& wx; - - /** @brief Gauss points for azimuthal integration */ - static arma::vec& y; - - /** @brief Gauss weights for azimuthal integration */ - static arma::vec& wy; + /// Inputs of the last fillT (medium and phase tangents, geometry, quadrature) and the + /// tensors it gave: called again with bitwise the same inputs (elastic phases, successive + /// increments), fillT restores S_loc, T_loc and T instead of computing them again. + struct fillT_memo { + bool valid = false; + arma::mat::fixed<6,6> Lt_m; + arma::mat::fixed<6,6> Lt; + double geometry[6] = {0., 0., 0., 0., 0., 0.}; + const eshelby_quadrature *quad = nullptr; + arma::mat::fixed<6,6> S_loc; + arma::mat::fixed<6,6> T_loc; + arma::mat::fixed<6,6> T; + } T_memo; /** @brief Default constructor */ ellipsoid_multi(); @@ -226,8 +216,9 @@ class ellipsoid_multi : public phase_multi * * @param L0 Matrix (reference medium) stiffness tensor (6×6) * @param ell Ellipsoid geometry (aspect ratios and orientation) + * @param quad Evaluation settings and quadrature tables of the Eshelby tensors (eshelby_quadrature_points) */ - virtual void fillS_loc(const arma::mat&, const ellipsoid &); + virtual void fillS_loc(const arma::mat&, const ellipsoid &, const eshelby_quadrature &); /** * @brief Compute Hill's polarization tensor in local coordinates @@ -236,8 +227,9 @@ class ellipsoid_multi : public phase_multi * * @param L0 Matrix stiffness tensor (6×6) * @param ell Ellipsoid geometry + * @param quad Evaluation settings and quadrature tables of the Eshelby tensors (eshelby_quadrature_points) */ - virtual void fillP_loc(const arma::mat&, const ellipsoid &); + virtual void fillP_loc(const arma::mat&, const ellipsoid &, const eshelby_quadrature &); /** * @brief Compute the interaction tensor @@ -248,23 +240,27 @@ class ellipsoid_multi : public phase_multi * @param L0 Matrix stiffness tensor (6×6) * @param Lt Inclusion tangent stiffness tensor (6×6) * @param ell Ellipsoid geometry + * @param quad Evaluation settings and quadrature tables of the Eshelby tensors (eshelby_quadrature_points) * @param memo Optional eshelby_memo of the current homogenization pass: the Eshelby * tensor is taken from it when the same inclusion geometry has already been met * in the same (rotated) medium, and stored in it otherwise. */ - virtual void fillT(const arma::mat&, const arma::mat&, const ellipsoid &, eshelby_memo * = nullptr); + virtual void fillT(const arma::mat&, const arma::mat&, const ellipsoid &, const eshelby_quadrature &, eshelby_memo * = nullptr); /** * @brief Compute the interaction tensor for isotropic matrix * * @details Optimized computation when the matrix is isotropic, using - * analytical expressions for the Eshelby tensor. + * analytical expressions for the Eshelby tensor. Isotropization method: the + * matrix tangent is first isotropized (Isotropize) and both the Eshelby tensor + * and the interaction tensor are formed with that isotropic medium. * * @param L0 Isotropic matrix stiffness tensor (6×6) * @param Lt Inclusion tangent stiffness tensor (6×6) * @param ell Ellipsoid geometry + * @param quad Evaluation settings and quadrature tables of the Eshelby tensors (eshelby_quadrature_points) */ - virtual void fillT_iso(const arma::mat&, const arma::mat&, const ellipsoid &, eshelby_memo * = nullptr); + virtual void fillT_iso(const arma::mat&, const arma::mat&, const ellipsoid &, const eshelby_quadrature &, eshelby_memo * = nullptr); /** * @brief Compute the inelastic interaction tensor @@ -275,13 +271,15 @@ class ellipsoid_multi : public phase_multi * @param L0 Matrix stiffness tensor (6×6) * @param Lt Inclusion tangent stiffness tensor (6×6) * @param ell Ellipsoid geometry + * @param quad Evaluation settings and quadrature tables of the Eshelby tensors (eshelby_quadrature_points) */ - virtual void fillT_mec_in(const arma::mat&, const arma::mat&, const ellipsoid &, eshelby_memo * = nullptr); + virtual void fillT_mec_in(const arma::mat&, const arma::mat&, const ellipsoid &, const eshelby_quadrature &, eshelby_memo * = nullptr); - /// Eshelby tensor of @p ell in the medium @p Lt_local (inclusion frame): reused from this - /// inclusion's last integration when the medium is unchanged, else from the pass memo, - /// else integrated (and recorded in both). - arma::mat eshelby_memoized(eshelby_memo *memo, const arma::mat &Lt_local, const ellipsoid &ell); + /// Eshelby tensor of @p ell in the medium @p Lt_local (inclusion frame), the first that applies + /// of: this inclusion's last tensor when the medium is unchanged, the pass memo when given, the + /// closed form when the settings allow it, the tensors this thread integrated lately (exact + /// key), else integrated (and recorded). + arma::mat eshelby_memoized(eshelby_memo *memo, const arma::mat &Lt_local, const ellipsoid &ell, const eshelby_quadrature &quad); /** * @brief Assignment operator diff --git a/include/simcoon/Continuum_mechanics/Homogenization/eshelby.hpp b/include/simcoon/Continuum_mechanics/Homogenization/eshelby.hpp index 85a3da975..ca64bf2ff 100755 --- a/include/simcoon/Continuum_mechanics/Homogenization/eshelby.hpp +++ b/include/simcoon/Continuum_mechanics/Homogenization/eshelby.hpp @@ -162,15 +162,17 @@ arma::mat Eshelby_oblate(const double &, const double &); * as a function of the Poisson ratio \f$ \nu \f$. This corresponds to the limit case of an oblate spheroid when the aspect ratio * \f$ a_r = \frac{a1}{a2} = \frac{a1}{a3} \rightarrow 0 \f$. * The penny-shaped inclusion is oriented such as the normal to the crack plane is the axis \f$ 1 \f$. - * Returns the Eshelby tensor as a mat, according to the conventions of a localisation tensor. + * Returns the Eshelby tensor as a mat, according to the conventions of a localisation tensor + * (Voigt order [11, 22, 33, 12, 13, 23]): \f$ S_{1212} = S_{1313} = 1/2 \f$ (1 with the engineering + * shear), the in-plane shear \f$ S_{2323} = 0 \f$. \f[ - \mathbf{S}= \begin{array}{cccccc} + \mathbf{S}= \left( \begin{array}{cccccc} 1 & \frac{\nu}{1-\nu} & \frac{\nu}{1-\nu} & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ 0 & 0 & 0 & 0 & 0 & 0 \\ - 0 & 0 & 0 & 0 & 0 & 0 \\ - 0 & 0 & 0 & 0 & 0 & 0 \\ - 0 & 0 & 0 & 0 & 0 & 1 \end{array} \right) + 0 & 0 & 0 & 1 & 0 & 0 \\ + 0 & 0 & 0 & 0 & 1 & 0 \\ + 0 & 0 & 0 & 0 & 0 & 0 \end{array} \right) \f] * @details Example: * @code @@ -179,7 +181,7 @@ arma::mat Eshelby_oblate(const double &, const double &); */ arma::mat Eshelby_penny(const double &); -//This methods is using the Voigt notations for the tensors. +/// Uses the Voigt notation for the tensors. void calG(const double &, const double &, const double &, const double &, const double &, const arma::Mat &, const arma::mat &, arma::mat &); //Weighted Gauss integration over a sphere to represent the integration over the ellipsoid @@ -232,7 +234,7 @@ arma::mat Eshelby(const arma::mat &, const double &, const double &, const doubl * as a function of the stiffness tensor, and the three semi-axis length of the ellipsoid in the direction * \f$ 1 \f$, \f$ 2 \f$ and \f$ 3 \f$, respectively. The list of integration and their respective weight * for the numerical integration is automatically determined from the number of integration points - * in the \f$ 1 \f$ and \f$ 2 \f$ directions. (The points and weights are calculated using the internal *point* function). + * in the \f$ 1 \f$ and \f$ 2 \f$ directions. (The points and weights are calculated once per (mp, np), see eshelby_quadrature_points). * Returns the Eshelby tensor as a mat, according to the convention of a localisation tensor. * @details Example: * @code @@ -294,7 +296,7 @@ arma::mat T_II(const arma::mat &, const double &, const double &, const double & * as a function of the stiffness tensor, and the three semi-axis length of the ellipsoid in the direction * \f$ 1 \f$, \f$ 2 \f$ and \f$ 3 \f$, respectively. The list of integration and their respective weight * for the numerical integration is automatically determined from the number of integration points - * in the \f$ 1 \f$ and \f$ 2 \f$ directions. (The points and weights are calculated using the internal *point* function). + * in the \f$ 1 \f$ and \f$ 2 \f$ directions. (The points and weights are calculated once per (mp, np), see eshelby_quadrature_points). * Returns the Hill interaction tensor as a mat, according to the convention of a localisation tensor. * @details Example: * @code @@ -335,6 +337,71 @@ arma::mat T_II(const arma::mat &, const double &, const double &, const double & */ void points(arma::vec &, arma::vec &, arma::vec &, arma::vec &, const int &, const int &); +/** + * @brief How the Eshelby tensors of a mean-field scheme are evaluated: the quadrature tables of + * the integral for one (mp, np, rule) and whether closed forms may replace it. Shared and immutable. + * + * @details The only way to obtain one is eshelby_quadrature_points(), which builds each + * (mp, np, rule, closed_form) once and hands out a const reference to it: never modified nor + * freed afterwards, so any thread can use it while other threads integrate with other settings. + * A setting identifies its tables exactly (the object address can serve as a cache key). + * + * Rules (the integrand is centrally symmetric, G(-xi) = G(xi)): + * - 0: Gauss-Legendre on the whole sphere, mp points in cos(theta) over [-1, 1] and np points in + * phi over [0, 2 pi] (the historical rule, mp x np evaluations); + * - 1: half sphere, (mp+1)/2 Gauss-Legendre points in cos(theta) over [0, 1] (weights doubled) + * and np equally spaced points in phi with the trapezoidal rule, which converges spectrally on + * this periodic integrand: half the evaluations for an accuracy as good or better. + */ +struct eshelby_quadrature { + int mp; ///< number of polar points in the tables (mp, or (mp+1)/2 for rule 1) + int np; ///< number of azimuthal points in the tables + int rule; ///< 0 Gauss-Legendre on the sphere, 1 half sphere with a trapezoidal rule in phi + bool closed_form; ///< use the closed forms (isotropic medium, sphere or spheroid) when they apply + arma::vec x; ///< polar points (cos(theta)) + arma::vec wx; ///< polar weights + arma::vec y; ///< azimuthal points + arma::vec wy; ///< azimuthal weights + + eshelby_quadrature(const eshelby_quadrature &) = delete; + eshelby_quadrature &operator=(const eshelby_quadrature &) = delete; + +private: + int mp_asked; ///< the mp of the request (the key) + eshelby_quadrature(const int &, const int &, const int &, const bool &); + friend const eshelby_quadrature &eshelby_quadrature_points(const int &, const int &, const int &, const bool &); +}; + +/** + * @brief The shared Eshelby evaluation settings and quadrature tables for (mp, np, rule, closed_form). + * @param mp the number of integration points in the polar direction (at least 1) + * @param np the number of integration points in the azimuthal direction (at least 1) + * @param rule 0 (default) Gauss-Legendre on the sphere, 1 half sphere with a trapezoidal rule in phi + * @param closed_form false (default): always integrate; true: the schemes use the closed forms + * (Eshelby_sphere, Eshelby_prolate, Eshelby_oblate) when the medium is isotropic and the + * inclusion a sphere or a spheroid of axis 1, and integrate otherwise + * @return A const reference, valid until the process exits. + * @details Thread-safe: each new setting is built once under a lock, and each thread remembers + * the last one it asked for. Rule 0 tables are those of *points*. + * @code + const eshelby_quadrature &q = eshelby_quadrature_points(50, 50); + mat S = Eshelby(L, a1, a2, a3, q.x, q.wx, q.y, q.wy, q.mp, q.np); + * @endcode +*/ +const eshelby_quadrature &eshelby_quadrature_points(const int &mp, const int &np, const int &rule = 0, const bool &closed_form = false); + +/** + * @brief Closed-form Eshelby tensor of an inclusion in an isotropic medium, when one applies. + * @param L stiffness of the medium (6x6), in the inclusion frame + * @param a1, a2, a3 semi-axes of the inclusion + * @param S the Eshelby tensor, set when the function returns true + * @return true when L is isotropic (every entry within 1e-10 of its largest one from the + * isotropic tensor of lambda = L(0,1), mu = L(3,3)) and the inclusion is a sphere or a spheroid + * of axis 1 (a2 = a3) whose aspect ratio a1/a2 is 1 or at least 1% away from 1 (closer to 1 the + * spheroid formulas lose accuracy by cancellation); false otherwise, S untouched. + */ +bool Eshelby_closed_form(const arma::mat &L, const double &a1, const double &a2, const double &a3, arma::mat &S); + /** @} */ // end of eshelby group diff --git a/include/simcoon/Continuum_mechanics/Material/variant.hpp b/include/simcoon/Continuum_mechanics/Material/variant.hpp index d6ba59749..87543ce38 100755 --- a/include/simcoon/Continuum_mechanics/Material/variant.hpp +++ b/include/simcoon/Continuum_mechanics/Material/variant.hpp @@ -52,12 +52,26 @@ class variant double g; arma::vec ETn; //Strain associated to each variant - variant(); //default constructor - variant(arma::vec, arma::vec, double); //Constructor with parameters - variant(const variant &); //Copy constructor + /** @brief Default constructor: zero normal, direction, Schmid tensor and transformation strain. */ + variant(); + /** + * @brief Constructor with parameters; builds the Schmid tensor and the transformation strain. + * @param n habit plane normal (size 3) + * @param m transformation direction (size 3) + * @param g transformation magnitude, \f$ \varepsilon^{T}_n = g\,\mathrm{Schmid\_v}(n, m) \f$ + */ + variant(arma::vec n, arma::vec m, double g); + /** @brief Copy constructor. */ + variant(const variant &); + /** @brief Destructor. */ ~variant(); - void build(const double &); + /** + * @brief Rebuild the Schmid tensor R and the transformation strain ETn from the stored n, m. + * @param g transformation magnitude (must be > 0) + */ + void build(const double &g); + /** @brief Copy assignment. */ virtual variant& operator = (const variant&); friend std::ostream& operator << (std::ostream&, const variant&); diff --git a/include/simcoon/Continuum_mechanics/Micromechanics/multiphase.hpp b/include/simcoon/Continuum_mechanics/Micromechanics/multiphase.hpp index 28d2eb70c..67c65dcf8 100755 --- a/include/simcoon/Continuum_mechanics/Micromechanics/multiphase.hpp +++ b/include/simcoon/Continuum_mechanics/Micromechanics/multiphase.hpp @@ -86,14 +86,46 @@ int sub_phase_shape(const std::string &umat_name); /** * @brief Check the sub-phases and the props of a mean-field phase. * - * The props hold the scheme's settings only: MIHEN [mp, np], MIMTN [mp, np, n_matrix], - * MISCN [mp, np, n_matrix, (start)], MIPLN []. Checked: at least one sub-phase, the - * exact props length, mp and np >= 1, n_matrix among the phases given. + * The props hold the scheme's settings only: MIHEN [mp, np], + * MIMTN [mp, np, n_matrix, (closed_form, rule)], + * MISCN [mp, np, n_matrix, (start, closed_form, rule, anderson, rtol)], MIPLN []; the + * optional slots are trailing (giving one gives the ones before it) and default to 0, the + * historical behaviour (see mean_field_options). Checked: at least one sub-phase, the props + * length, mp and np >= 1, n_matrix among the phases given, the option values. * @param phase the mean-field phase * @throws std::invalid_argument on any of the above */ void check_sub_phases(const phase_characteristics &phase); +/** + * @brief Numerical options of the ellipsoidal mean-field schemes, read from their props. + * + * All default to 0, which is the historical behaviour. + * - closed_form (MIMTN props(3), MISCN props(4)): 1 takes the Eshelby tensor in closed form + * (Eshelby_closed_form) when the reference medium is isotropic and the inclusion a sphere or + * a spheroid of axis 1, and integrates it otherwise. + * - rule (MIMTN props(4), MISCN props(5)): quadrature of the Eshelby integrals, + * 0 Gauss-Legendre on the sphere (mp x np points), 1 half sphere with a trapezoidal rule in phi + * ((mp+1)/2 x np points), see eshelby_quadrature. + * - anderson (MISCN props(6)): depth m of the Anderson acceleration of the self-consistent + * fixed point of the elastic homogenization (get_L_elastic, L_eff); 0 plain iterations. + * The solver does one self-consistent sweep per localization iteration and ignores it. + * - rtol (MISCN props(7)): stopping criterion of that fixed point, 0 the absolute + * ||Lt - Lt_prev||_F <= precision_micro, > 0 the relative ||Lt - Lt_prev||_F <= rtol ||Lt||_F + * (independent of the stiffness unit). + */ +struct mean_field_options { + int closed_form = 0; + int rule = 0; + int anderson = 0; + double rtol = 0.; +}; + +/** + * @brief The numerical options of a mean-field phase (defaults for the slots it omits). + */ +mean_field_options mean_field_options_of(const phase_characteristics &phase); + /** * @brief First guess of the self-consistent scheme: props(3) when given, else 1. * @return 1 Mori-Tanaka (default), 0 homogeneous strain (n_matrix < 0) diff --git a/include/simcoon/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.hpp b/include/simcoon/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.hpp index 7edfa0ff1..15301215a 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.hpp @@ -41,7 +41,9 @@ namespace simcoon{ * * The potential is selected by umat_name. Currently available: * - OGDEN: \f$ W = \sum_{i=1}^N \frac{2 \mu_i}{\alpha_i^2} \left( \bar{\lambda}_1^{\alpha_i} + \bar{\lambda}_2^{\alpha_i} + \bar{\lambda}_3^{\alpha_i} - 3 \right) + \kappa \left( J \, \textrm{ln} J - J + 1 \right) \f$ - * with props = { N, \f$ \kappa \f$, \f$ \mu_1 \f$, \f$ \alpha_1 \f$, ..., \f$ \mu_N \f$, \f$ \alpha_N \f$ } (nprops = 2 + 2N). + * with props = { N, \f$ \kappa \f$, \f$ \mu_1 \f$, \f$ \alpha_1 \f$, ..., \f$ \mu_N \f$, \f$ \alpha_N \f$ } (nprops = 2 + 2N), optionally followed by props(2 + 2N) + * selecting the volumetric term (see VolumetricPotential: absent or 0 for + * \f$ \kappa (J \ln J - J + 1) \f$, 1 for \f$ \frac{\kappa}{2} (J-1)^2 \f$). * Constraints (validated, throws std::invalid_argument): \f$ N \geq 1 \f$ and every \f$ \alpha_i \neq 0 \f$. * The ground-state shear modulus is \f$ \mu = \sum_i \mu_i \f$; N=1, \f$ \alpha_1 = 2 \f$ recovers the compressible neo-Hookean potential (NEOHC). * diff --git a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Damage/damage_weibull.hpp b/include/simcoon/Continuum_mechanics/Umat/Mechanical/Damage/damage_weibull.hpp deleted file mode 100755 index f6569cd7e..000000000 --- a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Damage/damage_weibull.hpp +++ /dev/null @@ -1,46 +0,0 @@ -/* This file is part of simcoon. - - simcoon is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - simcoon is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with simcoon. If not, see . - - */ - -///@file damage_LLD_0.hpp -///@brief Ladeveze-Le Dantec model that accounts for anisotropic damage and plasticity -///@brief Damage evolution is considered as a function of time - -#pragma once - -#include -#include -#include -#include - -namespace simcoon{ - -/** - * @file damage_weibull.hpp - * @brief Mechanical damage model. - */ - -/** @addtogroup umat_mechanical - * @{ - */ - - -void umat_damage_weibull(const std::string &umat_name, const arma::vec &Etot, const arma::vec &DEtot, arma::vec &stress, arma::mat &Lt, arma::mat &L, const arma::mat &DR, const int &nprops, const arma::vec &props, const int &nstatev, arma::vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode = tangent_default); - - -/** @} */ // end of umat_mechanical group - -} //namespace simcoon diff --git a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.hpp b/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.hpp index 41fb8aba5..c7cbe2871 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.hpp @@ -187,12 +187,12 @@ namespace simcoon{ * | statev[26..31] | \f$ \mathbf{X}_2 = \tfrac{2}{3} C_2 \boldsymbol{a}_2 \f$ | Backstress 2 (Voigt) | Stress | * | statev[32] | \f$ H_p \f$ | Voce isotropic hardening stress \f$ R(p) \f$ (output) | Stress | * + * @param umat_name Name of the law (EPCHA), unused by the kernel * @param Etot Total strain tensor at beginning of increment (Voigt notation: \f$6 \times 1\f$ vector) * @param DEtot Strain increment tensor (Voigt notation: \f$6 \times 1\f$ vector) * @param stress Stress tensor (Voigt notation: \f$6 \times 1\f$ vector) [output] * @param Lt Consistent tangent modulus \f$ \mathbf{L}_t \f$ (\f$6 \times 6\f$ matrix) [output] * @param L Elastic stiffness tensor (\f$6 \times 6\f$ matrix) [output] - * @param sigma_in Internal stress contribution for explicit solvers (\f$6 \times 1\f$ vector) [output] * @param DR Rotation increment matrix (\f$3 \times 3\f$) for objective integration * @param nprops Number of material properties * @param props Material properties vector (see table above) @@ -209,8 +209,9 @@ namespace simcoon{ * @param ndi Number of direct stress components (typically 3) * @param nshr Number of shear stress components (typically 3) * @param start Flag indicating first increment (true) or continuation (false) - * @param solver_type Solver type: 0=implicit, 1=explicit, 2=dynamic implicit * @param tnew_dt Suggested new time step size for adaptive time stepping [output] + * @param tangent_mode tangent_* constant (parameter.hpp); the closest point is the kernel code + * tangent_closest_point (integrator='closest_point') * * @note The Chaboche model excels at capturing ratcheting and cyclic plasticity * @note Multiple backstresses are essential for accurate multiaxial loading predictions @@ -261,8 +262,8 @@ namespace simcoon{ * Wm, Wm_r, Wm_ir, Wm_d, 3, 3, false, 0, tnew_dt); * * // Check backstress components - * vec X1 = statev.subvec(8, 13); // First backstress - * vec X2 = statev.subvec(14, 19); // Second backstress + * vec X1 = statev.subvec(20, 25); // First backstress + * vec X2 = statev.subvec(26, 31); // Second backstress * @endcode * * **References:** diff --git a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.hpp b/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.hpp index fa3a641de..7a07cac41 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.hpp @@ -93,6 +93,16 @@ namespace simcoon{ * \dot{p} = \sqrt{\frac{2}{3} \dot{\boldsymbol{\varepsilon}}^p : \dot{\boldsymbol{\varepsilon}}^p} = \dot{\lambda} * \f] * + * The Gibbs free energy of the single (lead) mechanism, with internal variables + * \f$ p \f$ and \f$ \boldsymbol{\varepsilon}^p \f$, reads + * \f[ + * G = -\frac{1}{2}\,\boldsymbol{\sigma} : \mathbf{S} : \boldsymbol{\sigma} + * - \boldsymbol{\sigma} : \boldsymbol{\varepsilon}^p + * - \boldsymbol{\sigma} : \boldsymbol{\alpha}\,(\theta - \theta_0) + * + c_0\left[(\theta - \theta_0) - \theta \ln\frac{\theta}{\theta_0}\right] + * - \eta_0\,\theta + E_0 + F(\theta, p) + * \f] + * * **Convex Cutting Plane Algorithm:** * * The CCP algorithm solves the return mapping problem by reformulating it as a complementarity problem: @@ -149,6 +159,20 @@ namespace simcoon{ * @param nshr Number of shear stress components (typically 3) * @param start Flag indicating first increment (true) or continuation (false) * @param tnew_dt Suggested new time step size for adaptive time stepping [output] + * @param tangent_mode tangent_* constant (parameter.hpp); under tangent_mode 2 the consistent tangent + * of the radial return is taken in closed form (Simo and Taylor 1985; de Souza Neto et al. + * 2008, box 7.4), exact at the converged return: + * \f[ + * \mathbf{L}_t = 3K\,\mathbf{I}_{vol} + 2G\,\theta\,\mathbf{I}_{dev} + * - 2G\,\bar{\theta}\,\mathbf{N}\otimes\mathbf{N},\quad + * \mathbf{N} = \frac{\mathbf{s}}{\|\mathbf{s}\|},\quad + * \theta = 1 - \frac{3G\,\Delta p}{q^{tr}},\quad + * \bar{\theta} = \frac{1}{1 + H/(3G)} - (1 - \theta) + * \f] + * with \f$ q^{tr} = q + 3G\,\Delta p \f$ the trial von Mises stress and + * \f$ H = \mathrm{d}H_p/\mathrm{d}p \f$ at the converged \f$ p \f$ + * @param ncp complementarity function of the cutting plane (NCPMethod code, state_variables::ncp): + * 0 Fischer-Burmeister (default), 1 min-NCP with a Fischer-Burmeister fallback * * @note Voigt notation convention: [11, 22, 33, 12, 13, 23] with engineering shear strains (\f$\gamma = 2\varepsilon\f$) * @note The consistent tangent modulus Lt ensures quadratic convergence in implicit Newton-Raphson schemes @@ -174,7 +198,7 @@ namespace simcoon{ * 3, 3, true, tnew_dt); * @endcode */ -void umat_plasticity_iso(const std::string &umat_name, const arma::vec &Etot, const arma::vec &DEtot, arma::vec &stress, arma::mat &Lt, arma::mat &L, const arma::mat &DR, const int &nprops, const arma::vec &props, const int &nstatev, arma::vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode = tangent_default); +void umat_plasticity_iso(const std::string &umat_name, const arma::vec &Etot, const arma::vec &DEtot, arma::vec &stress, arma::mat &Lt, arma::mat &L, const arma::mat &DR, const int &nprops, const arma::vec &props, const int &nstatev, arma::vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode = tangent_default, const int &ncp = 0); /** @} */ // end of umat_mechanical group diff --git a/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.hpp b/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.hpp index dffbc6ec5..f0fea0c61 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.hpp @@ -16,7 +16,7 @@ */ /** - * @file sma_mono.hpp + * @file SMA_mono.hpp * @brief Micromechanical monocrystal model for shape memory alloys based on Patoor et al. (1996) * @author Y. Chemisky, D. Chatziathanasiou * @version 1.0 @@ -49,8 +49,6 @@ namespace simcoon{ * |-----------|----------|-------------------| * | SMAMO | Isotropic | E, nu | * | SMAMC | Cubic | C11, C12, C44 | - * | SMAOT | Orthotropic | E1, E2, E3, nu12, nu13, nu23, G12, G13, G23 | - * | SMATI | Transverse Isotropic | EL, ET, nuTL, nuTT, GLT | * * **Key Features:** * - Explicit tracking of N martensite variant volume fractions \f$ f_n \f$ (n = 1, ..., N) @@ -175,6 +173,19 @@ namespace simcoon{ * | props[8] | nvariants | Number of martensite variants | * | props[9-16] | c_lambda0, p0_lambda0, ... | Lagrange multiplier parameters | * + * The 8 Lagrange parameters are \f$ (c, p_0, n, \alpha) \f$ of the per-variant penalty + * \f$ \lambda_0(f_n) \f$, then of the total-fraction penalty \f$ \lambda_1(\xi) \f$. + * + * **Crystallography (props, after the parameters above, offset \f$ o \f$ = 16 for SMAMO, 17 for SMAMC):** + * + * | Index | Description | + * |-------|-------------| + * | \f$ o + 6i \f$ .. \f$ o + 6i + 2 \f$ | habit plane normal \f$ \mathbf{n}_i \f$ of variant \f$ i \f$ | + * | \f$ o + 6i + 3 \f$ .. \f$ o + 6i + 5 \f$ | transformation direction \f$ \mathbf{m}_i \f$ of variant \f$ i \f$ | + * | \f$ o + 6N + iN + j \f$ | interaction matrix \f$ H_{ij} \f$, \f$ N \times N \f$, row by row | + * + * Nothing is read from a file; a shorter props vector throws `std::invalid_argument`. + * * **State Variables (statev):** * * Total state variables required: \f$ n_{statev} = 1 + N + 7 \f$ (for N variants) @@ -192,7 +203,7 @@ namespace simcoon{ * The macroscopic transformation strain is computed as: * \f$ \boldsymbol{\varepsilon}^{tr} = \sum_{n=1}^{N} f_n \boldsymbol{\varepsilon}^{tr}_n \f$ * - * @param umat_name Name of the constitutive model (SMAMO, SMAMC, SMAOT, SMATI) + * @param umat_name Name of the constitutive model (SMAMO or SMAMC) * @param Etot Total strain tensor at beginning of increment (Voigt notation: \f$6 \times 1\f$ vector) * @param DEtot Strain increment tensor (Voigt notation: \f$6 \times 1\f$ vector) * @param stress Cauchy stress tensor (Voigt notation: \f$6 \times 1\f$ vector) [output] @@ -223,8 +234,6 @@ namespace simcoon{ * * @see L_iso() for isotropic stiffness tensor (SMAMO) * @see L_cubic() for cubic stiffness tensor (SMAMC) - * @see L_ortho() for orthotropic stiffness tensor (SMAOT) - * @see L_isotrans() for transverse isotropic stiffness tensor (SMATI) * * **References:** * - Patoor, E., Eberhardt, A., & Berveiller, M. (1996). "Micromechanical modelling of diff --git a/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.hpp b/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.hpp index d3dda70aa..a6102bd3d 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.hpp @@ -67,7 +67,7 @@ namespace simcoon{ * * The maximum transformation strain magnitude \f$ H^{cur} \f$ evolves with stress: * \f[ - * H^{cur}(\bar{\sigma}) = H_{min} + (H_{max} - H_{min})(1 - e^{-k_1 \bar{\sigma} / \sigma_{crit}}) + * H^{cur}(\bar{\sigma}) = H_{min} + (H_{max} - H_{min})\left(1 - e^{-k_1 \langle \bar{\sigma} - \sigma_{crit} \rangle}\right),\qquad \langle x \rangle = \max(x, 0) * \f] * * **Material Parameters (props):** @@ -76,7 +76,7 @@ namespace simcoon{ * * | Index | Symbol | Description | Units | * |-------|--------|-------------|-------| - * | 0 | flagT | Temperature extrapolation (0=linear, 1=smooth) | - | + * | 0 | flagT | 0: the given transformation temperatures are those of the measured curve, converted to the model's with n1..n4; 1: used as given | - | * | 1 | \f$ E_A \f$ | Young's modulus of Austenite | MPa | * | 2 | \f$ E_M \f$ | Young's modulus of Martensite | MPa | * | 3 | \f$ \nu_A \f$ | Poisson's ratio of Austenite | - | @@ -85,8 +85,8 @@ namespace simcoon{ * | 6 | \f$ \alpha_M \f$ | CTE of Martensite | 1/K | * | 7 | \f$ H_{min} \f$ | Minimal transformation strain magnitude | - | * | 8 | \f$ H_{max} \f$ | Maximal transformation strain magnitude | - | - * | 9 | \f$ k_1 \f$ | Exponential evolution parameter for \f$ H^{cur} \f$ | - | - * | 10 | \f$ \sigma_{crit} \f$ | Critical stress for \f$ H^{cur} \f$ evolution | MPa | + * | 9 | \f$ k_1 \f$ | Exponential evolution parameter for \f$ H^{cur} \f$ | 1/MPa | + * | 10 | \f$ \sigma_{crit} \f$ | Von Mises stress below which \f$ H^{cur} = H_{min} \f$ | MPa | * | 11 | \f$ C_A \f$ | Clausius-Clapeyron slope (M \f$ \rightarrow \f$ A) | MPa/K | * | 12 | \f$ C_M \f$ | Clausius-Clapeyron slope (A \f$ \rightarrow \f$ M) | MPa/K | * | 13 | \f$ M_{s0} \f$ | Martensite start temperature at zero stress | K | @@ -109,7 +109,7 @@ namespace simcoon{ * * | Index | Symbol | Description | Units | * |-------|--------|-------------|-------| - * | 0 | flagT | Temperature extrapolation (0=linear, 1=smooth) | - | + * | 0 | flagT | 0: the given transformation temperatures are those of the measured curve, converted to the model's with n1..n4; 1: used as given | - | * | 1 | \f$ E_A \f$ | Young's modulus of Austenite ([100] direction) | MPa | * | 2 | \f$ E_M \f$ | Young's modulus of Martensite ([100] direction) | MPa | * | 3 | \f$ \nu_A \f$ | Poisson's ratio of Austenite | - | @@ -148,8 +148,8 @@ namespace simcoon{ * | 5 | \f$ \gamma^{tr}_{12} \f$ | Transformation strain component 12 (engineering) | - | * | 6 | \f$ \gamma^{tr}_{13} \f$ | Transformation strain component 13 (engineering) | - | * | 7 | \f$ \gamma^{tr}_{23} \f$ | Transformation strain component 23 (engineering) | - | - * | 8 | \f$ \xi_F \f$ | Forward martensitic volume fraction | - | - * | 9 | \f$ \xi_R \f$ | Reverse martensitic volume fraction | - | + * | 8 | \f$ \xi_F \f$ | Cumulated forward transformation, non-decreasing (\f$ \xi = \xi_F - \xi_R \f$) | - | + * | 9 | \f$ \xi_R \f$ | Cumulated reverse transformation, non-decreasing | - | * | 10 | \f$ \rho \Delta s_0 \f$ | Entropy difference between phases (M - A) | MPa/K | * | 11 | \f$ \rho \Delta E_0 \f$ | Internal energy difference between phases (M - A) | MPa | * | 12 | \f$ D \f$ | Stress dependence parameter for transformation limits | - | @@ -183,7 +183,8 @@ namespace simcoon{ * @param tnew_dt Suggested new time step ratio for adaptive time stepping [output] * * @note Elastic convention: isotropic uses "Enu", cubic uses "EnuG" for L_iso / L_cubic - * @note The flagT parameter controls temperature extrapolation: 0 for linear, 1 for smooth + * @note flagT = 0 converts the given Ms, Mf, As, Af (read on the measured curve) into the model's + * temperatures through the smoothing exponents n1..n4; flagT = 1 uses them as given * @note The pre-2.0 aliases SMAUT and SMANI were removed in 2.0: use SMADI and SMAAI. * @note **Convergence measure.** The local Fischer-Burmeister solve reports * \f$ \sum_j |FB_j| / Y^{crit}_j \f$, so each \f$ Y^{crit}_j \f$ must be a positive scale of @@ -198,6 +199,59 @@ namespace simcoon{ * \f$ Y^{crit}_R = |Y_0^t| + |D\,\boldsymbol{\sigma}:\bar{\boldsymbol{\varepsilon}}^{T}| \f$, * identical to the thresholds wherever \f$ D \ge 0 \f$. Shared by the thermomechanical * twin and by @ref umat_sma_unified_TR. + * @note **Reverse row of the local Jacobian.** With + * \f$ \bar{\boldsymbol{\varepsilon}}^{T} = \mathrm{dev}(\boldsymbol{\varepsilon}^{T})/\xi \f$ + * (ETMean), the direct partials \f$ \partial \Phi_R / \partial \xi \f$ and + * \f$ \partial \Phi_R / \partial \boldsymbol{\varepsilon}^{T} : \boldsymbol{\Lambda}^F \f$ carry + * \f$ \pm(1+D)\,\boldsymbol{\sigma}:\bar{\boldsymbol{\varepsilon}}^{T}/\xi \f$ terms that cancel + * analytically but lose about 12 digits at \f$ \xi \approx 10^{-12} \f$. The row is therefore + * built through \f$ \bar{\boldsymbol{\varepsilon}}^{T} \f$ as intermediate variable: + * \f[ + * K_{Rj} = \frac{\partial \Phi_R}{\partial \bar{\boldsymbol{\varepsilon}}^{T}} : \boldsymbol{\Lambda}_{\bar{\varepsilon}}^{j} + * + \left.\frac{\partial \Phi_R}{\partial \xi_j}\right|_{finite}, \qquad + * \frac{\partial \Phi_R}{\partial \bar{\boldsymbol{\varepsilon}}^{T}} = -(1+D)\,\boldsymbol{\sigma}, + * \f] + * \f[ + * \boldsymbol{\Lambda}_{\bar{\varepsilon}}^{F} = \frac{\boldsymbol{\Lambda}^{F} - \bar{\boldsymbol{\varepsilon}}^{T}}{\xi}, \qquad + * \boldsymbol{\Lambda}_{\bar{\varepsilon}}^{R} = \mathbf{0}, \qquad + * \left.\frac{\partial \Phi_R}{\partial \xi_{F}}\right|_{finite} = H_f^{R\prime} - \lambda_0^{\prime}, \quad + * \left.\frac{\partial \Phi_R}{\partial \xi_{R}}\right|_{finite} = -H_f^{R\prime} + \lambda_0^{\prime}, + * \f] + * (reverse transformation removes martensite proportionally, so the mean is unchanged; + * \f$ \boldsymbol{\Lambda}_{\bar{\varepsilon}}^{F} = \mathbf{0} \f$ when the fallback + * \f$ \bar{\boldsymbol{\varepsilon}}^{T} = \boldsymbol{\Lambda}^{F} \f$ is active). + * @note **Closest-point mode** (`tangent_mode = tangent_closest_point`). The closest-point + * projection replaces the cutting-plane loop, with \f$ \mathbf{L}(\xi) \f$ and + * \f$ \boldsymbol{\alpha}(\xi) \f$ frozen at \f$ \xi_n \f$. Since the Reuss mixture + * \f$ \mathbf{M}(\xi) \f$ and \f$ \boldsymbol{\alpha}(\xi) \f$ are linear in \f$ \xi \f$, the + * effective flows + * \f[ + * \boldsymbol{\Lambda}^{F}_{eff} = \boldsymbol{\Lambda}^{F} + \Delta\mathbf{M}\,\boldsymbol{\sigma} + \Delta\boldsymbol{\alpha}\,(T+\Delta T-T_{init}), \qquad + * \boldsymbol{\Lambda}^{R}_{eff} = -\bar{\boldsymbol{\varepsilon}}^{T} - \Delta\mathbf{M}\,\boldsymbol{\sigma} - \Delta\boldsymbol{\alpha}\,(T+\Delta T-T_{init}) + * \f] + * make the frozen-\f$ \mathbf{L} \f$ residual the exact discrete system. The state is rebuilt + * from the start values at every iterate, with \f$ \bar{\boldsymbol{\varepsilon}}^{T} \f$ in + * closed form (the fixed-point sweep diverges when \f$ \Delta\xi_R > \xi \f$): + * \f[ + * \bar{\boldsymbol{\varepsilon}}^{T}\,(\xi + \Delta\xi_R) = \mathrm{dev}(\boldsymbol{\varepsilon}^{T}_n) + \Delta\xi_F\,\mathrm{dev}(\boldsymbol{\Lambda}^{F}), + * \f] + * so that + * \f$ \partial\bar{\boldsymbol{\varepsilon}}^{T}/\partial\boldsymbol{\sigma} = \frac{\Delta\xi_F}{\xi_n+\Delta\xi_F}\,\mathrm{dev}(\partial\boldsymbol{\Lambda}^{F}/\partial\boldsymbol{\sigma}) \f$, + * \f$ \partial\bar{\boldsymbol{\varepsilon}}^{T}/\partial\Delta\xi_F = (\mathrm{dev}(\boldsymbol{\Lambda}^{F}) - \bar{\boldsymbol{\varepsilon}}^{T})/(\xi_n+\Delta\xi_F) \f$, + * with \f$ \boldsymbol{\Lambda}^{F} = H^{cur}\,\mathbf{g} \f$, + * \f$ \partial\boldsymbol{\Lambda}^{F}/\partial\boldsymbol{\sigma} = \mathbf{g}\otimes\partial H^{cur}/\partial\boldsymbol{\sigma} + H^{cur}\,\partial\mathbf{g}/\partial\boldsymbol{\sigma} \f$ + * (\f$ \mathbf{g} \f$ the Drucker gradient). The callbacks return the analytic total + * derivatives of this composed map; the flux chain of the forward row is + * \f$ \mathbf{c}_F = -\Delta\xi_R\,\partial\bar{\boldsymbol{\varepsilon}}^{T}/\partial\Delta\xi_F \f$. + * The hardening functions are evaluated at \f$ \xi \f$ clamped to \f$ [0,1] \f$ (fractional + * powers are NaN outside), and the FB scales are floored at + * \f$ 0.1\max(Y_0^t + D\,H_{max}\,\sigma_{caliber},\,10^{-2}) \f$ since both thresholds vanish in + * virgin states. A non-converged projection requests a step cut (`tnew_dt = 0.5`), never a + * cutting-plane fallback. The output \f$ \mathbf{L} \f$ is the mixture at the converged + * \f$ \xi \f$; the tangent uses the frozen one. + * @note `tangent_algorithmic` is clamped to the continuum tangent: its finite-difference flow + * Hessian makes the local system near-singular on the transformation plateau + * (rcond \f$ \sim 10^{-17} \f$). * * @see L_iso() for isotropic stiffness tensor (SMADI, SMAAI) * @see L_cubic() for cubic stiffness tensor (SMADC, SMAAC) diff --git a/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.hpp b/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.hpp index d1bfabf21..df9f050d6 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.hpp @@ -40,10 +40,12 @@ namespace simcoon { * * @details Extends umat_sma_unified_T with a third leading mechanism: martensite * reorientation. The macroscopic state is the martensitic volume fraction - * \f$ \xi \f$, the transformation strain \f$ \boldsymbol{\varepsilon}^{tr} \f$, the - * reorientation strain accumulator \f$ \mathbf{E}^{Reo} \f$, and three - * back-strain channels \f$ \mathbf{a}_F, \mathbf{a}_R, \mathbf{a}_{reo} \f$ that - * grow with the forward / reverse / reorientation increments respectively. + * \f$ \xi = \xi^F - \xi^R \f$, where the cumulated forward and reverse transformations + * \f$ \xi^F, \xi^R \f$ (non-decreasing: their increments are the non-negative multipliers of the + * two mechanisms) are kept apart so that both can be active in the same increment, the + * transformation strain \f$ \boldsymbol{\varepsilon}^{tr} \f$, the reorientation strain + * accumulator \f$ \mathbf{E}^{Reo} \f$ and its cumulative multiplier \f$ p^{TR} \f$, and a + * single back-strain \f$ \mathbf{v}^{re} \f$, which grows with the reorientation increments only. * * **Variants:** * @@ -57,8 +59,7 @@ namespace simcoon { * **Effective back-stress and saturation-coupled reorientation surface (Form B):** * * \f[ - * \mathbf{X} = H^{Reo} \, (\mathbf{a} \odot \mathbf{I}_{r05}), \qquad - * \mathbf{a} = \mathbf{a}_F + \mathbf{a}_R + \mathbf{a}_{reo} + * \mathbf{X} = H^{Reo} \, (\mathbf{v}^{re} \odot \mathbf{I}_{r05}) * \f] * \f[ * \Phi^{Reo} = \mathrm{Prager}\!\bigl( \boldsymbol{\sigma} - (1+\lambda_{1}^{Reo}) \, \mathbf{X} \bigr) - Y^{Reo} @@ -103,7 +104,12 @@ namespace simcoon { * * | Index | Description | * |-------|-------------| - * | 0..16 | Identical to umat_sma_unified_T (T_init, xi, ET(6), xiF, xiR, calibrated rho-Ds0, rho-DE0, D, a1, a2, a3, Y0t) | + * | 0 | \f$ T_{init} \f$ : initial temperature | + * | 1 | \f$ \xi \f$ : martensitic volume fraction | + * | 2..7 | \f$ \boldsymbol{\varepsilon}^{tr} \f$ : transformation strain | + * | 8 | \f$ \xi^F \f$ : cumulated forward transformation, non-decreasing (\f$ \xi = \xi^F - \xi^R \f$) | + * | 9 | \f$ \xi^R \f$ : cumulated reverse transformation, non-decreasing, kept apart from \f$ \xi^F \f$ so that both can be active in the same increment | + * | 10..16 | calibrated constants, as in umat_sma_unified_T: \f$ \rho\Delta s_0, \rho\Delta E_0, D, a_1, a_2, a_3, Y_0^t \f$ | * | 17 | \f$ p^{TR} \f$ : cumulative reorientation multiplier \f$ \sum \Delta s^{Reo} \f$ | * | 18..23 | \f$ \mathbf{v}^{re} \f$ : reorientation back-strain (Voigt, strain convention with engineering shears) | * | 24..29 | \f$ \mathbf{E}^{Reo} \f$ : macroscopic reorientation strain accumulator | @@ -128,6 +134,51 @@ namespace simcoon { * @ref umat_sma_unified_T (see its note); \f$ Y^{crit}_{Reo} = Y^{Reo} \f$. * @note When \f$ \Delta s^{Reo} = 0 \f$, the result is bit-identical to * @ref umat_sma_unified_T (same elastic mixing, same forward/reverse equations). + * @note **Reorientation flows** (thesis §4, eq. 503). Reorientation only re-orients the + * martensite present at the start of the increment, \f$ \xi_{start} \f$ (new martensite forms + * already aligned with \f$ \boldsymbol{\sigma} \f$), so with + * \f$ \bar{\boldsymbol{\Lambda}}^{re} = \partial\,\mathrm{Prager}/\partial\boldsymbol{\sigma}\,(\boldsymbol{\sigma}_{eff}) \f$: + * \f[ + * \boldsymbol{\Lambda}_{\varepsilon}^{re} = \xi_{start}\,\bar{\boldsymbol{\Lambda}}^{re}, \qquad + * \dot{\mathbf{v}}^{re} = \dot{p}^{re}\,\bar{\boldsymbol{\Lambda}}^{re}, \qquad + * \frac{\partial \Phi^{Reo}}{\partial \boldsymbol{\sigma}} = \bar{\boldsymbol{\Lambda}}^{re}. + * \f] + * The criterion and the back-strain flow are per unit martensite (not \f$ \xi \f$-scaled), which + * keeps \f$ B_{22} \f$ finite at \f$ \xi = 0 \f$ through the back-strain term of \f$ K_{22} \f$. + * The saturation argument is the intrinsic ratio \f$ \|\mathbf{v}^{re}\| / E_T^{Reo,max} \f$: + * dividing by \f$ \xi \f$ would blow the penalty up as \f$ \xi \to 0 \f$. Hence + * \f$ K_{2F} = K_{2R} = 0 \f$ and + * \f[ + * K_{22} = \frac{\partial \Phi^{Reo}}{\partial \mathbf{v}^{re}} : \bar{\boldsymbol{\Lambda}}^{re}, \qquad + * \frac{\partial \Phi^{Reo}}{\partial \mathbf{v}^{re}} = -(1+\lambda_1^{Reo})\,H^{Reo}\,(\boldsymbol{\eta}^{Reo}\odot\mathbf{I}_{r05}) + * - (\boldsymbol{\eta}^{Reo}:\mathbf{X})\,\frac{\partial \lambda_1^{Reo}}{\partial \mathbf{v}^{re}}. + * \f] + * The reverse row uses the same ETMean chain rule as @ref umat_sma_unified_T, with + * \f$ \boldsymbol{\Lambda}_{\bar{\varepsilon}}^{re} = \bar{\boldsymbol{\Lambda}}^{re} \f$ + * (\f$ \xi \f$ unchanged by reorientation). + * @note **Closest-point mode** (`tangent_closest_point`): same frozen-mixture construction as + * @ref umat_sma_unified_T, with \f$ \boldsymbol{\Lambda}^{re} = \xi_{start}\,\mathbf{r} \f$, + * \f$ \mathbf{r} = \bar{\boldsymbol{\Lambda}}^{re}(\boldsymbol{\sigma}_{eff}) \f$. The back-strain is + * solved by Newton on its backward-Euler equation + * \f$ \mathbf{R}(\mathbf{v}) = \mathbf{v} - \mathbf{v}_n - \Delta s^{re}\,\mathbf{r}(\boldsymbol{\sigma}_{eff}(\mathbf{v})) = \mathbf{0} \f$, + * Jacobian \f$ \mathbf{J} = \mathbf{I} - \Delta s^{re}\,\mathbf{G}\,\mathbf{A} \f$ with + * \f$ \mathbf{G} = \partial\mathbf{r}/\partial\boldsymbol{\sigma}_{eff} \f$ and + * \f$ \mathbf{A} = \partial\boldsymbol{\sigma}_{eff}/\partial\mathbf{v} \f$ (a fixed-point sweep only + * contracts while \f$ \Delta s^{re} H^{Reo}(1+\lambda_1^{Reo})/\bar{\sigma}_{eff} < 1 \f$); no root + * within the budget rejects the trial. Then + * \f[ + * \frac{\partial\mathbf{v}}{\partial\boldsymbol{\sigma}} = \mathbf{J}^{-1}\Delta s^{re}\,\mathbf{G}, \quad + * \frac{\partial\mathbf{v}}{\partial\Delta s^{re}} = \mathbf{J}^{-1}\mathbf{r}, \quad + * \frac{\partial\boldsymbol{\sigma}_{eff}}{\partial\boldsymbol{\sigma}} = \mathbf{I} + \mathbf{A}\,\frac{\partial\mathbf{v}}{\partial\boldsymbol{\sigma}}, \quad + * \frac{\partial\boldsymbol{\sigma}_{eff}}{\partial\Delta s^{re}} = \mathbf{A}\,\frac{\partial\mathbf{v}}{\partial\Delta s^{re}}, + * \f] + * and \f$ \bar{\boldsymbol{\varepsilon}}^{T}(\xi_n + \Delta\xi_F) = \mathrm{dev}(\boldsymbol{\varepsilon}^{T}_n) + * + \Delta\xi_F\,\mathrm{dev}(\boldsymbol{\Lambda}^F) + \Delta s^{re}\,\mathrm{dev}(\boldsymbol{\Lambda}^{re}) \f$. + * @note **Energy.** \f$ W_m^{ir} = 0 \f$: the back-strain is a kinematic history variable that + * shifts the reorientation criterion and stores no free energy (no \f$ \mathbf{X}:\Delta\mathbf{v} \f$ + * term); the reorientation strain increment enters \f$ W_m^{r} \f$ and \f$ W_m^{d} \f$ like the + * transformation strain. + * @note `tangent_algorithmic` is clamped to the continuum tangent (see @ref umat_sma_unified_T). * * @see umat_sma_unified_T for the transformation-only base model. * @see assemble_continuum_tangent for the shared 3-mechanism tangent operator. diff --git a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.hpp b/include/simcoon/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.hpp index 312ec0c3b..435436d2d 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.hpp @@ -17,7 +17,7 @@ /** * @file Zener_fast.hpp - * @brief Linear viscoelastic material model using the Zener (Standard Linear Solid) representation + * @brief Standard linear solid, Poynting--Thomson (Kelvin) form (ZENER) * @author Yves Chemisky * @version 1.0 */ @@ -36,37 +36,40 @@ namespace simcoon { */ /** - * @brief Linear viscoelastic constitutive model using the Zener (Standard Linear Solid) model + * @brief Standard linear solid in its Poynting--Thomson (Kelvin) form (``ZENER``): a spring + * \f$ \mathbf{L}_0 \f$ in series with one Kelvin--Voigt unit \f$ (\mathbf{L}_1, \mathbf{H}_1) \f$. * - * @details This function implements a linear viscoelastic material model using the Zener model, - * also known as the Standard Linear Solid (SLS). The Zener model consists of a spring in series - * with a Kelvin-Voigt element (spring and dashpot in parallel), providing both instantaneous - * elastic response and time-dependent relaxation. - * - * **Rheological Representation:** - * - * ``` - * E_0 (instantaneous) - * ─────────┬───────── - * │ - * ┌──────┴──────┐ - * │ E_1 η_1 │ (Kelvin-Voigt element) - * │ ──/\/\──┬──[=]──│ - * │ │ │ - * └─────────┴────────┘ - * ``` - * - * **Constitutive Equations:** + * @details The instantaneous spring and the Kelvin unit carry the same stress: + * \f[ + * \boldsymbol{\sigma} = \mathbf{L}_0 : \left( \boldsymbol{\varepsilon} - \boldsymbol{\alpha}\,(T - T_{init}) + * - \boldsymbol{\varepsilon}^{v} \right) + * = \mathbf{L}_1 : \boldsymbol{\varepsilon}^{v} + \mathbf{H}_1 : \dot{\boldsymbol{\varepsilon}}^{v}, + * \f] + * with \f$ \mathbf{L}_0 = \mathbf{L}_{iso}(E_0, \nu_0) \f$, \f$ \mathbf{L}_1 = \mathbf{L}_{iso}(E_1, \nu_1) \f$ + * and the viscosity tensor \f$ \mathbf{H}_1 = 3\eta_{B,1}\,\mathbf{I}_{vol} + 2\eta_{S,1}\,\mathbf{I}_{dev} \f$ + * (H_iso). ``ZENNK`` (umat_zener_Nfast()) chains \f$ N \f$ such units in series. * - * The Zener model stress-strain relationship is given by: + * **One-dimensional response.** With the dashpot viscosity \f$ \eta_1 \f$: * \f[ - * \boldsymbol{\sigma} + \tau \dot{\boldsymbol{\sigma}} = E_0 \boldsymbol{\varepsilon} + (E_0 + E_1) \tau \dot{\boldsymbol{\varepsilon}} + * \sigma + \tau_r\,\dot{\sigma} = E_\infty\,\varepsilon + E_0\,\tau_r\,\dot{\varepsilon},\qquad + * J(t) = \frac{1}{E_0} + \frac{1}{E_1}\left(1 - e^{-t/\tau_c}\right),\qquad + * E(t) = E_\infty + (E_0 - E_\infty)\,e^{-t/\tau_r}, * \f] - * where: - * - \f$ E_0 \f$ is the instantaneous (glassy) modulus - * - \f$ E_1 \f$ is the modulus of the Kelvin-Voigt element - * - \f$ \tau = \eta_1 / E_1 \f$ is the relaxation time - * - \f$ \eta_1 \f$ is the viscosity of the dashpot + * with the retardation time \f$ \tau_c = \eta_1 / E_1 \f$, the relaxation time + * \f$ \tau_r = \eta_1 / (E_0 + E_1) \f$ and the relaxed modulus \f$ E_\infty = E_0 E_1 / (E_0 + E_1) \f$. + * The same solid in its Zener (Maxwell) form, a spring \f$ E_\infty \f$ in parallel with a Maxwell + * branch \f$ (E_M, \eta_M) \f$, has \f$ E_M = E_0^2 / (E_0 + E_1) \f$ and + * \f$ \eta_M = \eta_1 E_0^2 / (E_0 + E_1)^2 \f$. In 3D the props of the two forms do not map one to + * one (each branch has its own Poisson ratio and bulk and shear viscosities): this law is not the + * one-branch umat_prony_Nfast() (``PRONK``, generalized Maxwell). + * + * **Discrete update.** Backward Euler on \f$ \boldsymbol{\varepsilon}^{v} \f$, solved in closed form + * (kelvin_series_step, linear_viscoelastic.hpp); the tangent is the exact derivative of that update + * (\f$ \mathbf{L}_0 \f$ under tangent_none). + * + * **Work split.** \f$ \Delta W_d = \tfrac{1}{2}\left(\mathbf{A}^{v}_n + \mathbf{A}^{v}_{n+1}\right) : \Delta\boldsymbol{\varepsilon}^{v} \f$ + * with \f$ \mathbf{A}^{v} = \boldsymbol{\sigma} - \mathbf{L}_1 : \boldsymbol{\varepsilon}^{v} \f$, + * \f$ W_r = W - W_d \f$, \f$ W_{ir} = 0 \f$. * * @note **Stress measure.** The stress returned by this model (the `stress` argument, * written \f$ \boldsymbol{\sigma} \f$ in the relations above) is the Cauchy stress under @@ -77,54 +80,25 @@ namespace simcoon { * Green--Naghdi and \f$ \log_R \f$, the logarithmic frame for the XBM/log rate, * \f$ \boldsymbol{F} \f$ for \f$ \log_F \f$). * - * **Relaxation Modulus:** + * **Material properties** (7; MPa, MPa.s): * - * The relaxation modulus for the Zener model is: - * \f[ - * E(t) = E_\infty + (E_0 - E_\infty) e^{-t/\tau} - * \f] - * where: - * - \f$ E_\infty = \frac{E_0 E_1}{E_0 + E_1} \f$ is the long-term (rubbery) modulus - * - \f$ E_0 \f$ is the instantaneous modulus - * - \f$ \tau \f$ is the relaxation time + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | props[0] | \f$ E_0 \f$ | Young modulus of the instantaneous spring | + * | props[1] | \f$ \nu_0 \f$ | Poisson ratio of the instantaneous spring | + * | props[2] | \f$ \alpha \f$ | isotropic thermal expansion coefficient | + * | props[3] | \f$ E_1 \f$ | Young modulus of the Kelvin unit | + * | props[4] | \f$ \nu_1 \f$ | Poisson ratio of the Kelvin unit | + * | props[5] | \f$ \eta_{B,1} \f$ | bulk viscosity of the Kelvin unit | + * | props[6] | \f$ \eta_{S,1} \f$ | shear viscosity of the Kelvin unit | * - * **Creep Compliance:** + * **State variables** (8; Voigt order [11, 22, 33, 12, 13, 23], engineering shear strains): * - * The creep compliance is: - * \f[ - * J(t) = \frac{1}{E_0} + \frac{1}{E_1} \left( 1 - e^{-t/\tau_c} \right) - * \f] - * where \f$ \tau_c = \eta_1 / E_1 \f$ is the retardation time. - * - * **Incremental Form:** - * - * For numerical integration over a time step \f$ \Delta t \f$: - * \f[ - * \boldsymbol{\sigma}_{n+1} = \mathbf{L}_\infty : \boldsymbol{\varepsilon}_{n+1} + \mathbf{q}_{n+1} - * \f] - * where the internal variable \f$ \mathbf{q} \f$ evolves as: - * \f[ - * \mathbf{q}_{n+1} = e^{-\Delta t/\tau} \mathbf{q}_n + \frac{E_1 - E_\infty}{E_\infty} \mathbf{L}_\infty : \left( e^{-\Delta t/\tau} - 1 \right) \Delta \boldsymbol{\varepsilon} - * \f] - * - * **Material Parameters (props):** - * - * | Index | Symbol | Description | Units | - * |-------|--------|-------------|-------| - * | props[0] | \f$ E_\infty \f$ | Equilibrium (long-term) Young's modulus | Stress | - * | props[1] | \f$ \nu \f$ | Poisson's ratio (assumed constant) | - | - * | props[2] | \f$ \alpha \f$ | Thermal expansion coefficient | 1/Temperature | - * | props[3] | \f$ E_1 \f$ | Modulus of Kelvin-Voigt element | Stress | - * | props[4] | \f$ \tau \f$ | Relaxation time | Time | - * - * **State Variables (statev):** - * - * Total state variables required: \f$ n_{statev} = 7 \f$ - * - * | Index | Symbol | Description | Units | - * |-------|--------|-------------|-------| - * | statev[0] | \f$ T_{init} \f$ | Initial temperature | Temperature | - * | statev[1:6] | \f$ \mathbf{q} \f$ | Internal stress-like variable (Voigt \f$6 \times 1\f$) | Stress | + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | statev[0] | \f$ T_{init} \f$ | initial temperature | + * | statev[1] | \f$ v \f$ | accumulated viscous strain \f$ \sum \|\Delta\boldsymbol{\varepsilon}^{v}\| \f$ | + * | statev[2..7] | \f$ \boldsymbol{\varepsilon}^{v} \f$ | viscous strain of the Kelvin unit | * * @param Etot Total strain tensor at beginning of increment (Voigt notation: \f$6 \times 1\f$ vector) * @param DEtot Strain increment tensor (Voigt notation: \f$6 \times 1\f$ vector) @@ -148,15 +122,9 @@ namespace simcoon { * @param start Flag indicating first increment (true) or continuation (false) * @param tnew_dt Suggested new time step size for adaptive time stepping [output] * - * @note The Zener model is the simplest model capturing both creep and relaxation - * @note For multiple relaxation times, use the Prony series model (umat_prony_Nfast) - * @note Time step should be small relative to relaxation time for accuracy - * @note The model assumes small strains and linear viscoelasticity - * * **References:** * - Zener, C. (1948). *Elasticity and Anelasticity of Metals*. University of Chicago Press. - * - Ferry, J. D. (1980). *Viscoelastic Properties of Polymers* (3rd ed.). Wiley. - * - Simo, J. C., & Hughes, T. J. R. (1998). *Computational Inelasticity*. Springer. + * - Tschoegl, N. W. (1989). *The Phenomenological Theory of Linear Viscoelastic Behavior*. Springer. */ void umat_zener_fast(const std::string &umat_name, const arma::vec &Etot, const arma::vec &DEtot, arma::vec &stress, arma::mat &Lt, arma::mat &L, const arma::mat &DR, const int &nprops, const arma::vec &props, const int &nstatev, arma::vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode = tangent_default); diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp index 578815a5a..7a0f3001d 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/damage_mechanism.hpp @@ -115,7 +115,15 @@ class DamageMechanism final : public StrainMechanism { */ explicit DamageMechanism(DamageType type = DamageType::LINEAR); - // StrainMechanism interface + /** + * @brief Reads the damage parameters from @p props at @p offset (advanced past them). + * + * Layout: props[offset] = \f$ Y_0 \f$ (damage threshold), props[offset+1] = \f$ Y_c \f$ + * (critical driving force, \f$ Y_c > Y_0 \f$), then the parameters of the law: none (LINEAR), + * \f$ A \f$ (EXPONENTIAL), \f$ n \f$ (POWER_LAW), \f$ A, n \f$ (WEIBULL). The law itself comes + * from the mechanism header, not from these props; \f$ D_c \f$ is fixed at 0.99. + * @throw std::invalid_argument when \f$ Y_c \le Y_0 \f$ + */ void configure(const arma::vec& props, int& offset) override; void register_variables() override; diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/elasticity_module.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/elasticity_module.hpp index bcf077930..361f05682 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/elasticity_module.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/elasticity_module.hpp @@ -149,7 +149,7 @@ class ElasticityModule { void refresh_tensors() { // Cholesky is the cheap positive-definiteness test (succeeds iff SPD, // ~2-3x faster than a full eigendecomposition) — matters because - // umat_modular reconstructs the module on every integration-point call. + // umat_modular reconstructs the module on every configuration change. arma::mat R; if (!arma::chol(R, L_)) { throw std::invalid_argument( @@ -250,7 +250,17 @@ class ElasticityModule { * + \partial W / \partial \bar{I}_2) \f$ built from the potential's own * derivatives, which is what evaluate() returns at zero strain. It is * validated as positive definite by refresh_tensors(), a genuine - * admissibility check on the parameters. + * admissibility check on the parameters. It is formed in closed form at + * \f$ \mathbf{b} = \mathbf{I} \f$, \f$ J = 1 \f$, \f$ U'(1) = 0 \f$ (the module is + * rebuilt on every UMAT call), not by a full evaluate(). + * + * For an anisotropic potential L0() is still this isotropic stiffness, exactly: + * a fibre family is unstretched at the ground state + * (\f$ \mathrm{tr}\,\mathbf{A} = 3\kappa_d + (1 - 3\kappa_d) = 1 \f$), so its + * derivatives vanish there (tension-only switch). No mechanism consumes it as an + * anisotropic stiffness: plasticity and damage are handed the current tangent, + * and the Prony branches are isotropic because each is built as + * L_iso\f$(E_i, \nu_i)\f$, L0() entering only as the compliance \f$ \mathbf{L}_0^{-1} \f$. * * @param potential the potential (see HyperPotential for its parameters) * @param params the potential's parameters diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/internal_variable_collection.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/internal_variable_collection.hpp index ee511a08c..5e71466b6 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/internal_variable_collection.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/internal_variable_collection.hpp @@ -110,6 +110,34 @@ class InternalVariableCollection { */ const InternalVariable& get(const std::string& name) const; + /** + * @brief Registration index of a variable (its rank in the statev layout) + * + * Stable for the collection's lifetime: a mechanism resolves it once after + * registration and reads the variable through at() in its hot loops, without + * the string lookup of get(). + * @param name The variable name + * @return Index accepted by at() + * @throws std::runtime_error if name not found + */ + size_t index_of(const std::string& name) const; + + /** + * @brief Get a variable by registration index (see index_of()) + * @param index The variable index + * @return Reference to the variable + * @throws std::out_of_range if index >= size() + */ + InternalVariable& at(size_t index) { return variables_.at(index); } + + /** + * @brief Get a variable by registration index (const) + * @param index The variable index + * @return Const reference to the variable + * @throws std::out_of_range if index >= size() + */ + const InternalVariable& at(size_t index) const { return variables_.at(index); } + /** * @brief Check if a variable exists * @param name The variable name diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/legacy_adapters.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/legacy_adapters.hpp index d598fb7ca..eb6c0eab3 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/legacy_adapters.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/legacy_adapters.hpp @@ -21,7 +21,7 @@ along with simcoon. If not, see . * * The legacy UMATs whose physics ModularUMAT replicates exactly (proven by * the equivalence tests in test_modular.py and gated by - * bench/bench_legacy_vs_modular.py) keep their NAME and PROPS ABI: a + * benchmark/bench_legacy_vs_modular.py) keep their NAME and PROPS ABI: a * per-name translator turns the legacy props vector into the MODUL props * stream and the call is forwarded to umat_modular. statev: the modular * engine claims the FIRST required slots of the caller's array (legacy @@ -49,7 +49,8 @@ bool has_legacy_adapter(const std::string& umat_name); /** * @brief UMAT-shaped entry for adapter-served legacy names: translate the * props for @p umat_name, then run umat_modular. All other arguments are - * forwarded untouched (signature identical to umat_modular). + * forwarded untouched (umat_modular's signature, without its kinematic form). @p ncp, the + * NCPMethod code of the run, is the complementarity function of the cutting plane (EPKCP). * * @throws std::invalid_argument (with the legacy name prefixed) if the name has * no registered translator or if the caller's nstatev is smaller than the @@ -79,7 +80,8 @@ void umat_legacy_modular( const int& nshr, const bool& start, double& tnew_dt, - const int& tangent_mode = tangent_default + const int& tangent_mode = tangent_default, + const int& ncp = 0 ); } // namespace simcoon diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/modular_umat.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/modular_umat.hpp index b2d018b67..aea665a73 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/modular_umat.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/modular_umat.hpp @@ -103,6 +103,8 @@ class ModularUMAT { // Return-mapping controls (local internal-equilibrium Newton loop) int maxiter_; ///< Maximum iterations for return mapping double precision_; ///< Convergence tolerance + NCPMethod ncp_ = NCPMethod::fischer_burmeister; ///< complementarity function of the cutting plane (set_ncp) + bool nonlinear_kinematic_ = false; ///< an Armstrong-Frederick back stress is configured (set_ncp refuses min-NCP) /// Composition invariant cached by initialize(): the flow-rule drift /// guard of run() is armed only when at least TWO guarded constraint @@ -255,6 +257,18 @@ class ModularUMAT { precision_ = precision; } + /** + * @brief Complementarity function of the cutting plane (the @c ncp option of the run). + * + * min-NCP is refused with an Armstrong-Frederick back stress: the cutting plane integrates it + * along its own iterates, so the full min-NCP steps give a different, and on coarse increments + * much less accurate, return than the damped Fischer-Burmeister ones (up to 0.46 off the + * closest-point update where Fischer-Burmeister stays at 0.04, on the Chaboche identification + * programmes), and the residual does not grow, so the fallback cannot catch it. + * @throw std::invalid_argument for min-NCP with an Armstrong-Frederick back stress + */ + void set_ncp(NCPMethod ncp); + /** * @brief Get total number of state variables required * @return nstatev @@ -281,6 +295,22 @@ class ModularUMAT { * 6. Compute work quantities * 7. Pack state variables * + * Rejection policy: an unusable committed state asks for a step cut + * (\f$ \Delta t_{new}/\Delta t = 0.5 \f$) instead of being packed. Rejected + * are a non-finite stress, a non-converged closest-point solve, a runaway + * COMMITTED multiplier (multiplier_cap, checked on the state, not on the raw + * FB row, which can be smaller after clipped excursions), and a state that + * drifted from the flow rule (state_drift: a spurious converged root of an + * oscillating FB Newton, invisible to every residual check; armed per + * drift_guard_armed_). Thresholds are per mechanism, infinity opts out. + * Negative multipliers need no check: PlasticityMechanism projects onto + * \f$ p \ge p_{start} \f$ and PowerLawHardening is regularized at onset. The + * closest-point branch satisfies the flow rule by construction (no drift guard). + * On rejection statev is left untouched, sigma is reset to its start value and + * Lt to the elastic tangent of the start state, so the output stays a + * self-consistent triple: the solver's inforce-at-Dn_mini branch commits it + * without re-running the UMAT. + * * @param umat_name UMAT identifier * @param Etot Total strain at end of increment * @param DEtot Strain increment @@ -564,7 +594,13 @@ class ModularUMAT { * @p kinematic_form is the one argument past that interface. Under KinematicForm::total the * strain is \f$ \ln\mathbf{U} \f$ in reference components (DR is the identity) and @p sigma is * its dual stress \f$ \mathbf{T} \f$; only a hyperelastic elasticity block reads it - * (ElasticityModule::evaluate). + * (ElasticityModule::evaluate). @p ncp, the NCPMethod code of the run (state_variables::ncp), + * selects the complementarity function of the cutting plane (ModularUMAT::set_ncp). + * + * Each thread keeps a few configured ModularUMAT instances, keyed on (props bit for bit, + * nstatev, kinematic_form) and reused while that configuration recurs; the state is + * always restored from @p statev, so the result is that of a fresh instance. An instance + * whose run() throws is discarded. */ void umat_modular( const std::string& umat_name, @@ -591,7 +627,8 @@ void umat_modular( const bool& start, double& tnew_dt, const int& tangent_mode = tangent_default, - const KinematicForm& kinematic_form = KinematicForm::rate + const KinematicForm& kinematic_form = KinematicForm::rate, + const int& ncp = 0 ); /** diff --git a/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp b/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp index d85df7b9f..49eb6f8a7 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Modular/plasticity_mechanism.hpp @@ -91,6 +91,8 @@ class PlasticityMechanism final : public StrainMechanism { std::unique_ptr iso_hard_; std::unique_ptr kin_hard_; double sigma_Y_; ///< Initial yield stress + size_t p_index_ = 0; ///< ivc_ index of "p", resolved by register_variables() + size_t EP_index_ = 0; ///< ivc_ index of "EP", resolved by register_variables() // Per-iteration caches populated by compute_constraints. dPhi_dsigma() and // kappa() return const-refs into these single-element buffers to avoid @@ -152,6 +154,11 @@ class PlasticityMechanism final : public StrainMechanism { // ========== Configuration ========== + /** + * @brief Reads, from @p props at @p offset (advanced past them): \f$ \sigma_Y \f$, then the + * yield criterion parameters (unless the criterion is already configured: von Mises and Tresca + * need none and are set in the constructor), the isotropic and the kinematic hardening parameters. + */ void configure(const arma::vec& props, int& offset) override; void register_variables() override; @@ -288,6 +295,15 @@ class PlasticityMechanism final : public StrainMechanism { arma::vec inelastic_strain() const override; + /** + * @brief Applies the FB correction \f$ \Delta s \f$, PROJECTED onto \f$ p \ge p_{start} \f$. + * + * Negative per-iteration corrections are kept, so an overshoot can walk back, but the + * multiplier never drops below its start-of-increment value. Without the projection a tiny + * negative \f$ \Delta p \f$ is a spurious Fischer-Burmeister root whenever \f$ \Phi > 0 \f$ is + * large (\f$ \phi(\Phi, \Delta p) \approx \Delta p \f$ for \f$ |\Delta p| \ll \Phi \f$), reached for + * near-singular hardening slopes such as \f$ R = k p^m \f$, \f$ m < 1 \f$, at first yield. + */ void update( const arma::vec& ds, int offset diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.hpp index 355a721d2..261a4ba09 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.hpp @@ -35,14 +35,32 @@ namespace simcoon{ */ - ///@brief The elastic UMAT requires 2 constants: - ///@brief props[0] : Young modulus - ///@brief props[1] : Poisson ratio - ///@brief props[2] : CTE - - ///@brief No statev is required for thermoelastic constitutive law - - void umat_elasticity_iso_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); +/** + * @brief Thermomechanical isotropic linear elasticity (ELISO, thermomechanical). + * + * The stress is \f$ \boldsymbol{\sigma} = \mathbf{L} : \left( \boldsymbol{\varepsilon} - \boldsymbol{\alpha}\,(T - T_{init}) \right) \f$, + * the tangents \f$ \partial\boldsymbol{\sigma}/\partial\boldsymbol{\varepsilon} = \mathbf{L} \f$ and + * \f$ \partial\boldsymbol{\sigma}/\partial T = -\mathbf{L} : \boldsymbol{\alpha} \f$; the reversible entropy is + * \f$ \eta_r = \rho c_p \ln(T/T_{init}) + \boldsymbol{\alpha} : \boldsymbol{\sigma} \f$ (no dissipation), + * with \f$ \mathbf{L} = \mathbf{L}_{iso}(E, \nu) \f$ and \f$ \boldsymbol{\alpha} = \alpha\,\mathbf{I} \f$. + * + * Voigt order [11, 22, 33, 12, 13, 23], engineering shear strains; MPa, K. + * + * **Material properties** (5): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | props[0] | \f$ \rho \f$ | density | + * | props[1] | \f$ c_p \f$ | specific heat capacity | + * | props[2] | \f$ E \f$ | Young modulus | + * | props[3] | \f$ \nu \f$ | Poisson ratio | + * | props[4] | \f$ \alpha \f$ | coefficient of thermal expansion | + * + * **State variables** (1): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | statev[0] | \f$ T_{init} \f$ | initial (reference) temperature | + */ +void umat_elasticity_iso_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); /** @} */ // end of umat_thermomechanical group diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.hpp index 3700adf1d..fd706490e 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.hpp @@ -35,14 +35,34 @@ namespace simcoon{ */ -///@brief The elastic UMAT requires 2 constants: -///@brief props[0] : Young modulus -///@brief props[1] : Poisson ratio -///@brief props[2] : CTE - -///@brief No statev is required for thermoelastic constitutive law - - void umat_elasticity_ortho_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); +/** + * @brief Thermomechanical orthotropic linear elasticity in the material axes (ELORT, thermomechanical). + * + * The stress is \f$ \boldsymbol{\sigma} = \mathbf{L} : \left( \boldsymbol{\varepsilon} - \boldsymbol{\alpha}\,(T - T_{init}) \right) \f$, + * the tangents \f$ \partial\boldsymbol{\sigma}/\partial\boldsymbol{\varepsilon} = \mathbf{L} \f$ and + * \f$ \partial\boldsymbol{\sigma}/\partial T = -\mathbf{L} : \boldsymbol{\alpha} \f$; the reversible entropy is + * \f$ \eta_r = \rho c_p \ln(T/T_{init}) + \boldsymbol{\alpha} : \boldsymbol{\sigma} \f$ (no dissipation), + * with \f$ \mathbf{L} \f$ = L_ortho(E1, E2, E3, nu12, nu13, nu23, G12, G13, G23, "EnuG") and + * \f$ \boldsymbol{\alpha} = (\alpha_1, \alpha_2, \alpha_3, 0, 0, 0) \f$. + * + * Voigt order [11, 22, 33, 12, 13, 23], engineering shear strains; MPa, K. + * + * **Material properties** (14): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | props[0] | \f$ \rho \f$ | density | + * | props[1] | \f$ c_p \f$ | specific heat capacity | + * | props[2..4] | \f$ E_1, E_2, E_3 \f$ | Young moduli | + * | props[5..7] | \f$ \nu_{12}, \nu_{13}, \nu_{23} \f$ | Poisson ratios | + * | props[8..10] | \f$ G_{12}, G_{13}, G_{23} \f$ | shear moduli | + * | props[11..13] | \f$ \alpha_1, \alpha_2, \alpha_3 \f$ | coefficients of thermal expansion | + * + * **State variables** (1): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | statev[0] | \f$ T_{init} \f$ | initial (reference) temperature | + */ +void umat_elasticity_ortho_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); /** @} */ // end of umat_thermomechanical group diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.hpp index a0d5f0b10..a77339bba 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.hpp @@ -35,14 +35,38 @@ namespace simcoon{ */ - ///@brief The elastic UMAT requires 2 constants: - ///@brief props[0] : Young modulus - ///@brief props[1] : Poisson ratio - ///@brief props[2] : CTE - - ///@brief No statev is required for thermoelastic constitutive law - - void umat_elasticity_trans_iso_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); +/** + * @brief Thermomechanical transversely isotropic linear elasticity (ELIST, thermomechanical). + * + * The stress is \f$ \boldsymbol{\sigma} = \mathbf{L} : \left( \boldsymbol{\varepsilon} - \boldsymbol{\alpha}\,(T - T_{init}) \right) \f$, + * the tangents \f$ \partial\boldsymbol{\sigma}/\partial\boldsymbol{\varepsilon} = \mathbf{L} \f$ and + * \f$ \partial\boldsymbol{\sigma}/\partial T = -\mathbf{L} : \boldsymbol{\alpha} \f$; the reversible entropy is + * \f$ \eta_r = \rho c_p \ln(T/T_{init}) + \boldsymbol{\alpha} : \boldsymbol{\sigma} \f$ (no dissipation), + * with \f$ \mathbf{L} \f$ = L_isotrans(EL, ET, nuTL, nuTT, GLT, axis) and + * \f$ \boldsymbol{\alpha} = \alpha_T\,\mathbf{I} + (\alpha_L - \alpha_T)\,\mathbf{e}_{axis} \otimes \mathbf{e}_{axis} \f$. + * + * Voigt order [11, 22, 33, 12, 13, 23], engineering shear strains; MPa, K. + * + * **Material properties** (10): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | props[0] | \f$ \rho \f$ | density | + * | props[1] | \f$ c_p \f$ | specific heat capacity | + * | props[2] | axis | longitudinal direction: 1, 2 or 3 | + * | props[3] | \f$ E_L \f$ | longitudinal Young modulus | + * | props[4] | \f$ E_T \f$ | transverse Young modulus | + * | props[5] | \f$ \nu_{TL} \f$ | Poisson ratio, loading along the longitudinal axis | + * | props[6] | \f$ \nu_{TT} \f$ | Poisson ratio in the transverse plane | + * | props[7] | \f$ G_{LT} \f$ | longitudinal shear modulus | + * | props[8] | \f$ \alpha_L \f$ | longitudinal coefficient of thermal expansion | + * | props[9] | \f$ \alpha_T \f$ | transverse coefficient of thermal expansion | + * + * **State variables** (1): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | statev[0] | \f$ T_{init} \f$ | initial (reference) temperature | + */ +void umat_elasticity_trans_iso_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); /** @} */ // end of umat_thermomechanical group diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.hpp index 130171187..d4245cc10 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.hpp @@ -43,27 +43,43 @@ namespace simcoon{ */ - ///@brief The elastic-plastic UMAT with isotropic hardening requires 8 constants for a full thermomechanical coupling: - - ///@brief props[0] : density - ///@brief props[1] : specific heat capacity - ///@brief props[2] : Young modulus - ///@brief props[3] : Poisson ratio - ///@brief props[4] : CTE - ///@brief props[5] : J2 equivalent yield stress limit : sigmaY - ///@brief props[6] : hardening parameter k - ///@brief props[7] : exponent m - - ///@brief The elastic-plastic UMAT with isotropic hardening requires 8 statev: - ///@brief statev[0] : T_init : Initial temperature - ///@brief statev[1] : Accumulative plastic parameter: p - ///@brief statev[2] : Plastic strain 11: EP(0,0) - ///@brief statev[3] : Plastic strain 22: EP(1,1) - ///@brief statev[4] : Plastic strain 33: EP(2,2) - ///@brief statev[5] : Plastic strain 12: EP(0,1) (*2) - ///@brief statev[6] : Plastic strain 13: EP(0,2) (*2) - ///@brief statev[7] : Plastic strain 23: EP(1,2) (*2) - +/** + * @brief Thermomechanical elastic-plastic model with isotropic hardening (J2, power-law hardening) + * + * @details Yield function \f$ \Phi = \sigma_{eq} - k\,p^m - \sigma_Y \leq 0 \f$ with associated + * J2 flow. The Gibbs free energy of the lead mechanism (internal variables \f$ p \f$ and + * \f$ \boldsymbol{\varepsilon}^p \f$) reads + * \f[ + * G = -\frac{1}{2}\,\boldsymbol{\sigma} : \mathbf{S} : \boldsymbol{\sigma} + * - \boldsymbol{\sigma} : \boldsymbol{\varepsilon}^p + * - \boldsymbol{\sigma} : \boldsymbol{\alpha}\,(\theta - \theta_0) + * + c_0\left[(\theta - \theta_0) - \theta \ln\frac{\theta}{\theta_0}\right] + * - \eta_0\,\theta + E_0 + F(\theta, p), \qquad c_0 = \rho\, c_p . + * \f] + * Under tangent_none the kernel still assembles the continuum operator: the sensitivities + * \f$ P_{\varepsilon} \f$ and \f$ P_\theta \f$ feed the physical heat source \f$ r \f$ and its + * linearization, not only the Newton operator. Under tangent_algorithmic only dSdE carries the + * consistent correction; dSdT, drdE and drdT keep the continuum form. + * + * **Material Parameters (props, 8):** + * | Index | Symbol | Description | Units | + * |-------|--------|-------------|-------| + * | props[0] | \f$ \rho \f$ | Density | Mass/Volume | + * | props[1] | \f$ c_p \f$ | Specific heat capacity | Energy/(Mass.Temperature) | + * | props[2] | \f$ E \f$ | Young's modulus | Stress (MPa) | + * | props[3] | \f$ \nu \f$ | Poisson's ratio | - | + * | props[4] | \f$ \alpha \f$ | Isotropic thermal expansion coefficient | 1/Temperature | + * | props[5] | \f$ \sigma_Y \f$ | J2 equivalent yield stress | Stress (MPa) | + * | props[6] | \f$ k \f$ | Hardening parameter | Stress (MPa) | + * | props[7] | \f$ m \f$ | Hardening exponent | - | + * + * **State Variables (statev, 8):** + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | statev[0] | \f$ T_{init} \f$ | Initial temperature | + * | statev[1] | \f$ p \f$ | Accumulated plastic strain | + * | statev[2..7] | \f$ \boldsymbol{\varepsilon}^p \f$ | Plastic strain, Voigt [11,22,33,12,13,23], engineering shear (\f$\times 2\f$) | + */ void umat_plasticity_iso_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.hpp index e245652e6..6cfc9b468 100644 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.hpp @@ -56,7 +56,7 @@ namespace simcoon{ * \f$ G^{ir} = f_\theta(\theta)\,B p^{n+1}/(n+1) \f$ depends on the temperature, so besides the * thermoelastic entropy \f$ \eta_r = c_0 \ln(\theta/\theta_0) + \boldsymbol{\alpha}:\boldsymbol{\sigma} \f$ * the kernel carries \f$ \eta_{ir} = -\partial G^{ir}/\partial\theta = -f'_\theta\, B p^{n+1}/(n+1) \geq 0 \f$ - * (\f$ W_t^{ir} \f$ accumulates \f$ (\theta + \tfrac12\Delta\theta)\,\Delta\eta_{ir} \f$). No + * (\f$ W_t^{ir} \f$ accumulates \f$ \theta_{n+1}\,\Delta\eta_{ir} \f$, the \f$ \theta_{n+1} \f$ of \f$ r \f$). No * Taylor-Quinney coefficient is introduced: \f$ r \f$ is the heat of the increment * \f$ r = \Delta\gamma - \theta_{n+1}\,\Delta\eta \f$ (heat_source_increment()), with * \f$ \eta = \eta_r + \eta_{ir} \f$ and the dissipation the kernel adds to \f$ W_m^d \f$, diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.hpp index 302355733..11d872bae 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.hpp @@ -17,7 +17,7 @@ ///@file plastic_kin_iso.hpp ///@brief User subroutine for elastic-plastic materials in 1D-2D-3D case -///@brief Convex cutting plane return mapping (tangent_mode 0-2) or closest-point projection (tangent_mode 3) +///@brief Convex cutting plane return mapping, or closest-point projection (integrator='closest_point') ///@brief Isotropic hardening with a power-law hardenig is considered ///@version 1.0 @@ -30,8 +30,8 @@ namespace simcoon{ * @file plastic_kin_iso.hpp * @brief Thermomechanical plasticity model. * - * @note **Integrator.** Under tangent_mode 0-2 the return mapping is the convex cutting plane - * loop; under tangent_closest_point (3, full 3D states) the kernel runs the modular + * @note **Integrator.** By default the return mapping is the convex cutting plane loop; under + * integrator='closest_point' (kernel code tangent_closest_point, full 3D states) the kernel runs the modular * PlasticityMechanism (von Mises + PowerLawHardening + PragerHardening with \f$ C = 3 k_X / 2 \f$ * for the kernel's \f$ \mathbf{X} = k_X \mathbf{T} \mathbf{a} \f$) on its own props and state through * closest_point_return_mapping() and returns cpp_consistent_tangent() for dSdE; the thermal @@ -43,34 +43,36 @@ namespace simcoon{ */ -///@brief The thermomechanical elastic-plastic UMAT with kinematic + isotropic hardening requires 9 constants: - -///@brief props[0] : density -///@brief props[1] : specific heat capacity -///@brief props[2] : Young modulus -///@brief props[3] : Poisson ratio -///@brief props[4] : CTE -///@brief props[5] : J2 equivalent yield stress limit : sigmaY -///@brief props[6] : hardening parameter k -///@brief props[7] : exponent m -///@brief props[8] : linear kinematical hardening h - -///@brief The elastic-plastic UMAT with kinematic + isotropic hardening requires 14 statev: -///@brief statev[0] : T_init : Initial temperature -///@brief statev[1] : Accumulative plastic parameter: p -///@brief statev[2] : Plastic strain 11: EP(0,0) -///@brief statev[3] : Plastic strain 22: EP(1,1) -///@brief statev[4] : Plastic strain 33: EP(2,2) -///@brief statev[5] : Plastic strain 12: EP(0,1) (*2) -///@brief statev[6] : Plastic strain 13: EP(0,2) (*2) -///@brief statev[7] : Plastic strain 23: EP(1,2) (*2) -///@brief statev[8] : Backstress 11: X(0,0) -///@brief statev[9] : Backstress 11: X(1,1) -///@brief statev[10] : Backstress 11: X(2,2) -///@brief statev[11] : Backstress 11: X(0,1) -///@brief statev[12] : Backstress 11: X(0,2) -///@brief statev[13] : Backstress 11: X(1,2) - +/** + * @brief Thermomechanical elastic-plastic UMAT with isotropic (power-law) and linear kinematic + * (Prager) hardening, von Mises criterion (EPKCP). + * + * The backstress is not stored: it follows from the back-strain \f$ \mathbf{a} \f$ (engineering + * shear components, rotated as a strain) as + * \f$ \mathbf{X} = k_X\,(\mathbf{a} \circ \mathbf{I}_{r05}) \f$, i.e. Prager hardening with + * \f$ C = 3 k_X / 2 \f$. + * + * **Material properties** (9; MPa, K): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | props[0] | \f$ \rho \f$ | density | + * | props[1] | \f$ c_p \f$ | specific heat capacity | + * | props[2] | \f$ E \f$ | Young modulus | + * | props[3] | \f$ \nu \f$ | Poisson ratio | + * | props[4] | \f$ \alpha \f$ | coefficient of thermal expansion | + * | props[5] | \f$ \sigma_Y \f$ | J2 equivalent yield stress | + * | props[6] | \f$ k \f$ | isotropic hardening modulus, \f$ H_p = k\,p^m \f$ | + * | props[7] | \f$ m \f$ | isotropic hardening exponent | + * | props[8] | \f$ k_X \f$ | linear kinematic hardening modulus | + * + * **State variables** (14; Voigt order [11, 22, 33, 12, 13, 23], engineering shear strains): + * | Index | Symbol | Description | + * |-------|--------|-------------| + * | statev[0] | \f$ T_{init} \f$ | initial temperature | + * | statev[1] | \f$ p \f$ | accumulated plastic strain | + * | statev[2..7] | \f$ \boldsymbol{\varepsilon}^p \f$ | plastic strain | + * | statev[8..13] | \f$ \mathbf{a} \f$ | kinematic back-strain (the backstress is \f$ \mathbf{X} = k_X\,(\mathbf{a} \circ \mathbf{I}_{r05}) \f$) | + */ void umat_plasticity_kin_iso_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.hpp index e14fb7fdf..d85f9b82d 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.hpp @@ -65,7 +65,7 @@ namespace simcoon{ * | 0 | \f$ \rho \f$ | Density | kg/m^3 | * | 1 | \f$ c_{pA} \f$ | Specific heat capacity of Austenite | J/(kg.K) | * | 2 | \f$ c_{pM} \f$ | Specific heat capacity of Martensite | J/(kg.K) | - * | 3 | flagT | Temperature extrapolation (0=linear, 1=smooth) | - | + * | 3 | flagT | 0: the given transformation temperatures are those of the measured curve, converted to the model's with n1..n4; 1: used as given | - | * | 4 | \f$ E_A \f$ | Young's modulus of Austenite | MPa | * | 5 | \f$ E_M \f$ | Young's modulus of Martensite | MPa | * | 6 | \f$ \nu_A \f$ | Poisson's ratio of Austenite | - | @@ -74,8 +74,8 @@ namespace simcoon{ * | 9 | \f$ \alpha_M \f$ | CTE of Martensite | 1/K | * | 10 | \f$ H_{min} \f$ | Minimal transformation strain magnitude | - | * | 11 | \f$ H_{max} \f$ | Maximal transformation strain magnitude | - | - * | 12 | \f$ k_1 \f$ | Exponential evolution parameter for \f$ H^{cur} \f$ | - | - * | 13 | \f$ \sigma_{crit} \f$ | Critical stress for \f$ H^{cur} \f$ evolution | MPa | + * | 12 | \f$ k_1 \f$ | Exponential evolution parameter for \f$ H^{cur} \f$ | 1/MPa | + * | 13 | \f$ \sigma_{crit} \f$ | Von Mises stress below which \f$ H^{cur} = H_{min} \f$ | MPa | * | 14 | \f$ C_A \f$ | Clausius-Clapeyron slope (M \f$ \rightarrow \f$ A) | MPa/K | * | 15 | \f$ C_M \f$ | Clausius-Clapeyron slope (A \f$ \rightarrow \f$ M) | MPa/K | * | 16 | \f$ M_{s0} \f$ | Martensite start temperature at zero stress | K | @@ -101,7 +101,7 @@ namespace simcoon{ * | 0 | \f$ \rho \f$ | Density | kg/m^3 | * | 1 | \f$ c_{pA} \f$ | Specific heat capacity of Austenite | J/(kg.K) | * | 2 | \f$ c_{pM} \f$ | Specific heat capacity of Martensite | J/(kg.K) | - * | 3 | flagT | Temperature extrapolation (0=linear, 1=smooth) | - | + * | 3 | flagT | 0: the given transformation temperatures are those of the measured curve, converted to the model's with n1..n4; 1: used as given | - | * | 4 | \f$ E_A \f$ | Young's modulus of Austenite ([100] direction) | MPa | * | 5 | \f$ E_M \f$ | Young's modulus of Martensite ([100] direction) | MPa | * | 6 | \f$ \nu_A \f$ | Poisson's ratio of Austenite | - | @@ -140,8 +140,8 @@ namespace simcoon{ * | 5 | \f$ \gamma^{tr}_{12} \f$ | Transformation strain component 12 (engineering) | - | * | 6 | \f$ \gamma^{tr}_{13} \f$ | Transformation strain component 13 (engineering) | - | * | 7 | \f$ \gamma^{tr}_{23} \f$ | Transformation strain component 23 (engineering) | - | - * | 8 | \f$ \xi_F \f$ | Forward martensitic volume fraction | - | - * | 9 | \f$ \xi_R \f$ | Reverse martensitic volume fraction | - | + * | 8 | \f$ \xi_F \f$ | Cumulated forward transformation, non-decreasing (\f$ \xi = \xi_F - \xi_R \f$) | - | + * | 9 | \f$ \xi_R \f$ | Cumulated reverse transformation, non-decreasing | - | * | 10 | \f$ \rho \Delta s_0 \f$ | Entropy difference between phases (M - A) | MPa/K | * | 11 | \f$ \rho \Delta E_0 \f$ | Internal energy difference between phases (M - A) | MPa | * | 12 | \f$ D \f$ | Stress dependence parameter for transformation limits | - | @@ -181,10 +181,28 @@ namespace simcoon{ * @param tnew_dt Suggested new time step ratio for adaptive time stepping [output] * * @note Elastic convention: isotropic uses "Enu", cubic uses "EnuG" for L_iso / L_cubic - * @note The flagT parameter controls temperature extrapolation: 0 for linear, 1 for smooth + * @note flagT = 0 converts the given Ms, Mf, As, Af (read on the measured curve) into the model's + * temperatures through the smoothing exponents n1..n4; flagT = 1 uses them as given * @note The pre-2.0 aliases SMAUT and SMANI were removed in 2.0: use SMADI and SMAAI. * @note Convergence measure: same magnitude scales \f$ Y^{crit}_j \f$ as the mechanical * @ref umat_sma_unified_T (see its note). + * @note The reverse row of the local Jacobian uses the ETMean chain rule of + * @ref umat_sma_unified_T (see its note). + * @note **Tangent modes.** `tangent_none`, `tangent_algorithmic` and `tangent_closest_point` are + * all clamped to the continuum tangent: the sensitivities \f$ \mathbf{P}_\varepsilon \f$ and + * \f$ \hat{\mathbf{B}}^{-1} \f$ also feed the physical heat source \f$ r \f$ and its + * linearisation, the finite-difference flow Hessian of the algorithmic mode is near-singular + * on the plateau, and this kernel has no closest-point branch. The thermal cross-tangents are + * \f[ + * \mathbf{P}_\theta = \hat{\mathbf{B}}^{-1}\left(\frac{\partial \Phi_l}{\partial \theta} + * - \frac{\partial \Phi_l}{\partial \boldsymbol{\sigma}} : \mathbf{L}\,\boldsymbol{\alpha}\right)_l, \qquad + * \frac{\partial \boldsymbol{\sigma}}{\partial T} = -\mathbf{L}\,\boldsymbol{\alpha} - \sum_j \boldsymbol{\kappa}_j\,P_{\theta,j}, + * \f] + * with \f$ \hat{B}_{lj} \f$ row = criterion \f$ l \f$, column = mechanism \f$ j \f$. The + * dissipation derivatives hold the transformation directions fixed over the increment + * (\f$ \mathrm{d}\Delta\boldsymbol{\varepsilon}^{T} = \boldsymbol{\Lambda}^F P_F + \boldsymbol{\Lambda}^R P_R \f$), + * and the entropy is + * \f$ \eta = c_0(\xi)\ln(T/T_{init}) + \boldsymbol{\alpha}(\xi):\boldsymbol{\sigma} + \rho\Delta s_0\,\xi \f$. * * @see umat_sma_unified_T() for the mechanical-only version (no thermal coupling) * @see L_iso() for isotropic stiffness tensor (SMADI, SMAAI) diff --git a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.hpp b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.hpp index 96e4acffa..e340e213f 100755 --- a/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.hpp @@ -17,7 +17,6 @@ ///@file Zener_fast.hpp ///@brief User subroutine for Zener viscoelastic model in 3D case, with thermoelastic effect(Poynting-Thomson model) -///@brief This implementation uses a single scalar internal variable for the evaluation of the viscoelastic strain increment ///@author Chemisky, Chatzigeorgiou ///@version 1.0 @@ -27,7 +26,58 @@ #include namespace simcoon { - + +/** @addtogroup umat_thermomechanical + * @{ + */ + +/** + * @brief Thermomechanical Zener (Poynting--Thomson) viscoelastic model with one Kelvin unit (``ZENER``, thermomechanical) + * + * @details Single-unit case of umat_zener_Nfast_T(): an instantaneous isotropic spring + * \f$ \mathbf{L}_0 = \mathbf{L}_{iso}(E_0, \nu_0) \f$ in series with one Kelvin--Voigt unit, + * spring \f$ \mathbf{L}_1 = \mathbf{L}_{iso}(E_1, \nu_1) \f$ in parallel with the dashpot + * \f$ \mathbf{H}_1 = 3\,\eta_{B,1}\,\mathbf{I}_{vol} + 2\,\eta_{S,1}\,\mathbf{I}_{dev} \f$: + * \f[ + * \boldsymbol{\sigma} = \mathbf{L}_0 : \left( \boldsymbol{\varepsilon} - \boldsymbol{\alpha}\,(T - T_{init}) - \boldsymbol{\varepsilon}^{v}_1 \right), + * \qquad + * \dot{\boldsymbol{\varepsilon}}^{v}_1 = \mathbf{H}_1^{-1} : \left( \boldsymbol{\sigma} - \mathbf{L}_1 : \boldsymbol{\varepsilon}^{v}_1 \right), + * \f] + * with \f$ \boldsymbol{\alpha} = \alpha\,\mathbf{I} \f$. Backward Euler in closed form + * (kelvin_series_step, linear_viscoelastic.hpp), exact consistent tangents `dSdE` and `dSdT` + * (`tangent_none` returns \f$ \mathbf{L}_0 \f$ and \f$ -\mathbf{L}_0\,\boldsymbol{\alpha} \f$). + * The volumetric heat capacity is \f$ c_0 = \rho\, c_p \f$, the entropy + * \f$ \eta = c_0 \ln(T/T_{init}) + \boldsymbol{\alpha}:\boldsymbol{\sigma} \f$, and the dissipation + * is the trapezoidal work of the viscous stress \f$ \boldsymbol{\sigma} - \mathbf{L}_1 : \boldsymbol{\varepsilon}^{v}_1 \f$ + * on \f$ \Delta\boldsymbol{\varepsilon}^{v}_1 \f$; the heat of the increment \f$ r \f$ and its + * derivatives come from heat_source_increment(). The arguments are those of umat_zener_Nfast_T(). + * + * **Material parameters (props), \f$ n_{props} = 9 \f$:** + * + * | Index | Symbol | Description | Units | + * |-------|--------|-------------|-------| + * | props[0] | \f$ \rho \f$ | Density | Mass/Volume | + * | props[1] | \f$ c_p \f$ | Specific heat capacity | Energy/(Mass x Temperature) | + * | props[2] | \f$ E_0 \f$ | Instantaneous Young's modulus | Stress | + * | props[3] | \f$ \nu_0 \f$ | Instantaneous Poisson's ratio | - | + * | props[4] | \f$ \alpha \f$ | Isotropic thermal expansion coefficient | 1/Temperature | + * | props[5] | \f$ E_1 \f$ | Young's modulus of the Kelvin unit | Stress | + * | props[6] | \f$ \nu_1 \f$ | Poisson's ratio of the Kelvin unit | - | + * | props[7] | \f$ \eta_{B,1} \f$ | Bulk viscosity of the Kelvin unit | Stress x Time | + * | props[8] | \f$ \eta_{S,1} \f$ | Shear viscosity of the Kelvin unit | Stress x Time | + * + * **State variables (statev), \f$ n_{statev} = 8 \f$:** + * + * | Index | Symbol | Description | Units | + * |-------|--------|-------------|-------| + * | statev[0] | \f$ T_{init} \f$ | Initial temperature | Temperature | + * | statev[1] | \f$ v \f$ | Accumulated flow length \f$ \sum \| \Delta\boldsymbol{\varepsilon}^{v}_1 \| \f$ | - | + * | statev[2:7] | \f$ \boldsymbol{\varepsilon}^{v}_1 \f$ | Viscous strain of the Kelvin unit (Voigt [11,22,33,12,13,23], engineering shears) | - | + * + * @see umat_zener_Nfast_T() the N-unit version (props shifted by the N slot, statev 7 + 7N) + */ void umat_zener_fast_T(const arma::vec &, const arma::vec &, arma::vec &, double &, arma::mat &, arma::mat &, arma::mat &, arma::mat &, const arma::mat &, const int &, const arma::vec &, const int &, arma::vec &, const double &, const double &,const double &,const double &, double &, double &, double &, double &, double &, double &, double &, const int &, const int &, const bool &, double &, const int & = 0); - + +/** @} */ // end of umat_thermomechanical group + } //namespace smart diff --git a/include/simcoon/Continuum_mechanics/Umat/fea_transfer.hpp b/include/simcoon/Continuum_mechanics/Umat/fea_transfer.hpp index d846af116..4beb2ddc6 100644 --- a/include/simcoon/Continuum_mechanics/Umat/fea_transfer.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/fea_transfer.hpp @@ -436,6 +436,42 @@ void smart2abaqus_T(double *stress, double *ddsdde, double *ddsddt, // ANSYS USERMAT TRANSFER FUNCTIONS //============================================================================= +/** + * @brief Correspondence between the components of an FE code and simcoon's Voigt order. + * @details FE entry i is simcoon component K[i] (simcoon order [11, 22, 33, 12, 13, 23]); the + * others are absent from the element (zero strain in 3D, stress-free in a condensed state). + */ +struct fea_components { + int ntens; ///< number of components of the element + int K[6]; ///< simcoon index of each FE component + int operator()(const int &i) const { return K[i]; } +}; + +/** + * @brief The Ansys components: 11, 22, 33, 12, 23, 13 in 3D (13 and 23 swapped), 11, 22, 33, 12 in + * plane strain and axisymmetry, 11, 22, 12 in plane stress, 11 in 1D. + * @throw std::invalid_argument for another (nDirect, nShear) + */ +fea_components ansys_components(const int &nDirect, const int &nShear); + +/** @brief FE vector (ntens) -> simcoon 6 entries, zero outside K. */ +template void fe2vec(const double *a, const fea_components &K, V &v) { + v.zeros(); + for (int i = 0; i < K.ntens; i++) v(K(i)) = a[i]; +} + +/** @brief simcoon 6 entries -> FE vector (ntens). */ +template void vec2fe(const V &v, const fea_components &K, double *a) { + for (int i = 0; i < K.ntens; i++) a[i] = v(K(i)); +} + +/** @brief FE ntens x ntens matrix, column-major (Fortran) -> the K x K block of a 6x6 (zero elsewhere). */ +void fe2mat(const double *a, const fea_components &K, arma::mat &M); + +/** @brief The K x K block of a 6x6 -> FE ntens x ntens matrix, column-major (Fortran). */ +void mat2fe(const arma::mat &M, const fea_components &K, double *a); + + /** * @brief Ansys USERMAT -> simcoon (mechanical). * @details Ansys passes the element's ncomp components (11, 22, 33, 12, 23, 13 in 3D: 13 and 23 diff --git a/include/simcoon/Continuum_mechanics/Umat/return_mapping.hpp b/include/simcoon/Continuum_mechanics/Umat/return_mapping.hpp index 82a61bd7b..db832b208 100644 --- a/include/simcoon/Continuum_mechanics/Umat/return_mapping.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/return_mapping.hpp @@ -89,6 +89,7 @@ #include #include #include +#include namespace simcoon { @@ -154,6 +155,7 @@ struct ReturnMappingControl { int maxiter = 0; ///< 0 => simcoon::maxiter_umat double precision = 0.; ///< 0 => simcoon::precision_umat int max_backtrack = 5; ///< halvings of the Newton step when the merit grows by more than x2 + NCPMethod ncp = NCPMethod::fischer_burmeister; ///< complementarity function of the cutting plane (see ncp_step) }; /** diff --git a/include/simcoon/Continuum_mechanics/Umat/umat_smart.hpp b/include/simcoon/Continuum_mechanics/Umat/umat_smart.hpp index f0322145b..6a1429151 100755 --- a/include/simcoon/Continuum_mechanics/Umat/umat_smart.hpp +++ b/include/simcoon/Continuum_mechanics/Umat/umat_smart.hpp @@ -146,6 +146,19 @@ void size_statev(phase_characteristics &rve, unsigned int &size); /** * @brief From mechanical variables stored in a statev vector, parse a rve object that contains all information about the phases. + * + * Layout, per sub-phase and recursively (depth first), \f$ 57 + n_{statev} \f$ slots from @p pos: + * | Slots | Content | + * |-------|---------| + * | 0-5 | Etot | + * | 6-11 | DEtot | + * | 12-17 | sigma | + * | 18 | unused | + * | 19 | T | + * | 20 | DT | + * | 21-56 | Lt (6x6, column by column) | + * | 57 ... | the statev of the phase, then its own sub-phases | + * phases_2_statev writes the same layout; size_statev counts it. * @param rve Reference to the phase characteristics object. * @param pos Reference to the position in the statev vector that contain subphase information * @param statev Reference to the statev vector that hold the information. @@ -454,11 +467,17 @@ void select_umat_T(phase_characteristics &rve, const arma::mat &DR_global,const * Every NATIVE simcoon kernel is @c kirchhoff: in the logarithmic framework the potential * differentiates to \f$ \boldsymbol{\tau} \f$ per REFERENCE volume. It is the default route * stress, and \f$ \boldsymbol{\sigma} = \boldsymbol{\tau}/J \f$ is formed at the output - * boundaries only. @c cauchy is reserved for the plugin adapters (UMEXT, UMABA), whose - * contract belongs to the host code. + * boundaries only. @c cauchy is the contract of a kernel whose convention belongs to a host code + * (none on the finite route today: the external plugins serve the small-strain route only). */ enum class StressMeasure { kirchhoff, cauchy }; +/** + * @brief True for the external plugin names (UMEXT, UMABA, UMANS): laws loaded from a library at + * run time, served by the solver's small-strain route only. + */ +bool is_external_plugin(const std::string &umat_name); + // Declared in Functions/tensor.hpp (kept out of this widely included header). enum class Tensor2Type; @@ -491,6 +510,13 @@ struct StatevTensor { * @c layout_declared says the list is complete (an empty list: no tensorial state); a material * frame kernel that has not declared it is refused under corates 4 and 5. * + * @c material_tangent marks a kernel built on \f$ \mathbf{F} \f$ whose native tangent is the + * material one, \f$ \partial\mathbf{S}/\partial\mathbf{E} \f$ (a pull-back of its spatial tangent): + * called with corate_type_material_tangent it returns that tangent instead of a box, and its + * stress and work do not depend on the corate it is given. A caller that needs + * \f$ \partial\mathbf{S}/\partial\mathbf{E} \f$ asks for it (select_umat_M_finite) instead of + * converting the box back. + * * @see output_convention_of */ struct umat_convention { @@ -498,6 +524,7 @@ struct umat_convention { bool material_frame = false; bool layout_declared = false; std::vector statev_tensors = {}; + bool material_tangent = false; }; /** @@ -538,6 +565,61 @@ bool stress_output_is_kirchhoff(const std::string &umat_name); */ bool total_formulation_serves(const std::string &umat_name); +/** + * @brief True when the named kernel can return its tangent as \f$ \partial\mathbf{S}/\partial\mathbf{E} \f$ + * (umat_convention::material_tangent, corate_type_material_tangent): the kernels built on + * \f$ \mathbf{F} \f$ (hyperelastic potentials, Saint-Venant-Kirchhoff). Never throws. + */ +bool material_tangent_serves(const std::string &umat_name); + +/** + * @brief Checks the complementarity option of a run (solver_params::ncp, the @c ncp of + * @c sim.umat) against the law it drives: the default (0) passes for every law; min-NCP + * (1) only for the mechanical kernels whose cutting plane reads it, EPICP, EPKCP and MODUL + * (MODUL refuses it further for an Armstrong-Frederick back stress, ModularUMAT::set_ncp). + * @param umat_name the law of the run + * @param ncp the NCPMethod code + * @param thermomechanical a thermomechanical run (no thermomechanical kernel reads it) + * @throw std::invalid_argument for a code other than 0 or 1, or a law that does not read it + */ +void check_ncp_serves(const std::string &umat_name, const int &ncp, const bool &thermomechanical, + const int &tangent_mode = tangent_default); + +/** + * @brief Checks the closest-point integrator (kernel code tangent_closest_point) against the law it + * drives, so that it is never ignored silently and the code 3 never reaches a law that does + * not read it (a Python law, PYEXT, receives 0, 1 or 2 only). + * + * Accepted: the mechanical EPICP, EPCHA, MODUL and the names routed to its adapters (code 201), + * the SMA kernels SMADI/SMADC/SMAAI/SMAAC and SMRDI/SMRDC/SMRAI/SMRAC, and EPJCK, whose radial + * cutting plane is the closest-point projection by construction; the thermomechanical EPICP and + * EPKCP. Every other code passes. + * @param umat_name the law of the run + * @param tangent_mode the kernel code (kernel_tangent_mode) + * @param thermomechanical a thermomechanical run or block + * @throw std::invalid_argument for the closest point on a law without one + */ +void check_integrator_serves(const std::string &umat_name, const int &tangent_mode, const bool &thermomechanical); + +/** + * @brief The tangent code the kernels read for a choice of local integrator and tangent operator. + * + * The run chooses three things apart: the local integrator (@p integrator, integrator_* constants), + * the complementarity function inside it (ncp) and the operator handed to the global Newton + * (@p tangent_mode: none, continuum or algorithmic, the consistent operator of the integrator that + * ran). The kernels read one code, tangent_closest_point standing for the closest-point integrator + * with its exact operator: + * - integrator_cutting_plane: @p tangent_mode none, continuum or algorithmic, as given; + * - integrator_closest_point: tangent_closest_point, for the algorithmic tangent only (its + * continuum operator and the explicit form are not provided). + * @p tangent_mode = tangent_closest_point is refused: the closest point is chosen by @p integrator. + * Which laws accept the result is check_integrator_serves; inside one of them, a row without a + * closest-point update (a Tresca or Drucker row of MODUL) keeps the cutting plane (see the UMAT + * catalogue). + * @throw std::invalid_argument for an unknown code or a combination outside the above + */ +int kernel_tangent_mode(const int &integrator, const int &tangent_mode); + /** * @brief True when the named thermomechanical kernel can run under the total formulation. * @@ -584,6 +666,10 @@ arma::vec total_formulation_tau(const arma::vec &sigma, const arma::vec &Detot, * or corate_type_total for the total formulation: the kernel then works on \f$ \ln\mathbf{U} \f$ and its * dual stress \f$ \mathbf{T} \f$ instead of \f$ \ln\mathbf{V} \f$ and \f$ \boldsymbol{\tau} \f$ * @param tnew_dt New increment of time if the max number of iteration has not converged + * @param material_tangent true: Lt is returned as the material tangent \f$ \partial\mathbf{S}/\partial\mathbf{E} \f$ + * instead of the box tangent of @p corate_type. Honoured only for a kernel that + * material_tangent_serves() (it is then called with corate_type_material_tangent; the + * stress, the work and statev are unchanged); the caller checks it. * @details Under the logarithmic corates (2, 3, 5) the mechanical work is corrected to the true * work per reference volume (see Delta_work_conjugacy). Example: * @code @@ -617,7 +703,7 @@ arma::vec total_formulation_tau(const arma::vec &sigma, const arma::vec &Detot, select_umat_M_finite(rve, DR, Time, DTime, ndi, nshr, start, solver_type, corate_type, pnewdt); * @endcode */ -void select_umat_M_finite(phase_characteristics &rve, const arma::mat &DR_global,const double &Time,const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, const int &corate_type, double &tnew_dt); +void select_umat_M_finite(phase_characteristics &rve, const arma::mat &DR_global,const double &Time,const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, const int &corate_type, double &tnew_dt, const bool &material_tangent = false); /** * @brief From the name of the umat, select the appropriate function to determine the mechanical response considering small strain assumption @@ -669,9 +755,23 @@ void select_umat_M_finite(phase_characteristics &rve, const arma::mat &DR_global void select_umat_M(phase_characteristics &rve, const arma::mat &DR_global,const double &Time,const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, double &tnew_dt); #ifndef DOXYGEN_SHOULD_SKIP_THIS +/** + * @brief Solver entry point of a thermomechanical phase: resets @p tnew_dt to 1, clears @p start once + * \f$ t > \f$ simcoon::limit, then dispatches on @p control_type. + * @param control_type 1: small strain (select_umat_T); 2-6: finite strain (select_umat_T_finite, with @p corate_type). + * Any other value throws simcoon::exception_solver. + * @details The other arguments are those of select_umat_T / select_umat_T_finite. + */ void run_umat_T(phase_characteristics &rve, const arma::mat &DR,const double &Time,const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, const unsigned int &control_type, const int &corate_type, double &tnew_dt); -void run_umat_M(phase_characteristics &, const arma::mat &, const double &, const double &, const int &, const int &, bool &, const int &, const unsigned int &, const int &, double &); +/** + * @brief Solver entry point of a mechanical phase: resets @p tnew_dt to 1, clears @p start once + * \f$ t > \f$ simcoon::limit, then dispatches on the control type. + * @details Arguments in order: rve, DR, Time, DTime, ndi, nshr, start, solver_type, control_type + * (1: select_umat_M; 2-6: select_umat_M_finite with corate_type and material_tangent; any other + * value throws simcoon::exception_solver), corate_type, tnew_dt, material_tangent. + */ +void run_umat_M(phase_characteristics &, const arma::mat &, const double &, const double &, const int &, const int &, bool &, const int &, const unsigned int &, const int &, double &, const bool &material_tangent = false); #endif /* DOXYGEN_SHOULD_SKIP_THIS */ /** diff --git a/include/simcoon/Simulation/Geometry/cylinder.hpp b/include/simcoon/Simulation/Geometry/cylinder.hpp index 48d33a2a4..ef7311feb 100755 --- a/include/simcoon/Simulation/Geometry/cylinder.hpp +++ b/include/simcoon/Simulation/Geometry/cylinder.hpp @@ -15,7 +15,7 @@ */ -///@file ellipsoid.hpp +///@file cylinder.hpp ///@brief Characteristics of an ellipsoidal phase, which hereditates from: ///-phase characteristics ///@version 1.0 @@ -58,12 +58,27 @@ class cylinder : public geometry double theta_geom; //geometric orientation of the cylinder theta double phi_geom; //geometric orientation of the cylinder phi - cylinder(); //default constructor - cylinder(const double &Lval, const int &coatingof, const int &coatedby, const double &Rval, const double &psi_geom, const double &theta_geom, const double &phi_geom, const double &dummy); + /** @brief Default constructor: zero concentration, dimensions and angles, no coating. */ + cylinder(); + /** + * @brief Constructor with parameters. + * @param concentration volume fraction of the phase + * @param coatingof index of the phase this cylinder is coating (0 if none) + * @param coatedby index of the phase coating this cylinder (0 if none) + * @param L length of the cylinder + * @param R radius of the cylinder + * @param psi_geom first Euler angle of the geometric orientation (radians) + * @param theta_geom second Euler angle of the geometric orientation (radians) + * @param phi_geom third Euler angle of the geometric orientation (radians) + */ + cylinder(const double &concentration, const int &coatingof, const int &coatedby, const double &L, const double &R, const double &psi_geom, const double &theta_geom, const double &phi_geom); - cylinder(const cylinder&); //Copy constructor + /** @brief Copy constructor. */ + cylinder(const cylinder&); + /** @brief Destructor. */ virtual ~cylinder(); + /** @brief Copy assignment. */ virtual cylinder& operator = (const cylinder&); friend std::ostream& operator << (std::ostream& os, const cylinder &cyl); diff --git a/include/simcoon/Simulation/Maths/eig_sym3.hpp b/include/simcoon/Simulation/Maths/eig_sym3.hpp new file mode 100644 index 000000000..131769528 --- /dev/null +++ b/include/simcoon/Simulation/Maths/eig_sym3.hpp @@ -0,0 +1,109 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file eig_sym3.hpp +///@brief Fixed-size spectral decomposition of symmetric 3x3 matrices and the spectral functions built on it +///@version 1.0 + +#pragma once +#include + +namespace simcoon{ + +/** @addtogroup maths + * @{ + */ + +/** + * @brief Eigen-decomposition of a symmetric 3x3 matrix, \f$ \mathbf{A} = \mathbf{Q}\,\mathrm{diag}(\boldsymbol{\lambda})\,\mathbf{Q}^T \f$. + * + * Cyclic Jacobi rotations (Kopp 2008, dsyevj3) on fixed-size storage: no heap allocation and no + * LAPACK call, about three times cheaper than arma::eig_sym (dsyevd) on a 3x3, and accurate to + * the last digits for every eigenvalue, the small ones of a stiff SPD matrix included. Only the + * upper triangle of @p A is read. The eigenvalues come in ascending order, as with + * arma::eig_sym; the eigenvectors are the orthonormal columns of @p Q (sign arbitrary). A + * diagonal @p A is returned exactly (@p Q a permutation). + * + * This is the solver behind the per-point spectral functions of the finite-strain kinematics + * (polar decompositions, logarithmic strains, Daleckii-Krein derivatives). + * + * @param[out] lam eigenvalues, ascending + * @param[out] Q eigenvectors (columns) + * @param[in] A symmetric matrix (upper triangle read) + * @return false when @p A is not finite or the iteration does not converge (as arma::eig_sym) + */ +bool eig_sym_3x3(arma::vec::fixed<3> &lam, arma::mat::fixed<3,3> &Q, const arma::mat::fixed<3,3> &A); + +/** + * @brief Symmetric matrix of given eigenvalues on a given eigenbasis, \f$ \mathbf{Q}\,\mathrm{diag}(\mathbf{f})\,\mathbf{Q}^T \f$. + * + * The upper triangle is computed and mirrored: the result is exactly symmetric. + * + * @param Q eigenvectors (columns) + * @param f eigenvalues of the result + * @return the 3x3 symmetric matrix + */ +arma::mat::fixed<3,3> spectral_compose_3x3(const arma::mat::fixed<3,3> &Q, const arma::vec::fixed<3> &f); + +/** + * @brief Spectral congruence of a 3x3 matrix on an eigenbasis, + * \f$ \sum_{a,b} K_{ab}\,\mathbf{P}_a \mathbf{X} \mathbf{P}_b + * = \mathbf{N}\left(\mathbf{K} \circ \mathbf{N}^T \mathbf{X} \mathbf{N}\right)\mathbf{N}^T \f$. + * + * \f$ \mathbf{P}_a = \mathbf{n}_a \otimes \mathbf{n}_a \f$ are the eigenprojectors of the + * orthonormal columns of @p N and \f$ \circ \f$ the entrywise product: the form of the + * Daleckii-Krein derivatives of isotropic matrix functions, with \f$ K_{ab} \f$ their divided + * differences. + * + * @param N eigenvectors (orthonormal columns), e.g. from eig_sym_3x3 + * @param K coefficients \f$ K_{ab} \f$ + * @param X the 3x3 matrix + * @return \f$ \mathbf{N}(\mathbf{K} \circ \mathbf{N}^T \mathbf{X} \mathbf{N})\mathbf{N}^T \f$ + */ +inline arma::mat::fixed<3,3> spectral_congruence(const arma::mat::fixed<3,3> &N, const arma::mat::fixed<3,3> &K, + const arma::mat::fixed<3,3> &X) { + const arma::mat::fixed<3,3> Xp = N.t()*X*N; + return N*(K % Xp)*N.t(); +} + +/** + * @brief Square root of a symmetric positive semi-definite 3x3 matrix (as arma::sqrtmat_sympd). + * @param[out] out the square root + * @param[in] A symmetric matrix + * @return false when the decomposition fails or an eigenvalue is negative + */ +bool sqrtmat_sympd_3x3(arma::mat::fixed<3,3> &out, const arma::mat::fixed<3,3> &A); + +/** + * @brief Logarithm of a symmetric positive definite 3x3 matrix (as arma::logmat_sympd). + * @param[out] out the logarithm + * @param[in] A symmetric matrix + * @return false when the decomposition fails or an eigenvalue is not positive + */ +bool logmat_sympd_3x3(arma::mat::fixed<3,3> &out, const arma::mat::fixed<3,3> &A); + +/** + * @brief Exponential of a symmetric 3x3 matrix (as arma::expmat_sym). + * @param[out] out the exponential + * @param[in] A symmetric matrix + * @return false when the decomposition fails + */ +bool expmat_sym_3x3(arma::mat::fixed<3,3> &out, const arma::mat::fixed<3,3> &A); + +/** @} */ // end of maths group + +} //namespace simcoon diff --git a/include/simcoon/Simulation/Maths/lagrange.hpp b/include/simcoon/Simulation/Maths/lagrange.hpp index 5943af738..c9a9ee1b2 100755 --- a/include/simcoon/Simulation/Maths/lagrange.hpp +++ b/include/simcoon/Simulation/Maths/lagrange.hpp @@ -33,26 +33,84 @@ namespace simcoon{ */ -//This function is used to determine an exponential Lagrange Multiplier (like contact in Abaqus) -double lagrange_exp(const double &, const double &, const double &); +/** + * @brief Exponential Lagrange multiplier (penalty), as the exponential contact pressure in Abaqus. + * @details \f[ \lambda(h) = \frac{p_0}{e - 1}\left(\frac{h}{c} + 1\right)\left(e^{h/c + 1} - 1\right) \f] + * for \f$ h > -c \f$, and \f$ \lambda = 0 \f$ for \f$ h \leq -c \f$. + * @param h constraint function (gap) + * @param c width of the transition zone + * @param p0 multiplier value at \f$ h = 0 \f$ + * @return \f$ \lambda(h) \f$ + */ +double lagrange_exp(const double &h, const double &c, const double &p0); -//This function is used to determine the first derivative of an exponential Lagrange Multiplier -double dlagrange_exp(const double &, const double &, const double &); +/** + * @brief First derivative \f$ \mathrm{d}\lambda/\mathrm{d}h \f$ of lagrange_exp. + * @param h constraint function (gap) + * @param c width of the transition zone + * @param p0 multiplier value at \f$ h = 0 \f$ + * @return \f$ \mathrm{d}\lambda/\mathrm{d}h \f$ (0 for \f$ h \leq -c \f$) + */ +double dlagrange_exp(const double &h, const double &c, const double &p0); -//This function is used to determine a power-law Lagrange Multiplier for problem such x >= 0 -double lagrange_pow_0(const double &, const double &, const double &, const double &, const double &); +/** + * @brief Power-law Lagrange multiplier enforcing \f$ x \geq 0 \f$. + * @details \f$ \lambda(x) = p_0 (1 - x)\, x^{-n} \f$ for \f$ x \geq c \f$; below \f$ c \f$ the law is + * continued by its tangent at \f$ c \f$ plus \f$ \alpha (x - c)^2 \f$, so it stays finite at \f$ x = 0 \f$. + * @param x constrained variable + * @param c regularisation threshold + * @param p0 multiplier scale + * @param n power-law exponent + * @param alpha coefficient of the quadratic continuation below \f$ c \f$ + * @return \f$ \lambda(x) \f$ + */ +double lagrange_pow_0(const double &x, const double &c, const double &p0, const double &n, const double &alpha); -//This function is used to determine the first derivative of a power-law Lagrange Multiplier for problem such x >= 0 -double dlagrange_pow_0(const double &, const double &, const double &, const double &, const double &); +/** + * @brief First derivative \f$ \mathrm{d}\lambda/\mathrm{d}x \f$ of lagrange_pow_0. + * @param x constrained variable + * @param c regularisation threshold + * @param p0 multiplier scale + * @param n power-law exponent + * @param alpha coefficient of the quadratic continuation below \f$ c \f$ + * @return \f$ \mathrm{d}\lambda/\mathrm{d}x \f$ + */ +double dlagrange_pow_0(const double &x, const double &c, const double &p0, const double &n, const double &alpha); -//This function is used to determine a power-law Lagrange Multiplier for problem such x <= 1 -double lagrange_pow_1(const double &, const double &, const double &, const double &, const double &); +/** + * @brief Power-law Lagrange multiplier enforcing \f$ x \leq 1 \f$. + * @details \f$ \lambda(x) = p_0\, x\, (1 - x)^{-n} \f$ for \f$ x \leq 1 - c \f$; above \f$ 1 - c \f$ the law is + * continued by its tangent at \f$ 1 - c \f$ plus \f$ \alpha (x - 1 + c)^2 \f$, so it stays finite at \f$ x = 1 \f$. + * @param x constrained variable + * @param c regularisation threshold (distance to 1) + * @param p0 multiplier scale + * @param n power-law exponent + * @param alpha coefficient of the quadratic continuation above \f$ 1 - c \f$ + * @return \f$ \lambda(x) \f$ + */ +double lagrange_pow_1(const double &x, const double &c, const double &p0, const double &n, const double &alpha); -//This function is used to determine the first derivative of a power-law Lagrange Multiplier for problem such x <= 1 -double dlagrange_pow_1(const double &, const double &, const double &, const double &, const double &); +/** + * @brief First derivative \f$ \mathrm{d}\lambda/\mathrm{d}x \f$ of lagrange_pow_1. + * @param x constrained variable + * @param c regularisation threshold (distance to 1) + * @param p0 multiplier scale + * @param n power-law exponent + * @param alpha coefficient of the quadratic continuation above \f$ 1 - c \f$ + * @return \f$ \mathrm{d}\lambda/\mathrm{d}x \f$ + */ +double dlagrange_pow_1(const double &x, const double &c, const double &p0, const double &n, const double &alpha); -//This function is used to determine the SECOND derivative of a power-law Lagrange Multiplier for problem such x <= 1 -double d2lagrange_pow_1(const double &, const double &, const double &, const double &, const double &); +/** + * @brief Second derivative \f$ \mathrm{d}^2\lambda/\mathrm{d}x^2 \f$ of lagrange_pow_1 (\f$ 2\alpha \f$ above \f$ 1 - c \f$). + * @param x constrained variable + * @param c regularisation threshold (distance to 1) + * @param p0 multiplier scale + * @param n power-law exponent + * @param alpha coefficient of the quadratic continuation above \f$ 1 - c \f$ + * @return \f$ \mathrm{d}^2\lambda/\mathrm{d}x^2 \f$ + */ +double d2lagrange_pow_1(const double &x, const double &c, const double &p0, const double &n, const double &alpha); /** @} */ // end of maths group diff --git a/include/simcoon/Simulation/Maths/num_solve.hpp b/include/simcoon/Simulation/Maths/num_solve.hpp index 467ed5adb..2a8b71f78 100755 --- a/include/simcoon/Simulation/Maths/num_solve.hpp +++ b/include/simcoon/Simulation/Maths/num_solve.hpp @@ -10,10 +10,11 @@ */ ///@file num_solve.hpp -///@brief random number generators +///@brief Newton and Fischer-Burmeister solvers of the local return-mapping systems ///@author Chemisky #pragma once +#include #include namespace simcoon{ @@ -28,12 +29,89 @@ namespace simcoon{ */ -void Newton_Raphon(const arma::vec &, const arma::vec &, const arma::mat &, arma::vec &, arma::vec &, double &); +/** + * @brief One plain Newton step on the constraints: \f$ \delta p = -B^{-1}\,\Phi \f$, no complementarity. + * + * The solvers of this file target the Kuhn-Tucker conditions \f$ \Phi_i \le 0,\ \Delta p_i \ge 0,\ + * \Phi_i\,\Delta p_i = 0 \f$, with \f$ \Delta p = \dot{p}\,\Delta t \f$. A singular \f$ B \f$ gives a zero step. + * @param[in] Phi constraints \f$ \Phi_i \f$ (N) + * @param[in] Y_crit normalisations of the error (N, non-zero) + * @param[in] denom local Jacobian \f$ B = \partial \Phi / \partial \Delta p \f$ (N x N) + * @param[in,out] Dp multipliers, updated: \f$ \Delta p \leftarrow \Delta p + \delta p \f$ + * @param[out] dp the step \f$ \delta p \f$ + * @param[out] error \f$ \sum_i |\Phi_i| / |Y^{crit}_i| \f$ of the entering iterate + * @throw simcoon::exception_det when the determinant or solve fails + */ +void Newton_Raphon(const arma::vec &Phi, const arma::vec &Y_crit, const arma::mat &denom, arma::vec &Dp, arma::vec &dp, double &error); -void Fischer_Burmeister(const arma::vec &, const arma::vec &, const arma::mat &, arma::vec &, arma::vec &, double &); +/** + * @brief One Newton step of the non-normalised Fischer-Burmeister reformulation of the Kuhn-Tucker conditions. + * + * As Fischer_Burmeister_m with \f$ \Delta p_i \f$ in place of the normalised \f$ \Delta p_i^* \f$: + * \f$ \phi_i = \sqrt{\Phi_i^2 + \Delta p_i^2} + \Phi_i - \Delta p_i \f$. A singular Jacobian gives a zero step. + * @param[in] Phi constraints \f$ \Phi_i \f$ (N) + * @param[in] Y_crit normalisations of the error (N, non-zero) + * @param[in] denom local Jacobian \f$ B = \partial \Phi / \partial \Delta p \f$ (N x N) + * @param[in,out] Dp multipliers, updated: \f$ \Delta p \leftarrow \Delta p + \delta p \f$ + * @param[out] dp the step \f$ \delta p \f$ + * @param[out] error \f$ \sum_i |\phi_i| / |Y^{crit}_i| \f$ of the entering iterate + * @throw simcoon::exception_det when the determinant or solve fails + */ +void Fischer_Burmeister(const arma::vec &Phi, const arma::vec &Y_crit, const arma::mat &denom, arma::vec &Dp, arma::vec &dp, double &error); -void Fischer_Burmeister_limits(const arma::vec &, const arma::vec &, const arma::vec &, const arma::mat &, const arma::mat &, arma::vec &, arma::vec &, double &); +/** + * @brief Fischer_Burmeister with an upper limit on each multiplier, \f$ \Phi^l_i \le 0 \f$ (e.g. \f$ \Phi^l = p - 1 \f$). + * + * On a row where \f$ \Phi^l_i \neq 0 \f$ the equation is \f$ \phi_{1,i}^2 + \phi_{2,i}^2 = 0 \f$, with + * \f$ \phi_1 \f$ the Fischer-Burmeister function of \f$ (\Phi^p_i, \Delta p_i) \f$ and + * \f$ \phi_2 = |\Phi^l_i| + \Phi^l_i \f$; other rows are those of Fischer_Burmeister. + * @param[in] Phi_p constraints of the mechanisms (N) + * @param[in] Phi_l limit constraints (N) + * @param[in] Y_crit normalisations of the error (N, non-zero) + * @param[in] denom Jacobian \f$ \partial \Phi^p / \partial \Delta p \f$ (N x N) + * @param[in] denom_l Jacobian \f$ \partial \Phi^l / \partial \Delta p \f$ (N x N) + * @param[in,out] Dp multipliers, updated: \f$ \Delta p \leftarrow \Delta p + \delta p \f$ + * @param[out] dp the step \f$ \delta p \f$ + * @param[out] error \f$ \sum_i |\phi_i| / |Y^{crit}_i| \f$ of the entering iterate + * @throw simcoon::exception_det when the determinant or solve fails + */ +void Fischer_Burmeister_limits(const arma::vec &Phi_p, const arma::vec &Phi_l, const arma::vec &Y_crit, const arma::mat &denom, const arma::mat &denom_l, arma::vec &Dp, arma::vec &dp, double &error); +/** + * @brief One Newton step of the normalised Fischer-Burmeister reformulation of the local + * Kuhn-Tucker conditions (the default complementarity function of the cutting planes). + * + * The return mapping seeks multipliers \f$ \Delta p_i \f$ with + * \f$ \Phi_i \le 0,\ \Delta p_i \ge 0,\ \Phi_i\,\Delta p_i = 0 \f$ for each surface \f$ i \f$. + * With the normalised multiplier \f$ \Delta p_i^* = \Delta p_i\,|B_{ii}| \f$ (a stress, as + * \f$ \Phi_i \f$) these three conditions are the single equation + * \f[ + * \phi_i = \sqrt{\Phi_i^2 + \Delta p_i^{*2}} + \Phi_i - \Delta p_i^* = 0, + * \f] + * the Fischer-Burmeister function \f$ \sqrt{a^2+b^2} - a - b \f$ with \f$ a = -\Phi_i \f$, + * \f$ b = \Delta p_i^* \f$. It is smooth away from \f$ (0, 0) \f$ and decides by itself whether + * a surface is active, so the same equation serves one surface or a coupled system of N. The + * Newton step solves \f$ J\,\delta p = -\phi \f$ with, \f$ r_i = \sqrt{\Phi_i^2 + \Delta p_i^{*2}} \f$, + * \f[ + * J_{ij} = \left(\frac{\Phi_i}{r_i} + 1\right) B_{ij} + * + \delta_{ij}\,|B_{ii}| \left(\frac{\Delta p_i^*}{r_i} - 1\right), + * \qquad B_{ij} = \frac{\partial \Phi_i}{\partial \Delta p_j}. + * \f] + * From \f$ \Delta p = 0 \f$ on a violated surface (\f$ \Phi > 0 \f$, \f$ B < 0 \f$) one row + * gives \f$ \phi = 2\Phi \f$, \f$ J = 3B \f$: the step is two thirds of the full cutting-plane + * step \f$ -\Phi/B \f$, which ncp_step with NCPMethod::min_fb_fallback takes instead. A row with + * \f$ \Phi_i = \Delta p_i^* = 0 \f$ is frozen (\f$ J_{ii} = 10^{12} \f$); a singular \f$ J \f$ + * gives a zero step. One and two surfaces are solved in closed form, more by LAPACK. + * + * @param[in] Phi constraints \f$ \Phi_i \f$ (N) + * @param[in] Y_crit normalisations of the error (N, non-zero), typically the yield stresses + * @param[in] denom local Jacobian \f$ B = \partial \Phi / \partial \Delta p \f$ (N x N) + * @param[in,out] Dp multipliers, updated: \f$ \Delta p \leftarrow \Delta p + \delta p \f$ + * @param[out] dp the step \f$ \delta p \f$ + * @param[out] error \f$ \sum_i |\phi_i| / |Y^{crit}_i| \f$ of the entering iterate + * (Fischer_Burmeister_residual) + * @throw simcoon::exception_det when LAPACK fails on a system of more than two surfaces + */ void Fischer_Burmeister_m(const arma::vec &, const arma::vec &, const arma::mat &, arma::vec &, arma::vec &, double &); /** @@ -55,9 +133,83 @@ void Fischer_Burmeister_m(const arma::vec &, const arma::vec &, const arma::mat */ double Fischer_Burmeister_residual(const arma::vec &Phi, const arma::vec &Y_crit, const arma::mat &denom, const arma::vec &Dp); -void Fischer_Burmeister_m_limits(const arma::vec &, const arma::vec &, const arma::vec &, const arma::mat &, const arma::mat &, arma::vec &, arma::vec &, double &); +/** + * @brief Fischer_Burmeister_limits with the normalised multipliers \f$ \Delta p_i^* = \Delta p_i\,|B_{ii}| \f$ of Fischer_Burmeister_m. + * @param[in] Phi_p constraints of the mechanisms (N) + * @param[in] Phi_l limit constraints (N) + * @param[in] Y_crit normalisations of the error (N, non-zero) + * @param[in] denom Jacobian \f$ B = \partial \Phi^p / \partial \Delta p \f$ (N x N) + * @param[in] denom_l Jacobian \f$ \partial \Phi^l / \partial \Delta p \f$ (N x N) + * @param[in,out] Dp multipliers, updated: \f$ \Delta p \leftarrow \Delta p + \delta p \f$ + * @param[out] dp the step \f$ \delta p \f$ + * @param[out] error \f$ \sum_i |\phi_i| / |Y^{crit}_i| \f$ of the entering iterate + * @throw simcoon::exception_det when the determinant or solve fails + */ +void Fischer_Burmeister_m_limits(const arma::vec &Phi_p, const arma::vec &Phi_l, const arma::vec &Y_crit, const arma::mat &denom, const arma::mat &denom_l, arma::vec &Dp, arma::vec &dp, double &error); + +/** + * @brief Complementarity function of a local return-mapping Newton (the loops on Fischer_Burmeister_m). + * + * A numerical setting of the run, not a material parameter: solver_params::ncp and + * state_variables::ncp carry its code, set by the @c ncp option of @c solve and @c sim.umat + * (see the UMAT catalogue). Fischer-Burmeister, the historical smoothing, is the default. + */ +enum class NCPMethod : int { + fischer_burmeister = 0, ///< normalised Fischer-Burmeister function (Fischer_Burmeister_m) + min_fb_fallback = 1 ///< min-NCP semi-smooth Newton on a single surface, Fischer-Burmeister once the error grows +}; + +/** + * @brief The NCP method of a code (solver_params::ncp, state_variables::ncp), checked: 0 or 1. + * @param code the code + * @param where the caller, for the error message + * @throw std::invalid_argument for any other value + */ +NCPMethod ncp_method_of(const int &code, const std::string &where); + +/** + * @brief One Newton step per call of a local complementarity solve, with the chosen function. + * + * NCPMethod::fischer_burmeister forwards to Fischer_Burmeister_m. NCPMethod::min_fb_fallback takes + * the semi-smooth Newton step of \f$ \min(\Delta p^*_i, -\Phi_i) = 0 \f$ (\f$ \Delta p^*_i = \Delta p_i |B_{ii}| \f$): + * on an active row (\f$ \Phi_i > 0 \f$ or \f$ -\Phi_i < \Delta p^*_i \f$) the linearised constraint + * \f$ \Phi_i + B_{i\cdot}\,\delta p = 0 \f$, the classic cutting plane (Ortiz and Simo 1986), on an + * inactive one the multiplier back to zero (primal-dual active set, Hintermueller, Ito and Kunisch + * 2002). The first step from \f$ \Delta p = 0 \f$ is the full Newton step, where the Fischer-Burmeister + * function only takes two thirds of it: a linear hardening returns in one step instead of six. + * Its @c error is \f$ \sum_i 2|r_i|/|Y^{crit}_i| \f$ of the entering iterate, the scale of the + * Fischer-Burmeister measure near the solution. It applies row by row when the rows are + * uncoupled (@p denom diagonal): a single surface, or a surface next to the always-satisfied + * rows of the viscoelastic and damage mechanisms of MODUL (\f$ \Phi = -Y^{crit} \f$, no + * multiplier, no cross term), which stay at zero. A coupled system (off-diagonal terms, e.g. + * several yield surfaces) keeps the Fischer-Burmeister step, the active set of coupled surfaces + * making the plain semi-smooth Newton cycle. When the error grows from one call to the next, or + * a row is singular, the solve falls back to Fischer-Burmeister for the remaining calls (a + * nonlinear kinematic hardening can make the full step overshoot). + * + * One object per local solve (its state is the fallback). + */ +class ncp_step { +public: + explicit ncp_step(const NCPMethod &method = NCPMethod::fischer_burmeister) : method_(method) {} + /// Same arguments and effects as Fischer_Burmeister_m: Dp += dp, error of the entering iterate + void operator()(const arma::vec &Phi, const arma::vec &Y_crit, const arma::mat &denom, arma::vec &Dp, arma::vec &dp, double &error); + /// Whether this solve fell back to Fischer-Burmeister + bool fell_back() const { return fallback_; } +private: + NCPMethod method_; + bool fallback_ = false; + double prev_error_ = -1.; +}; -arma::mat denom_FB_m(const arma::vec &, const arma::mat &, const arma::vec &); +/** + * @brief Jacobian \f$ J \f$ of the normalised Fischer-Burmeister system of Fischer_Burmeister_m, without the step. + * @param[in] Phi constraints (N) + * @param[in] denom local Jacobian \f$ B = \partial \Phi / \partial \Delta p \f$ (N x N) + * @param[in] Dp multipliers (N) + * @return \f$ J \f$ (N x N); a row with \f$ \Phi_i = \Delta p_i^* = 0 \f$ is frozen (\f$ J_{ii} = 10^{12} \f$) + */ +arma::mat denom_FB_m(const arma::vec &Phi, const arma::mat &denom, const arma::vec &Dp); /** @} */ // end of maths group diff --git a/include/simcoon/Simulation/Maths/small_lu.hpp b/include/simcoon/Simulation/Maths/small_lu.hpp new file mode 100644 index 000000000..312a21584 --- /dev/null +++ b/include/simcoon/Simulation/Maths/small_lu.hpp @@ -0,0 +1,120 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file small_lu.hpp +///@brief LU factorization with partial pivoting of small dense systems, on the stack +///@version 1.0 + +#pragma once +#include +#include +#include +#include +#include + +namespace simcoon{ + +/** @addtogroup maths + * @{ + */ + +/** + * @brief LU factorization with partial pivoting of a small dense matrix (at most @p Nmax rows), on + * the stack. + * + * For the per-point linear systems of the integrators (the 6x6 / 7x7 global Newton Jacobian, the + * 6x6 condensation of the closest-point return mapping): a LAPACK inverse (getrf + getri) and the + * products with it cost 0.5 to 0.7 us on a 6x6, far more than the arithmetic, and allocate. + * factor() once, then solve() for each right-hand side. A pivot below + * \f$ n\,\epsilon \max|A_{ij}| \f$, or a non-finite entry, reports a singular matrix. + */ +template +class small_lu { +public: + /// Factorizes the square matrix @p A (n <= Nmax); false when it is (numerically) singular. + bool factor(const arma::mat &A) { + n_ = static_cast(A.n_rows); + if (n_ > Nmax || static_cast(A.n_cols) != n_) return false; + double amax = 0.; + for (int i = 0; i < n_; ++i) { + for (int j = 0; j < n_; ++j) { + LU_[i][j] = A(i, j); + if (!std::isfinite(LU_[i][j])) return false; + amax = std::max(amax, std::fabs(LU_[i][j])); + } + } + const double tiny = amax*n_*std::numeric_limits::epsilon(); + for (int k = 0; k < n_; ++k) { + int p = k; + for (int i = k + 1; i < n_; ++i) + if (std::fabs(LU_[i][k]) > std::fabs(LU_[p][k])) p = i; + piv_[k] = p; + if (!(std::fabs(LU_[p][k]) > tiny)) return false; + if (p != k) + for (int j = 0; j < n_; ++j) std::swap(LU_[k][j], LU_[p][j]); + const double inv_piv = 1./LU_[k][k]; + for (int i = k + 1; i < n_; ++i) { + const double f = LU_[i][k]*inv_piv; + LU_[i][k] = f; + if (f == 0.) continue; + for (int j = k + 1; j < n_; ++j) LU_[i][j] -= f*LU_[k][j]; + } + } + return true; + } + + /// Solves A x = b in place (b: n values), after a successful factor(). + void solve_in_place(double *b) const { + for (int k = 0; k < n_; ++k) + if (piv_[k] != k) std::swap(b[k], b[piv_[k]]); + for (int i = 1; i < n_; ++i) { + double v = b[i]; + for (int j = 0; j < i; ++j) v -= LU_[i][j]*b[j]; + b[i] = v; + } + for (int i = n_ - 1; i >= 0; --i) { + double v = b[i]; + for (int j = i + 1; j < n_; ++j) v -= LU_[i][j]*b[j]; + b[i] = v/LU_[i][i]; + } + } + + /// x = A^-1 b + arma::vec solve(const arma::vec &b) const { + arma::vec x(b); + solve_in_place(x.memptr()); + return x; + } + + /// X = A^-1 B, column by column + arma::mat solve(const arma::mat &B) const { + arma::mat X(B); + for (arma::uword j = 0; j < X.n_cols; ++j) solve_in_place(X.colptr(j)); + return X; + } + + int n_rows() const { return n_; } + +private: + double LU_[Nmax][Nmax]; + int piv_[Nmax]; + int n_ = 0; +}; + +/** @} */ // end of maths group + +} //namespace simcoon diff --git a/include/simcoon/Simulation/Maths/solve.hpp b/include/simcoon/Simulation/Maths/solve.hpp index 3f24e3744..1bb5ca301 100755 --- a/include/simcoon/Simulation/Maths/solve.hpp +++ b/include/simcoon/Simulation/Maths/solve.hpp @@ -16,7 +16,7 @@ */ ///@file solve.hpp -///@brief random number generators +///@brief Roots of quadratic equations (real and complex) ///@version 1.0 #pragma once @@ -34,11 +34,32 @@ namespace simcoon{ */ -arma::vec quadratic(const double &, const double &, const double &); +/** + * @brief Real roots of \f$ a t^2 + b t + c = 0 \f$. + * @param a quadratic coefficient (if 0, the linear root \f$ -c/b \f$ is returned) + * @param b linear coefficient + * @param c constant coefficient + * @return the two roots \f$ (-b \pm \sqrt{\Delta})/(2a) \f$, \f$ \Delta = b^2 - 4ac \f$ (asserted \f$ > 0 \f$), or one root when \f$ a = 0 \f$ + */ +arma::vec quadratic(const double &a, const double &b, const double &c); -arma::cx_vec cx_quadratic(const double &, const double &, const double &); +/** + * @brief Complex roots of \f$ a t^2 + b t + c = 0 \f$ with real coefficients. + * @param a quadratic coefficient (if 0, the linear root \f$ -c/b \f$ is returned) + * @param b linear coefficient + * @param c constant coefficient + * @return the two roots \f$ (-b \pm \sqrt{\Delta})/(2a) \f$, or a single root when \f$ a = 0 \f$ or \f$ \Delta = 0 \f$ + */ +arma::cx_vec cx_quadratic(const double &a, const double &b, const double &c); -arma::cx_vec cx_quadratic(const arma::cx_double &, const arma::cx_double &, const arma::cx_double &); +/** + * @brief Complex roots of \f$ a t^2 + b t + c = 0 \f$ with complex coefficients. + * @param a quadratic coefficient (if 0, the linear root \f$ -c/b \f$ is returned) + * @param b linear coefficient + * @param c constant coefficient + * @return the two roots \f$ (-b \pm \sqrt{\Delta})/(2a) \f$, or a single root when \f$ a = 0 \f$ or \f$ \Delta = 0 \f$ + */ +arma::cx_vec cx_quadratic(const arma::cx_double &a, const arma::cx_double &b, const arma::cx_double &c); /** @} */ // end of maths group diff --git a/include/simcoon/Simulation/Phase/output.hpp b/include/simcoon/Simulation/Phase/output.hpp index 9b9db6426..cb52803ce 100755 --- a/include/simcoon/Simulation/Phase/output.hpp +++ b/include/simcoon/Simulation/Phase/output.hpp @@ -33,9 +33,21 @@ namespace simcoon{ */ -void output_modulus(const arma::mat &, const int &, arma::vec &); +/** + * @brief Copy a matrix into the state variables, row by row. + * @param L matrix to store (n_rows x n_cols) + * @param i0 index of statev where the first entry is written + * @param statev state variables, entries i0 .. i0 + n_rows*n_cols - 1 overwritten + */ +void output_modulus(const arma::mat &L, const int &i0, arma::vec &statev); -void output_moduli(const std::vector &, const int &, arma::vec &); +/** + * @brief Copy a list of matrices into the state variables, one after the other, each row by row. + * @param list matrices to store + * @param i0 index of statev where the first entry of the first matrix is written + * @param statev state variables, overwritten from i0 on by the total number of entries of the list + */ +void output_moduli(const std::vector &list, const int &i0, arma::vec &statev); /** @} */ // end of phase group diff --git a/include/simcoon/Simulation/Phase/state_variables.hpp b/include/simcoon/Simulation/Phase/state_variables.hpp index 77790b15f..24558a3ce 100755 --- a/include/simcoon/Simulation/Phase/state_variables.hpp +++ b/include/simcoon/Simulation/Phase/state_variables.hpp @@ -84,6 +84,7 @@ class state_variables arma::vec statev; ///< Internal state variables vector arma::vec statev_start; ///< Internal state variables at start of increment; crosses both ways + int ncp = 0; ///< complementarity function of the local cutting plane (NCPMethod code): 0 Fischer-Burmeister (default), 1 min-NCP; served by EPICP, EPKCP and MODUL int tangent_mode = tangent_default; // tangent_* constants (parameter.hpp): 0 = none/explicit, 1 = continuum, 2 = algorithmic/Simo-Hughes (default), 3 = closest-point (exact operator of the closest-point integrator where a kernel has one, else the algorithmic operator). diff --git a/include/simcoon/Simulation/Phase/state_variables_M.hpp b/include/simcoon/Simulation/Phase/state_variables_M.hpp index f32961305..30d59e7cd 100755 --- a/include/simcoon/Simulation/Phase/state_variables_M.hpp +++ b/include/simcoon/Simulation/Phase/state_variables_M.hpp @@ -15,7 +15,7 @@ */ -///@file state_variables.hpp +///@file state_variables_M.hpp ///@brief State variables of a phase, in a defined coordinate system: ///@version 1.0 @@ -57,12 +57,46 @@ class state_variables_M : public state_variables arma::mat L; ///< elastic stiffness; plugin-kernel input, crosses g2l only (the solver zeroes the global one) arma::mat Lt; ///< tangent stiffness; crosses both ways - state_variables_M(); //default constructor - state_variables_M(const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::vec &, const arma::vec &, const double &, const double &, const int &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::mat &, const arma::mat &); //Constructor with parameters + /** @brief Default constructor (zero strains and stresses, identity F, U, R, DR). */ + state_variables_M(); + /** + * @brief Constructor with parameters; asserts the Voigt and matrix sizes. + * @param Etot Green-Lagrange strain + * @param DEtot increment of Green-Lagrange strain + * @param etot logarithmic strain + * @param Detot increment of logarithmic strain + * @param PKII 2nd Piola-Kirchhoff stress + * @param PKII_start 2nd Piola-Kirchhoff stress at start + * @param tau Kirchhoff stress + * @param tau_start Kirchhoff stress at start + * @param sigma Cauchy stress + * @param sigma_start Cauchy stress at start + * @param F0 deformation gradient at start + * @param F1 deformation gradient at end + * @param U0 stretch tensor at start + * @param U1 stretch tensor at end + * @param R rotation tensor + * @param DR rotation increment + * @param sigma_in inelastic stress (size 6) + * @param sigma_in_start inelastic stress at start (size 6) + * @param T temperature + * @param DT temperature increment + * @param nstatev number of internal state variables + * @param statev internal state variables + * @param statev_start internal state variables at start + * @param Wm mechanical works (size 4) + * @param Wm_start mechanical works at start (size 4) + * @param L elastic stiffness (6x6) + * @param Lt tangent stiffness (6x6) + */ + state_variables_M(const arma::vec &Etot, const arma::vec &DEtot, const arma::vec &etot, const arma::vec &Detot, const arma::vec &PKII, const arma::vec &PKII_start, const arma::vec &tau, const arma::vec &tau_start, const arma::vec &sigma, const arma::vec &sigma_start, const arma::mat &F0, const arma::mat &F1, const arma::mat &U0, const arma::mat &U1, const arma::mat &R, const arma::mat &DR, const arma::vec &sigma_in, const arma::vec &sigma_in_start, const double &T, const double &DT, const int &nstatev, const arma::vec &statev, const arma::vec &statev_start, const arma::vec &Wm, const arma::vec &Wm_start, const arma::mat &L, const arma::mat &Lt); - state_variables_M(const state_variables_M &); //Copy constructor + /** @brief Copy constructor. */ + state_variables_M(const state_variables_M &); + /** @brief Destructor. */ virtual ~state_variables_M(); + /** @brief Copy assignment. */ virtual state_variables_M& operator = (const state_variables_M&); virtual state_variables_M& copy_fields_M (const state_variables_M&); diff --git a/include/simcoon/Simulation/Phase/state_variables_T.hpp b/include/simcoon/Simulation/Phase/state_variables_T.hpp index 27efb8fb4..958f99795 100755 --- a/include/simcoon/Simulation/Phase/state_variables_T.hpp +++ b/include/simcoon/Simulation/Phase/state_variables_T.hpp @@ -15,7 +15,7 @@ */ -///@file state_variables.hpp +///@file state_variables_T.hpp ///@brief State variables of a phase, in a defined coordinate system: ///@version 1.0 @@ -78,11 +78,53 @@ namespace simcoon{ arma::mat drdE; arma::mat drdT; ///< temperature derivative of the heat of the increment r; kernel output, crosses l2g only - state_variables_T(); //default constructor - state_variables_T(const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::vec &, const arma::vec &, const double &, const double &, const int &, const arma::vec &, const arma::vec &, const double &, const double &, const double &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::vec &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &, const arma::mat &); //Constructor with parameters - state_variables_T(const state_variables_T &); //Copy constructor + /** @brief Default constructor (zero strains and stresses, identity F, U, R, DR). */ + state_variables_T(); + /** + * @brief Constructor with parameters; asserts the Voigt and matrix sizes. + * @param Etot Green-Lagrange strain + * @param DEtot increment of Green-Lagrange strain + * @param etot logarithmic strain + * @param Detot increment of logarithmic strain + * @param PKII 2nd Piola-Kirchhoff stress + * @param PKII_start 2nd Piola-Kirchhoff stress at start + * @param tau Kirchhoff stress + * @param tau_start Kirchhoff stress at start + * @param sigma Cauchy stress + * @param sigma_start Cauchy stress at start + * @param F0 deformation gradient at start + * @param F1 deformation gradient at end + * @param U0 stretch tensor at start + * @param U1 stretch tensor at end + * @param R rotation tensor + * @param DR rotation increment + * @param sigma_in inelastic stress (size 6) + * @param sigma_in_start inelastic stress at start (size 6) + * @param T temperature + * @param DT temperature increment + * @param nstatev number of internal state variables + * @param statev internal state variables + * @param statev_start internal state variables at start + * @param Q heat flux (rate) + * @param r heat of the increment + * @param r_in stored as is (read by nobody in-tree) + * @param Wm mechanical works (size 4) + * @param Wt thermal works (size 3) + * @param Wm_start mechanical works at start (size 4) + * @param Wt_start thermal works at start (size 3) + * @param dSdE mechanical tangent (6x6) + * @param dSdEt stored as is (6x6, read by nobody) + * @param dSdT thermal stress tangent (1x6) + * @param drdE strain derivative of r (1x6) + * @param drdT temperature derivative of r (1x1) + */ + state_variables_T(const arma::vec &Etot, const arma::vec &DEtot, const arma::vec &etot, const arma::vec &Detot, const arma::vec &PKII, const arma::vec &PKII_start, const arma::vec &tau, const arma::vec &tau_start, const arma::vec &sigma, const arma::vec &sigma_start, const arma::mat &F0, const arma::mat &F1, const arma::mat &U0, const arma::mat &U1, const arma::mat &R, const arma::mat &DR, const arma::vec &sigma_in, const arma::vec &sigma_in_start, const double &T, const double &DT, const int &nstatev, const arma::vec &statev, const arma::vec &statev_start, const double &Q, const double &r, const double &r_in, const arma::vec &Wm, const arma::vec &Wt, const arma::vec &Wm_start, const arma::vec &Wt_start, const arma::mat &dSdE, const arma::mat &dSdEt, const arma::mat &dSdT, const arma::mat &drdE, const arma::mat &drdT); + /** @brief Copy constructor. */ + state_variables_T(const state_variables_T &); + /** @brief Destructor. */ virtual ~state_variables_T(); + /** @brief Copy assignment. */ virtual state_variables_T& operator = (const state_variables_T&); virtual state_variables_T& copy_fields_T (const state_variables_T&); diff --git a/include/simcoon/Simulation/Solver/output.hpp b/include/simcoon/Simulation/Solver/output.hpp index 8eaf02bf1..4a9ef8bdf 100755 --- a/include/simcoon/Simulation/Solver/output.hpp +++ b/include/simcoon/Simulation/Solver/output.hpp @@ -68,11 +68,19 @@ public : arma::Col o_nfreq; arma::vec o_tfreq; - solver_output(); //default constructor - solver_output(const int &o_nb_strain); //Constructor with parameters - solver_output(const solver_output &so); //Copy constructor + /** @brief Default constructor: no output requested, all types set to 0. */ + solver_output(); + /** + * @brief Constructor with parameters; zero-sized output types and frequencies per block. + * @param nblock number of blocks (size of o_type, o_nfreq, o_tfreq) + */ + solver_output(const int &nblock); + /** @brief Copy constructor. */ + solver_output(const solver_output &so); + /** @brief Destructor. */ ~solver_output(); + /** @brief Copy assignment. */ virtual solver_output& operator = (const solver_output &so); friend std::ostream& operator << (std::ostream& os, const solver_output &so); diff --git a/include/simcoon/Simulation/Solver/solver_sink.hpp b/include/simcoon/Simulation/Solver/solver_sink.hpp index 3a6589889..b2aeca830 100644 --- a/include/simcoon/Simulation/Solver/solver_sink.hpp +++ b/include/simcoon/Simulation/Solver/solver_sink.hpp @@ -46,9 +46,11 @@ struct solver_params { int miniter = 10; ///< iteration count below which the step may grow int maxiter = 100; ///< maximum Newton-Raphson iterations per increment int inforce = 1; ///< 0: stop at Dn_mini, 1: enforce & carry residual, 2: enforce silently - double precision = 1.E-6; ///< convergence tolerance on the residual norm + double precision = 1.E-6; ///< convergence tolerance on the residual norm (absolute) + double precision_rel = 0.; ///< > 0: tolerance max(precision, precision_rel |tau|), |tau| the Euclidean norm of the Kirchhoff stress (Voigt), so that stiff or highly loaded cases are not asked for more digits than soft ones (de Souza Neto et al. 2008, sec. 4.2); 0: precision alone (default) double lambda = 10000.; ///< penalty stiffness for strain-controlled components int tangent_mode = tangent_default; ///< tangent_* constants (parameter.hpp) + int ncp = 0; ///< complementarity function of the local cutting plane (NCPMethod code), see state_variables::ncp }; /** @@ -118,6 +120,10 @@ class solver_memory_sink : public solver_results_sink { void record(phase_characteristics &rve, const solver_output &so, const int &kblock, const int &kcycle, const int &kstep, const int &kinc, const double &Time) override; + /// Capacity hint for @p n_records more records, for the fields of sv_type (call it + /// after init()); solver_run passes its expected count. The move constructor of the + /// arma objects is not noexcept, so each growth of these vectors would copy them all. + void reserve(std::size_t n_records); /// number of records captured so far unsigned int n_records() const { return static_cast(time.size()); } }; diff --git a/include/simcoon/Simulation/Solver/step.hpp b/include/simcoon/Simulation/Solver/step.hpp index 9a341f702..0ec6b583c 100755 --- a/include/simcoon/Simulation/Solver/step.hpp +++ b/include/simcoon/Simulation/Solver/step.hpp @@ -64,7 +64,7 @@ class step double Dn_inc; ///< Maximum fraction of the step (maximum time increment ratio) int ninc; ///< Number of milestones/increments in the step int mode; ///< Loading mode identifier - unsigned int control_type; ///< Control type (0: strain, 1: stress, 2: mixed) + unsigned int control_type; ///< Control type: 1 small strain, 2 Green-Lagrange, 3 logarithmic, 4 Biot, 5 F, 6 grad u (CONTROL_TYPES) arma::vec times; ///< Vector of time values for the step double BC_Time; ///< Boundary condition application time @@ -96,16 +96,9 @@ class step */ step(const int &number, const double &Dn_init, const double &Dn_mini, const double &Dn_inc, const int &mode, const unsigned int &control_type); - /** - * @brief Copy constructor. - * @param s Step to copy - */ - step(const step &s); - /** - * @brief Virtual destructor. - */ - virtual ~step(); + /** @brief Virtual destructor; copies are memberwise (implicit). */ + virtual ~step() = default; /** * @brief Generate the time discretization for the step. @@ -139,12 +132,6 @@ class step /// then aborts with status 1 (same protocol as a non-converged Newton loop). virtual bool compute_inc(double &tnew_dt, const int &inc, double &tinc, double &Dtinc, double &Dn, const int &control); - /** - * @brief Assignment operator. - * @param s Step to assign - * @return Reference to this object - */ - virtual step& operator = (const step& s); /** * @brief Stream output operator. diff --git a/include/simcoon/Simulation/Solver/step_meca.hpp b/include/simcoon/Simulation/Solver/step_meca.hpp index 94b25ba09..a4fe46054 100755 --- a/include/simcoon/Simulation/Solver/step_meca.hpp +++ b/include/simcoon/Simulation/Solver/step_meca.hpp @@ -59,12 +59,34 @@ class step_meca : public step arma::vec Ts; arma::vec BC_Ts; - step_meca(); //default constructor - step_meca(const unsigned int &size); //constructor that allocates BC_meca and cBC_meca - step_meca(const int &number, const double &Dn_init, const double &Dn_mini, const double &Dn_inc, const int &mode, const unsigned int &control_type, const arma::Col& cBC_meca, const arma::vec& BC_meca, const arma::mat& mecas, const arma::mat& BC_mecas, const double& BC_T, const int& cBC_T, const arma::vec& Ts, const arma::vec& BC_Ts, const arma::mat& BC_w, const arma::mat& BC_R); //Constructor with parameters + /** @brief Default constructor: BC_T = 0, cBC_T = 0. */ + step_meca(); + /** + * @brief Constructor that allocates cBC_meca and BC_meca (size 9 for control_type 5/6, else 6), BC_w = 0, BC_R = I. + * @param control_type control type of the step + */ + step_meca(const unsigned int &control_type); + /** + * @brief Constructor with parameters. + * @param number step identification number + * @param Dn_init initial time increment fraction + * @param Dn_mini minimum time increment fraction + * @param Dn_inc maximum time increment fraction + * @param mode loading mode + * @param control_type control type + * @param cBC_meca type of each mechanical boundary condition (1 stress, 0 strain) + * @param BC_meca target values of the mechanical boundary conditions + * @param mecas mechanical increments, one row per increment + * @param BC_mecas cumulated mechanical boundary values, one row per increment + * @param BC_T target temperature + * @param cBC_T type of the thermal boundary condition + * @param Ts temperature increments, one per increment + * @param BC_Ts cumulated thermal boundary values, one per increment + * @param BC_w spin of the step (3x3) + * @param BC_R rotation of the step (3x3) + */ + step_meca(const int &number, const double &Dn_init, const double &Dn_mini, const double &Dn_inc, const int &mode, const unsigned int &control_type, const arma::Col& cBC_meca, const arma::vec& BC_meca, const arma::mat& mecas, const arma::mat& BC_mecas, const double& BC_T, const int& cBC_T, const arma::vec& Ts, const arma::vec& BC_Ts, const arma::mat& BC_w, const arma::mat& BC_R); - step_meca(const step_meca &s); //Copy constructor - virtual ~step_meca(); using step::generate; virtual void generate(const double&, const arma::vec &, const arma::vec&, const double&); @@ -79,7 +101,6 @@ class step_meca : public step virtual void generate_kin(const double&, const arma::mat &, const double &); virtual void assess_inc(const double &, double &, const double &, phase_characteristics &, double &, const double &, const arma::mat &, const int &); - virtual step_meca& operator = (const step_meca &s); friend std::ostream& operator << (std::ostream& os, const step_meca &s); }; diff --git a/include/simcoon/Simulation/Solver/step_thermomeca.hpp b/include/simcoon/Simulation/Solver/step_thermomeca.hpp index 0810b0b04..cd9d8bb8b 100755 --- a/include/simcoon/Simulation/Solver/step_thermomeca.hpp +++ b/include/simcoon/Simulation/Solver/step_thermomeca.hpp @@ -59,11 +59,33 @@ class step_thermomeca : public step arma::vec Ts; arma::vec BC_Ts; - step_thermomeca(); //default constructor - step_thermomeca(const unsigned int &); //constructor that allocates BC_meca and cBC_meca - step_thermomeca(const int &, const double &, const double &, const double &, const int &, const unsigned int &, const arma::Col&, const arma::vec&, const arma::mat&, const arma::mat&, const double&, const int&, const arma::vec&, const arma::vec&, const arma::mat&, const arma::mat&); //Constructor with parameters - step_thermomeca(const step_thermomeca&); //Copy constructor - virtual ~step_thermomeca(); + /** @brief Default constructor: BC_T = 0, cBC_T = 0. */ + step_thermomeca(); + /** + * @brief Constructor that allocates cBC_meca and BC_meca (size 9 for control_type 5/6, else 6), BC_w = 0, BC_R = I. + * @param control_type control type of the step + */ + step_thermomeca(const unsigned int &control_type); + /** + * @brief Constructor with parameters. + * @param number step identification number + * @param Dn_init initial time increment fraction + * @param Dn_mini minimum time increment fraction + * @param Dn_inc maximum time increment fraction + * @param mode loading mode + * @param control_type control type + * @param cBC_meca type of each mechanical boundary condition (1 stress, 0 strain) + * @param BC_meca target values of the mechanical boundary conditions + * @param mecas mechanical increments, one row per increment + * @param BC_mecas cumulated mechanical boundary values, one row per increment + * @param BC_T target temperature or heat flux (see cBC_T) + * @param cBC_T type of the thermal boundary condition (1 heat flux, 0 fixed temperature) + * @param Ts temperature/heat increments, one per increment + * @param BC_Ts cumulated thermal boundary values, one per increment + * @param BC_w spin of the step (3x3) + * @param BC_R rotation of the step (3x3) + */ + step_thermomeca(const int &number, const double &Dn_init, const double &Dn_mini, const double &Dn_inc, const int &mode, const unsigned int &control_type, const arma::Col& cBC_meca, const arma::vec& BC_meca, const arma::mat& mecas, const arma::mat& BC_mecas, const double& BC_T, const int& cBC_T, const arma::vec& Ts, const arma::vec& BC_Ts, const arma::mat& BC_w, const arma::mat& BC_R); using step::generate; virtual void generate(const double&, const arma::vec&, const arma::vec&, const double&); @@ -77,7 +99,6 @@ class step_thermomeca : public step virtual void generate_kin(const double&, const arma::mat&m, const double &); virtual void assess_inc(const double &, double &, const double &, phase_characteristics &, double &, const double &, const arma::mat &, const int &); - virtual step_thermomeca& operator = (const step_thermomeca&); friend std::ostream& operator << (std::ostream&, const step_thermomeca&); }; diff --git a/include/simcoon/parallel.hpp b/include/simcoon/parallel.hpp index e936b16a8..57bd5b6be 100644 --- a/include/simcoon/parallel.hpp +++ b/include/simcoon/parallel.hpp @@ -29,6 +29,8 @@ #if defined(__APPLE__) #include +#elif defined(_OPENMP) + #include #endif /// Batches of at most this many items run serially: the parallel set-up would cost more. @@ -57,14 +59,22 @@ inline int chunk_size(int N) { return std::max(N / (8 * hw), 32); } +/// Number of workers for @p n_threads (0 = one per hardware thread), at most one per hardware +/// thread and one per range. +inline int worker_count(unsigned int n_threads, int nchunks) { + const unsigned int hw = std::max(1u, std::thread::hardware_concurrency()); + return std::max(1, std::min(nchunks, static_cast(n_threads ? std::min(n_threads, hw) : hw))); +} + } // namespace simcoon_parallel_detail /// @brief Exception-safe parallel loop over [0,N) for batch kernels that can throw. /// /// GCD on macOS, OpenMP where the build has it (Linux), std::thread otherwise (Windows, whose /// Python stack would carry a second OpenMP runtime); serial up to @p cutoff items. -/// @p n_threads = 1 runs serially on every platform; otherwise it caps the std::thread workers -/// (0 = one per hardware thread), while GCD and OpenMP keep their own runtime sizing. The first +/// @p n_threads caps the number of threads running @p func on every platform (the calling +/// thread included): 1 runs serially, 0 leaves the sizing to the runtime (GCD, OpenMP) or uses +/// one worker per hardware thread (std::thread). The first /// exception thrown by @p func is rethrown AFTER the loop: an exception escaping an active /// OpenMP parallel region (or a GCD block, or a thread) would std::terminate, killing e.g. a /// Python session on the first singular slice of a batch. @@ -89,19 +99,35 @@ void simcoon_parallel_for_safe(int N, F&& func, int cutoff = simcoon_parallel_cu #if defined(__APPLE__) const int chunk = simcoon_parallel_detail::chunk_size(N); - const size_t nblocks = static_cast((N + chunk - 1) / chunk); + const int nchunks = (N + chunk - 1) / chunk; // dispatch_apply is synchronous, so pointers to these stack locals stay valid; the block // captures the pointers by value (no __block C++-object machinery needed). std::exception_ptr *pe = &eptr; std::mutex *pm = &mtx; auto *pf = &func; - dispatch_apply(nblocks, DISPATCH_APPLY_AUTO, ^(size_t b) { - const int start = static_cast(b) * chunk; - simcoon_parallel_detail::run_range(start, std::min(N, start + chunk), *pf, *pe, *pm); - }); + if (n_threads == 0) { + dispatch_apply(static_cast(nchunks), DISPATCH_APPLY_AUTO, ^(size_t b) { + const int start = static_cast(b) * chunk; + simcoon_parallel_detail::run_range(start, std::min(N, start + chunk), *pf, *pe, *pm); + }); + } + else { + // GCD runs at most as many iterations at once as there are: n_threads iterations, each + // pulling ranges from a shared counter, cap the concurrency at n_threads. + std::atomic next{0}; + std::atomic *pn = &next; + const size_t workers = static_cast(simcoon_parallel_detail::worker_count(n_threads, nchunks)); + dispatch_apply(workers, DISPATCH_APPLY_AUTO, ^(size_t) { + for (int c = (*pn)++; c < nchunks; c = (*pn)++) { + simcoon_parallel_detail::run_range(c * chunk, std::min(N, (c + 1) * chunk), *pf, *pe, *pm); + } + }); + } #elif defined(_OPENMP) - #pragma omp parallel for schedule(static) + const int nt = n_threads ? static_cast(std::min(n_threads, std::max(1u, std::thread::hardware_concurrency()))) + : omp_get_max_threads(); + #pragma omp parallel for schedule(static) num_threads(nt) for (int i = 0; i < N; i++) { simcoon_parallel_detail::run_range(i, i + 1, func, eptr, mtx); } @@ -109,8 +135,7 @@ void simcoon_parallel_for_safe(int N, F&& func, int cutoff = simcoon_parallel_cu #else const int chunk = simcoon_parallel_detail::chunk_size(N); const int nchunks = (N + chunk - 1) / chunk; - const unsigned int hw = std::max(1u, std::thread::hardware_concurrency()); - const int workers = std::min(nchunks, static_cast(n_threads ? std::min(n_threads, hw) : hw)); + const int workers = simcoon_parallel_detail::worker_count(n_threads, nchunks); std::atomic next{0}; auto worker = [&]() { for (int c = next++; c < nchunks; c = next++) { diff --git a/include/simcoon/parameter.hpp b/include/simcoon/parameter.hpp index 3e64ecc41..778e09a7b 100755 --- a/include/simcoon/parameter.hpp +++ b/include/simcoon/parameter.hpp @@ -62,15 +62,34 @@ constexpr double precision_umat = 1E-9; constexpr int tangent_none = 0; ///< skip tangent assembly, Lt = elastic L (explicit integration) constexpr int tangent_continuum = 1; ///< continuum operator (pre-2.0 mode 0) constexpr int tangent_algorithmic = 2; ///< Simo-Hughes consistent operator (pre-2.0 mode 1) -constexpr int tangent_closest_point = 3; ///< closest-point-projection integrator with its exact operator (return_mapping.hpp) +constexpr int tangent_closest_point = 3; ///< kernel code: closest-point-projection integrator with its exact operator (return_mapping.hpp; kernel_tangent_mode) constexpr int tangent_default = tangent_algorithmic; +// Local integrator of the laws with a return mapping (the integrator option of solve / sim.umat). +// Not a kernel argument: kernel_tangent_mode() folds it with the tangent choice into the code +// the kernels read, where the closest point is tangent_closest_point (a kernel code, not a +// choice the Python API takes as tangent_mode). +constexpr int integrator_cutting_plane = 0; ///< convex cutting plane (default) +constexpr int integrator_closest_point = 1; ///< closest-point projection, backward Euler (return_mapping.hpp) + /// Finite-strain formulation carried in the corate_type slot. Rate formulation: corate_type 0..5 /// (corate_kinematics), the law works on \f$ \ln\mathbf{V} \f$ and the Kirchhoff stress /// \f$ \boldsymbol{\tau} \f$. Total formulation (no rate): the law works on \f$ \ln\mathbf{U} \f$, /// a state function of \f$ \mathbf{F} \f$, and on its dual stress \f$ \mathbf{T} \f$. constexpr int corate_type_total = 6; +/// Tangent form carried in the corate_type slot of a finite-strain kernel CALL: the kernel returns +/// its tangent as the material tangent \f$ \partial\mathbf{S}/\partial\mathbf{E} \f$ instead of the box +/// tangent of a rate. Not a kinematic choice and never a solver corate: the caller runs its own +/// kinematics and passes this value only to a kernel built on \f$ \mathbf{F} \f$ that declares it +/// (umat_convention::material_tangent, material_tangent_serves()), whose stress and work do not +/// depend on the corate. \f$ \partial\mathbf{S}/\partial\mathbf{E} \f$ is the form those kernels close +/// on (a pull-back of their spatial tangent), so a caller that needs it (the solver under +/// Green-Lagrange or Biot control, sim.umat with tangent_output 'material' or 'spatial') skips the +/// box map and its inverse, two spectral maps per call. The corate dispatchers of +/// objective_rates.hpp accept it (identity on \f$ \partial\mathbf{S}/\partial\mathbf{E} \f$). +constexpr int corate_type_material_tangent = 7; + /// Kinematic form of the finite-strain constitutive update, for the interfaces that name it: /// @c rate (objective rates, \f$ \ln\mathbf{V} \f$ and \f$ \boldsymbol{\tau} \f$) or @c total /// (\f$ \ln\mathbf{U} \f$ and its dual stress \f$ \mathbf{T} \f$, no rate). In a corate_type diff --git a/include/simcoon/simcoon_doxygen_groups.hpp b/include/simcoon/simcoon_doxygen_groups.hpp index a74a1e43d..582984965 100644 --- a/include/simcoon/simcoon_doxygen_groups.hpp +++ b/include/simcoon/simcoon_doxygen_groups.hpp @@ -1,3 +1,20 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + /** * @file simcoon_doxygen_groups.hpp * @brief Doxygen group definitions for Simcoon library documentation diff --git a/python-setup/simcoon/__init__.py b/python-setup/simcoon/__init__.py index 6175869ac..e88269c32 100755 --- a/python-setup/simcoon/__init__.py +++ b/python-setup/simcoon/__init__.py @@ -22,18 +22,15 @@ from simcoon.solver.micromechanics import L_eff # override _core.L_eff: Rotation + phase objects from simcoon.solver.micromechanics import get_densities_ODF from simcoon.tensor import Basis, Tensor2, Tensor4, dyadic, auto_dyadic, sym_dyadic, auto_sym_dyadic, double_contract # unified tensor classes -from simcoon.identify import identification, calc_cost +from simcoon.identify import identification, calc_cost, calc_residuals from simcoon.parameter import ( Parameter, read_parameters, - copy_parameters, - apply_parameters, + KeyTemplate, ) from simcoon.constant import ( Constant, read_constants, - copy_constants, - apply_constants, ) # Backward compatibility alias - simmit was the legacy module name diff --git a/python-setup/simcoon/constant.py b/python-setup/simcoon/constant.py index 92108e354..1c3b85c57 100644 --- a/python-setup/simcoon/constant.py +++ b/python-setup/simcoon/constant.py @@ -3,8 +3,6 @@ """ import os -import re -import shutil from typing import List, NamedTuple, Union import numpy as np @@ -72,67 +70,3 @@ def read_constants( consts.append(const) return consts - -def copy_constants( - consts: List[Constant], - src_path: Union[str, os.PathLike], - dst_path: Union[str, os.PathLike], -) -> None: - """ - Copy constants file from a source path to a destination path, so that key values can be applied - :param consts: List of Constant objects - :param src_path: Source path - :param dst_path: Destination path - :return: None - """ - if not isinstance(src_path, (str, os.PathLike)): - raise TypeError( - f"Invalid type: {type(src_path).__name__}. Expected str or os.PathLike." - ) - - if not isinstance(dst_path, (str, os.PathLike)): - raise TypeError( - f"Invalid type: {type(dst_path).__name__}. Expected str or os.PathLike." - ) - - for co in consts: - if not all(isinstance(item, str) for item in co.sim_input_files): - raise TypeError("All elements in sim_input_files must be strings.") - - for ifiles in co.sim_input_files: - src_files = os.path.join(src_path, ifiles) - dst_files = os.path.join(dst_path, ifiles) - shutil.copy(src_files, dst_files) - - -def apply_constants( - consts: List[Constant], - dst_path: Union[str, os.PathLike], -) -> None: - """ - Apply constants, i.e. replace in file the value of the key with the value of the Constant - :param params: List of Constant objects - :param src_path: Source path - :param dst_path: Destination path - :return: None - """ - if not isinstance(dst_path, (str, os.PathLike)): - raise TypeError( - f"Invalid type: {type(dst_path).__name__}. Expected str or os.PathLike." - ) - - # Longest keys first + ``(?!\w)`` lookahead so a shorter key cannot - # clobber a longer key that contains it (e.g. ``@E`` vs ``@Et``). - ordered = sorted(consts, key=lambda c: -len(c.key)) - - for co in ordered: - pattern = re.compile(re.escape(co.key) + r"(?!\w)") - replacement = str(co.value) - for ifiles in co.sim_input_files: - mod_files = os.path.join(dst_path, ifiles) - - with open(mod_files, "r", encoding="utf-8") as in_files: - content = in_files.read() - modified_content = pattern.sub(lambda _m: replacement, content) - with open(mod_files, "w", encoding="utf-8") as ou_files: - ou_files.write(modified_content) diff --git a/python-setup/simcoon/identify.py b/python-setup/simcoon/identify.py index e9c8d651a..e37e2d45c 100644 --- a/python-setup/simcoon/identify.py +++ b/python-setup/simcoon/identify.py @@ -1,50 +1,127 @@ """ Identification utilities for simcoon. -Provides ``identification()`` (wraps ``scipy.optimize.differential_evolution``) -and ``calc_cost()`` (multi-level weighted cost function for parameter -identification from multiple tests). +Provides ``identification()`` (bounded global search with +``scipy.optimize.differential_evolution``, or bounded multi-start least +squares with ``scipy.optimize.least_squares``), ``calc_cost()`` (multi-level +weighted cost function for parameter identification from multiple tests) and +``calc_residuals()`` (the residual vector whose sum of squares is that cost, +for the least-squares route). """ +import os +import threading +from concurrent.futures import ThreadPoolExecutor from typing import Callable, List, Optional import numpy as np -from scipy.optimize import differential_evolution +from scipy.optimize import OptimizeResult, differential_evolution, least_squares + +#: Optimizers accepted by :func:`identification` +IDENTIFICATION_METHODS = ("differential_evolution", "least_squares") def identification( cost_fn: Callable, parameters: list, + method: str = "differential_evolution", + *, + n_starts: int = 1, + scale="auto", + n_threads: int = 1, **kwargs, ): """ - Run parameter identification using scipy's differential evolution. - - Wraps ``scipy.optimize.differential_evolution`` using the bounds - defined in the ``Parameter`` objects. After optimization, the - identified values are written back to each ``Parameter.value``. - - For other optimizers (``minimize``, ``shgo``, ``dual_annealing``…), - call ``scipy.optimize`` directly — the ``Parameter`` objects expose - the bounds and values you need. + Run parameter identification within the bounds of the ``Parameter`` objects. + + Two optimizers are available: + + - ``method="differential_evolution"`` (default): wraps + ``scipy.optimize.differential_evolution``. Global and derivative-free, + but it needs many evaluations (``popsize * n_parameters`` per + generation) and usually stops on ``maxiter``. + - ``method="least_squares"``: bounded trust-region reflective least + squares (``scipy.optimize.least_squares``, ``method="trf"``) from + ``n_starts`` starting points. ``cost_fn`` returns the vector of + residuals instead of a scalar (see :func:`calc_residuals`). It converges + in typically 7 to 20 times fewer evaluations than differential + evolution on smooth problems, and several starts guard against local + minima. + + After optimization, the identified values are written back to each + ``Parameter.value``. For other optimizers (``minimize``, ``shgo``, + ``dual_annealing``…), call ``scipy.optimize`` directly — the ``Parameter`` + objects expose the bounds and values you need. Parameters ---------- cost_fn : callable - Cost function ``f(x) -> float`` where ``x`` is a 1-D array of - parameter values (same order as *parameters*). + ``f(x, *args)`` where ``x`` is a 1-D array of parameter values (same + order as *parameters*). Returns a float for + ``"differential_evolution"``, a 1-D array of residuals of constant + length for ``"least_squares"`` (the cost minimized is half their sum of + squares). For ``"least_squares"``, a model failure is best reported by + residuals set to NaN: the trial step is then rejected and the trust + region shrunk. parameters : list of Parameter - Parameter objects whose ``.bounds`` define the search space. + Parameter objects whose ``.bounds`` define the search space. With + ``"least_squares"``, the first starting point is ``Parameter.value`` + where it has been set, and the middle of the scaled range otherwise + (the geometric mean of the bounds for a log-scaled parameter). + method : {"differential_evolution", "least_squares"}, optional + Optimizer. Default ``"differential_evolution"``. + n_starts : int, optional + ``"least_squares"`` only: number of starting points. The first one is + described under *parameters*; the others are drawn by Latin + hypercube sampling inside the (scaled) bounds, reproducibly with + ``seed``. The best final cost is kept. Default 1. + scale : {"auto", "linear", "log"} or sequence of them, optional + ``"least_squares"`` only: how each parameter is mapped onto [0, 1] for + the optimizer. ``"log"`` (``x = lb (ub/lb)^z``, which requires + ``lb > 0``) suits parameters spanning decades, such as hardening + moduli; ``"linear"`` is ``x = lb + z (ub - lb)``. ``"auto"`` (default) + picks ``"log"`` when the lower bound is strictly positive and + ``"linear"`` otherwise. One value for all parameters, or one per + parameter. + n_threads : int, optional + Number of threads evaluating ``cost_fn`` concurrently. ``1`` (default) + is serial; ``0`` uses ``os.cpu_count()``. For + ``"differential_evolution"``, a generation is evaluated in parallel + (``workers`` set to a thread pool and ``updating="deferred"``, so the + trajectory differs from the default serial ``updating="immediate"``, + but not with the number of threads). For ``"least_squares"``, the + finite-difference columns of the Jacobian are evaluated in parallel: + the result does not depend on ``n_threads``. Threads only help when + ``cost_fn`` spends its time in code releasing the GIL + (``simcoon.solver.solve`` and ``simcoon.L_eff`` do), and require a + thread-safe ``cost_fn``. **kwargs - Extra keyword arguments forwarded to ``differential_evolution``. - Common options: ``maxiter``, ``popsize``, ``tol``, ``seed``, - ``polish`` (default ``True``), ``disp``. + ``"differential_evolution"``: forwarded to + ``differential_evolution``. Common options: ``maxiter``, ``popsize``, + ``tol``, ``seed``, ``polish`` (default ``True``), ``disp``, ``args``. + + ``"least_squares"``: ``args`` (extra positional arguments of + ``cost_fn``), ``seed`` (sampling of the extra starts), ``diff_step`` + (relative finite-difference step in the scaled [0, 1] coordinates, + default ``sqrt(eps)``; increase it, e.g. to ``1e-6``, when the model + response is computed to a tolerance), and any option of + ``least_squares`` but ``x0``, ``bounds``, ``method`` and ``jac`` + (``xtol``, ``ftol``, ``gtol``, ``max_nfev``, ``loss``, ``verbose``…). Returns ------- scipy.optimize.OptimizeResult - The full scipy result object. ``result.x`` contains the - identified parameter values. + The scipy result object; ``result.x`` contains the identified + parameter values. For ``"least_squares"`` it is the result of the best + start with ``x`` mapped back to the parameter values, and: + + - ``fun``: the residuals at the solution, ``cost``: half their sum of + squares (scipy convention); + - ``z``: the solution in the scaled [0, 1] coordinates; + - ``nfev``: evaluations of ``cost_fn`` over all starts, Jacobians + included; + - ``starts``: one ``OptimizeResult`` per start (``x0``, ``x``, + ``cost``, ``nfev``, ``status``, ``message``). Examples -------- @@ -54,17 +131,47 @@ def identification( >>> result = identification(my_cost, params, seed=42) >>> print(f"Identified E = {params[0].value:.0f}") + The least-squares route with residuals, three starts: + + >>> def residuals(x): + ... return calc_residuals(y_exp, model(x), metric="nmse_per_response") + >>> result = identification(residuals, params, method="least_squares", + ... n_starts=3, seed=0) + ``my_cost`` receives the trial values and runs the model in memory (:func:`simcoon.solver.solve` takes ``props`` directly). ``key`` and ``sim_input_files`` are only needed to drive an external, file-based solver by substituting the keys into its input decks. """ - bounds = [p.bounds for p in parameters] - - # Sensible defaults - kwargs.setdefault("polish", True) - - result = differential_evolution(cost_fn, bounds=bounds, **kwargs) + if method not in IDENTIFICATION_METHODS: + raise ValueError( + f"Unknown method '{method}'. Available: {', '.join(IDENTIFICATION_METHODS)}" + ) + n_threads = int(n_threads) + if n_threads < 0: + raise ValueError(f"n_threads must be >= 0, got {n_threads}") + if n_threads == 0: + n_threads = os.cpu_count() or 1 + + if method == "least_squares": + result = _identification_least_squares( + cost_fn, parameters, n_starts, scale, n_threads, kwargs, + ) + else: + bounds = [p.bounds for p in parameters] + + # Sensible defaults + kwargs.setdefault("polish", True) + + if n_threads > 1: + if kwargs.get("workers", 1) != 1: + raise ValueError("Pass either n_threads or workers, not both") + kwargs["updating"] = "deferred" + with ThreadPoolExecutor(max_workers=n_threads) as pool: + kwargs["workers"] = pool.map + result = differential_evolution(cost_fn, bounds=bounds, **kwargs) + else: + result = differential_evolution(cost_fn, bounds=bounds, **kwargs) # Write back identified values for p, val in zip(parameters, result.x): @@ -73,6 +180,253 @@ def identification( return result +class _ScaledBounds: + """Map between parameter values x in [lb, ub] and scaled z in [0, 1].""" + + def __init__(self, parameters, scale): + self.lb = np.array([p.bounds[0] for p in parameters], dtype=float) + self.ub = np.array([p.bounds[1] for p in parameters], dtype=float) + n = len(parameters) + if n == 0: + raise ValueError("At least one Parameter is required") + if not np.all(self.ub > self.lb): + raise ValueError("Every Parameter needs bounds (lb, ub) with lb < ub") + if isinstance(scale, str): + scale = [scale] * n + if len(scale) != n: + raise ValueError(f"scale has {len(scale)} entries for {n} parameters") + self.log = np.zeros(n, dtype=bool) + for i, s in enumerate(scale): + if s == "auto": + self.log[i] = self.lb[i] > 0.0 + elif s == "log": + if self.lb[i] <= 0.0: + raise ValueError( + f"scale='log' needs a positive lower bound (parameter {i}: " + f"bounds {tuple(parameters[i].bounds)})" + ) + self.log[i] = True + elif s != "linear": + raise ValueError(f"Unknown scale '{s}': use 'auto', 'linear' or 'log'") + self._a = np.where(self.log, np.log(np.where(self.log, self.lb, 1.0)), self.lb) + self._b = np.where(self.log, np.log(np.where(self.log, self.ub, 1.0)), self.ub) + + def to_x(self, z): + u = self._a + np.asarray(z, dtype=float) * (self._b - self._a) + x = np.where(self.log, np.exp(u), u) + # exp/log round trips must not leave the bounds by an ulp + return np.clip(x, self.lb, self.ub) + + def to_z(self, x): + x = np.clip(np.asarray(x, dtype=float), self.lb, self.ub) + u = np.where(self.log, np.log(np.where(self.log, x, 1.0)), x) + return np.clip((u - self._a) / (self._b - self._a), 0.0, 1.0) + + +def _identification_least_squares(cost_fn, parameters, n_starts, scale, n_threads, kwargs): + """Multi-start bounded TRF least squares in scaled coordinates.""" + for key in ("x0", "bounds", "method", "jac"): + if key in kwargs: + raise TypeError(f"identification(method='least_squares') does not accept '{key}'") + args = tuple(kwargs.pop("args", ())) + seed = kwargs.pop("seed", kwargs.pop("rng", None)) + rel_step = kwargs.pop("diff_step", None) + rel_step = np.sqrt(np.finfo(float).eps) if rel_step is None else float(rel_step) + kwargs.setdefault("x_scale", 1.0) + n_starts = int(n_starts) + if n_starts < 1: + raise ValueError(f"n_starts must be >= 1, got {n_starts}") + + sb = _ScaledBounds(parameters, scale) + n = sb.lb.size + lock = threading.Lock() + counter = [0] + + def residuals(z): + with lock: + counter[0] += 1 + r = np.atleast_1d(np.asarray(cost_fn(sb.to_x(z), *args), dtype=float)) + if r.ndim != 1: + raise ValueError("cost_fn must return a 1-D array of residuals") + return r + + last = {} # residuals at the last point least_squares evaluated (reused by jac) + + def fun(z): + r = residuals(z) + last["z"], last["r"] = z.copy(), r + return r + + pool = ThreadPoolExecutor(max_workers=n_threads) if n_threads > 1 else None + mapper = pool.map if pool is not None else map + + def jac(z): + # Forward differences in the scaled coordinates, one-sided away from + # the upper bound (as scipy's '2-point' scheme), columns evaluated + # concurrently when a pool is given. The same arithmetic in both + # cases, so the result does not depend on n_threads. + if "z" in last and np.array_equal(last["z"], z): + r0 = last["r"] + else: + r0 = residuals(z) + h = np.where(z + rel_step <= 1.0, rel_step, -rel_step) + trial = [] + for i in range(n): + zi = z.copy() + zi[i] = z[i] + h[i] + trial.append(zi) + cols = list(mapper(residuals, trial)) + J = np.empty((r0.size, n)) + for i, (zi, ri) in enumerate(zip(trial, cols)): + dz = zi[i] - z[i] # the step actually representable + if not np.all(np.isfinite(ri)): + # model failure on that side: try the other one, else freeze + zi = z.copy() + zi[i] = z[i] - h[i] + ri = residuals(zi) if 0.0 <= zi[i] <= 1.0 else ri + dz = zi[i] - z[i] + if not np.all(np.isfinite(ri)): + J[:, i] = 0.0 + continue + J[:, i] = (ri - r0) / dz + return J + + # First start: the values set on the Parameters, the middle of the scaled + # range for those never set (the geometric mean of the bounds in log scale) + z_set = sb.to_z([p.value for p in parameters]) + starts = [np.array([0.5 if getattr(p, "_value", None) is None else z + for p, z in zip(parameters, z_set)])] + if n_starts > 1: + # Latin hypercube: one point per stratum of [0.05, 0.95] in each direction + rng = np.random.default_rng(seed) + m = n_starts - 1 + strata = np.argsort(rng.random((m, n)), axis=0) + starts += list(0.05 + 0.9 * (strata + rng.random((m, n))) / m) + + best = None + runs = [] + try: + for z0 in starts: + n0 = counter[0] + last.clear() + try: + res = least_squares(fun, z0, jac=jac, bounds=(0.0, 1.0), method="trf", + **kwargs) + except ValueError as e: + if "not finite" not in str(e): + raise + # cost_fn failed at this starting point: try the next one + runs.append(OptimizeResult(x0=sb.to_x(z0), x=None, cost=np.inf, + nfev=counter[0] - n0, status=-1, + message=str(e), success=False)) + continue + runs.append(OptimizeResult(x0=sb.to_x(z0), x=sb.to_x(res.x), cost=res.cost, + nfev=counter[0] - n0, status=res.status, + message=res.message, success=res.success)) + if best is None or res.cost < best.cost: + best = res + finally: + if pool is not None: + pool.shutdown() + + if best is None: + raise RuntimeError( + "least_squares failed from every starting point: cost_fn returned " + "non-finite residuals there" + ) + best.z = best.x + best.x = sb.to_x(best.z) + best.nfev = counter[0] + best.starts = runs + return best + + +def calc_residuals( + y_exp: List[np.ndarray], + y_num: List[np.ndarray], + w_test: Optional[np.ndarray] = None, + w_response: Optional[List[np.ndarray]] = None, + w_point: Optional[List[np.ndarray]] = None, + metric: str = "mse", +) -> np.ndarray: + """ + Weighted residual vector whose sum of squares is :func:`calc_cost`. + + For the least-squares route of :func:`identification`, which minimizes a + sum of squared residuals instead of a scalar cost: + ``np.sum(calc_residuals(...) ** 2) == calc_cost(...)`` with the same + arguments, for the three metrics that are sums of squares: ``"mse"``, + ``"nmse"`` and ``"nmse_per_response"``. + + Parameters + ---------- + y_exp, y_num, w_test, w_response, w_point, metric + As in :func:`calc_cost`; *metric* must be ``"mse"``, ``"nmse"`` or + ``"nmse_per_response"``. + + Returns + ------- + numpy.ndarray + 1-D residuals ``sqrt(weight) * (y_num - y_exp)``, tests concatenated + in order, each test flattened row by row (point by point). + """ + if metric not in ("mse", "nmse", "nmse_per_response"): + raise ValueError( + f"calc_residuals supports the sum-of-squares metrics 'mse', 'nmse' " + f"and 'nmse_per_response', not '{metric}'" + ) + n_tests = len(y_exp) + if len(y_num) != n_tests: + raise ValueError( + f"y_exp has {n_tests} tests but y_num has {len(y_num)}" + ) + + exp_2d, res_2d, w_2d = [], [], [] + for i in range(n_tests): + exp_i = np.asarray(y_exp[i], dtype=float) + num_i = np.asarray(y_num[i], dtype=float) + if exp_i.ndim == 1: + exp_i = exp_i.reshape(-1, 1) + if num_i.ndim == 1: + num_i = num_i.reshape(-1, 1) + if num_i.shape != exp_i.shape: + raise ValueError( + f"Test {i}: y_exp shape {exp_i.shape} != " + f"y_num shape {num_i.shape}" + ) + W_i = np.ones_like(exp_i) + if w_test is not None: + W_i *= w_test[i] + if w_response is not None: + W_i *= np.asarray(w_response[i], dtype=float)[None, :] + if w_point is not None: + W_i *= np.abs(w_point[i]) + exp_2d.append(exp_i) + res_2d.append(num_i - exp_i) + w_2d.append(W_i) + + if metric == "nmse_per_response": + # mean over response columns k of sum_k(w r^2) / sum_k(y_exp^2) + n_cols = max(e.shape[1] for e in exp_2d) + denom = np.zeros(n_cols) + for e in exp_2d: + denom[: e.shape[1]] += np.sum(e ** 2, axis=0) + denom = np.where(denom > 1e-30, denom, 1.0) + return np.concatenate([ + (np.sqrt(w / (n_cols * denom[: r.shape[1]])) * r).ravel() + for r, w in zip(res_2d, w_2d) + ]) + + w_flat = np.concatenate([w.ravel() for w in w_2d]) + r = np.sqrt(w_flat / np.sum(w_flat)) * np.concatenate([r.ravel() for r in res_2d]) + if metric == "nmse": + e_flat = np.concatenate([e.ravel() for e in exp_2d]) + var = np.average((e_flat - np.average(e_flat, weights=w_flat)) ** 2, weights=w_flat) + if var > 1e-30: + r = r / np.sqrt(var) + return r + + def calc_cost( y_exp: List[np.ndarray], y_num: List[np.ndarray], @@ -115,9 +469,12 @@ def calc_cost( - ``"mse"`` — Mean Squared Error (default) - ``"nmse"`` — Normalized MSE (divided by variance of all data) - ``"nmse_per_response"`` — NMSE computed per response column, - then averaged. Each column is divided by its own - ``sum(y_exp²)``, balancing responses of different magnitudes - (e.g., force in N vs displacement in mm). + then averaged. Column *k* gives ``sum_tests SSE_k / + sum_tests sum(y_exp[:, k]²)``: the tests are pooled within a + column, so the responses of different magnitudes are balanced + (e.g., force in N vs displacement in mm), but the tests of one + column are not (a test with larger values weighs more; use + *w_test* to rebalance them). - ``"rmse"`` — Root Mean Squared Error - ``"mae"`` — Mean Absolute Error - ``"mape"`` — Mean Absolute Percentage Error (same definition as @@ -218,7 +575,11 @@ def calc_cost( def _nmse_per_response( y_exp_2d, y_num_2d, n_tests, w_test, w_response, w_point, ): - """Compute NMSE independently per response column, then average.""" + """Compute NMSE independently per response column, then average. + + The tests are pooled within a column: NMSE_k = sum_i SSE_ik / sum_i sum(y_exp_ik**2), + not the mean over the tests of each test's own NMSE. + """ # Collect all columns across tests # First pass: determine n_resp per test and validate col_costs = {} # col_index -> list of (mse_col, denom_col, weight) diff --git a/python-setup/simcoon/parameter.py b/python-setup/simcoon/parameter.py index f2ed154e5..170e95d6c 100644 --- a/python-setup/simcoon/parameter.py +++ b/python-setup/simcoon/parameter.py @@ -1,7 +1,10 @@ +import functools import os import re -import shutil -from typing import List, Optional, Tuple, Union +import warnings +from typing import List, Optional, Sequence, Tuple, Union + +import numpy as np class Parameter: @@ -90,70 +93,146 @@ def read_parameters( return params -def copy_parameters( - params: List[Parameter], - src_path: Union[str, os.PathLike], - dst_path: Union[str, os.PathLike], -) -> None: - """ - Copy parameters file from a source path to a destination path, so that key values can be applied - :param params: List of Parameter objects - :param src_path: Source path - :param dst_path: Destination path - :return: None - """ +def _input_files(items) -> List[str]: + """Distinct ``sim_input_files`` of *items*, in order of first appearance.""" + files = [] + for item in items: + if not all(isinstance(f, str) for f in item.sim_input_files): + raise TypeError("All elements in sim_input_files must be strings.") + for f in item.sim_input_files: + if f not in files: + files.append(f) + return files - if not isinstance(src_path, (str, os.PathLike)): - raise TypeError( - f"Invalid type: {type(src_path).__name__}. Expected str or os.PathLike." - ) - if not isinstance(dst_path, (str, os.PathLike)): - raise TypeError( - f"Invalid type: {type(dst_path).__name__}. Expected str or os.PathLike." - ) +@functools.lru_cache(maxsize=256) +def _key_pattern(key: str) -> "re.Pattern": + # Trailing ``(?!\w)`` lookahead prevents ``@E`` from matching ``@Et``. + # ``re.escape`` keeps user keys with regex metachars literal. + return re.compile(re.escape(key) + r"(?!\w)") - for param in params: - if not all(isinstance(item, str) for item in param.sim_input_files): - raise TypeError("All elements in sim_input_files must be strings.") - for ifiles in param.sim_input_files: - src_files = os.path.join(src_path, ifiles) - dst_files = os.path.join(dst_path, ifiles) - shutil.copy(src_files, dst_files) +def substitute_keys(content: str, items, fname: Optional[str] = None) -> str: + """ + Replace the keys of *items* by their values in the text *content*. + + :param content: text of a template file + :param items: Parameter or Constant objects (anything with ``key``, + ``value`` and ``sim_input_files``) + :param fname: when given, only the items listing this file in their + ``sim_input_files`` are substituted + :return: the substituted text + """ + # Longest keys first so e.g. ``@Et`` is replaced before ``@E`` and a + # shorter key cannot clobber a longer one that contains it. + ordered = sorted(items, key=lambda it: -len(it.key)) + for item in ordered: + if fname is not None and fname not in item.sim_input_files: + continue + replacement = str(item.value) + # the ``lambda`` repl avoids backref interpretation in the value + content = _key_pattern(item.key).sub(lambda _m: replacement, content) + return content -def apply_parameters( - params: List[Parameter], - dst_path: Union[str, os.PathLike], -) -> None: +class KeyTemplate: """ - Apply parameters, i.e. replace in file the value of the key with the value of the Parameter - :param params: List of Parameter objects - :param src_path: Source path - :param dst_path: Destination path - :return: None + Template files held in memory, written with the keys substituted. + + The templates (e.g. the files of ``keys/``) are read once, at construction. + Each :meth:`write` substitutes the keys of the parameters, then those of the + constants, by their current values and writes every working file once. A key + is replaced only in the files its object lists in ``sim_input_files``. + + :param src_path: directory holding the template files (e.g. ``keys``) + :param params: Parameter objects whose ``sim_input_files`` name the templates + :param consts: Constant objects, substituted after the parameters. A + constant holds one value per test (``input_values``): + ``write(dst, test=i)`` substitutes the value of test *i*. + + Example:: + + template = KeyTemplate("keys", params, consts) + def cost(x): + for p, v in zip(params, x): + p.value = v + for i in range(n_tests): # e.g. one fibre angle per specimen + template.write(f"data/test_{i}", test=i) + ... """ - if not isinstance(dst_path, (str, os.PathLike)): - raise TypeError( - f"Invalid type: {type(dst_path).__name__}. Expected str or os.PathLike." - ) - # Longest keys first so e.g. ``@Et`` is replaced before ``@E`` and a - # shorter key cannot clobber a longer one that contains it. - ordered = sorted(params, key=lambda p: -len(p.key)) - - for param in ordered: - # Trailing ``(?!\w)`` lookahead prevents ``@E`` from matching ``@Et``. - # ``re.escape`` keeps user keys with regex metachars literal; the - # ``lambda`` repl avoids backref interpretation in the value. - pattern = re.compile(re.escape(param.key) + r"(?!\w)") - replacement = str(param.value) - for ifiles in param.sim_input_files: - mod_files = os.path.join(dst_path, ifiles) - - with open(mod_files, "r", encoding="utf-8") as in_files: - content = in_files.read() - modified_content = pattern.sub(lambda _m: replacement, content) - with open(mod_files, "w", encoding="utf-8") as ou_files: - ou_files.write(modified_content) + def __init__( + self, + src_path: Union[str, os.PathLike], + params: Sequence = (), + consts: Sequence = (), + ) -> None: + if not isinstance(src_path, (str, os.PathLike)): + raise TypeError( + f"Invalid type: {type(src_path).__name__}. Expected str or os.PathLike." + ) + self.params = list(params) + self.consts = list(consts) + self._single_value_warned = set() + self.templates = {} + for ifiles in _input_files(self.params + self.consts): + with open(os.path.join(src_path, ifiles), "r", encoding="utf-8") as f: + self.templates[ifiles] = f.read() + + def _consts_of_test(self, test: Optional[int]) -> list: + """Constants with the value of test *test* (``input_values[test]``); + their own ``value`` when *test* is None.""" + if test is None: + return self.consts + consts = [] + for c in self.consts: + values = np.atleast_1d(c.input_values) + if values.size == 1: + if c.key not in self._single_value_warned: + self._single_value_warned.add(c.key) + warnings.warn( + f"constant {c.key} has a single value for several tests: " + "it is not a constant of the identification but a fixed " + "parameter of the simulation, write it in the template " + "instead of a key", + stacklevel=3, + ) + consts.append(c._replace(value=values[0])) + elif not -values.size <= test < values.size: + raise IndexError( + f"test {test} out of range for constant {c.key}, which has " + f"{values.size} values" + ) + else: + consts.append(c._replace(value=values[test])) + return consts + + def render(self, fname: str, test: Optional[int] = None) -> str: + """Text of the template *fname* with the current values substituted. + + :param test: index of the test: each constant takes its value + ``input_values[test]``; by default, its ``value`` + """ + return self._render(fname, self._consts_of_test(test)) + + def _render(self, fname: str, consts: list) -> str: + content = substitute_keys(self.templates[fname], self.params, fname) + return substitute_keys(content, consts, fname) + + def write(self, dst_path: Union[str, os.PathLike], test: Optional[int] = None) -> None: + """Write every template, substituted with the current values, to *dst_path*. + + :param test: index of the test: each constant takes its value + ``input_values[test]`` (e.g. the fibre angle of that + specimen); by default, its ``value``. A constant with a + single value raises a warning: it is the same in every + test, so it belongs in the template, not in a key. + """ + if not isinstance(dst_path, (str, os.PathLike)): + raise TypeError( + f"Invalid type: {type(dst_path).__name__}. Expected str or os.PathLike." + ) + consts = self._consts_of_test(test) + for fname in self.templates: + with open(os.path.join(dst_path, fname), "w", encoding="utf-8") as f: + f.write(self._render(fname, consts)) diff --git a/python-setup/simcoon/pyumat.py b/python-setup/simcoon/pyumat.py index c203b5379..9d1a4ba98 100644 --- a/python-setup/simcoon/pyumat.py +++ b/python-setup/simcoon/pyumat.py @@ -94,7 +94,8 @@ class PythonUMAT(ABC): out-of-plane strain), 2 = plane stress, 1 = uniaxial. ``nshr`` = number of shear components. * ``tangent_mode``: 0 = return the elastic operator as ``Lt``, 1 = continuum - tangent, 2 = algorithmic (consistent) tangent. + tangent, 2 = algorithmic (consistent) tangent. ``integrator="closest_point"`` + is refused for a Python law. * ``Wm`` is the accumulated ``(Wm, Wm_r, Wm_ir, Wm_d)`` at the beginning of the increment and must be returned accumulated. * returns ``(sigma (6,), Lt (6,6), statev (nstatev,), Wm (4,)[, L (6,6)])`` as diff --git a/python-setup/simcoon/solver/__init__.py b/python-setup/simcoon/solver/__init__.py index 9a26c92c2..a6f7f7c3a 100644 --- a/python-setup/simcoon/solver/__init__.py +++ b/python-setup/simcoon/solver/__init__.py @@ -18,12 +18,13 @@ BLOCK_TYPES, CONTROL_TYPES, CORATE_TYPES, + INTEGRATORS, KINEMATIC_FORMS, + NCP_METHODS, STEP_MODES, TANGENT_MODES, THERMAL_CONTROL, tangent_algorithmic, - tangent_closest_point, tangent_continuum, tangent_default, tangent_none, @@ -46,9 +47,8 @@ "micromechanics", "Block", "StepMeca", "StepThermomeca", "SolverResults", "solve", "BLOCK_TYPES", "CONTROL_TYPES", "CORATE_TYPES", "KINEMATIC_FORMS", "STEP_MODES", - "TANGENT_MODES", "THERMAL_CONTROL", - "tangent_none", "tangent_continuum", "tangent_algorithmic", - "tangent_closest_point", "tangent_default", + "INTEGRATORS", "NCP_METHODS", "TANGENT_MODES", "THERMAL_CONTROL", + "tangent_none", "tangent_continuum", "tangent_algorithmic", "tangent_default", "load_material_json", "load_path_json", "load_simulation_json", "save_material_json", "save_path_json", ] diff --git a/python-setup/simcoon/solver/core.py b/python-setup/simcoon/solver/core.py index c7720b740..b7343f4ec 100644 --- a/python-setup/simcoon/solver/core.py +++ b/python-setup/simcoon/solver/core.py @@ -11,7 +11,7 @@ from simcoon.pyumat import UMAT_NAME, registered from .blocks import Block, StepMeca, StepThermomeca -from .maps import TANGENT_MODES, as_code, kinematic_code, tangent_default +from .maps import INTEGRATORS, NCP_METHODS, TANGENT_MODES, as_code, kinematic_code, tangent_default from .micromechanics import to_phase_dicts, euler_angles from .results import SolverResults @@ -30,6 +30,8 @@ def solve( record_tangent: bool = True, raise_on_abort: bool = True, kinematic_form: Union[str, int, None] = None, + ncp: str = "fischer_burmeister", + integrator: str = "cutting_plane", **params, ) -> SolverResults: """Solve a homogeneous loading path with the C++ simcoon solver, in memory. @@ -73,9 +75,9 @@ def solve( there). Under the rate form MODUL requires log_R (the modular Hencky composition). tangent_mode : str or int - Tangent operator mode: 'none', 'continuum', 'algorithmic' (default) - or 'closest_point' (closest-point projection integrator with its exact - operator where a law has one; the algorithmic operator elsewhere). + Tangent operator handed to the global Newton: 'none' (the elastic + operator), 'continuum' or 'algorithmic' (default), the consistent + operator of the local integrator that ran (see ``integrator``). solver_type : int 0 = classic Newton-Raphson (default), 1 = RNL (control_type 1 only). orientation : simcoon.Rotation, dict or sequence of 3 floats @@ -95,10 +97,34 @@ def solve( instead of returning the partial history. The solver aborts when the Newton loop does not converge at the minimal increment, or when the increment falls below ``Dn_mini`` with ``inforce=0``. + ncp : str + Complementarity function of the local cutting plane: + 'fischer_burmeister' (default), or 'min', the classic cutting-plane step + on a single surface with a Fischer-Burmeister fallback: about half the + local iterations for isotropic or linear (Prager) kinematic hardening, + the same return to the local tolerance. Read by EPICP, EPKCP and MODUL + (refused for an Armstrong-Frederick back stress); any other law, and the + thermomechanical blocks, raise ValueError for 'min', as does the + closest-point integrator. See the UMAT catalog. + integrator : str, optional + Local integrator of the laws with a return mapping: 'cutting_plane' + (the default) or 'closest_point', the backward-Euler closest-point + projection, a different discrete update (EPCHA: 1 to 2 % apart on + coarse increments) with its exact operator. It takes + ``tangent_mode='algorithmic'``. Served by EPICP, EPCHA, EPJCK, MODUL + and its adapters and the SMA kernels (mechanical), EPICP and EPKCP + (thermomechanical): any other law raises ValueError. Inside MODUL, a + Tresca or Drucker row keeps the cutting plane. **params Numeric solver controls forwarded to the C++ loop: div_tnew_dt, - mul_tnew_dt, miniter, maxiter, inforce, precision, lambda_solver - (penalty stiffness of the strain-driven components). + mul_tnew_dt, miniter, maxiter, inforce, precision, precision_rel, + lambda_solver (penalty stiffness of the strain-driven components). + ``precision`` is the absolute tolerance on the norm of the global + residual (default 1e-6). ``precision_rel > 0`` makes it + ``max(precision, precision_rel * |tau|)``, ``|tau|`` the norm of the + Kirchhoff stress (Voigt): a stress-scaled criterion, independent of the + stress unit (1e-7 to 1e-8 is typical); 0, the default, keeps the + absolute one. Returns ------- @@ -135,7 +161,17 @@ def solve( corate_type = kinematic_code(kinematic_form, corate) run_params = dict(params) + # Migration message of 2.x, removed in 3.0 (CHANGELOG.md, "Planned for 3.0") + if isinstance(tangent_mode, (str, int)) and tangent_mode in ("closest_point", 3): + raise ValueError("tangent_mode 'closest_point' (3) is no longer a tangent choice: give " + "integrator='closest_point' with tangent_mode='algorithmic'") run_params["tangent_mode"] = as_code(tangent_mode, TANGENT_MODES, "tangent mode") + if not isinstance(ncp, str) or ncp not in NCP_METHODS: + raise ValueError(f"ncp must be one of {sorted(NCP_METHODS)}; got {ncp!r}") + run_params["ncp"] = NCP_METHODS[ncp] + if not isinstance(integrator, str) or integrator not in INTEGRATORS: + raise ValueError(f"integrator must be one of {sorted(INTEGRATORS)}; got {integrator!r}") + run_params["integrator"] = INTEGRATORS[integrator] blocks_py = [] T_run = float(T_init) diff --git a/python-setup/simcoon/solver/maps.py b/python-setup/simcoon/solver/maps.py index 2e8ceba3b..4d2f7a965 100644 --- a/python-setup/simcoon/solver/maps.py +++ b/python-setup/simcoon/solver/maps.py @@ -4,8 +4,9 @@ tangent_none, tangent_continuum, tangent_algorithmic, - tangent_closest_point, tangent_default, + integrator_cutting_plane, + integrator_closest_point, corate_type_total as _corate_type_total, ) @@ -58,12 +59,23 @@ "tabular": 3, } -#: Tangent operator modes (tangent_mode carried by the state variables) +#: Tangent operators handed to the global Newton (the closest point is an integrator, INTEGRATORS) TANGENT_MODES = { "none": tangent_none, "continuum": tangent_continuum, "algorithmic": tangent_algorithmic, - "closest_point": tangent_closest_point, +} + +#: Local integrators of the laws with a return mapping (folded with tangent_mode into the kernel code) +INTEGRATORS = { + "cutting_plane": integrator_cutting_plane, + "closest_point": integrator_closest_point, +} + +#: Complementarity functions of the local cutting plane (ncp carried by the state variables) +NCP_METHODS = { + "fischer_burmeister": 0, + "min": 1, } diff --git a/python-setup/simcoon/solver/micromechanics.py b/python-setup/simcoon/solver/micromechanics.py index 5c17ea667..d6a723132 100644 --- a/python-setup/simcoon/solver/micromechanics.py +++ b/python-setup/simcoon/solver/micromechanics.py @@ -814,6 +814,7 @@ def discretize_odf( axis=(0.0, 0.0, 1.0), angle_range=(0.0, 180.0), rotate_material: bool = True, + weights: str = "simpson", ) -> List: """Split one phase into ``nphases`` phases whose orientations follow an ODF. @@ -840,6 +841,23 @@ def discretize_odf( ``phi`` from an unrotated phase is ``axis=(0, 0, 1)``, a sweep of ``theta`` is ``axis=(1, 0, 0)``. + ``weights="fourier"`` replaces the Simpson fractions by Fourier-projected ones: + ``w_k = d * (r_0 + 2 Re(r_1 e^{2i alpha_k} + r_2 e^{4i alpha_k}))``, ``r_m`` the Fourier + coefficients of the density over one half turn. A tensor that rotates with the phase + about ``axis`` (``R T R^T`` in Voigt notation) is a trigonometric polynomial of degree + 4 in ``alpha``: only ``r_0``, ``r_1``, ``r_2`` enter its average, and these weights + give that average exactly with ``nphases >= 5``. This is + the case of the Mori-Tanaka scheme (MIMTN) with an isotropic matrix and a swept phase + whose material turns with it (``rotate_material``) or is isotropic: a handful of + phases then replaces hundreds. For other schemes (the self-consistent medium turns + with nothing) the weights are only a quadrature among others. They need + ``angle_range`` to span one half turn. The projected fractions are negative for most + peaked densities: a non-negative set reproducing the same three modes exactly is then + taken instead (non-negative least squares; it keeps only a few of the ``nphases`` + directions, the phases of zero fraction being dropped from the result). When the + density is too peaked for any such set on these nodes (a few degrees wide), the + Simpson fractions are used, with a warning. ``nphases`` of 12 to 36 is a good start. + Returns a new list of new objects, numbered by position; the caller's phases are left untouched. ``coatingof`` indices pointing past the swept phase are shifted; a coating OF the swept phase is refused (which of its copies would it coat?). @@ -863,23 +881,37 @@ def discretize_odf( if a_max <= a_min: raise ValueError("angle_range must be (min, max) with max > min, in degrees") + if weights not in ("simpson", "fourier"): + raise ValueError(f"weights must be 'simpson' or 'fourier', got {weights!r}") + parent = phases[num_phase] d = (a_max - a_min) / nphases alphas = a_min + d * np.arange(nphases) # Simpson over each bin, the density being periodic over 180 deg (director distribution) x = np.concatenate([alphas - d / 2, alphas, alphas + d / 2]) % 180.0 rho = get_densities_ODF(x, peaks) - weights = ( + fractions = ( d / 6.0 * (rho[:nphases] + 4.0 * rho[nphases : 2 * nphases] + rho[2 * nphases :]) ) - if weights.sum() <= 0.0: + if fractions.sum() <= 0.0: raise ValueError("the ODF density is zero over the whole angle_range") - weights *= parent.concentration / weights.sum() + if weights == "fourier": + fourier = _fourier_fractions(peaks, alphas, d, a_max - a_min, nphases) + if fourier is None: + warnings.warn( + f"discretize_odf: no non-negative Fourier weights on {nphases} phases for " + "this density (too peaked); Simpson weights used instead" + ) + else: + # the phases of zero weight are dropped + keep = fourier > 0.0 + alphas, fractions = alphas[keep], fourier[keep] + fractions *= parent.concentration / fractions.sum() swept = [] - for alpha, w in zip(alphas, weights): + for alpha, w in zip(alphas, fractions): rot = Rotation.from_rotvec(np.deg2rad(alpha) * n) copy_k = copy.deepcopy(parent) copy_k.concentration = float(w) @@ -896,13 +928,45 @@ def discretize_odf( f"phase {ph.number} coats phase {num_phase}, which is being discretised" ) if coated > num_phase: - ph.coatingof = coated + nphases - 1 + ph.coatingof = coated + len(swept) - 1 out = others[:num_phase] + swept + others[num_phase + 1 :] for i, ph in enumerate(out): ph.number = i return out +def _fourier_fractions(peaks, alphas, d, span, nphases) -> Optional[np.ndarray]: + """Fractions of :func:`discretize_odf` that reproduce the Fourier modes 0, 1, 2 (in + 2 alpha) of the density exactly: the projected ones when none is negative, else a + non-negative set (non-negative least squares on the five moment equations, a few + non-zero), else None when no non-negative set of these nodes is exact.""" + from scipy.optimize import nnls + + if not np.isclose(span, 180.0, rtol=0.0, atol=1e-9): + raise ValueError("weights='fourier' needs angle_range to span one half turn (180 degrees)") + if nphases < 5: + raise ValueError("weights='fourier' needs nphases >= 5 (exact for the modes up to 4 alpha)") + # coefficients over one period of the director density: the trapezoidal rule is spectral + xf = np.arange(4096) * (180.0 / 4096) + rf = get_densities_ODF(xf, peaks) + two_a = 2.0 * np.deg2rad(xf) + r = [np.mean(rf * np.exp(-1j * m * two_a)) for m in range(3)] + two_alpha = 2.0 * np.deg2rad(alphas) + projected = d * np.real( + r[0] + 2.0 * (r[1] * np.exp(1j * two_alpha) + r[2] * np.exp(2j * two_alpha)) + ) + if projected.min() >= 0.0: + return projected + # sum_k w_k e^{2im alpha_k} = 180 conj(r_m), m = 0, 1, 2 (real and imaginary parts) + moments = np.vstack([np.ones(nphases), np.cos(two_alpha), np.sin(two_alpha), + np.cos(2.0 * two_alpha), np.sin(2.0 * two_alpha)]) + target = 180.0 * np.array([r[0].real, r[1].real, -r[1].imag, r[2].real, -r[2].imag]) + w, residual = nnls(moments, target) + if residual > 1e-12 * np.linalg.norm(target): + return None + return w + + # ============================================================================= # Handing phases to the C++ side, in memory # ============================================================================= @@ -1008,6 +1072,16 @@ def L_eff( mean-field model (MIHEN, MIMTN, MISCN, MIPLN) ``phases`` gives its sub-phases, as the dataclasses of this module or the dicts :func:`to_phase_dicts` makes of them. This is ``sim.L_eff``; ``sim._core.L_eff`` is the extension entry it wraps. + + The ``props`` of a mean-field model hold its settings: ``[mp, np]`` (MIHEN), + ``[mp, np, n_matrix, closed_form, rule]`` (MIMTN), + ``[mp, np, n_matrix, start, closed_form, rule, anderson, rtol]`` (MISCN), ``[]`` + (MIPLN), the trailing slots being optional and 0 by default (the historical + behaviour): ``closed_form=1`` takes the Eshelby tensors in closed form when the + medium is isotropic and the inclusion a sphere or a spheroid of axis 1, ``rule=1`` + integrates them on a half sphere with a trapezoidal rule in phi (half the points), + ``anderson=m`` accelerates the self-consistent fixed point (m = 2 is a good choice) + and ``rtol > 0`` stops it on a relative criterion. See the UMAT catalog. """ angles = None if orientation is None else euler_angles(orientation) dicts = None if phases is None else to_phase_dicts(phases) diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_constitutive.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_constitutive.hpp index 0c779e638..48b0577f4 100755 --- a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_constitutive.hpp +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_constitutive.hpp @@ -684,12 +684,13 @@ constexpr auto umat = R"pbdoc( (2, 1) plane stress, (2, 3) plane stress with transverse shears; any other ndi < 3 combination raises ValueError. n_threads : int - Point-loop threads: 1 runs serially on every platform; otherwise it - caps the worker threads on Windows (0 = all hardware threads), while - macOS (GCD) and Linux (OpenMP) size the pool themselves. Batches of - 100 points or fewer always run serially. + Point-loop threads, at most (the calling thread included): 1 runs + serially, 0 lets the runtime use the whole machine (GCD on macOS, + OpenMP on Linux, one thread per hardware thread on Windows). Batches + of 100 points or fewer always run serially. tangent_mode : int - 0 none, 1 continuum, 2 algorithmic (default). + Operator returned as Lt: 0 none (the elastic L), 1 continuum, 2 algorithmic + (default), the consistent operator of the integrator that ran. corate : int or str, optional Former name of ``kinematic_form``: the same value under either name. As an objective rate, it is the one the returned tangent is expressed @@ -780,6 +781,19 @@ constexpr auto umat = R"pbdoc( sees T = temp at the start and T + DT at the end of the increment, as umat_T does: thermal expansion and temperature-dependent properties follow the prescribed temperature. Zero when not given. + ncp : str + Complementarity function of the local cutting plane: + 'fischer_burmeister' (default), or 'min', the classic cutting-plane step on a + single surface with a Fischer-Burmeister fallback, about half the local + iterations for isotropic or linear (Prager) kinematic hardening. Read by + EPICP, EPKCP and MODUL (refused for an Armstrong-Frederick back stress); + any other law, and the closest-point integrator, raise ValueError for 'min'. + See the UMAT catalog. + integrator : str + Local integrator of the laws with a return mapping: 'cutting_plane' + (default) or 'closest_point' (backward Euler, a different discrete + update, with its exact operator; it takes tangent_mode=2). A law or a + row without a closest-point update keeps the cutting plane. Returns ------- @@ -797,8 +811,8 @@ constexpr auto umat = R"pbdoc( and F1 given, sigma is multiplied by det(F0) on the way in and divided by det(F1) on the way out, so the caller always exchanges Cauchy. Without F0/F1 (or with a degenerate F) no conversion is applied, which is exact at small - strain. Only the plugin adapters (UMEXT, UMABA) are Cauchy-native and pass - through unconverted. + strain. The external plugins (UMEXT, UMABA, UMANS) are not served here: they run + through the solver (simcoon.solver.solve), small-strain route only. Wm is the work per reference volume. The kernel alone accumulates 1/2 (tau_start + tau_end) : Detot on the strain increment it is handed, @@ -849,7 +863,7 @@ constexpr auto umat_T = R"pbdoc( T, DT : numpy.ndarray Temperature at the start of the increment and its increment, one entry per point. - ndi, nshr, n_threads, tangent_mode, start, condense + ndi, nshr, n_threads, tangent_mode, start, condense, integrator As umat. Under condense='material_face' (finite strain, ndi < 3) dSdE, dSdT and drdE are the material derivatives dS/dE, dS/dtheta and dr/dE, condensed on the kept components (tangent_output='material'). diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_hyperelastic.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_hyperelastic.hpp index 3bfcc6879..668ed249a 100644 --- a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_hyperelastic.hpp +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_hyperelastic.hpp @@ -8,16 +8,20 @@ constexpr auto isochoric_invariants = R"pbdoc( Parameters ---------- input : pybind11::array_t - A 3x3 matrix representing the left Cauchy-Green deformation tensor \( \mathbf{b} \). + A 3x3 matrix representing the left Cauchy-Green deformation tensor \( \mathbf{b} \), its + Voigt vector (6), or the three principal stretches (3). A batch: a 3x3xN array of + tensors, or a 6xN array of Voigt columns. J : double, optional - The determinant of the transformation gradient \( \mathbf{F} \). Default is 0. + The determinant of the transformation gradient \( \mathbf{F} \). Default is 0, to compute + it from each tensor; a batch shares the J given. copy : bool, optional Whether to copy the input data. Default is true. Returns ------- pybind11::array_t - A column vector of dimension 3 containing the three isochoric invariants. + A column vector of dimension 3 containing the three isochoric invariants; for a batch, a + 3xN array (one column per point, computed in parallel, the GIL released). Notes ----- @@ -48,7 +52,8 @@ constexpr auto isochoric_pstretch = R"pbdoc( Parameters ---------- input : pybind11::array_t - A 3x3 matrix representing the left Cauchy-Green tensor \( \mathbf{b} \) or the Eulerian stretch tensor \( \mathbf{v} \). + A 3x3 matrix representing the left Cauchy-Green tensor \( \mathbf{b} \) or the Eulerian stretch tensor \( \mathbf{v} \), + or its Voigt vector (6). A batch: a 3x3xN array of such tensors, or a 6xN array of Voigt columns. input_tensor : str, optional A string indicating the type of input tensor ("b" for \( \mathbf{b} \) or "V" for \( \mathbf{v} \)). Default is "V". J : double, optional @@ -59,7 +64,10 @@ constexpr auto isochoric_pstretch = R"pbdoc( Returns ------- pybind11::array_t - A column vector of dimension 3 containing the three isochoric principal stretches. + A column vector of dimension 3 containing the three isochoric principal stretches; for a + batch, a 3xN array (one column per point, computed in parallel, the GIL released; J is + shared by every point, 0 to compute it from each tensor, for a single point as for a + batch). Notes ----- diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_kinematics.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_kinematics.hpp index 5a0713b97..7755771bc 100644 --- a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_kinematics.hpp +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_kinematics.hpp @@ -164,12 +164,13 @@ constexpr auto RU_decomposition = R"pbdoc( Parameters ---------- F : pybind11::array_t - Transformation gradient (3x3 matrix). + Transformation gradient (3x3 matrix), or a batch of them (3x3xN array). Returns ------- tuple - Rotation matrix (3x3) and right stretch tensor (3x3). + Rotation matrix (3x3) and right stretch tensor (3x3); for a batch, two 3x3xN arrays + (computed per point in parallel, the GIL released). Examples -------- @@ -189,12 +190,13 @@ constexpr auto VR_decomposition = R"pbdoc( Parameters ---------- F : pybind11::array_t - Transformation gradient (3x3 matrix). + Transformation gradient (3x3 matrix), or a batch of them (3x3xN array). Returns ------- tuple - Left stretch tensor (3x3) and rotation matrix (3x3). + Left stretch tensor (3x3) and rotation matrix (3x3); for a batch, two 3x3xN arrays + (computed per point in parallel, the GIL released). Examples -------- diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_stress.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_stress.hpp index 11a1ffdc6..db564dce1 100644 --- a/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_stress.hpp +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Continuum_mechanics/doc_stress.hpp @@ -17,6 +17,7 @@ sigma : pybind11::array_t - 1D array with 6 elements: Voigt vector [s11, s22, s33, s23, s13, s12]. - 2D array with shape (3,3): full 3x3 stress matrix. - 2D array with shape (6,N): list of N Voigt stress vectors (columns). + - 3D array with shape (3,3,N): N 3x3 stress matrices. F : pybind11::array_t The deformation gradient. Accepted shapes: - (3,3) for a single conversion. diff --git a/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp index 2886d9057..85fb7b383 100644 --- a/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp +++ b/simcoon-python-builder/include/simcoon/docs/Libraries/Maths/doc_rotation.hpp @@ -322,4 +322,63 @@ constexpr auto dR_drotvec_free = R"pbdoc( Rotation in Exponential Coordinates", J. Math. Imaging Vis., 2015. )pbdoc"; +constexpr auto rotate_voigt = R"pbdoc( + Rotate a batch of Voigt tensors by rotation matrices, one per tensor. + + The batch counterpart of ``Rotation.apply_stress`` / ``apply_strain`` / + ``apply_stiffness`` / ``apply_compliance`` / ``apply_strain_concentration`` / + ``apply_stress_concentration`` on the layout of :func:`simcoon.umat` and + :func:`simcoon.objective_rate`: tensors as columns ``(6, N)`` or slices + ``(6, 6, N)``, rotations as slices ``(3, 3, N)`` (e.g. the ``DR`` returned by + ``objective_rate``). It builds no ``Rotation`` object: the loop over the points + runs in C++, in parallel and without the GIL. + + ``rotate_voigt(values, DR, kind, active)`` equals + ``Rotation.from_matrix(DR.transpose(2, 0, 1)).apply_(values, active)`` up to + round-off (a few ulps). + + Parameters + ---------- + values : numpy.ndarray + Stress or strain vectors, shape ``(6,)`` or ``(6, N)``, or 6x6 tensors, shape + ``(6, 6)`` or ``(6, 6, N)``, depending on ``kind``. Strains use engineering + shear components (``2 eps_12``, ...), as everywhere in simcoon. + DR : numpy.ndarray + Rotation matrices, shape ``(3, 3, N)``, or ``(3, 3)`` / ``(3, 3, 1)`` for one + rotation applied to every tensor. + kind : str + ``"stress"``, ``"strain"`` (vectors), ``"stiffness"``, ``"compliance"``, + ``"strain_concentration"`` or ``"stress_concentration"`` (6x6 tensors): the rule + of the corresponding ``Rotation.apply_*`` method. + active : bool, optional + If True (default), active rotation (``R . X . R^T``); if False, the inverse + (passive) one. + n_threads : int, optional + Number of threads of the loop: 1 runs serially, 0 uses all the cores. Default 4, + as :func:`simcoon.umat`. Batches of at most 100 tensors always run serially. + + Returns + ------- + numpy.ndarray + The rotated tensors, with the shape of ``values`` (Fortran order). + + Examples + -------- + .. code-block:: python + + import numpy as np + import simcoon as smc + + N = 1000 + F0 = np.repeat(np.eye(3)[:, :, None], N, axis=2) + F1 = F0 + 0.01 * np.random.rand(3, 3, N) + D, DR, Omega = smc.objective_rate("log_R", F0, F1, 1.0) + + stress = np.random.rand(6, N) + stress_rot = smc.rotate_voigt(stress, DR, "stress", n_threads=0) + + L = np.repeat(smc.L_iso([70000.0, 0.3], "Enu")[:, :, None], N, axis=2) + L_rot = smc.rotate_voigt(L, DR, "stiffness", n_threads=0) +)pbdoc"; + } // namespace simcoon_docs diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/objective_rates.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/objective_rates.hpp index 08256506d..9fac9c49c 100755 --- a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/objective_rates.hpp +++ b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/objective_rates.hpp @@ -11,6 +11,6 @@ pybind11::array_t A_R(const pybind11::array_t &F, const bool &co pybind11::array_t A_F(const pybind11::array_t &F, const bool ©=true); pybind11::tuple objective_rate(const std::string &corate_name, const pybind11::array_t &F0, const pybind11::array_t &F1, const double &DTime, const bool &return_de, const unsigned int &n_threads); pybind11::array_t Delta_log_strain(const pybind11::array_t &D, const pybind11::array_t &Omega, const double &DTime, const bool ©=true); -pybind11::array_t Lt_convert(const pybind11::array_t &Lt, const pybind11::array_t &F, const pybind11::array_t &stress, const std::string &converter_key); +pybind11::array_t Lt_convert(const pybind11::array_t &Lt, const pybind11::array_t &F, const pybind11::array_t &stress, const std::string &converter_key, const unsigned int &n_threads); } //namespace simpy diff --git a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/umat.hpp b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/umat.hpp index 38b0b2ad6..959922e84 100644 --- a/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/umat.hpp +++ b/simcoon-python-builder/include/simcoon/python_wrappers/Libraries/Continuum_mechanics/umat.hpp @@ -8,6 +8,6 @@ using namespace std; namespace simpy { // Whether sim.umat condenses the (ndi, nshr) state of the named law to a validated, exact point bool condensation_serves(const std::string &umat_name, const int &ndi, const std::optional &nshr_py, const py::object &kinematic_form_py); - py::tuple launch_umat(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &F0_py, const py::array_t &F1_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const std::optional> &T_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode, const py::object &corate_py, const bool &work_correction_on, const std::string &tangent_output, const std::optional &start_py, const py::object &kinematic_form_py, const std::optional> &DT_py, const std::string &condense); - py::tuple launch_umat_T(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const py::array_t &Wt_py, const py::array_t &T_py, const py::array_t &DT_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode, const std::optional &start_py, const std::optional> &F0_py, const std::optional> &F1_py, const py::object &kinematic_form_py, const std::string &condense, const std::string &tangent_output); + py::tuple launch_umat(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &F0_py, const py::array_t &F1_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const std::optional> &T_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode_py, const py::object &corate_py, const bool &work_correction_on, const std::string &tangent_output, const std::optional &start_py, const py::object &kinematic_form_py, const std::optional> &DT_py, const std::string &condense, const std::string &ncp_name, const std::string &integrator_py); + py::tuple launch_umat_T(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const py::array_t &Wt_py, const py::array_t &T_py, const py::array_t &DT_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode_py, const std::optional &start_py, const std::optional> &F0_py, const std::optional> &F1_py, const py::object &kinematic_form_py, const std::string &condense, const std::string &tangent_output, const std::string &integrator_py); } diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/Leff.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/Leff.cpp index 3aac09839..eb684ddda 100755 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/Leff.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/Leff.cpp @@ -47,8 +47,12 @@ py::array_t L_eff(const std::string &umat_name, const py::array_tLt); } diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/hyperelastic.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/hyperelastic.cpp index e65e08fc2..3a1671839 100755 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/hyperelastic.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/hyperelastic.cpp @@ -4,6 +4,7 @@ #include #include +#include #include #include #include @@ -16,94 +17,72 @@ namespace py=pybind11; namespace simpy { -//This function returns the isochoric invariants -py::array_t isochoric_invariants(const py::array_t &input, const double &J, const bool ©) { - - if(input.ndim() == 1) { - if(input.size() == 3) { - vec lambdas = simpy::numpy_to_arma::arr_to_col(input); - vec t = simcoon::isochoric_invariants(lambdas, J); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - else if(input.size() == 6) { - vec vec_input = simpy::numpy_to_arma::arr_to_col(input); - mat b = simcoon::v2t_strain(vec_input); - vec t = simcoon::isochoric_invariants(b, J); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - else - throw std::invalid_argument("Invalid size of the one-dimensional array. Expected 3 (from principal stretches) or 6 (from left Cauchy-Green tensor in Voigt notation)"); - } - if(input.ndim() == 2) { - if((input.shape(0) == 3)&&(input.shape(1) == 3)) { - mat b = simpy::numpy_to_arma::arr_to_mat(input); - vec t = simcoon::isochoric_invariants(b, J); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - else if((input.shape(0) == 6)&&(input.shape(1) == 1)) { - mat mat_input = simpy::numpy_to_arma::arr_to_mat(input); - mat sim_input = simcoon::v2t_strain(mat_input.as_col()); - vec t = simcoon::isochoric_invariants(sim_input, J); - return simpy::arma_to_numpy::col_to_arr(t, copy); +namespace { + +// (3,3,N) or (6,N>1): (3,N) per point in parallel, GIL released; one point: (3,) +template +py::array_t per_tensor_point(const py::array_t &input, const bool ©, const F &f) +{ + const bool tensors = input.ndim() == 3; + const bool voigt_cols = input.ndim() == 2 && input.shape(0) == 6 && input.shape(1) > 1; + if (tensors || voigt_cols) { + cube X_t; + mat X_v; + if (tensors) { + X_t = simpy::numpy_to_arma::arr_to_cube_view(input); + if (X_t.n_rows != 3 || X_t.n_cols != 3) { + throw std::invalid_argument("Invalid shape of the three-dimensional array. Expected (3,3,N)"); + } } else { - throw std::invalid_argument("Invalid size of the two-dimensional array. Expected a 3x3 array or a 6x1 array considering Voigt notation"); - } + X_v = simpy::numpy_to_arma::arr_to_mat_view(input); + } + const int nb_points = tensors ? X_t.n_slices : X_v.n_cols; + mat out(3, nb_points); // every column written below + parallel_for_nogil(nb_points, [&](int pt) { + const mat X = tensors ? mat(const_cast(X_t.slice_memptr(pt)), 3, 3, false, true) + : simcoon::v2t_strain(X_v.col(pt)); + out.col(pt) = f(X); + }); + return simpy::arma_to_numpy::mat_to_arr(out, false); } - throw std::invalid_argument("Invalid array dimensions"); + mat X; + if (input.ndim() == 1 && input.size() == 6) { + X = simcoon::v2t_strain(simpy::numpy_to_arma::arr_to_col(input)); + } + else if (input.ndim() == 2 && input.shape(0) == 3 && input.shape(1) == 3) { + X = simpy::numpy_to_arma::arr_to_mat(input); + } + else if (input.ndim() == 2 && input.shape(0) == 6 && input.shape(1) == 1) { + X = simcoon::v2t_strain(simpy::numpy_to_arma::arr_to_mat(input).as_col()); + } + else { + throw std::invalid_argument("Invalid array shape. Expected a 3x3 tensor or its Voigt vector (6 or 6x1), " + "or a batch: 3x3xN tensors or 6xN Voigt columns"); + } + vec t = f(X); + return simpy::arma_to_numpy::col_to_arr(t, copy); } -py::array_t isochoric_pstretch(const py::array_t &input, const std::string &input_tensor, const double &J, const bool ©) { +} // namespace - if(input.ndim() == 1) { - if(input.size() == 6) { - vec vec_input = simpy::numpy_to_arma::arr_to_col(input); - if(input_tensor == "b") { - mat b = simcoon::v2t_strain(vec_input); - vec t = simcoon::isochoric_pstretch_from_b(b); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - else if(input_tensor == "V" || input_tensor == "v") { - mat V = simcoon::v2t_strain(vec_input); - vec t = simcoon::isochoric_pstretch_from_V(V); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - else { - throw std::invalid_argument("Invalid input string to describe the input vector: it should be *b* for left Cauchy-Green tensor or *v* or *V* for Eulerian stretch tensor"); - } - } - else { - throw std::invalid_argument("Invalid size of the one-dimensional array. Expected 6 (from left Cauchy-Green tensor or Eulerian stretch tensor in Voigt notation)"); - } - } - if(input.ndim() == 2) { - mat mat_input = simpy::numpy_to_arma::arr_to_mat(input); - if((input.shape(0) == 3)&&(input.shape(1) == 3)) { - if(input_tensor == "b") { - vec t = simcoon::isochoric_pstretch_from_b(mat_input); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - else if(input_tensor == "V" || input_tensor == "v") { - vec t = simcoon::isochoric_pstretch_from_V(mat_input); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - } - else if((input.shape(0) == 6)&&(input.shape(1) == 1)) { - mat sim_input = simcoon::v2t_strain(mat_input.as_col()); - if(input_tensor == "b") { - vec t = simcoon::isochoric_pstretch_from_b(sim_input); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - else if(input_tensor == "V" || input_tensor == "v") { - vec t = simcoon::isochoric_pstretch_from_V(sim_input); - return simpy::arma_to_numpy::col_to_arr(t, copy); - } - } - else { - throw std::invalid_argument("Invalid size of the two-dimensional array. Expected a 3x3 array or a 6x1 array considering Voigt notation"); - } +//This function returns the isochoric invariants +py::array_t isochoric_invariants(const py::array_t &input, const double &J, const bool ©) { + if (input.ndim() == 1 && input.size() == 3) { // the principal stretches of one point + vec t = simcoon::isochoric_invariants(vec(simpy::numpy_to_arma::arr_to_col(input)), J); + return simpy::arma_to_numpy::col_to_arr(t, copy); } - throw std::invalid_argument("Invalid array dimensions"); + return per_tensor_point(input, copy, [&](const mat &b) { return simcoon::isochoric_invariants(b, J); }); +} + +py::array_t isochoric_pstretch(const py::array_t &input, const std::string &input_tensor, const double &J, const bool ©) { + const bool is_b = (input_tensor == "b"); + if (!is_b && input_tensor != "V" && input_tensor != "v") { + throw std::invalid_argument("Invalid input string to describe the input tensor: it should be *b* for left Cauchy-Green tensor or *v* or *V* for Eulerian stretch tensor"); + } + return per_tensor_point(input, copy, [&](const mat &X) { + return is_b ? simcoon::isochoric_pstretch_from_b(X, J) : simcoon::isochoric_pstretch_from_V(X, J); + }); } py::array_t tau_iso_hyper_invariants(const double &dWdI_1_bar, const double &dWdI_2_bar, const py::array_t &input, const double &J, const bool ©) { diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/kinematics.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/kinematics.cpp index 2ab5644a1..008634fb3 100755 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/kinematics.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/kinematics.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -63,8 +64,33 @@ py::array_t L_Cauchy_Green(const py::array_t &F, const bool &cop return simpy::arma_to_numpy::mat_to_arr(L_Cauchy_Green, copy); } -//Provides the RU decomposition of the transformation gradient F +namespace { + +// A polar decomposition per 3x3 slice of F (3,3,N), in parallel with the GIL released: the two +// factors as (3,3,N) cubes, in the order of the single-matrix call +template +py::tuple polar_batch(const py::array_t &F, Decomposition decompose) { + const cube F_cpp = simpy::numpy_to_arma::arr_to_cube_view(F); + if (F_cpp.n_rows != 3 || F_cpp.n_cols != 3) { + throw std::invalid_argument("F must be a 3x3 matrix or a (3,3,N) array of them"); + } + const int nb_points = F_cpp.n_slices; + cube A(3, 3, nb_points), B(3, 3, nb_points); // every slice written below + parallel_for_nogil(nb_points, [&](int pt) { + const mat F_pt(const_cast(F_cpp.slice_memptr(pt)), 3, 3, false, true); + mat A_pt(A.slice_memptr(pt), 3, 3, false, true), B_pt(B.slice_memptr(pt), 3, 3, false, true); + decompose(A_pt, B_pt, F_pt); + }); + return py::make_tuple(simpy::arma_to_numpy::cube_to_arr(A, false), simpy::arma_to_numpy::cube_to_arr(B, false)); +} + +} // namespace + +//Provides the RU decomposition of the transformation gradient F (or of each slice of a (3,3,N) array) py::tuple RU_decomposition(const pybind11::array_t &F, const bool ©) { + if (F.ndim() == 3) { + return polar_batch(F, [](mat &R, mat &U, const mat &F_pt) { simcoon::RU_decomposition(R, U, F_pt); }); + } mat F_cpp = simpy::numpy_to_arma::arr_to_mat(F); mat R_cpp = zeros(3,3); mat U_cpp = zeros(3,3); @@ -72,8 +98,11 @@ py::tuple RU_decomposition(const pybind11::array_t &F, const bool ©) return py::make_tuple(simpy::arma_to_numpy::mat_to_arr(R_cpp, copy), simpy::arma_to_numpy::mat_to_arr(U_cpp, copy)); } -//Provides the VR decomposition of the transformation gradient F +//Provides the VR decomposition of the transformation gradient F (or of each slice of a (3,3,N) array) py::tuple VR_decomposition(const pybind11::array_t &F, const bool ©) { + if (F.ndim() == 3) { + return polar_batch(F, [](mat &V, mat &R, const mat &F_pt) { simcoon::VR_decomposition(V, R, F_pt); }); + } mat F_cpp = simpy::numpy_to_arma::arr_to_mat(F); mat V_cpp = zeros(3,3); mat R_cpp = zeros(3,3); diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/objective_rates.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/objective_rates.cpp index 119f8ab51..467239477 100755 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/objective_rates.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/objective_rates.cpp @@ -86,10 +86,14 @@ py::tuple objective_rate(const std::string& corate_name, const py::array_t Delta_log_strain(const py::array_t &D, const py::arr } //This function computes the logarithmic strain velocity and the logarithmic spin, along with the correct rotation increment -py::array_t Lt_convert(const py::array_t &Lt, const py::array_t &F, const py::array_t &stress, const std::string &converter_key) { +py::array_t Lt_convert(const py::array_t &Lt, const py::array_t &F, const py::array_t &stress, const std::string &converter_key, const unsigned int &n_threads) { static const std::map list_Lt_convert = { {"Dsigma_LieDD_2_DSDE",0}, {"DsigmaDe_2_DSDE",1},{"DsigmaDe_JaumannDD_2_DSDE",2}, {"Dsigma_LieDD_Dsigma_JaumannDD",3}, {"Dsigma_LieDD_Dsigma_GreenNaghdiDD",4}, {"Dsigma_LieDD_Dsigma_logarithmicDD",5}, {"DsigmaDe_GreenNaghdiDD_2_DSDE",6}, {"DSDE_2_Dsigma_GreenNaghdiDD",7}, {"DSDE_2_Dsigma_JaumannDD",8}, {"DSDE_2_Dsigma_LieDD",9}, {"DSDE_2_Dsigma_logarithmicDD",10}, {"DTDH_2_DSDE",11}}; // guarded lookup: operator[] would default-insert 0 = Dsigma_LieDD_2_DSDE, so a // misspelled or renamed key silently performed a DIFFERENT conversion and returned a @@ -228,7 +236,7 @@ py::array_t Lt_convert(const py::array_t &Lt, const py::array_t< mat out_pt = simcoon::slice_view(Lt_converted, pt); out_pt = convert_pt(simcoon::slice_view(Lt_cpp, pt), simcoon::slice_view(F_cpp, pt), simcoon::v2t_stress(stress_cpp.unsafe_col(pt))); - }); + }, n_threads); return simpy::arma_to_numpy::cube_to_arr(Lt_converted, false); } throw std::invalid_argument("Lt.ndim() must be 2 or 3"); diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/stress.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/stress.cpp index 5ab0ec8ad..5b9484273 100755 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/stress.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/stress.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include @@ -21,10 +22,9 @@ namespace simpy { py::array_t stress_convert(const py::array_t &sigma, const py::array_t &F, const string &converter_key, const double &J, const bool & copy) { - std::map list_stress_convert; // NOTE: Be strict about keys. Using operator[] would insert unknown keys // with a default value (0), silently mapping to Cauchy2PKI. - list_stress_convert = { + static const std::map list_stress_convert = { {"Cauchy2PKI",0}, {"Cauchy2PKII",1}, // Historically misspelled as Kirchoff in simcoon @@ -159,6 +159,37 @@ py::array_t stress_convert(const py::array_t &sigma, const py::a } };*/ + // F of a batch of N stresses: (3,3,N), or (N,3,3) transposed to it + auto batch_F = [&F](const unsigned int N) -> py::array_t { + if ((int)F.ndim() == 3) { + if ((unsigned int)F.shape(0) == N && F.shape(1) == 3 && F.shape(2) == 3) { + return F.attr("transpose")(py::make_tuple(1,2,0)).cast>(); + } + if ((unsigned int)F.shape(0) == 3 && F.shape(1) == 3 && (unsigned int)F.shape(2) == N) { + return F; + } + } + throw std::invalid_argument("For a batch of N stresses, F must be shaped (3,3,N) or (N,3,3) with matching N"); + }; + + if(sigma.ndim() == 3) { + // Batch of 3x3 stresses (3,3,N): per point, GIL released, through the slice memory + if ((sigma.shape(0) != 3) || (sigma.shape(1) != 3)) { + throw std::invalid_argument("Invalid shape of the three-dimensional array. Expected (3,3,N): one 3x3 stress per slice"); + } + const unsigned int N = sigma.shape(2); + const py::array_t F_in = batch_F(N); + const cube sigma_cpp_list = simpy::numpy_to_arma::arr_to_cube_view(sigma); + const cube F_cpp_list = simpy::numpy_to_arma::arr_to_cube_view(F_in); + cube stress(3, 3, N); // every slice written below + parallel_for_nogil(static_cast(N), [&](int i) { + const mat sigma_cpp(const_cast(sigma_cpp_list.slice_memptr(i)), 3, 3, false, true); + const mat F_cpp(const_cast(F_cpp_list.slice_memptr(i)), 3, 3, false, true); + mat out(stress.slice_memptr(i), 3, 3, false, true); + out = functor_stress_converter(sigma_cpp, F_cpp, J); + }); + return simpy::arma_to_numpy::cube_to_arr(stress, copy); + } if(sigma.ndim() == 1) { if(sigma.size() == 6) { mat sigma_cpp = simcoon::v2t_stress(simpy::numpy_to_arma::arr_to_col(sigma)); @@ -231,15 +262,15 @@ py::array_t stress_convert(const py::array_t &sigma, const py::a } assert(N == (unsigned int)F_in.shape(2)); - mat stress = zeros(6,N); - mat sigma_cpp_list = simpy::numpy_to_arma::arr_to_mat_view(sigma); - cube F_cpp_list = simpy::numpy_to_arma::arr_to_cube_view(F_in); + mat stress(6, N); // every column written below + const mat sigma_cpp_list = simpy::numpy_to_arma::arr_to_mat_view(sigma); + const cube F_cpp_list = simpy::numpy_to_arma::arr_to_cube_view(F_in); - for (unsigned int i=0; i < sigma_cpp_list.n_cols; i++) { - vec sigma_cpp = sigma_cpp_list.unsafe_col(i); - mat F_cpp = F_cpp_list.slice(i); - stress.col(i) = simcoon::t2v_stress(functor_stress_converter(simcoon::v2t_stress(sigma_cpp), F_cpp, J)); - } + // per point, GIL released, through the slice memory (no Cube::slice() header) + parallel_for_nogil(static_cast(sigma_cpp_list.n_cols), [&](int i) { + const mat F_cpp(const_cast(F_cpp_list.slice_memptr(i)), 3, 3, false, true); + stress.col(i) = simcoon::t2v_stress(functor_stress_converter(simcoon::v2t_stress(sigma_cpp_list.col(i)), F_cpp, J)); + }); return simpy::arma_to_numpy::mat_to_arr(stress, copy); } @@ -248,7 +279,7 @@ py::array_t stress_convert(const py::array_t &sigma, const py::a } } else { - throw std::invalid_argument("Invalid number of dimensions for input 'sigma'. Expected 1 or 2."); + throw std::invalid_argument("Invalid number of dimensions for input 'sigma'. Expected 1, 2 or 3."); } } diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/umat.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/umat.cpp index 3feaf2db2..36d6caca0 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/umat.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Continuum_mechanics/umat.cpp @@ -115,6 +115,13 @@ arma::vec::fixed<6> lab_start_stress(const arma::vec::fixed<6> &tau_start_tr, co throw py::error_already_set(); } +// Integrator code of the integrator name of sim.umat / sim.umat_T +int integrator_code(const std::string &name) { + if (name == "cutting_plane") return simcoon::integrator_cutting_plane; + if (name == "closest_point") return simcoon::integrator_closest_point; + throw std::invalid_argument("umat: integrator must be 'cutting_plane' or 'closest_point'; got '" + name + "'"); +} + // Code of a kinematic form given as "total", "rate" (log_R), or an objective rate by name or // code; -1 when not given. The names are those of simcoon.solver.maps. int kinematic_code(const py::object &value) { @@ -197,6 +204,21 @@ void write_back_free_strain(const py::array_t &Detot_py, const arma::uve } } +// private copy of an updated input; an F-contiguous buffer is copied per column in the parallel region +struct batch_output { + arma::mat in; + arma::mat out; + bool per_column = false; + explicit batch_output(const py::array_t &a) : in(simpy::numpy_to_arma::arr_to_mat_view(a)) { + per_column = in.n_elem > 0 && static_cast(in.memptr()) == a.data(); + if (per_column) out.set_size(in.n_rows, in.n_cols); // written by copy_col + else out.steal_mem(in); + } + void copy_col(const arma::uword pt) { + if (per_column) std::memcpy(out.colptr(pt), in.colptr(pt), in.n_rows*sizeof(double)); + } +}; + void require_writable_Detot(const py::array_t &Detot_py, const simcoon::condensed_state &cs, const std::string &fname) { if (cs.active() && !Detot_py.writeable()) throw std::invalid_argument(fname + ": with ndi < 3 the solved stress-free components are written back into Detot, which must be writable"); @@ -227,19 +249,23 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: return (total_form ? total : rate).count(umat_name) > 0; } - py::tuple launch_umat(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &F0_py, const py::array_t &F1_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const std::optional> &T_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode, const py::object &corate_py, const bool &work_correction_on, const std::string &tangent_output, const std::optional &start_py, const py::object &kinematic_form_py, const std::optional> &DT_py, const std::string &condense){ + py::tuple launch_umat(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &F0_py, const py::array_t &F1_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const std::optional> &T_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode_py, const py::object &corate_py, const bool &work_correction_on, const std::string &tangent_output, const std::optional &start_py, const py::object &kinematic_form_py, const std::optional> &DT_py, const std::string &condense, const std::string &ncp_name, const std::string &integrator_py){ // tangent_mode: 0 = none (explicit integration, Lt = elastic L), - // 1 = continuum, 2 = algorithmic (Simo-Hughes, DEFAULT), - // 3 = closest-point (reserved). See parameter.hpp tangent_* constants. + // 1 = continuum, 2 = algorithmic (DEFAULT); the closest point is + // integrator='closest_point'. See parameter.hpp. // Validate up front, in serial context: the per-point dispatch below // runs inside a non-exception-safe parallel region (GCD/OpenMP) where a // throw would std::terminate the host process. const bool condense_material = condense_material_face(condense); // checked on every call, condensed or not - if (tangent_mode < simcoon::tangent_none || tangent_mode > simcoon::tangent_closest_point) { - throw std::invalid_argument("tangent_mode must be 0 (none), 1 (continuum), 2 (algorithmic) or 3 (closest-point); got " - + std::to_string(tangent_mode)); - } + // integrator (cutting plane / closest point) and tangent operator, folded into the kernel code + const int tangent_mode = simcoon::kernel_tangent_mode(integrator_code(integrator_py), tangent_mode_py); + // ncp: complementarity function of the local cutting plane, by name (NCPMethod) + if (ncp_name != "fischer_burmeister" && ncp_name != "min") + throw std::invalid_argument("umat: ncp must be 'fischer_burmeister' or 'min'; got '" + ncp_name + "'"); + const int ncp = (ncp_name == "min") ? 1 : 0; + simcoon::check_ncp_serves(umat_name_py, ncp, false, tangent_mode); + simcoon::check_integrator_serves(umat_name_py, tangent_mode, false); // kinematic_form and corate (its former name) are one value; the dispatch carries the // total formulation in the corate slot const int form = kinematic_code(kinematic_form_py), former = kinematic_code(corate_py); @@ -261,11 +287,13 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: throw std::invalid_argument("tangent_output must be 'box', 'material' or 'spatial'; got '" + tangent_output + "'"); } const int tangent_out = it_tangent_out->second; - static const std::map list_umat = { {"UMEXT",0},{"UMABA",1},{"ELISO",201},{"ELIST",201},{"ELORT",201},{"EPICP",5},{"EPKCP",201},{"EPCHA",7},{"EPHIL",201},{"EPTRI",201},{"EPHAC",201},{"EPANI",201},{"EPDFA",201},{"EPHIN",201},{"SMADI",13},{"SMADC",13},{"SMAAI",13},{"SMAAC",13},{"LLDM0",15},{"ZENER",16},{"ZENNK",17},{"PRONK",18},{"SMAMO",19},{"SMAMC",20},{"NEOHC",21},{"MOORI",22},{"YEOHH",23},{"ISHAH",24},{"GETHH",25},{"SWANH",26},{"HOLZA",27},{"EPCHG",201},{"SMRDI",28},{"SMRDC",28},{"SMRAI",28},{"SMRAC",28},{"SNTVE",29},{"NEOHI",30},{"OGDEN",31},{"HYPOO",32},{"EPJCK",33},{"MODUL",200},{"MIHEN",100},{"MIMTN",101},{"MISCN",103},{"MIPLN",104},{"PYEXT",300} }; - // guarded lookup (serial context): operator[] would default-insert - // 0 = UMEXT, silently routing typos to the external-plugin path + static const std::map list_umat = { {"ELISO",201},{"ELIST",201},{"ELORT",201},{"EPICP",5},{"EPKCP",201},{"EPCHA",7},{"EPHIL",201},{"EPTRI",201},{"EPHAC",201},{"EPANI",201},{"EPDFA",201},{"EPHIN",201},{"SMADI",13},{"SMADC",13},{"SMAAI",13},{"SMAAC",13},{"LLDM0",15},{"ZENER",16},{"ZENNK",17},{"PRONK",18},{"SMAMO",19},{"SMAMC",20},{"NEOHC",21},{"MOORI",22},{"YEOHH",23},{"ISHAH",24},{"GETHH",25},{"SWANH",26},{"HOLZA",27},{"EPCHG",201},{"SMRDI",28},{"SMRDC",28},{"SMRAI",28},{"SMRAC",28},{"SNTVE",29},{"NEOHI",30},{"OGDEN",31},{"HYPOO",32},{"EPJCK",33},{"MODUL",200},{"MIHEN",100},{"MIMTN",101},{"MISCN",103},{"MIPLN",104},{"PYEXT",300} }; + // guarded lookup (serial context): an unknown name throws here, outside the parallel region const auto it_umat = list_umat.find(umat_name_py); if (it_umat == list_umat.end()) { + if (simcoon::is_external_plugin(umat_name_py)) + throw std::invalid_argument("umat: '" + umat_name_py + "' is an external plugin, served by the solver " + "(simcoon.solver.solve), not by sim.umat"); throw std::invalid_argument("Unknown umat name: " + umat_name_py); } const int id_umat = it_umat->second; @@ -314,7 +342,8 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: + std::to_string(nb_points) + "); got " + std::to_string(vec_DT.n_elem)); mat list_Detot = simpy::numpy_to_arma::arr_to_mat_view(Detot_py); require_writable_Detot(Detot_py, cs, "umat"); - mat list_sigma = simpy::numpy_to_arma::arr_to_mat(sigma_py); // copy: the umat updates it and it is returned to python + batch_output sigma_out(sigma_py); // copy: the umat updates it and it is returned to python + mat &list_sigma = sigma_out.out; cube DR = simpy::numpy_to_arma::arr_to_cube_view(DR_py); cube F0, F1; @@ -341,10 +370,13 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: } } - mat list_statev = simpy::numpy_to_arma::arr_to_mat(statev_py); // copy: the umat updates it and it is returned to python - mat list_Wm = simpy::numpy_to_arma::arr_to_mat(Wm_py); // copy: the umat updates it and it is returned to python - cube L(ncomp, ncomp, nb_points); - cube Lt(ncomp, ncomp, nb_points); + batch_output statev_out(statev_py); // copy: the umat updates it and it is returned to python + batch_output Wm_out(Wm_py); // copy: the umat updates it and it is returned to python + mat &list_statev = statev_out.out; + mat &list_Wm = Wm_out.out; + // Lt is returned: allocated here, each slice zeroed by its point in the parallel region. + // The elastic stiffness L the laws also return is never handed back: a stack 6x6 per point. + cube Lt(ncomp, ncomp, nb_points, fill::none); int nprops = list_props.n_rows; int nstatev = list_statev.n_rows; @@ -352,13 +384,11 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: case 201: { // legacy names served by the modular adapter // one shared id (mirrors umat_smart): the adapter dispatches on // umat_name, so every legacy-modular name takes this case - umat_function = &simcoon::umat_legacy_modular; - arguments_type = 1; + arguments_type = 4; // called by name: it takes the ncp option break; } case 5: { - umat_function = &simcoon::umat_plasticity_iso; - arguments_type = 1; + arguments_type = 5; // EPICP, called by name: it takes the ncp option break; } case 33: { @@ -526,6 +556,10 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: // log-corate work correction (finite-strain calls only): decided once, not per point const bool work_correction = work_correction_on && kirchhoff_normalize && simcoon::work_correction_applies(corate_type); + // a law built on F returns its native dS/dE for tangent_output 'material' or 'spatial' + const bool native_dSdE = tangent_out != tangent_out_box && !cs.active() && arguments_type == 2 + && simcoon::material_tangent_serves(umat_name_py); + const int corate_law = native_dSdE ? simcoon::corate_type_material_tangent : corate_type; const mat I3 = eye(3, 3); auto point_kernel = [&](int pt) { // Alias the props column without copying: the parallel region makes no @@ -533,6 +567,11 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: // props (unique) / list_props (per-point) outlive the lambda and are read-only. const double* _props_ptr = unique_props ? props.memptr() : list_props.colptr(pt); const vec local_props(const_cast(_props_ptr), nprops, false, true); + sigma_out.copy_col(pt); + statev_out.copy_col(pt); + Wm_out.copy_col(pt); + std::fill_n(Lt.slice_memptr(pt), Lt.n_elem_slice, 0.); + mat::fixed<6,6> L_pt(fill::zeros); vec statev = list_statev.unsafe_col(pt); vec sigma = list_sigma.unsafe_col(pt); @@ -549,7 +588,7 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: const bool have_F0 = F0.n_slices == static_cast(nb_points); const bool have_F1 = F1.n_slices == static_cast(nb_points); // slice aliases: never Cube::slice in the parallel region (slice_view.hpp) - mat Lt_s = simcoon::slice_view(Lt, pt), L_s = simcoon::slice_view(L, pt); + mat Lt_s = simcoon::slice_view(Lt, pt); const mat F0_s = have_F0 ? simcoon::slice_view(F0, pt) : mat(); const mat F1_s = have_F1 ? simcoon::slice_view(F1, pt) : mat(); mat F0_pt = !have_F0 ? mat() : cs.active() ? mat(F0_s) : simcoon::slice_view(F0, pt); @@ -597,15 +636,23 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: auto run_law = [&](double &tnew) { switch (arguments_type) { case 1: { - umat_function(umat_name_py, etot, Detot_pt, sigma, Lt_s, L_s, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, tangent_mode); + umat_function(umat_name_py, etot, Detot_pt, sigma, Lt_s, L_pt, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, tangent_mode); break; } case 3: { - simcoon::umat_modular(umat_name_py, etot, Detot_pt, sigma, Lt_s, L_s, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, tangent_mode, total ? simcoon::KinematicForm::total : simcoon::KinematicForm::rate); + simcoon::umat_modular(umat_name_py, etot, Detot_pt, sigma, Lt_s, L_pt, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, tangent_mode, total ? simcoon::KinematicForm::total : simcoon::KinematicForm::rate, ncp); + break; + } + case 4: { + simcoon::umat_legacy_modular(umat_name_py, etot, Detot_pt, sigma, Lt_s, L_pt, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, tangent_mode, ncp); + break; + } + case 5: { + simcoon::umat_plasticity_iso(umat_name_py, etot, Detot_pt, sigma, Lt_s, L_pt, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, tangent_mode, ncp); break; } case 2: { - umat_function_finite(umat_name_py, etot, Detot_pt, F0_pt, F1_pt, sigma, Lt_s, L_s, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, corate_type, tangent_mode); + umat_function_finite(umat_name_py, etot, Detot_pt, F0_pt, F1_pt, sigma, Lt_s, L_pt, DR_pt, nprops, local_props, nstatev, statev, T, DT, Time, DTime, Wm(0), Wm(1), Wm(2), Wm(3), ndi_law, nshr_law, start, tnew, corate_law, tangent_mode); break; } } @@ -652,9 +699,9 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: F1_pt = F1_of->R*arma::sqrtmat_sympd(C1); if (total) Detot_pt = simcoon::t2v_strain(0.5*arma::logmat_sympd(C1)) - etot; else { - mat D, Omega; - simcoon::corate_kinematics(3, DR_pt, D, Omega, F0_pt, F1_pt, dt); - Detot_pt = simcoon::t2v_strain(simcoon::Delta_log_strain_corate(F0_pt, F1_pt, DR_pt, D, Omega, dt, 3)); + mat D, Omega, De; + simcoon::corate_kinematics(3, DR_pt, D, Omega, De, F0_pt, F1_pt, dt); + Detot_pt = simcoon::t2v_strain(De); } run_law(tnew); mat tau = simcoon::v2t_stress(sigma); @@ -705,8 +752,10 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: } if (tangent_out != tangent_out_box && !material_face) { // material_face: already dS/dE // same maps as Lt_convert on the returned (Cauchy) stress, with the corate the law ran with - const mat dSdE = simcoon::DtauDe_corate_2_DSDE(Lt_s, corate_type, F1_s, - arma::det(F1_s)*simcoon::v2t_stress(sigma)); + // (none: the law returned dS/dE itself) + const mat dSdE = native_dSdE ? mat(Lt_s) + : simcoon::DtauDe_corate_2_DSDE(Lt_s, corate_type, F1_s, + arma::det(F1_s)*simcoon::v2t_stress(sigma)); Lt_s = (tangent_out == tangent_out_material) ? dSdE : simcoon::DSDE_2_Dsigma_LieDD(dSdE, F1_s); } }; @@ -733,16 +782,15 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: } - py::tuple launch_umat_T(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const py::array_t &Wt_py, const py::array_t &T_py, const py::array_t &DT_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode, const std::optional &start_py, const std::optional> &F0_py, const std::optional> &F1_py, const py::object &kinematic_form_py, const std::string &condense, const std::string &tangent_output){ + py::tuple launch_umat_T(const std::string &umat_name_py, const py::array_t &etot_py, const py::array_t &Detot_py, const py::array_t &sigma_py, const py::array_t &DR_py, const py::array_t &props_py, const py::array_t &statev_py, const double Time, const double DTime, const py::array_t &Wm_py, const py::array_t &Wt_py, const py::array_t &T_py, const py::array_t &DT_py, const int &ndi, const std::optional &nshr_py, const unsigned int &n_threads, const int &tangent_mode_py, const std::optional &start_py, const std::optional> &F0_py, const std::optional> &F1_py, const py::object &kinematic_form_py, const std::string &condense, const std::string &tangent_output, const std::string &integrator_py){ // Point-wise thermomechanical UMAT batch entry, mirroring launch_umat. // Dispatch follows the select_umat_T table (umat_smart.cpp). // Returns (sigma, statev, Wm, Wt, r, dSdE, dSdT, drdE, drdT). const bool condense_material = condense_material_face(condense); // checked on every call, condensed or not - if (tangent_mode < simcoon::tangent_none || tangent_mode > simcoon::tangent_closest_point) { - throw std::invalid_argument("tangent_mode must be 0 (none), 1 (continuum), 2 (algorithmic) or 3 (closest-point); got " - + std::to_string(tangent_mode)); - } + // integrator (cutting plane / closest point) and tangent operator, folded into the kernel code + const int tangent_mode = simcoon::kernel_tangent_mode(integrator_code(integrator_py), tangent_mode_py); + simcoon::check_integrator_serves(umat_name_py, tangent_mode, true); static const std::map list_umat = { {"ELISO",1},{"ELIST",2},{"ELORT",3},{"EPICP",4},{"EPKCP",5},{"ZENER",6},{"ZENNK",7},{"PRONK",8},{"SMADI",9},{"SMADC",9},{"SMAAI",9},{"SMAAC",9},{"EPJCK",10} }; auto it_umat = list_umat.find(umat_name_py); if (it_umat == list_umat.end()) { @@ -1013,9 +1061,9 @@ void require_writable_Detot(const py::array_t &Detot_py, const simcoon:: F1_pt = F1_of->R*arma::sqrtmat_sympd(C1); if (total) Detot_pt = simcoon::t2v_strain(0.5*arma::logmat_sympd(C1)) - etot; else { - mat D, Omega; - simcoon::corate_kinematics(3, DR_pt, D, Omega, F0_pt, F1_pt, dt); - Detot_pt = simcoon::t2v_strain(simcoon::Delta_log_strain_corate(F0_pt, F1_pt, DR_pt, D, Omega, dt, 3)); + mat D, Omega, De; + simcoon::corate_kinematics(3, DR_pt, D, Omega, De, F0_pt, F1_pt, dt); + Detot_pt = simcoon::t2v_strain(De); } run_law(tnew); const mat tau = kirchhoff(simcoon::v2t_stress(sigma)); diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp index e83340b76..502ca8dfe 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Maths/rotation.cpp @@ -2,12 +2,15 @@ #include #include +#include +#include #include #include #include #include #include +#include #include #include @@ -38,6 +41,23 @@ namespace { to_string(cols) + "), got (" + to_string(shape[0]) + ", " + to_string(shape[1]) + ")"); } } + + // A 2-D array as a cube of one slice (copy), a 3-D array as a cube (borrowed when + // F-contiguous, copied otherwise). Returned as a prvalue so that the view stays a view. + cube as_cube(const py::array_t& arr) { + if (arr.ndim() == 3) { + return simpy::numpy_to_arma::arr_to_cube_view(arr); + } + const mat m = simpy::numpy_to_arma::arr_to_mat(arr); + return cube(m.memptr(), m.n_rows, m.n_cols, 1); + } + + void check_rotation_count(const cube& R, uword N) { + if (R.n_slices != 1 && R.n_slices != N) { + throw invalid_argument("rotate_voigt: DR holds " + to_string(R.n_slices) + + " rotations, expected 1 or " + to_string(N) + " (one per tensor)"); + } + } } // anonymous namespace void register_rotation(py::module_& m) { @@ -204,6 +224,73 @@ void register_rotation(py::module_& m) { py::arg("quats"), py::arg("active") = true, "Batch-build (N, 6, 6) strain Voigt rotation matrices from (N, 4) quaternions"); + // Batch rotation of Voigt tensors by rotation matrices, on the (6,N) / (6,6,N) layout of umat + // and objective_rate: no scipy Rotation, no transposes, parallel loop without the GIL. + m.def("rotate_voigt", + [](py::array_t values, py::array_t DR, const string& kind, bool active, + unsigned int n_threads) -> py::array_t { + static const map tensor2_kinds = { + {"stress", simcoon::Tensor2Type::stress}, {"strain", simcoon::Tensor2Type::strain}}; + static const map tensor4_kinds = { + {"stiffness", simcoon::Tensor4Type::stiffness}, + {"compliance", simcoon::Tensor4Type::compliance}, + {"strain_concentration", simcoon::Tensor4Type::strain_concentration}, + {"stress_concentration", simcoon::Tensor4Type::stress_concentration}}; + // DR: one (3,3) rotation for every tensor, or a (3,3,N_r) cube + if ((DR.ndim() != 2 && DR.ndim() != 3) || DR.shape(0) != 3 || DR.shape(1) != 3) { + throw invalid_argument("rotate_voigt: DR must have shape (3, 3) or (3, 3, N)"); + } + const cube R = as_cube(DR); + + const auto it2 = tensor2_kinds.find(kind); + if (it2 != tensor2_kinds.end()) { + // (6,) one vector, (6,N) one vector per column + if ((values.ndim() != 1 && values.ndim() != 2) || values.shape(0) != 6) { + throw invalid_argument("rotate_voigt: " + kind + " values must have shape (6,) or (6, N)"); + } + const bool single = values.ndim() == 1; + const mat v = single ? mat(simpy::numpy_to_arma::arr_to_col(values)) + : simpy::numpy_to_arma::arr_to_mat_view(values); + check_rotation_count(R, v.n_cols); + mat result; + { + py::gil_scoped_release release; + result = simcoon::batch_rotate(v, it2->second, R, active, n_threads); + } + if (single) { + py::array_t out(6); + std::memcpy(out.mutable_data(), result.memptr(), 6 * sizeof(double)); + return out; + } + return simpy::arma_to_numpy::mat_to_arr(std::move(result)); + } + + const auto it4 = tensor4_kinds.find(kind); + if (it4 == tensor4_kinds.end()) { + throw invalid_argument("rotate_voigt: unknown kind '" + kind + "'. Valid: stress, strain, " + "stiffness, compliance, strain_concentration, stress_concentration"); + } + // (6,6) one tensor, (6,6,N) one tensor per slice + if ((values.ndim() != 2 && values.ndim() != 3) || values.shape(0) != 6 || values.shape(1) != 6) { + throw invalid_argument("rotate_voigt: " + kind + " values must have shape (6, 6) or (6, 6, N)"); + } + const bool single = values.ndim() == 2; + const cube t4 = as_cube(values); + check_rotation_count(R, t4.n_slices); + cube result; + { + py::gil_scoped_release release; + result = simcoon::batch_rotate(t4, it4->second, R, active, n_threads); + } + if (single) { + return simpy::arma_to_numpy::mat_to_arr(mat(result.slice(0))); + } + return simpy::arma_to_numpy::cube_to_arr(std::move(result)); + }, + py::arg("values"), py::arg("DR"), py::arg("kind"), py::arg("active") = true, + py::arg("n_threads") = 4, + simcoon_docs::rotate_voigt); + m.def("dR_drotvec", [](py::array_t rotvec) { validate_vector_size(rotvec, 3, "rotvec"); diff --git a/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp b/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp index 2e3409ecd..42f2723f7 100644 --- a/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp +++ b/simcoon-python-builder/src/python_wrappers/Libraries/Solver/solver_run.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -165,9 +166,16 @@ py::dict solver_run(const py::list &blocks_py, const double &T_init, ctrl.maxiter = dget(params_py, "maxiter", ctrl.maxiter); ctrl.inforce = dget(params_py, "inforce", ctrl.inforce); ctrl.precision = dget(params_py, "precision", ctrl.precision); + ctrl.precision_rel = dget(params_py, "precision_rel", ctrl.precision_rel); + if (ctrl.precision_rel < 0.) { + throw std::invalid_argument("solver_run: precision_rel must be >= 0 (0: absolute tolerance only)"); + } //"lambda_solver" is the documented spelling (lambda is a Python keyword); "lambda" kept as alias ctrl.lambda = dget(params_py, "lambda_solver", dget(params_py, "lambda", ctrl.lambda)); - ctrl.tangent_mode = dget(params_py, "tangent_mode", ctrl.tangent_mode); + // integrator (cutting plane / closest point) and tangent operator, folded into the kernel code + ctrl.tangent_mode = simcoon::kernel_tangent_mode(dget(params_py, "integrator", simcoon::integrator_cutting_plane), + dget(params_py, "tangent_mode", ctrl.tangent_mode)); + ctrl.ncp = dget(params_py, "ncp", ctrl.ncp); //Loading blocks unsigned int nblocks = static_cast(py::len(blocks_py)); diff --git a/simcoon-python-builder/src/python_wrappers/python_module.cpp b/simcoon-python-builder/src/python_wrappers/python_module.cpp index 87db765ec..92c2377fd 100755 --- a/simcoon-python-builder/src/python_wrappers/python_module.cpp +++ b/simcoon-python-builder/src/python_wrappers/python_module.cpp @@ -185,7 +185,7 @@ PYBIND11_MODULE(_core, m) m.def("A_F", &A_F, "F"_a, "copy"_a = true, "Log-strain concentration tensor A^F (convected / log_F), 6x6 Voigt (De = A^F:D)"); m.def("Delta_log_strain", &Delta_log_strain, "D"_a, "Omega"_a, "DTime"_a, "copy"_a = true, "This function computes the gradient of displacement (Eulerian) from the deformation gradient tensor"); m.def("objective_rate", &objective_rate, "corate_name"_a, "F0"_a, "F1"_a, "dtime"_a, "return_de"_a = false, "n_threads"_a = 4, "Rate of deformation D, frame increment DR and spin Omega of the named objective rate between F0 and F1 (DR is the increment DF of the transformation gradient and Omega the velocity gradient under truesdell and logarithmic_F). With return_de=True the strain increment of that rate comes first: it is the increment the solver integrates, and it accumulates to ln V under the logarithmic rates and to the Almansi strain under truesdell once the stored strain is transported with DR (rotation; lower-convected under truesdell; symmetrised similarity under logarithmic_F)."); - m.def("Lt_convert", &Lt_convert, "Lt"_a, "F"_a, "stress"_a, "converter_key"_a); + m.def("Lt_convert", &Lt_convert, "Lt"_a, "F"_a, "stress"_a, "converter_key"_a, "n_threads"_a = 4); // register the hyperelastic library m.def("isochoric_invariants", &isochoric_invariants, "input"_a, "J"_a = 0., "copy"_a = true, simcoon_docs::isochoric_invariants); @@ -239,8 +239,8 @@ PYBIND11_MODULE(_core, m) // 0 Jaumann, 1 Green-Naghdi, 2 XBM/log, 3 log_R, 4 Truesdell, 5 log_F). Default 3 = log_R, // the framework's route. The small-strain/in-rate kernels ignore it, since // their tangent is already in the solver's frame. - m.def("umat", &launch_umat, "umat_name"_a, "etot"_a, "Detot"_a, "F0"_a, "F1"_a, "sigma"_a, "DR"_a, "props"_a, "statev"_a, "time"_a, "dtime"_a, "Wm"_a, "temp"_a = pybind11::none(), "ndi"_a = 3, "nshr"_a = pybind11::none(), "n_threads"_a = 4, "tangent_mode"_a = simcoon::tangent_default, "corate"_a = pybind11::none(), "work_correction"_a = true, "tangent_output"_a = "box", "start"_a = pybind11::none(), "kinematic_form"_a = pybind11::none(), "DT"_a = pybind11::none(), "condense"_a = "spatial_face", simcoon_docs::umat); - m.def("umat_T", &launch_umat_T, "umat_name"_a, "etot"_a, "Detot"_a, "sigma"_a, "DR"_a, "props"_a, "statev"_a, "time"_a, "dtime"_a, "Wm"_a, "Wt"_a, "T"_a, "DT"_a, "ndi"_a = 3, "nshr"_a = pybind11::none(), "n_threads"_a = 4, "tangent_mode"_a = simcoon::tangent_default, "start"_a = pybind11::none(), "F0"_a = pybind11::none(), "F1"_a = pybind11::none(), "kinematic_form"_a = pybind11::none(), "condense"_a = "spatial_face", "tangent_output"_a = "box", simcoon_docs::umat_T); + m.def("umat", &launch_umat, "umat_name"_a, "etot"_a, "Detot"_a, "F0"_a, "F1"_a, "sigma"_a, "DR"_a, "props"_a, "statev"_a, "time"_a, "dtime"_a, "Wm"_a, "temp"_a = pybind11::none(), "ndi"_a = 3, "nshr"_a = pybind11::none(), "n_threads"_a = 4, "tangent_mode"_a = simcoon::tangent_default, "corate"_a = pybind11::none(), "work_correction"_a = true, "tangent_output"_a = "box", "start"_a = pybind11::none(), "kinematic_form"_a = pybind11::none(), "DT"_a = pybind11::none(), "condense"_a = "spatial_face", "ncp"_a = "fischer_burmeister", "integrator"_a = "cutting_plane", simcoon_docs::umat); + m.def("umat_T", &launch_umat_T, "umat_name"_a, "etot"_a, "Detot"_a, "sigma"_a, "DR"_a, "props"_a, "statev"_a, "time"_a, "dtime"_a, "Wm"_a, "Wt"_a, "T"_a, "DT"_a, "ndi"_a = 3, "nshr"_a = pybind11::none(), "n_threads"_a = 4, "tangent_mode"_a = simcoon::tangent_default, "start"_a = pybind11::none(), "F0"_a = pybind11::none(), "F1"_a = pybind11::none(), "kinematic_form"_a = pybind11::none(), "condense"_a = "spatial_face", "tangent_output"_a = "box", "integrator"_a = "cutting_plane", simcoon_docs::umat_T); // Register the from-python converters for read and solver // tangent_mode named constants (see parameter.hpp): 0 = none/explicit, @@ -250,6 +250,8 @@ PYBIND11_MODULE(_core, m) m.attr("tangent_algorithmic") = simcoon::tangent_algorithmic; m.attr("tangent_closest_point") = simcoon::tangent_closest_point; m.attr("tangent_default") = simcoon::tangent_default; + m.attr("integrator_cutting_plane") = simcoon::integrator_cutting_plane; + m.attr("integrator_closest_point") = simcoon::integrator_closest_point; m.attr("corate_type_total") = simcoon::corate_type_total; m.def("total_formulation_serves", &simcoon::total_formulation_serves, "umat_name"_a, "Whether the named law can run under kinematic_form='total' (ln U and its dual stress T): the log-strain box laws, MODUL among them, and the hyperelastic laws built on F; not HYPOO nor a law whose stress is not the Kirchhoff stress."); m.def("condensation_serves", &condensation_serves, "umat_name"_a, "ndi"_a, "nshr"_a = pybind11::none(), "kinematic_form"_a = pybind11::none(), "Whether sim.umat(umat_name, ..., ndi=ndi, nshr=nshr, kinematic_form=kinematic_form) serves the condensed state, under condense='spatial_face' or 'material_face' alike: the law runs 3D, the stress the condition is written on vanishes at convergence on the stress-free components, and the tangent is the Schur complement of the law's own, exact whenever that one is. Under condense='spatial_face' that stress is the law's own (tau_hat under the rate form, T under the total one): it coincides with the traction-free condition only when the free directions are principal, otherwise the second Piola-Kirchhoff S_C is of second order in the shears coupling kept and free directions (first order for anisotropy axes tilted out of the free planes); condense='material_face' sets S_C = 0. True for ndi = 3 (nothing condensed); False for a law not validated in that form, an invalid (ndi, nshr), or an objective rate other than log_R. Raises ValueError on an unknown kinematic_form."); diff --git a/simcoon-python-builder/test/parameter_test/test.py b/simcoon-python-builder/test/parameter_test/test.py index 2f41b6688..bfa1704b8 100644 --- a/simcoon-python-builder/test/parameter_test/test.py +++ b/simcoon-python-builder/test/parameter_test/test.py @@ -4,8 +4,8 @@ import numpy as np import pytest -from simcoon.parameter import Parameter, read_parameters, copy_parameters, apply_parameters -from simcoon.constant import Constant, read_constants, copy_constants, apply_constants +from simcoon.parameter import KeyTemplate, Parameter, read_parameters +from simcoon.constant import Constant, read_constants from simcoon.identify import identification, calc_cost @@ -73,28 +73,22 @@ def test_skiprows_negative_raises(self): read_parameters("nonexistent.inp", skiprows=-1) -class TestCopyParameters: - def test_copy_parameters(self, data_dir, keys_dir): +class TestKeyTemplateFiles: + def test_write_creates_the_working_file(self, data_dir, keys_dir): params = read_parameters(os.path.join(data_dir, "parameters.inp")) with tempfile.TemporaryDirectory() as tmp: - copy_parameters(params, keys_dir, tmp) + KeyTemplate(keys_dir, params).write(tmp) assert os.path.exists(os.path.join(tmp, "Nellipsoids0.dat")) - def test_copy_parameters_invalid_src(self, data_dir): - params = read_parameters(os.path.join(data_dir, "parameters.inp")) - with pytest.raises(TypeError): - copy_parameters(params, 123, "/tmp") - -class TestApplyParameters: - def test_apply_parameters_replaces_keys(self, data_dir, keys_dir): +class TestKeyTemplateParameters: + def test_write_replaces_keys(self, data_dir, keys_dir): params = read_parameters(os.path.join(data_dir, "parameters.inp")) params[0].value = 45.0 params[1].value = 72000.0 with tempfile.TemporaryDirectory() as tmp: - copy_parameters(params, keys_dir, tmp) - apply_parameters(params, tmp) + KeyTemplate(keys_dir, params).write(tmp) with open(os.path.join(tmp, "Nellipsoids0.dat"), "r") as f: content = f.read() @@ -105,7 +99,7 @@ def test_apply_parameters_replaces_keys(self, data_dir, keys_dir): assert "72000.0" in content -class TestApplyParametersKeyBoundary: +class TestKeyBoundary: """Substring keys must not clobber longer keys that contain them.""" def test_prefix_key_does_not_clobber_longer_key(self): @@ -122,7 +116,7 @@ def test_prefix_key_does_not_clobber_longer_key(self): with open(fpath, "w") as f: f.write("E=@E Et=@Et") - apply_parameters(params, tmp) + KeyTemplate(tmp, params).write(tmp) with open(fpath) as f: content = f.read() @@ -138,7 +132,7 @@ def test_key_value_with_backslash_safe(self): fpath = os.path.join(tmp, "m.dat") with open(fpath, "w") as f: f.write("v=@x") - apply_parameters([p], tmp) + KeyTemplate(tmp, [p]).write(tmp) with open(fpath) as f: assert f.read() == r"v=\1abc" @@ -156,7 +150,7 @@ def test_repr(self): class TestConstants: - """Coverage for Constant + read/copy/apply_constants.""" + """Coverage for Constant, read_constants and their substitution.""" def _write_constants_file(self, path, n_consts, rows): with open(path, "w") as f: @@ -188,7 +182,7 @@ def test_read_constants_skiprows_zero(self): assert len(consts) == 1 assert consts[0].key == "@c" - def test_apply_constants_replaces_key(self): + def test_constants_replace_keys(self): with tempfile.TemporaryDirectory() as tmp: template_dir = os.path.join(tmp, "keys") work_dir = os.path.join(tmp, "data") @@ -211,13 +205,12 @@ def test_apply_constants_replaces_key(self): ), ] - copy_constants(consts, template_dir, work_dir) - apply_constants(consts, work_dir) + KeyTemplate(template_dir, consts=consts).write(work_dir) with open(os.path.join(work_dir, "mat.dat")) as f: assert f.read() == "density=7800.0 mu=0.3" - def test_apply_constants_prefix_collision_safe(self): + def test_constants_prefix_collision_safe(self): """``@m`` must not clobber ``@mu`` (same hardening as parameters).""" with tempfile.TemporaryDirectory() as tmp: with open(os.path.join(tmp, "mat.dat"), "w") as f: @@ -227,29 +220,28 @@ def test_apply_constants_prefix_collision_safe(self): Constant(0, "@m", np.array([1.0]), 1.0, ["mat.dat"]), Constant(1, "@mu", np.array([0.3]), 0.3, ["mat.dat"]), ] - apply_constants(consts, tmp) + KeyTemplate(tmp, consts=consts).write(tmp) with open(os.path.join(tmp, "mat.dat")) as f: assert f.read() == "a=1.0 b=0.3" class TestIdentificationWorkflow: - """Test the full identification workflow: template -> apply -> forward model -> cost.""" + """Test the full identification workflow: template -> substitution -> forward model -> cost.""" - def test_template_apply_no_residual_keys(self, data_dir, keys_dir): + def test_template_no_residual_keys(self, data_dir, keys_dir): """End-to-end: key substitution produces valid input (no residual @).""" params = read_parameters(os.path.join(data_dir, "parameters.inp")) params[0].value = 0.8 params[1].value = 73000 with tempfile.TemporaryDirectory() as tmp: - copy_parameters(params, keys_dir, tmp) - apply_parameters(params, tmp) + KeyTemplate(keys_dir, params).write(tmp) with open(os.path.join(tmp, "Nellipsoids0.dat"), "r") as f: content = f.read() - assert "@" not in content, "Residual keys found after apply_parameters" + assert "@" not in content, "Residual keys found after the substitution" assert "0.8" in content assert "73000" in content @@ -257,12 +249,12 @@ def test_multiple_iterations_fresh_copy(self, data_dir, keys_dir): """Each iteration starts from a fresh template (no stale values).""" params = read_parameters(os.path.join(data_dir, "parameters.inp")) + template = KeyTemplate(keys_dir, params) with tempfile.TemporaryDirectory() as tmp: # First iteration params[0].value = 0.7 params[1].value = 50000 - copy_parameters(params, keys_dir, tmp) - apply_parameters(params, tmp) + template.write(tmp) with open(os.path.join(tmp, "Nellipsoids0.dat"), "r") as f: content1 = f.read() @@ -271,8 +263,7 @@ def test_multiple_iterations_fresh_copy(self, data_dir, keys_dir): # Second iteration with different values params[0].value = 0.9 params[1].value = 80000 - copy_parameters(params, keys_dir, tmp) - apply_parameters(params, tmp) + template.write(tmp) with open(os.path.join(tmp, "Nellipsoids0.dat"), "r") as f: content2 = f.read() diff --git a/simcoon-python-builder/test/test_core/run_test.py b/simcoon-python-builder/test/test_core/run_test.py index 1c4c46fbb..4240acb4b 100755 --- a/simcoon-python-builder/test/test_core/run_test.py +++ b/simcoon-python-builder/test/test_core/run_test.py @@ -346,3 +346,12 @@ def test_umat_ogden_stress_free_reference(): assert np.allclose(sig, np.zeros(6), atol=1e-12) # ground-state shear stiffness: Lt(3,3) = mu = sum(mu_i) assert np.isclose(Lt[3, 3], 0.5673, rtol=1e-6) + + +def test_invalid_elastic_convention_raises(): + """An unknown convention raises ValueError (it used to end the process, or return zero).""" + import pytest + with pytest.raises(ValueError, match="L_iso: unknown convention 'Emu'"): + sim.L_iso([70000., 0.3], "Emu") + with pytest.raises(ValueError, match="L_ortho: unknown convention"): + sim.L_ortho([1., 1., 1., 0.3, 0.3, 0.3, 1., 1., 1.], "Enu") diff --git a/simcoon-python-builder/test/test_core/solver_harness.py b/simcoon-python-builder/test/test_core/solver_harness.py index 2753ebdcf..2fa7a1469 100644 --- a/simcoon-python-builder/test/test_core/solver_harness.py +++ b/simcoon-python-builder/test/test_core/solver_harness.py @@ -16,6 +16,21 @@ import simcoon as sim from simcoon.solver import Block, StepMeca +# Closest point in the tests: the tests sweep a tangent "mode" with the kernel codes, 3 standing +# for the closest-point integrator (sim.tangent_closest_point), which the API chooses with +# integrator='closest_point' and the algorithmic tangent. +CLOSEST_POINT = sim.tangent_closest_point + + +def tangent_kwargs(mode): + """Keyword arguments of solve / sim.umat / sim.umat_T for a kernel tangent code: + CLOSEST_POINT (3) is integrator='closest_point' with tangent_mode=2, any other code is + tangent_mode itself on the cutting plane.""" + if mode == CLOSEST_POINT: + return {"integrator": "closest_point", "tangent_mode": sim.tangent_algorithmic} + return {"tangent_mode": mode} + + # Historical column layout of res_global-0.txt: # 3 inc | 4 time | 8:14 log strain | 14:20 Kirchhoff stress | 20:29 R | # 29:38 F | 38:42 Wm, Wm_r, Wm_ir, Wm_d diff --git a/simcoon-python-builder/test/test_core/test_batch_kinematics.py b/simcoon-python-builder/test/test_core/test_batch_kinematics.py new file mode 100644 index 000000000..40eb2a59f --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_batch_kinematics.py @@ -0,0 +1,89 @@ +"""Batch (3,3,N) forms of RU_decomposition, VR_decomposition, isochoric_pstretch and stress_convert: +the same values as the single-matrix calls, point by point (they run in parallel past 100 points).""" + +import numpy as np +import pytest + +import simcoon as sim + +N = 150 # past the parallel cutoff (100) + + +def _F(n=N, seed=3): + rng = np.random.default_rng(seed) + return np.asfortranarray(np.eye(3)[:, :, None] + 0.2 * rng.standard_normal((3, 3, n))) + + +def _slice(a, i): + return np.ascontiguousarray(a[:, :, i]) + + +@pytest.mark.parametrize("name", ["RU_decomposition", "VR_decomposition"]) +def test_polar_batch_is_the_single_call(name): + F = _F() + A, B = getattr(sim, name)(F) + assert A.shape == B.shape == (3, 3, N) + for i in range(N): + a, b = getattr(sim, name)(_slice(F, i)) + np.testing.assert_array_equal(A[:, :, i], a) + np.testing.assert_array_equal(B[:, :, i], b) + + +@pytest.mark.parametrize("tensor", ["V", "b"]) +def test_isochoric_pstretch_batch(tensor): + F = _F() + V, _ = sim.VR_decomposition(F) + X = V if tensor == "V" else np.asfortranarray(np.einsum("ijn,kjn->ikn", F, F)) + lam = sim.isochoric_pstretch(X, tensor) + assert lam.shape == (3, N) + for i in range(N): + np.testing.assert_array_equal(lam[:, i], np.ravel(sim.isochoric_pstretch(_slice(X, i), tensor))) + # Voigt columns (6, N), as the single (6,) vector + Xv = np.asfortranarray(np.stack([np.ravel(sim.t2v_strain(_slice(X, i))) for i in range(N)], axis=1)) + np.testing.assert_array_equal(sim.isochoric_pstretch(Xv, tensor), lam) + + +def test_isochoric_invariants_batch(): + F = _F() + b = np.asfortranarray(np.einsum("ijn,kjn->ikn", F, F)) + inv = sim.isochoric_invariants(b) + assert inv.shape == (3, N) + for i in range(N): + np.testing.assert_array_equal(inv[:, i], np.ravel(sim.isochoric_invariants(_slice(b, i)))) + bv = np.asfortranarray(np.stack([np.ravel(sim.t2v_strain(_slice(b, i))) for i in range(N)], axis=1)) + np.testing.assert_array_equal(sim.isochoric_invariants(bv), inv) + # the same J for one point and for a batch + np.testing.assert_array_equal(sim.isochoric_invariants(b, 1.3)[:, 0], + np.ravel(sim.isochoric_invariants(_slice(b, 0), 1.3))) + + +def test_isochoric_pstretch_single_point_reads_J(): + """A single point honours J as the batch does (it used to recompute it from the tensor).""" + b = np.asfortranarray(np.einsum("ijn,kjn->ikn", _F(n=2), _F(n=2))) + for X in (_slice(b, 0), np.ravel(sim.t2v_strain(_slice(b, 0)))): + single = np.ravel(sim.isochoric_pstretch(X, "b", 1.3)) + np.testing.assert_array_equal(single, sim.isochoric_pstretch(b, "b", 1.3)[:, 0]) + assert not np.array_equal(single, np.ravel(sim.isochoric_pstretch(X, "b"))) + with pytest.raises(ValueError, match="input string"): + sim.isochoric_pstretch(_slice(b, 0), "C") + + +@pytest.mark.parametrize("key", ["Cauchy2PKI", "Cauchy2PKII", "PKII2Cauchy", "Kirchoff2LogStressU"]) +def test_stress_convert_tensor_batch(key): + F = _F() + rng = np.random.default_rng(5) + s = 100.0 * rng.standard_normal((3, 3, N)) + s = np.asfortranarray(0.5 * (s + s.transpose(1, 0, 2))) + out = sim.stress_convert(s, F, key) + assert out.shape == (3, 3, N) + for i in range(N): + np.testing.assert_array_equal(out[:, :, i], sim.stress_convert(_slice(s, i), _slice(F, i), key)) + # F given as (N, 3, 3) + np.testing.assert_array_equal(sim.stress_convert(s, np.ascontiguousarray(F.transpose(2, 0, 1)), key), out) + + +def test_batch_shape_errors(): + with pytest.raises(Exception): + sim.VR_decomposition(np.zeros((3, 2, 4))) + with pytest.raises(Exception): + sim.stress_convert(np.zeros((3, 3, 4)), _F(5), "Cauchy2PKI") diff --git a/simcoon-python-builder/test/test_core/test_closest_point_kernels.py b/simcoon-python-builder/test/test_core/test_closest_point_kernels.py index 043e8d94a..a88fa267d 100644 --- a/simcoon-python-builder/test/test_core/test_closest_point_kernels.py +++ b/simcoon-python-builder/test/test_core/test_closest_point_kernels.py @@ -1,10 +1,11 @@ -"""tangent_mode = 3 on the dedicated plasticity kernels (EPICP, EPCHA): the closest-point +"""integrator="closest_point" on the dedicated plasticity kernels (EPICP, EPCHA): the closest-point projection of return_mapping.hpp replaces their cutting-plane loops, with the exact operator.""" import numpy as np import pytest import simcoon as sim from simcoon.solver import StepMeca, solve +from solver_harness import tangent_kwargs _UNIAXIAL = ["strain"] + ["stress"] * 5 EPICP_LIN = [70000.0, 0.3, 1.0e-5, 300.0, 1000.0, 1.0] # E nu alpha sigmaY k m (linear hardening) @@ -16,7 +17,7 @@ def _run(name, props, nstatev, mode, ninc=10): step = StepMeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=ninc, time=0.5) - r = solve(step, name, props, nstatev, T_init=290.0, tangent_mode=mode) + r = solve(step, name, props, nstatev, T_init=290.0, **tangent_kwargs(mode)) assert r.status == 0 return r @@ -24,7 +25,7 @@ def _run(name, props, nstatev, mode, ninc=10): def _umat(name, props, e, s, sv, De, mode): return sim.umat(name, col(e), col(De), I3, I3, col(s), I3, col(props), col(sv), 0.5, 0.3, np.zeros((4, 1), order="F"), temp=np.full(1, 290.0), n_threads=1, - tangent_mode=mode, start=False) + **tangent_kwargs(mode), start=False) def _tangent_vs_fd(name, props, e, s, sv, mode, h=1e-8): @@ -80,7 +81,7 @@ def test_epcha_closest_point_cyclic_path_runs_at_full_increments(): StepMeca(control=["strain", "stress", "stress", "strain", "stress", "stress"], value=[-0.01, 0, 0, 0.012, 0, 0], ninc=10, time=1.0), StepMeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=10, time=1.0)] - r = solve(steps, "EPCHA", EPCHA, 33, T_init=290.0, tangent_mode=3) + r = solve(steps, "EPCHA", EPCHA, 33, T_init=290.0, integrator="closest_point") assert r.status == 0 and len(r) == 30 np.testing.assert_allclose(np.diff(r["Time"]), 0.1) assert r["Stress"][0, 9] > 300.0 and r["Stress"][0, 19] < -300.0 @@ -96,7 +97,7 @@ def test_thermomechanical_kernel_closest_point_tangent_is_exact(name, props, nst Jacobian (the Prager backstress makes mode 2 approximate on EPKCP).""" from simcoon.solver import StepThermomeca step = StepThermomeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=10, T_final=300.0) - r = solve(step, name, props, nstatev, T_init=290.0, tangent_mode=3) + r = solve(step, name, props, nstatev, T_init=290.0, integrator="closest_point") assert r.status == 0 e, s, sv, Tn = r["Strain"][:, -1], r["Stress"][:, -1], r["Statev"][:, -1], r["Temp"][-1] De0 = np.array([4e-4, 1e-4, -1e-4, 1.5e-4, 0.0, 0.0]) @@ -104,7 +105,7 @@ def test_thermomechanical_kernel_closest_point_tangent_is_exact(name, props, nst def call(d, mode): out = sim.umat_T(name, col(e), col(d), col(s), I3, col(props), col(sv), 0.5, 0.3, np.zeros((4, 1), order="F"), np.zeros((3, 1), order="F"), - np.array([Tn]), np.array([0.0]), tangent_mode=mode) + np.array([Tn]), np.array([0.0]), **tangent_kwargs(mode)) return out[0][:, 0], out[5][:, :, 0] h = 1e-8 @@ -128,7 +129,7 @@ def test_sma_unified_T_closest_point_superelastic_loop_and_exact_tangent(): steps = [StepMeca(control=_UNIAXIAL, value=[0.04, 0, 0, 0, 0, 0], time=1.0, ninc=100, Dn_mini=0.01), StepMeca(control=_UNIAXIAL, value=[0.0] * 6, time=1.0, ninc=100, Dn_mini=0.01)] r2 = solve(steps, "SMADI", SMADI, 30, T_init=T_SUPERELASTIC, corate=3, tangent_mode=2) - r3 = solve(steps, "SMADI", SMADI, 30, T_init=T_SUPERELASTIC, corate=3, tangent_mode=3) + r3 = solve(steps, "SMADI", SMADI, 30, T_init=T_SUPERELASTIC, corate=3, integrator="closest_point") assert r3.status == 0 and len(r3) == 200 np.testing.assert_allclose(np.diff(r3["Time"]), 0.01) np.testing.assert_allclose(r3["Stress"][0], r2["Stress"][0], atol=0.5) # same loop, O(h^2) apart @@ -141,7 +142,7 @@ def test_sma_unified_T_closest_point_superelastic_loop_and_exact_tangent(): def call(d, mode): return sim.umat("SMADI", col(e), col(d), I3, I3, col(s), I3, col(SMADI), col(sv), 0.5, 0.01, np.zeros((4, 1), order="F"), temp=np.full(1, T_SUPERELASTIC), n_threads=1, - tangent_mode=mode, start=False) + **tangent_kwargs(mode), start=False) h = 1e-7 Lt = call(De0, 3)[3][:, :, 0] @@ -174,7 +175,7 @@ def _unified_TR_loop_and_tangent(name, extra, Y_Reo, points): steps = [StepMeca(control=_UNIAXIAL, value=[0.04, 0, 0, 0, 0, 0], time=1.0, ninc=200, Dn_mini=0.01), StepMeca(control=["stress"] * 6, value=[0.0] * 6, time=1.0, ninc=200, Dn_mini=0.01)] r2 = solve(steps, name, props, 30, T_init=T_SUPERELASTIC, corate=3, tangent_mode=2) - r3 = solve(steps, name, props, 30, T_init=T_SUPERELASTIC, corate=3, tangent_mode=3) + r3 = solve(steps, name, props, 30, T_init=T_SUPERELASTIC, corate=3, integrator="closest_point") assert r2.status == 0 and r3.status == 0 and len(r3) == 400 np.testing.assert_allclose(r3["Stress"][0], r2["Stress"][0], atol=0.5) assert abs(r3["Strain"][0, -1]) < 1e-3 # the loop closes (full reverse transformation) @@ -185,7 +186,7 @@ def _unified_TR_loop_and_tangent(name, extra, Y_Reo, points): def call(d): return sim.umat(name, col(e), col(d), I3, I3, col(s), I3, col(props), col(sv), 0.5, 0.005, np.zeros((4, 1), order="F"), temp=np.full(1, T_SUPERELASTIC), n_threads=1, - tangent_mode=3, start=False) + integrator="closest_point", start=False) h = 1e-7 out = call(De0) diff --git a/simcoon-python-builder/test/test_core/test_constitutive_options.py b/simcoon-python-builder/test/test_core/test_constitutive_options.py new file mode 100644 index 000000000..12886bd6f --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_constitutive_options.py @@ -0,0 +1,152 @@ +"""Numerical options of the local and global Newton loops (all off by default). + +- solve(ncp=...), sim.umat(ncp=...): complementarity function of the local cutting plane, + 'fischer_burmeister' (default) or 'min' (min-NCP with a Fischer-Burmeister fallback; single + surface, isotropic or Prager hardening), read by EPICP, EPKCP and MODUL; +- solve(precision_rel=...): stress-scaled tolerance of the global residual. +The default is the historical behaviour, bit for bit. +""" + +import numpy as np +import pytest + +import simcoon as sim +from simcoon.modular import ( + IsotropicElasticity, + ModularMaterial, + Plasticity, + PowerLawHardening, + PragerHardening, +) +from simcoon.solver import Block, StepMeca + +EPICP = [200000.0, 0.3, 1e-5, 300.0, 1000.0, 0.5] +EPICP_LIN = [200000.0, 0.3, 1e-5, 300.0, 2000.0, 1.0] # linear hardening: one return step +EPKCP = [200000.0, 0.3, 1e-5, 300.0, 1000.0, 0.5, 10000.0] +EPCHA = [140000.0, 0.3, 1e-6, 62.859017, 416.004456, 4.788635, 30382.293921, 172.425687, 195142.490843, + 3012.614659] + + +def cyclic(emax=0.02, ninc=50, ncycle=2): + uni = ["strain"] + ["stress"] * 5 + steps = [] + for _ in range(ncycle): + steps.append(StepMeca(control=uni, value=[emax, 0, 0, 0, 0, 0], time=1.0, ninc=ninc)) + steps.append(StepMeca(control=uni, value=[-emax, 0, 0, 0, 0, 0], time=1.0, ninc=2 * ninc)) + return Block(steps=steps) + + +def run(name, props, nstatev, **kw): + res = sim.solver.solve(cyclic(), name, np.asarray(props, float), nstatev, T_init=293.15, **kw) + assert res.status == 0 + return res + + +def rel(a, b): + return np.max(np.abs(np.asarray(a) - np.asarray(b))) / np.max(np.abs(np.asarray(b))) + + +def test_default_ncp_is_the_default(): + ref = run("EPKCP", EPKCP, 14) + res = run("EPKCP", EPKCP, 14, ncp="fischer_burmeister") + for key in ("Stress", "Strain", "Statev", "TangentMatrix"): + np.testing.assert_array_equal(res[key], ref[key]) + + +def test_min_ncp_epicp(): + # linear hardening: the min-NCP step is the exact return, so the result is the FB one to the + # local tolerance; power law: within the local tolerance of the return + for props, tol in ((EPICP_LIN, 1e-10), (EPICP, 1e-7)): + ref = run("EPICP", props, 8) + res = run("EPICP", props, 8, ncp="min") + assert rel(res["Stress"], ref["Stress"]) < tol + assert rel(res["Statev"][1], ref["Statev"][1]) < 10 * tol + + +def test_closed_form_tangent_epicp(): + # tangent_mode 2 takes the radial-return tangent in closed form: the exact operator of the + # closest-point update (integrator="closest_point", generic assembly) at the same converged state + ref = run("EPICP", EPICP, 8, integrator="closest_point") + res = run("EPICP", EPICP, 8) + assert rel(res["Stress"], ref["Stress"]) < 1e-8 + assert rel(res["TangentMatrix"], ref["TangentMatrix"]) < 1e-7 + + +def test_min_ncp_batch_umat(): + # the same option through sim.umat + N = 200 + rng = np.random.default_rng(2) + F = np.asfortranarray + Detot = F(4e-3 * rng.standard_normal((6, N))) + props = F(np.asarray(EPICP, float).reshape(-1, 1)) + out = [] + for ncp in ("fischer_burmeister", "min"): + sig, sv, Wm, Lt = sim.umat("EPICP", F(np.zeros((6, N))), Detot, np.array([]), np.array([]), F(np.zeros((6, N))), + F(np.repeat(np.eye(3)[:, :, None], N, axis=2)), props, F(np.zeros((8, N))), + 0.0, 1.0, F(np.zeros((4, N))), temp=np.array([293.15]), n_threads=1, ncp=ncp) + out.append((sig, Lt)) + assert rel(out[1][0], out[0][0]) < 1e-7 + assert rel(out[1][1], out[0][1]) < 1e-6 + + +def test_min_ncp_modular_and_epkcp_adapter(): + mat = ModularMaterial(IsotropicElasticity(C1=200000.0, C2=0.3, alpha=1e-5), + [Plasticity(sigma_Y=300.0, isotropic_hardening=PowerLawHardening(k=1000.0, m=0.5), + kinematic_hardening=PragerHardening(C=15000.0))]) + ref = run("EPKCP", EPKCP, 14) + res = run("EPKCP", EPKCP, 14, ncp="min") + mod = run("MODUL", mat.props, mat.nstatev, ncp="min") + assert rel(res["Stress"], ref["Stress"]) < 1e-6 + assert rel(mod["Stress"], res["Stress"]) < 1e-12 # the adapter forwards the option + + +@pytest.mark.parametrize("extra", ["prony", "damage"]) +def test_min_ncp_modular_with_inert_rows(extra): + # viscoelastic and damage rows are always satisfied and uncoupled: min-NCP applies to the + # yield surface next to them (the option has an effect) and gives the same return + from simcoon.modular import Damage, Viscoelasticity + other = (Viscoelasticity(terms=((20000.0, 0.3, 2000.0, 2000.0),) * 2) if extra == "prony" + else Damage(Y_0=0.1, Y_c=5.0)) + mat = ModularMaterial(IsotropicElasticity(C1=200000.0, C2=0.3, alpha=1e-5), + [Plasticity(sigma_Y=300.0, isotropic_hardening=PowerLawHardening(k=1000.0, m=0.5)), other]) + ref = run("MODUL", mat.props, mat.nstatev) + res = run("MODUL", mat.props, mat.nstatev, ncp="min") + assert not np.array_equal(res["Stress"], ref["Stress"]) + assert rel(res["Stress"], ref["Stress"]) < 1e-6 + + +def test_min_ncp_refuses_armstrong_frederick(): + # the cutting plane integrates AF back stresses along its iterates: min-NCP would change the + # return far beyond the tolerance on coarse increments, so MODUL refuses the combination + from simcoon.modular import ArmstrongFrederickHardening + mat = ModularMaterial(IsotropicElasticity(C1=200000.0, C2=0.3), + [Plasticity(sigma_Y=300.0, kinematic_hardening=ArmstrongFrederickHardening(C=30000.0, D=200.0))]) + run("MODUL", mat.props, mat.nstatev) + with pytest.raises(Exception, match="Armstrong-Frederick"): + run("MODUL", mat.props, mat.nstatev, ncp="min") + + +def test_ncp_values_and_laws_are_checked(): + with pytest.raises(ValueError): + run("EPICP", EPICP, 8, ncp="smooth") + with pytest.raises(ValueError): + run("EPICP", EPICP, 8, ncp=1) + with pytest.raises(ValueError, match="EPCHA"): + run("EPCHA", EPCHA, 33, ncp="min") + with pytest.raises(ValueError, match="cutting-plane"): + run("EPICP", EPICP, 8, ncp="min", integrator="closest_point") + with pytest.raises(ValueError, match="ncp"): + sim.umat("EPCHA", np.zeros((6, 1)), np.zeros((6, 1)), np.array([]), np.array([]), np.zeros((6, 1)), + np.eye(3).reshape(3, 3, 1), np.asarray(EPCHA, float), np.zeros((33, 1)), 0.0, 1.0, + np.zeros((4, 1)), ncp="min") + + +def test_relative_global_tolerance(): + ref = run("EPICP", EPICP, 8) + same = run("EPICP", EPICP, 8, precision_rel=0.0) + for key in ("Stress", "Strain", "Statev"): + np.testing.assert_array_equal(same[key], ref[key]) + res = run("EPICP", EPICP, 8, precision_rel=1e-7) + assert rel(res["Stress"], ref["Stress"]) < 1e-5 + with pytest.raises(Exception): + run("EPICP", EPICP, 8, precision_rel=-1.0) diff --git a/simcoon-python-builder/test/test_core/test_eshelby_quadrature_threads.py b/simcoon-python-builder/test/test_core/test_eshelby_quadrature_threads.py new file mode 100644 index 000000000..198206fa6 --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_eshelby_quadrature_threads.py @@ -0,0 +1,62 @@ +"""Concurrent mean-field solves with different Eshelby quadratures. + +The Gauss tables of the Eshelby integrals used to be process-wide statics, rewritten at the +start of every ellipsoidal solve and every L_eff while solver_run releases the GIL: two +threads solving with different (mp, np) read each other's tables (IndexError from the +Armadillo bounds check, or silently wrong results). Each (mp, np) now has its own immutable +tables, read by every pass of the scheme. +""" + +from concurrent.futures import ThreadPoolExecutor + +import numpy as np + +import simcoon as sim +from simcoon import solver as slv +from simcoon.solver.micromechanics import Ellipsoid + + +def _phases(elastic=False): + matrix = Ellipsoid(number=0, umat_name="EPICP", save=1, concentration=0.7, nstatev=8, + props=np.array([2250.0, 0.19, 8.8e-5, 30.0, 300.0, 0.4])) + if elastic: + matrix = Ellipsoid(number=0, umat_name="ELISO", save=1, concentration=0.7, nstatev=1, + props=np.array([2250.0, 0.19, 8.8e-5])) + fibre = Ellipsoid(number=1, umat_name="ELISO", save=1, concentration=0.3, nstatev=1, + props=np.array([73000.0, 0.22, 0.5e-6]), a1=2.0) + return [matrix, fibre] + + +_STEP = slv.StepMeca(control=["strain"] + ["stress"] * 5, value=[0.03, 0, 0, 0, 0, 0], + time=1.0, ninc=50) + + +def _solve(mp): + return slv.solve(_STEP, "MIMTN", np.array([mp, mp, 0.0]), 1, phases=_phases(), + tangent_mode="continuum")["Stress"][0, -1] + + +def _l_eff(mp): + return np.asarray(sim.L_eff("MISCN", np.array([mp, mp, 0.0]), 1, phases=_phases(elastic=True))) + + +def test_threaded_solves_match_the_sequential_ones(): + ref = {mp: _solve(mp) for mp in (10, 40)} + mps = [10, 40] * 8 + with ThreadPoolExecutor(8) as ex: + out = list(ex.map(_solve, mps)) + assert [o == ref[m] for o, m in zip(out, mps)] == [True] * len(mps) + + +def test_threaded_l_eff_and_solves_do_not_interfere(): + ref_solve = {mp: _solve(mp) for mp in (10, 40)} + ref_leff = {mp: _l_eff(mp) for mp in (12, 30)} + jobs = [("solve", 10), ("leff", 12), ("solve", 40), ("leff", 30)] * 4 + def run(job): + kind, mp = job + return _solve(mp) if kind == "solve" else _l_eff(mp) + with ThreadPoolExecutor(8) as ex: + out = list(ex.map(run, jobs)) + for (kind, mp), o in zip(jobs, out): + expected = ref_solve[mp] if kind == "solve" else ref_leff[mp] + np.testing.assert_array_equal(o, expected) diff --git a/simcoon-python-builder/test/test_core/test_identification_options.py b/simcoon-python-builder/test/test_core/test_identification_options.py new file mode 100644 index 000000000..9ea848964 --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_identification_options.py @@ -0,0 +1,350 @@ +"""Options of simcoon.identify.identification (least squares, threads), calc_residuals, +and the in-memory key templates of simcoon.parameter.""" + +import os + +import numpy as np +import pytest +from scipy.optimize import differential_evolution + +from simcoon.constant import Constant +from simcoon.identify import calc_cost, calc_residuals, identification +from simcoon.parameter import KeyTemplate, Parameter + +T = np.linspace(0.0, 1.0, 40) +A_REF, B_REF = 3.0, 250.0 + + +def decay_residuals(x): + """Residuals of y = a exp(-b t / 100) against the reference curve.""" + return x[0] * np.exp(-x[1] * T / 100.0) - A_REF * np.exp(-B_REF * T / 100.0) + + +def decay_params(): + return [Parameter(0, bounds=(0.1, 10.0)), Parameter(1, bounds=(1.0, 1000.0))] + + +class TestLeastSquares: + def test_recovers_parameters_and_writes_back(self): + params = decay_params() + result = identification(decay_residuals, params, method="least_squares") + assert result.x == pytest.approx([A_REF, B_REF], rel=1e-8) + assert [p.value for p in params] == pytest.approx([A_REF, B_REF], rel=1e-8) + assert np.sum(result.fun ** 2) == pytest.approx(2 * result.cost) + assert result.cost < 1e-16 + assert np.all((result.z >= 0) & (result.z <= 1)) + assert result.nfev >= len(result.starts[0].x0) + 1 + assert len(result.starts) == 1 + + def test_first_start_is_middle_of_scaled_range(self): + params = [Parameter(0, bounds=(1.0, 100.0)), Parameter(1, bounds=(-1.0, 1.0))] + result = identification(lambda x: x - [20.0, 0.3], params, method="least_squares") + # log scale (positive bounds): geometric mean; linear otherwise: midpoint + assert result.starts[0].x0 == pytest.approx([10.0, 0.0]) + assert result.x == pytest.approx([20.0, 0.3]) + + def test_first_start_uses_set_value(self): + params = decay_params() + params[1].value = 300.0 + result = identification(decay_residuals, params, method="least_squares") + assert result.starts[0].x0 == pytest.approx([np.sqrt(0.1 * 10.0), 300.0]) + + def test_multi_start_reproducible(self): + r1 = identification(decay_residuals, decay_params(), method="least_squares", + n_starts=4, seed=3) + r2 = identification(decay_residuals, decay_params(), method="least_squares", + n_starts=4, seed=3) + assert len(r1.starts) == 4 + assert [s.x0.tolist() for s in r1.starts] == [s.x0.tolist() for s in r2.starts] + assert r1.cost == min(s.cost for s in r1.starts) + assert r1.nfev == sum(s.nfev for s in r1.starts) + for s in r1.starts: + lb, ub = np.array([0.1, 1.0]), np.array([10.0, 1000.0]) + assert np.all((s.x0 >= lb) & (s.x0 <= ub)) + + @pytest.mark.parametrize("scale", ["linear", "log", ["log", "linear"]]) + def test_scales(self, scale): + result = identification(decay_residuals, decay_params(), method="least_squares", + scale=scale) + assert result.x == pytest.approx([A_REF, B_REF], rel=1e-6) + + def test_threads_do_not_change_the_result(self): + r1 = identification(decay_residuals, decay_params(), method="least_squares", + n_starts=3, seed=0) + r4 = identification(decay_residuals, decay_params(), method="least_squares", + n_starts=3, seed=0, n_threads=4) + assert np.array_equal(r1.x, r4.x) + assert r1.nfev == r4.nfev + + def test_args_forwarded(self): + def res(x, target, scale): + return scale * (x - target) + params = [Parameter(0, bounds=(-5.0, 5.0))] + result = identification(res, params, method="least_squares", args=([1.5], 2.0)) + assert result.x == pytest.approx([1.5]) + + def test_nan_residuals_reject_the_step(self): + """A model failing (NaN) in part of the box does not stop the optimizer.""" + def res(x): + if x[1] > 600.0: + return np.full(T.size, np.nan) + return decay_residuals(x) + params = decay_params() + params[1].value = 550.0 + result = identification(res, params, method="least_squares") + assert result.x == pytest.approx([A_REF, B_REF], rel=1e-6) + + def test_failed_start_is_skipped(self): + def res(x): + return np.full(3, np.nan) if x[0] > 9.0 else np.array([x[0] - 2.0, 0.0, 0.0]) + params = [Parameter(0, bounds=(1.0, 10.0))] + params[0].value = 9.5 + result = identification(res, params, method="least_squares", n_starts=2, seed=0) + assert result.starts[0].status == -1 + assert result.x == pytest.approx([2.0]) + + def test_all_starts_failing_raises(self): + params = [Parameter(0, bounds=(1.0, 10.0))] + with pytest.raises(RuntimeError, match="every starting point"): + identification(lambda x: np.full(3, np.nan), params, method="least_squares") + + def test_invalid_options(self): + with pytest.raises(ValueError, match="method"): + identification(decay_residuals, decay_params(), method="nelder") + with pytest.raises(ValueError, match="positive lower bound"): + identification(decay_residuals, [Parameter(0, bounds=(-1.0, 1.0))], + method="least_squares", scale="log") + with pytest.raises(ValueError, match="scale"): + identification(decay_residuals, decay_params(), method="least_squares", + scale="exp") + with pytest.raises(ValueError, match="n_starts"): + identification(decay_residuals, decay_params(), method="least_squares", + n_starts=0) + with pytest.raises(TypeError, match="jac"): + identification(decay_residuals, decay_params(), method="least_squares", + jac="3-point") + with pytest.raises(ValueError, match="n_threads"): + identification(decay_residuals, decay_params(), n_threads=-1) + + +class TestDifferentialEvolutionOptions: + @staticmethod + def cost(x): + return float(np.sum(decay_residuals(x) ** 2)) + + def test_default_is_unchanged(self): + """No option: the plain scipy call with polish=True.""" + params = decay_params() + result = identification(self.cost, params, seed=1, maxiter=30) + ref = differential_evolution(self.cost, [p.bounds for p in decay_params()], + seed=1, maxiter=30, polish=True) + assert np.array_equal(result.x, ref.x) + assert result.nfev == ref.nfev + + def test_threads_match_deferred_serial(self): + r1 = identification(self.cost, decay_params(), seed=1, maxiter=30, + updating="deferred") + r3 = identification(self.cost, decay_params(), seed=1, maxiter=30, n_threads=3) + assert np.array_equal(r1.x, r3.x) + assert r1.nfev == r3.nfev + + def test_threads_and_workers_exclusive(self): + with pytest.raises(ValueError, match="workers"): + identification(self.cost, decay_params(), n_threads=2, workers=2) + + +class TestSolverIdentificationThreads: + """End to end on the simcoon solver, whose solve releases the GIL.""" + + @staticmethod + def setup_case(): + import simcoon as sim + from simcoon.solver import Block, StepMeca + + block = Block(steps=[StepMeca(control=["strain"] + ["stress"] * 5, + value=[0.02, 0, 0, 0, 0, 0], ninc=20)]) + + def stress(x): + props = np.array([70000.0, 0.3, 0.0, x[0], x[1], 0.3]) + res = sim.solver.solve(block, "EPICP", props, 8, T_init=293.15) + return np.asarray(res["Stress"][0]) + + sigma_exp = stress([300.0, 1000.0]) + + def residuals(x): + return calc_residuals([sigma_exp], [stress(x)], metric="nmse_per_response") + + return residuals + + def test_epicp_serial_and_threads_identical(self): + residuals = self.setup_case() + params = [Parameter(0, bounds=(100.0, 600.0)), Parameter(1, bounds=(100.0, 5000.0))] + r1 = identification(residuals, params, method="least_squares") + params = [Parameter(0, bounds=(100.0, 600.0)), Parameter(1, bounds=(100.0, 5000.0))] + r2 = identification(residuals, params, method="least_squares", n_threads=2) + assert np.array_equal(r1.x, r2.x) + assert r1.x == pytest.approx([300.0, 1000.0], rel=1e-4) + + +class TestHomogenizationIdentificationThreads: + """L_eff releases the GIL and its quadrature tables are per (mp, np): a + threaded identification of a self-consistent composite equals the serial one.""" + + @staticmethod + def residuals(x, c_exp, E_exp): + import simcoon as sim + from simcoon.solver.micromechanics import Ellipsoid + + matrix = Ellipsoid(number=0, umat_name="ELISO", save=1, nstatev=1, + props=np.array([2250.0, 0.19, 8.8e-5])) + fibre = Ellipsoid(number=1, umat_name="ELISO", save=1, nstatev=1, + props=np.array([x[0], x[1], 0.5e-6])) + E = [] + for c in c_exp: + matrix.concentration, fibre.concentration = 1.0 - c, c + L = sim.L_eff("MISCN", np.array([10.0, 10.0, 0.0]), 0, phases=[matrix, fibre]) + E.append(sim.L_iso_props(L).flatten()[0]) + return np.array(E) - E_exp + + def test_miscn_serial_and_threads_identical(self): + c_exp = np.array([0.1, 0.2, 0.3, 0.4]) + E_exp = self.residuals([60000.0, 0.25], c_exp, np.zeros(4)) + results = [] + for nt in (1, 2): + params = [Parameter(0, bounds=(10000.0, 200000.0)), + Parameter(1, bounds=(0.01, 0.45))] + results.append(identification(self.residuals, params, method="least_squares", + args=(c_exp, E_exp), n_threads=nt)) + assert np.array_equal(results[0].x, results[1].x) + assert results[0].x[0] == pytest.approx(60000.0, rel=1e-4) + + +class TestCalcResiduals: + @staticmethod + def data(): + rng = np.random.default_rng(0) + y_exp = [rng.normal(size=(20, 2)) * [100.0, 0.1], + rng.normal(size=(15, 2)) * [50.0, 0.05], + rng.normal(size=10)] + y_num = [y + rng.normal(size=y.shape) * 0.1 * np.abs(y).mean(axis=0) for y in y_exp] + weights = dict( + w_test=np.array([1.0, 2.0, 0.5]), + w_response=[np.array([1.0, 3.0]), np.array([2.0, 1.0]), np.array([1.0])], + w_point=[rng.uniform(size=y.shape) for y in y_exp[:2]] + + [rng.uniform(size=(10, 1))], + ) + return y_exp, y_num, weights + + @pytest.mark.parametrize("metric", ["mse", "nmse", "nmse_per_response"]) + @pytest.mark.parametrize("weighted", [False, True]) + def test_sum_of_squares_is_calc_cost(self, metric, weighted): + y_exp, y_num, weights = self.data() + kw = weights if weighted else {} + r = calc_residuals(y_exp, y_num, metric=metric, **kw) + assert r.ndim == 1 and r.size == 20 * 2 + 15 * 2 + 10 + assert np.sum(r ** 2) == pytest.approx(calc_cost(y_exp, y_num, metric=metric, **kw), + rel=1e-12) + + def test_sign_is_model_minus_experiment(self): + r = calc_residuals([np.array([1.0, 2.0])], [np.array([2.0, 2.0])]) + assert r[0] > 0 and r[1] == 0 + + def test_unsupported_metric(self): + with pytest.raises(ValueError, match="sum-of-squares"): + calc_residuals([np.ones(3)], [np.ones(3)], metric="mae") + + def test_shape_mismatch(self): + with pytest.raises(ValueError, match="shape"): + calc_residuals([np.ones(3)], [np.ones(4)]) + with pytest.raises(ValueError, match="tests"): + calc_residuals([np.ones(3)], [np.ones(3), np.ones(3)]) + + +class TestKeyTemplate: + @staticmethod + def setup(tmp_path): + keys = tmp_path / "keys" + keys.mkdir() + (keys / "mat.json").write_text('{"props": [@E, @Et, @nu, @m]}') + (keys / "other.dat").write_text("E=@E m=@m mu=@mu") + params = [ + Parameter(0, bounds=(0, 1), key="@E", sim_input_files=["mat.json", "other.dat"]), + Parameter(1, bounds=(0, 1), key="@Et", sim_input_files=["mat.json"]), + Parameter(2, bounds=(0, 1), key="@nu", sim_input_files=["mat.json"]), + ] + consts = [ + Constant(0, "@m", np.array([0.3]), 0.3, ["mat.json", "other.dat"]), + Constant(1, "@mu", np.array([0.5]), 0.5, ["other.dat"]), + ] + for p, v in zip(params, (70000.0, 2000.0, 0.3)): + p.value = v + return keys, params, consts + + def test_parameters_then_constants_per_file(self, tmp_path): + keys, params, consts = self.setup(tmp_path) + tpl = tmp_path / "tpl" + tpl.mkdir() + KeyTemplate(keys, params, consts).write(tpl) + assert sorted(os.listdir(tpl)) == ["mat.json", "other.dat"] + assert (tpl / "mat.json").read_text() == '{"props": [70000.0, 2000.0, 0.3, 0.3]}' + assert (tpl / "other.dat").read_text() == "E=70000.0 m=0.3 mu=0.5" + + def test_templates_read_once(self, tmp_path): + keys, params, consts = self.setup(tmp_path) + template = KeyTemplate(keys, params, consts) + (keys / "mat.json").write_text("changed on disk") + out = tmp_path / "out" + out.mkdir() + params[0].value = 1.5 + template.write(out) + assert (out / "mat.json").read_text() == '{"props": [1.5, 2000.0, 0.3, 0.3]}' + assert template.render("other.dat") == "E=1.5 m=0.3 mu=0.5" + + def test_invalid_paths(self, tmp_path): + keys, params, consts = self.setup(tmp_path) + with pytest.raises(TypeError): + KeyTemplate(123, params) + with pytest.raises(TypeError): + KeyTemplate(keys, params).write(123) + + +class TestKeyTemplatePerTest: + """Constants with one value per test: ``write(dst, test=i)``.""" + + @staticmethod + def setup(tmp_path): + keys = tmp_path / "keys" + keys.mkdir() + (keys / "ply.json").write_text('{"props": [@E, @theta]}') + params = [Parameter(0, bounds=(0, 1), key="@E", sim_input_files=["ply.json"])] + params[0].value = 70000.0 + theta = Constant(0, "@theta", np.array([0.0, 45.0, 90.0]), 0.0, ["ply.json"]) + return keys, params, theta + + def test_fibre_angle_of_each_test(self, tmp_path): + keys, params, theta = self.setup(tmp_path) + template = KeyTemplate(keys, params, [theta]) + for i, angle in enumerate((0.0, 45.0, 90.0)): + out = tmp_path / f"test_{i}" + out.mkdir() + template.write(out, test=i) + assert (out / "ply.json").read_text() == f'{{"props": [70000.0, {angle}]}}' + # without test, the constant keeps its own value + assert template.render("ply.json") == '{"props": [70000.0, 0.0]}' + + def test_test_out_of_range(self, tmp_path): + keys, params, theta = self.setup(tmp_path) + with pytest.raises(IndexError, match="@theta"): + KeyTemplate(keys, params, [theta]).render("ply.json", test=3) + + def test_single_value_constant_warns_once(self, tmp_path): + keys, params, _theta = self.setup(tmp_path) + single = Constant(0, "@theta", np.array([30.0]), 30.0, ["ply.json"]) + template = KeyTemplate(keys, params, [single]) + with pytest.warns(UserWarning, match="single value"): + assert template.render("ply.json", test=0) == '{"props": [70000.0, 30.0]}' + import warnings + with warnings.catch_warnings(): + warnings.simplefilter("error") + assert template.render("ply.json", test=1) == '{"props": [70000.0, 30.0]}' + template.render("ply.json") # no test: no warning either diff --git a/simcoon-python-builder/test/test_core/test_johnson_cook.py b/simcoon-python-builder/test/test_core/test_johnson_cook.py index d80ae9dfe..12cb9c8ac 100644 --- a/simcoon-python-builder/test/test_core/test_johnson_cook.py +++ b/simcoon-python-builder/test/test_core/test_johnson_cook.py @@ -13,6 +13,7 @@ import simcoon as sim from simcoon.solver import StepMeca, StepThermomeca, solve +from solver_harness import tangent_kwargs # AISI 4340 (Johnson & Cook, 1983): E, nu, alpha, A, B, n, C, edot0, m, T_ref, T_melt JC = np.array([200000.0, 0.33, 1.0e-5, 792.0, 510.0, 0.26, 0.014, 1.0, 1.03, 293.0, 1793.0]) @@ -115,7 +116,7 @@ def _point(tangent_mode, DTime=1.0e-3): DR = np.asfortranarray(np.eye(3)[:, :, None]) return sim.umat("EPJCK", col(np.zeros(6)), col(De), np.empty(0), np.empty(0), col(np.zeros(6)), DR, col(JC), col(np.zeros(NSTATEV)), 0.0, DTime, col(np.zeros(4)), - temp=np.array([293.15]), n_threads=1, tangent_mode=tangent_mode, start=True) + temp=np.array([293.15]), n_threads=1, **tangent_kwargs(tangent_mode), start=True) def test_tangent_mode_none_returns_elastic_operator_for_explicit_integration(): @@ -131,8 +132,8 @@ def test_tangent_mode_none_returns_elastic_operator_for_explicit_integration(): def test_closest_point_mode_is_the_cutting_plane_result_by_construction(): """Radial J2 flow with isotropic (rate- and temperature-dependent) hardening: the cutting-plane update IS the closest-point projection and the algorithmic operator is the - exact Jacobian of the discrete map, so mode 3 returns the mode-2 state and tangent - bit for bit (no separate branch to maintain).""" + exact Jacobian of the discrete map, so integrator='closest_point' returns the cutting-plane + state and tangent bit for bit (no separate branch to maintain).""" sigma2, statev2, _, Lt2 = _point(2) sigma3, statev3, _, Lt3 = _point(3) assert statev2[1, 0] > 0.0, "the point must yield" diff --git a/simcoon-python-builder/test/test_core/test_l_eff_phases.py b/simcoon-python-builder/test/test_core/test_l_eff_phases.py index 1a1bd8ae9..c43a7c66c 100644 --- a/simcoon-python-builder/test/test_core/test_l_eff_phases.py +++ b/simcoon-python-builder/test/test_core/test_l_eff_phases.py @@ -73,7 +73,8 @@ def test_matrix_index_must_be_a_phase(self): np.testing.assert_allclose(L, L_iso, rtol=1e-9) # a one-phase composite is that phase @pytest.mark.parametrize("props, message", [ - ([2.0, 0.0, 20.0, 20.0, 0.0], "got 5 values"), # the pre-2.0 layout + ([2.0, 0.0, 20.0, 20.0, 0.0], "pre-2.0 layout"), # [nphases, file, mp, np, n_matrix]: closed_form = 20 + ([2.0, 1.0, 20.0, 20.0, 0.0, 0.0], "got 6 values"), ([20.0, 20.0], "got 2 values"), ([20.0, 20.0, 2.0], "n_matrix = 2.* is not one of the 2 phases"), ([20.0, 20.0, -1.0], "n_matrix = -1"), @@ -139,3 +140,28 @@ def test_homogeneous_model_needs_no_phases(self): L = np.asarray(sim._core.L_eff("ELISO", np.array([70000.0, 0.3, 1.0e-5]), 1)) np.testing.assert_allclose(L, np.asarray(sim.L_iso([70000.0, 0.3], "Enu")), rtol=1e-12, atol=1e-9) + + +def test_l_eff_releases_the_gil(): + """The homogenization runs without the GIL: a Python thread keeps running meanwhile + (a held GIL would freeze it for the whole call).""" + import threading + import time + phases = two_phase_composite() + props = np.array([300.0, 300.0, 0.0]) # some 30 ms of integrations (a fresh thread caches none) + done = threading.Event() + elapsed = [] + def homogenize(): + t0 = time.perf_counter() + sim.L_eff("MISCN", props, NSTATEV, phases=phases) + elapsed.append(time.perf_counter() - t0) + done.set() + worker = threading.Thread(target=homogenize) + last, gaps = time.perf_counter(), [] + worker.start() + while not done.is_set(): + now = time.perf_counter() + gaps.append(now - last) + last = now + worker.join() + assert max(gaps) < 0.5 * elapsed[0], (max(gaps), elapsed[0]) diff --git a/simcoon-python-builder/test/test_core/test_lt_convert_batch.py b/simcoon-python-builder/test/test_core/test_lt_convert_batch.py index 79d9cbed8..97c3e2ee8 100644 --- a/simcoon-python-builder/test/test_core/test_lt_convert_batch.py +++ b/simcoon-python-builder/test/test_core/test_lt_convert_batch.py @@ -38,3 +38,11 @@ def test_batch_rejects_mismatched_point_counts(): Lt, F, stress = _batch(10) with pytest.raises(ValueError, match="one entry per point"): sim.Lt_convert(Lt, np.asfortranarray(F[:, :, :9]), stress, "DsigmaDe_2_DSDE") + + +@pytest.mark.parametrize("n_threads", [1, 2, 0]) +def test_batch_is_the_same_for_any_thread_count(n_threads): + Lt, F, stress = _batch(500) + ref = np.asarray(sim.Lt_convert(Lt, F, stress, "DsigmaDe_2_DSDE", n_threads=4)) + out = np.asarray(sim.Lt_convert(Lt, F, stress, "DsigmaDe_2_DSDE", n_threads=n_threads)) + np.testing.assert_array_equal(out, ref) diff --git a/simcoon-python-builder/test/test_core/test_mean_field_options.py b/simcoon-python-builder/test/test_core/test_mean_field_options.py new file mode 100644 index 000000000..dadd2405d --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_mean_field_options.py @@ -0,0 +1,160 @@ +"""Numerical options of the ellipsoidal mean-field schemes (trailing props slots) and the +Fourier weights of discretize_odf. Every option defaults to the historical behaviour. + +MIMTN props: [mp, np, n_matrix, closed_form, rule] +MISCN props: [mp, np, n_matrix, start, closed_form, rule, anderson, rtol] +""" + +import warnings + +import numpy as np +import pytest + +import simcoon as sim +from simcoon import solver as slv +from simcoon.solver.micromechanics import Ellipsoid, Peak, discretize_odf + + +def eliso(n, c, E, nu, **kw): + return Ellipsoid(number=n, umat_name="ELISO", save=1, concentration=c, nstatev=1, + props=np.array([E, nu, 1e-5]), **kw) + + +def composite(c=0.3, a1=1.0, E_m=2250.0): + return [eliso(0, 1 - c, E_m, 0.19), eliso(1, c, E_m * 73000.0 / 2250.0, 0.19, a1=a1)] + + +def L_eff(scheme, props, phases): + return np.asarray(sim.L_eff(scheme, np.asarray(props, dtype=float), 1, phases=phases)) + + +def rel(a, b): + return np.linalg.norm(a - b) / np.linalg.norm(b) + + +class TestDefaults: + + @pytest.mark.parametrize("scheme, base, zeros", [("MIMTN", [30, 30, 0], [0, 0]), + ("MISCN", [30, 30, 0], [1, 0, 0, 0, 0])]) + def test_explicit_zero_options_are_the_default(self, scheme, base, zeros): + ph = composite(a1=4.0) + np.testing.assert_array_equal(L_eff(scheme, base + zeros, ph), L_eff(scheme, base, ph)) + + @pytest.mark.parametrize("props, message", [ + ([30, 30, 0, 2], "closed_form"), ([30, 30, 0, 0, 0.5], "rule"), ([30, 30, 0, 0, 0, 0], "MIMTN takes"), + ]) + def test_mori_tanaka_options_are_checked(self, props, message): + with pytest.raises(ValueError, match=message): + L_eff("MIMTN", props, composite()) + + @pytest.mark.parametrize("props, message", [ + ([30, 30, 0, 1, 0, 0, 11], "anderson"), ([30, 30, 0, 1, 0, 0, 1.5], "anderson"), + ([30, 30, 0, 1, 0, 0, 2, -1e-8], "rtol"), ([30, 30, 0, 1, 0, 0, 2, 1e-8, 0], "MISCN takes"), + ]) + def test_self_consistent_options_are_checked(self, props, message): + with pytest.raises(ValueError, match=message): + L_eff("MISCN", props, composite()) + + +class TestClosedFormEshelby: + + @pytest.mark.parametrize("a1", [1.0, 3.0, 0.3]) + def test_mori_tanaka_matches_a_fine_quadrature(self, a1): + ph = composite(a1=a1) + closed = L_eff("MIMTN", [20, 20, 0, 1], ph) + fine = L_eff("MIMTN", [200, 200, 0], ph) + assert rel(closed, fine) < 1e-8 + + def test_self_consistent_spheres(self): + """Spheres in isotropic phases: the self-consistent medium stays isotropic, every + Eshelby tensor of the fixed point is the closed form.""" + ph = composite(c=0.5) + closed = L_eff("MISCN", [20, 20, 0, 1, 1], ph) + assert rel(closed, L_eff("MISCN", [50, 50, 0], ph)) < 1e-10 + + def test_anisotropic_medium_is_integrated(self): + ph = composite(a1=4.0) + ph[0] = Ellipsoid(number=0, umat_name="ELIST", save=1, concentration=0.7, nstatev=1, + props=np.array([3, 20000.0, 3000.0, 0.3, 0.35, 1500.0, 1e-5, 1e-5])) + np.testing.assert_array_equal(L_eff("MIMTN", [30, 30, 0, 1], ph), L_eff("MIMTN", [30, 30, 0], ph)) + + +class TestHalfSphereRule: + + def test_fibre_against_the_closed_form(self): + """Isotropic matrix, fibre of aspect ratio 10: the closed form is exact. The half-sphere + rule evaluates half the points of Gauss-Legendre for an error of the same order, and + converges.""" + ph = composite(a1=10.0) + exact = L_eff("MIMTN", [20, 20, 0, 1], ph) + gauss = rel(L_eff("MIMTN", [50, 50, 0], ph), exact) + half = rel(L_eff("MIMTN", [50, 50, 0, 0, 1], ph), exact) + finer = rel(L_eff("MIMTN", [100, 100, 0, 0, 1], ph), exact) + assert half < 1.5 * gauss + assert finer < 0.2 * half + + def test_solver_takes_it(self): + phases = [Ellipsoid(number=0, umat_name="EPICP", save=1, nstatev=8, concentration=0.7, + props=np.array([70000.0, 0.3, 1e-5, 200.0, 1000.0, 0.3])), + eliso(1, 0.3, 400000.0, 0.2)] + step = slv.StepMeca(control=["strain"] + ["stress"] * 5, value=[0.02, 0, 0, 0, 0, 0], + time=1.0, ninc=20) + ref = slv.solve(step, "MIMTN", np.array([50.0, 50.0, 0.0]), 1, phases=phases)["Stress"] + half = slv.solve(step, "MIMTN", np.array([50.0, 50.0, 0.0, 0.0, 1.0]), 1, phases=phases)["Stress"] + np.testing.assert_allclose(half[0], ref[0], rtol=1e-6) # lateral stresses: solver residuals + + +class TestSelfConsistentFixedPoint: + + def test_anderson_reaches_the_same_fixed_point(self): + ph = composite(c=0.5) + converged = L_eff("MISCN", [20, 20, 0, 1, 1, 0, 0, 1e-15], ph) # plain iterations, tight + anderson = L_eff("MISCN", [20, 20, 0, 1, 1, 0, 2, 1e-12], ph) + assert rel(anderson, converged) < 1e-11 + assert rel(L_eff("MISCN", [20, 20, 0, 1, 1], ph), converged) < 1e-9 + + def test_relative_tolerance_does_not_depend_on_the_unit(self): + mpa = L_eff("MISCN", [20, 20, 0, 1, 1, 0, 2, 1e-12], composite(c=0.5)) + pa = L_eff("MISCN", [20, 20, 0, 1, 1, 0, 2, 1e-12], composite(c=0.5, E_m=2250.0e6)) + assert rel(pa / 1e6, mpa) < 1e-11 + + +class TestFourierOrientationWeights: + """Mori-Tanaka, isotropic matrix, fibres swept about z: the average is exact with Fourier + weights, whatever the number of phases.""" + + matrix = Ellipsoid(number=0, umat_name="ELISO", save=1, nstatev=1, concentration=0.7, + props=np.array([3000.0, 0.35, 1e-5])) + fibre = Ellipsoid(number=1, umat_name="ELISO", save=1, nstatev=1, concentration=0.3, + props=np.array([72000.0, 0.22, 1e-6]), a1=20.0) + props = np.array([30.0, 30.0, 0.0]) + + def average(self, n, weights, s_dev): + peaks = [Peak(method=3, mean=47.0, s_dev=s_dev)] + ph = discretize_odf([self.matrix, self.fibre], 1, peaks, n, axis=(0, 0, 1), weights=weights) + return np.asarray(sim.L_eff("MIMTN", self.props, 0, phases=ph)), len(ph) - 1 + + @pytest.mark.parametrize("s_dev", [40.0, 20.0]) # projected weights, then non-negative ones + def test_exact_with_few_phases(self, s_dev): + L6, _ = self.average(6, "fourier", s_dev) + L36, kept = self.average(36, "fourier", s_dev) + assert kept <= 36 + assert rel(L6, L36) < 1e-12 + simpson, _ = self.average(720, "simpson", s_dev) + assert rel(L6, simpson) < 5e-6 # the error of 720 Simpson phases + + def test_too_peaked_density_falls_back_on_simpson(self): + with pytest.warns(UserWarning, match="Simpson weights used instead"): + fourier, n = self.average(6, "fourier", 2.0) + simpson, _ = self.average(6, "simpson", 2.0) + assert n == 6 + np.testing.assert_array_equal(fourier, simpson) + + def test_arguments_are_checked(self): + peaks = [Peak(method=3, mean=47.0, s_dev=20.0)] + with pytest.raises(ValueError, match="half turn"): + discretize_odf([self.matrix, self.fibre], 1, peaks, 12, angle_range=(0, 360), weights="fourier") + with pytest.raises(ValueError, match="nphases >= 5"): + discretize_odf([self.matrix, self.fibre], 1, peaks, 4, weights="fourier") + with pytest.raises(ValueError, match="'simpson' or 'fourier'"): + discretize_odf([self.matrix, self.fibre], 1, peaks, 12, weights="gauss") diff --git a/simcoon-python-builder/test/test_core/test_modular_closest_point.py b/simcoon-python-builder/test/test_core/test_modular_closest_point.py index 049cb9f55..8da00fef2 100644 --- a/simcoon-python-builder/test/test_core/test_modular_closest_point.py +++ b/simcoon-python-builder/test/test_core/test_modular_closest_point.py @@ -1,4 +1,4 @@ -"""tangent_mode = 3 (closest-point projection) on the modular UMAT. +"""integrator="closest_point" (closest-point projection) on the modular UMAT. The cutting-plane loop (modes 1/2) accumulates the plastic strain along the flow direction of each iterate; the closest-point branch solves stress, multipliers and backward-Euler state @@ -14,6 +14,7 @@ LinearIsotropicHardening, PragerHardening, ArmstrongFrederickHardening, ChabocheHardening, Viscoelasticity, Damage) from simcoon.solver import StepMeca, solve +from solver_harness import tangent_kwargs _UNIAXIAL = ["strain"] + ["stress"] * 5 HILL = dict(F=0.5, G=0.7, H=0.4, L=1.6, M=1.3, N=1.8) @@ -48,7 +49,7 @@ def _material(*mechs): def _plastic_state(mat, mode, ninc=10): r = solve(StepMeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=ninc, time=0.5), - "MODUL", mat.props, mat.nstatev, T_init=290., tangent_mode=mode) + "MODUL", mat.props, mat.nstatev, T_init=290., **tangent_kwargs(mode)) assert r.status == 0 return r @@ -56,7 +57,7 @@ def _plastic_state(mat, mode, ninc=10): def _umat(mat, e, s, sv, De, mode, time=0.5, dtime=0.3): """One increment of the modular UMAT from state (e, s, sv): (sigma, statev, Wm, Lt).""" return sim.umat("MODUL", col(e), col(De), I3, I3, col(s), I3, col(mat.props), col(sv), - time, dtime, np.zeros((4, 1), order="F"), n_threads=1, tangent_mode=mode) + time, dtime, np.zeros((4, 1), order="F"), n_threads=1, **tangent_kwargs(mode)) def _tangent_and_fd(mat, e, s, sv, mode, De0=None, h=1e-8): @@ -175,7 +176,7 @@ def test_two_mechanisms_reload_converges_without_step_cut(): steps = [StepMeca(control=_UNIAXIAL, value=[0.02, 0, 0, 0, 0, 0], ninc=10, time=1.), StepMeca(control=_UNIAXIAL, value=[-0.02, 0, 0, 0, 0, 0], ninc=4, time=1.), StepMeca(control=_UNIAXIAL, value=[0.02, 0, 0, 0, 0, 0], ninc=4, time=1.)] - r3 = solve(steps, "MODUL", mat.props, mat.nstatev, T_init=290., tangent_mode=3) + r3 = solve(steps, "MODUL", mat.props, mat.nstatev, T_init=290., integrator="closest_point") assert r3.status == 0 and len(r3) == 18 s = r3["Stress"][0] assert s[9] > 20. and s[13] < -20. and s[-1] > 20. # loads, reverses, reloads @@ -198,7 +199,7 @@ def test_two_active_rows_closest_point_tangent_is_exact(): isotropic_hardening=LinearIsotropicHardening(H=100.)) mat = _material(p1, p2) r = solve(StepMeca(control=_UNIAXIAL, value=[0.06, 0, 0, 0, 0, 0], ninc=20, time=1.), - "MODUL", mat.props, mat.nstatev, T_init=290., tangent_mode=3) + "MODUL", mat.props, mat.nstatev, T_init=290., integrator="closest_point") assert r.status == 0 e, s, sv = r["Strain"][:, -1], r["Stress"][:, -1], r["Statev"][:, -1] assert sv[1] > 1e-3 and sv[14] > 1e-3 # both rows carried multipliers diff --git a/simcoon-python-builder/test/test_core/test_rotate_voigt.py b/simcoon-python-builder/test/test_core/test_rotate_voigt.py new file mode 100644 index 000000000..f552796a7 --- /dev/null +++ b/simcoon-python-builder/test/test_core/test_rotate_voigt.py @@ -0,0 +1,124 @@ +"""rotate_voigt: batch rotation of Voigt tensors by (3,3,N) rotation matrices. + +It must match Rotation.from_matrix(DR).apply_ to round-off, for one point and for +batches past the parallel cutoff, and give the same bits whatever n_threads.""" + +import numpy as np +import pytest + +import simcoon as sim + +_VECTOR_KINDS = ["stress", "strain"] +_TENSOR_KINDS = ["stiffness", "compliance", "strain_concentration", "stress_concentration"] + + +def _rotations(n, seed=0): + """(3,3,n) F-ordered rotation matrices, as objective_rate returns DR.""" + mats = sim.Rotation.random(n, random_state=seed).as_matrix().reshape(n, 3, 3) + return np.asfortranarray(mats.transpose(1, 2, 0)) + + +def _values(kind, n, seed=1): + rng = np.random.default_rng(seed) + shape = (6, n) if kind in _VECTOR_KINDS else (6, 6, n) + return np.asfortranarray(100.0 * rng.standard_normal(shape)) + + +def _reference(values, DR, kind, active): + """The scipy path fedoo used: one Rotation object for the batch, then apply_.""" + rot = sim.Rotation.from_matrix(DR.transpose(2, 0, 1) if DR.ndim == 3 else DR) + return getattr(rot, "apply_" + kind)(values, active=active) + + +def _assert_roundoff(result, reference): + scale = max(1.0, float(np.abs(reference).max())) + np.testing.assert_allclose(result, reference, rtol=0, atol=1e-14 * scale) + + +@pytest.mark.parametrize("active", [True, False]) +@pytest.mark.parametrize("kind", _VECTOR_KINDS + _TENSOR_KINDS) +@pytest.mark.parametrize("n", [1, 7, 1000]) +def test_matches_rotation_apply(kind, n, active): + DR = _rotations(n) + values = _values(kind, n) + result = sim.rotate_voigt(values, DR, kind, active) + assert result.shape == values.shape + _assert_roundoff(result, _reference(values, DR, kind, active)) + + +@pytest.mark.parametrize("kind", _VECTOR_KINDS + _TENSOR_KINDS) +def test_single_tensor_single_rotation(kind): + DR = _rotations(1)[:, :, 0] + values = _values(kind, 1)[..., 0] + result = sim.rotate_voigt(values, DR, kind) + assert result.shape == values.shape + _assert_roundoff(result, _reference(values, DR, kind, True)) + + +@pytest.mark.parametrize("kind", ["stress", "stiffness"]) +@pytest.mark.parametrize("one_rotation", [lambda DR: DR[:, :, 0], lambda DR: DR[:, :, :1]]) +def test_one_rotation_for_every_tensor(kind, one_rotation): + n = 250 + DR = _rotations(n) + values = _values(kind, n) + repeated = np.asfortranarray(np.repeat(DR[:, :, :1], n, axis=2)) + np.testing.assert_array_equal(sim.rotate_voigt(values, one_rotation(DR), kind), + sim.rotate_voigt(values, repeated, kind)) + + +@pytest.mark.parametrize("kind", ["strain", "stiffness"]) +def test_same_bits_for_any_thread_count(kind): + n = 5000 + DR = _rotations(n) + values = _values(kind, n) + serial = sim.rotate_voigt(values, DR, kind, n_threads=1) + for n_threads in (0, 2, 4): + np.testing.assert_array_equal(sim.rotate_voigt(values, DR, kind, n_threads=n_threads), serial) + + +@pytest.mark.parametrize("kind", ["stress", "compliance"]) +def test_any_memory_layout(kind): + n = 300 + DR = _rotations(n) + values = _values(kind, n) + expected = sim.rotate_voigt(values, DR, kind) + # C-ordered copies, and DR as the (3,3,N) view of an (N,3,3) stack + DR_view = np.ascontiguousarray(DR.transpose(2, 0, 1)).transpose(1, 2, 0) + np.testing.assert_array_equal( + sim.rotate_voigt(np.ascontiguousarray(values), DR_view, kind), expected) + + +def test_inputs_are_not_modified(): + DR = _rotations(200) + values = _values("stiffness", 200) + DR_copy, values_copy = DR.copy(), values.copy() + sim.rotate_voigt(values, DR, "stiffness") + np.testing.assert_array_equal(DR, DR_copy) + np.testing.assert_array_equal(values, values_copy) + + +def test_objective_rate_increment(): + """The fedoo use: transport start strain and stress with the DR of objective_rate.""" + n = 400 + rng = np.random.default_rng(5) + F0 = np.asfortranarray(np.eye(3)[:, :, None] + 0.05 * rng.standard_normal((3, 3, n))) + F1 = np.asfortranarray(F0 + 0.02 * rng.standard_normal((3, 3, n))) + _, DR, _ = sim.objective_rate("log_R", F0, F1, 1.0) + for kind in ("strain", "stress"): + values = _values(kind, n) + _assert_roundoff(sim.rotate_voigt(values, DR, kind, n_threads=0), + _reference(values, DR, kind, True)) + + +@pytest.mark.parametrize("values, DR, kind, match", [ + (np.zeros((6, 4)), np.zeros((3, 3, 4)), "tangent", "unknown kind"), + (np.zeros((3, 4)), np.zeros((3, 3, 4)), "stress", r"shape \(6,\) or \(6, N\)"), + (np.zeros((6, 6, 4)), np.zeros((3, 3, 4)), "strain", r"shape \(6,\) or \(6, N\)"), + (np.zeros((6, 4)), np.zeros((3, 3, 4)), "stiffness", r"shape \(6, 6\) or \(6, 6, N\)"), + (np.zeros((6, 4)), np.zeros((3, 4)), "stress", r"DR must have shape"), + (np.zeros((6, 4)), np.zeros((3, 3, 3)), "stress", "expected 1 or 4"), + (np.zeros(6), np.zeros((3, 3, 2)), "stress", "expected 1 or 1"), +]) +def test_rejects_bad_shapes(values, DR, kind, match): + with pytest.raises(ValueError, match=match): + sim.rotate_voigt(values, DR, kind) diff --git a/simcoon-python-builder/test/test_core/test_solver_phases.py b/simcoon-python-builder/test/test_core/test_solver_phases.py index 6dec4393e..ecd039c40 100644 --- a/simcoon-python-builder/test/test_core/test_solver_phases.py +++ b/simcoon-python-builder/test/test_core/test_solver_phases.py @@ -116,6 +116,38 @@ def test_concentrations_must_sum_to_one(self): slv.solve(uniaxial_step(), "MIMTN", MIMTN_PROPS, NSTATEV, phases=phases) +class TestOtherSchemesInTheSolver: + + def test_homogeneous_strain_scheme_is_the_voigt_bound(self): + """MIHEN in the solver (it used to dereference a null sub-phase pointer): the apparent + modulus is the Voigt one that L_eff gives on the same phases.""" + phases = two_phase_composite() + props = np.array([20.0, 20.0]) + res = slv.solve(uniaxial_step(), "MIHEN", props, NSTATEV, phases=phases) + apparent = res["Stress"][0, -1] / res["Strain"][0, -1] + direct = sim.L_iso_props(sim.L_eff("MIHEN", props, NSTATEV, phases=phases)).flatten()[0] + assert apparent == pytest.approx(direct, rel=1e-9) + + def test_self_consistent_runs_over_several_blocks(self): + """MISCN failed at the priming call of the second block (the solver clears the + effective tangent, the scheme then inverted a zero reference medium): a load split + into two blocks follows the one-block run, and unloading runs.""" + props = np.array([50.0, 50.0, 0.0]) + def plastic_composite(): + matrix = Ellipsoid(number=0, umat_name="EPICP", save=1, nstatev=8, concentration=0.7, + props=np.array([70000.0, 0.3, 1e-5, 200.0, 1000.0, 0.3])) + sphere = Ellipsoid(number=1, umat_name="ELISO", save=1, nstatev=1, concentration=0.3, + props=np.array([400000.0, 0.2, 1e-6])) + return [matrix, sphere] + one = slv.solve([uniaxial_step(0.01, 40)], "MISCN", props, 1, phases=plastic_composite()) + two = slv.solve([uniaxial_step(0.005, 20), uniaxial_step(0.01, 20)], "MISCN", props, 1, + phases=plastic_composite()) + np.testing.assert_allclose(two["Stress"][0], one["Stress"][0], rtol=1e-6) + back = slv.solve([uniaxial_step(0.01, 20), uniaxial_step(0.005, 20)], "MISCN", props, 1, + phases=plastic_composite()) + assert back["Stress"][0, -1] < back["Stress"][0, 19] + + class TestNestedComposite: """A sub-phase that is itself a mean-field model, as Nellipsoids0.dat -> Nellipsoids1.dat chained through props[1] in the file era.""" diff --git a/simcoon-python-builder/test/test_core/test_solver_run.py b/simcoon-python-builder/test/test_core/test_solver_run.py index 19b518c90..9d6282aba 100644 --- a/simcoon-python-builder/test/test_core/test_solver_run.py +++ b/simcoon-python-builder/test/test_core/test_solver_run.py @@ -511,23 +511,26 @@ def test_record_tangent_false_omits_tangent_history(): assert "TangentMatrix" in solve(step, "ELISO", ELISO_PROPS, 1, T_init=290.0) -@pytest.mark.parametrize("mode", ["none", "continuum", "algorithmic", "closest_point"]) -def test_tangent_modes_converge_to_same_state(mode): +@pytest.mark.parametrize("mode", ["none", "continuum", "algorithmic"]) +@pytest.mark.parametrize("integrator", ["cutting_plane", "closest_point"]) +def test_tangent_modes_converge_to_same_state(mode, integrator): + if integrator == "closest_point" and mode != "algorithmic": + pytest.skip("the closest point is offered with its algorithmic operator only") step = StepMeca(control="stress", value=[400.0, 0, 0, 0, 0, 0], ninc=50) res = solve(step, "EPICP", EPICP_PROPS, EPICP_NSTATEV, T_init=290.0, - tangent_mode=mode) + tangent_mode=mode, integrator=integrator) assert res.status == 0 np.testing.assert_allclose(res["Stress"][0, -1], 400.0, atol=1e-4) -def test_closest_point_mode_is_accepted_everywhere(): - """tangent_mode 3 is a valid mode on every entry point (solver, batched sim.umat). On EPICP - with linear hardening the closest-point and cutting-plane integrators solve the same radial - return, so modes 2 and 3 agree to the local tolerance.""" +def test_closest_point_integrator_is_accepted_everywhere(): + """integrator='closest_point' is accepted on every entry point (solver, batched sim.umat). On + EPICP with linear hardening the closest-point and cutting-plane integrators solve the same + radial return, so they agree to the local tolerance.""" lin = [70000.0, 0.3, 1.0e-5, 300.0, 1000.0, 1.0] step = StepMeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=10) r2 = solve(step, "EPICP", lin, EPICP_NSTATEV, T_init=290.0, tangent_mode=2) - r3 = solve(step, "EPICP", lin, EPICP_NSTATEV, T_init=290.0, tangent_mode=sim.tangent_closest_point) + r3 = solve(step, "EPICP", lin, EPICP_NSTATEV, T_init=290.0, integrator="closest_point") assert r3.status == 0 np.testing.assert_allclose(r3["Stress"], r2["Stress"], rtol=1e-7, atol=1e-7) np.testing.assert_allclose(r3["TangentMatrix"], r2["TangentMatrix"], rtol=1e-6) @@ -536,11 +539,70 @@ def test_closest_point_mode_is_accepted_everywhere(): eye = np.asfortranarray(np.eye(3)[:, :, None]) out = [sim.umat("EPICP", col(np.zeros(6)), col([0.01, 0, 0, 0, 0, 0]), np.empty(0), np.empty(0), col(np.zeros(6)), eye, col(lin), col(np.zeros(EPICP_NSTATEV)), 0.0, 1.0, - col(np.zeros(4)), temp=np.full(1, 290.0), n_threads=1, tangent_mode=m) - for m in (2, 3)] + col(np.zeros(4)), temp=np.full(1, 290.0), n_threads=1, integrator=i) + for i in ("cutting_plane", "closest_point")] np.testing.assert_allclose(out[1][0], out[0][0], rtol=1e-7, atol=1e-7) +def test_integrator_and_tangent_choices_are_checked(): + step = StepMeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=2) + args = (step, "EPICP", EPICP_PROPS, EPICP_NSTATEV) + # the closest point is an integrator, no longer a tangent choice + for old in (3, "closest_point"): + with pytest.raises(ValueError, match="integrator='closest_point'"): + solve(*args, T_init=290.0, tangent_mode=old) + with pytest.raises(ValueError, match="algorithmic"): + solve(*args, T_init=290.0, integrator="closest_point", tangent_mode="continuum") + with pytest.raises(ValueError): + solve(*args, T_init=290.0, integrator="radial") + col = lambda a: np.asfortranarray(np.asarray(a, dtype=float).reshape(-1, 1)) + call = lambda **kw: sim.umat("EPICP", col(np.zeros(6)), col([0.01, 0, 0, 0, 0, 0]), np.empty(0), np.empty(0), + col(np.zeros(6)), np.asfortranarray(np.eye(3)[:, :, None]), col(EPICP_PROPS), + col(np.zeros(EPICP_NSTATEV)), 0.0, 1.0, col(np.zeros(4)), + temp=np.full(1, 290.0), n_threads=1, **kw) + with pytest.raises(ValueError, match="integrator='closest_point'"): + call(tangent_mode=3) + with pytest.raises(ValueError, match="algorithmic"): + call(integrator="closest_point", tangent_mode=0) + with pytest.raises(ValueError): + call(integrator="radial") + + +def test_closest_point_refused_for_laws_without_one(): + """integrator='closest_point' is refused, never ignored, for a law without a closest-point + update; the kernel code 3 never reaches it (a Python law reads 0, 1 or 2 only).""" + col = lambda a: np.asfortranarray(np.asarray(a, dtype=float).reshape(-1, 1)) + eye = np.asfortranarray(np.eye(3)[:, :, None]) + zener = [3000., 0.35, 0., 1500., 0.35, 3000., 1200.] + step = StepMeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=2) + with pytest.raises(ValueError, match="'ZENER' has none"): + solve(step, "ZENER", zener, 8, T_init=290.0, integrator="closest_point") + for name in ("ZENER", "PYEXT"): + with pytest.raises(ValueError, match=f"'{name}' has none"): + sim.umat(name, col(np.zeros(6)), col([1e-3, 0, 0, 0, 0, 0]), np.empty(0), np.empty(0), + col(np.zeros(6)), eye, col(zener), col(np.zeros(8)), 0.0, 1.0, col(np.zeros(4)), + temp=np.full(1, 290.0), n_threads=1, integrator="closest_point") + # thermomechanical: EPICP and EPKCP only (the SMA kernel keeps its cutting plane) + with pytest.raises(ValueError, match=r"'SMADI' \(thermomechanical\) has none"): + sim.umat_T("SMADI", col(np.zeros(6)), col(np.zeros(6)), col(np.zeros(6)), eye, col(np.zeros(30)), + col(np.zeros(30)), 0.0, 1.0, np.zeros((4, 1), order="F"), np.zeros((3, 1), order="F"), + np.array([290.0]), np.array([0.0]), integrator="closest_point") + + + +@pytest.mark.parametrize("name", ["UMEXT", "UMABA", "UMANS"]) +def test_external_plugins_refused_under_finite_strain(name): + """The plugin API has no finite-strain entry: the finite route refuses the plugins instead of + returning an unchanged (zero) stress.""" + step = StepMeca(control=_UNIAXIAL, value=[0.01, 0, 0, 0, 0, 0], ninc=2) + with pytest.raises(ValueError, match="small-strain route only"): + solve(Block(steps=[step], control_type="logarithmic"), name, np.array([70000., 0.3]), 1, T_init=290.0) + col = lambda v: np.asfortranarray(np.asarray(v, dtype=float).reshape(-1, 1)) + with pytest.raises(ValueError, match="external plugin, served by the solver"): + sim.umat(name, col(np.zeros(6)), col([1e-3, 0, 0, 0, 0, 0]), np.empty(0), np.empty(0), col(np.zeros(6)), + np.asfortranarray(np.eye(3)[:, :, None]), col([70000., 0.3]), col(np.zeros(1)), 0.0, 1.0, + col(np.zeros(4)), temp=np.full(1, 290.0), n_threads=1) + # --------------------------------------------------------------------------- # error paths # --------------------------------------------------------------------------- diff --git a/simcoon-python-builder/test/test_core/test_umat_condensed.py b/simcoon-python-builder/test/test_core/test_umat_condensed.py index 986c268ef..c8fbf1b70 100644 --- a/simcoon-python-builder/test/test_core/test_umat_condensed.py +++ b/simcoon-python-builder/test/test_core/test_umat_condensed.py @@ -17,6 +17,7 @@ import simcoon as sim from simcoon.modular import IsotropicElasticity, LinearIsotropicHardening, ModularMaterial, Plasticity +from solver_harness import tangent_kwargs E, NU, SY = 70000.0, 0.3, 200.0 G = E / (2.0 * (1.0 + NU)) @@ -125,7 +126,7 @@ def test_planar_beam_keeps_11_12(): def _umat(name, props, nstatev, e, de, s, sv, wm, T, tm, start, ndi=3, nshr=None): out = sim.umat(name, _col(e), _col(de), np.empty(0), np.empty(0), _col(s), I3, _col(props), _col(sv), 0.0, 0.05, _col(wm), temp=np.array([T]), ndi=ndi, nshr=nshr, n_threads=1, start=start, - tangent_mode=tm) + **tangent_kwargs(tm)) return out[0].ravel(), out[1].ravel(), out[2].ravel(), out[3][:, :, 0] @@ -287,7 +288,8 @@ def solved(l): # 3D reference: Newton on ln V1(C) so that tau_C = 0 np.testing.assert_allclose(s, s3, rtol=1e-9, atol=1e-9 * np.abs(s3).max()) np.testing.assert_array_equal(s[C], 0.0) # the reference Newton stalls near 1e-12 relative on tau_C for the nearly incompressible laws - # (MOORI: kappa/mu ~ 4e4); h = 1e-6 divides that noise up to the 1e-6 bound + # (MOORI: kappa/mu ~ 4e4): at h = 1e-6 that noise over 2h reaches the 1e-6 bound; at h = 1e-4 + # the error is truncation, ~1e-8 h = 1e-4 fd = np.zeros((len(K), len(K))) for j, k in enumerate(K): @@ -473,7 +475,7 @@ def _total(name, H, ndi=3, nshr=None, condense="spatial_face", Rm=None): out = sim.umat(name, _col(np.zeros(6)), Detot, I3, np.asfortranarray((Rm @ expm(sim.v2t_strain(H)))[:, :, None]), _col(np.zeros(6)), I3, _col(props), _col(np.zeros(nstatev)), 0.0, 1.0, _col(np.zeros(4)), ndi=ndi, nshr=nshr, n_threads=1, kinematic_form="total", start=True, condense=condense, - tangent_output="material" if condense == "material_face" else "box", tangent_mode=(tm or [2])[0]) + tangent_output="material" if condense == "material_face" else "box", **tangent_kwargs((tm or [2])[0])) H1 = Detot.ravel() return out[0].ravel(), out[3][:, :, 0], H1, Rm @ expm(sim.v2t_strain(H1)) @@ -527,7 +529,9 @@ def test_material_face_frees_the_material_faces(name, ndi, nshr): P = F1 @ sim.v2t_stress(S) faces = {(1, 0): [1, 2], (1, 1): [2], (2, 1): [2]}.get((ndi, nshr), []) for f in faces: - assert np.abs(P[:, f]).max() < 1e-10 * np.abs(P).max() + # same margin as S_C above: P e_f is F S e_f, so the Newton's 1e-10 |S| leaves no room at 1e-10 |P| + # (MOORI on Windows: 1.08e-10) + assert np.abs(P[:, f]).max() < 1e-9 * np.abs(P).max() Fc = R_ @ expm(sim.v2t_strain(H)) E_of = lambda F: sim.t2v_strain(0.5 * (F.T @ F - np.eye(3))).ravel() np.testing.assert_allclose(E_of(F1)[K], E_of(Fc)[K], atol=1e-14) @@ -755,6 +759,6 @@ def test_small_strain_faces_coincide(name): props, nstatev, T, scale, tm = LAWS[name] out = [sim.umat(name, _col(np.zeros(6)), _col(scale * PATH), np.empty(0), np.empty(0), _col(np.zeros(6)), I3, _col(props), _col(np.zeros(nstatev)), 0.0, 0.05, _col(np.zeros(4)), temp=np.array([T]), ndi=2, - n_threads=1, start=True, tangent_mode=tm, condense=c) for c in ("spatial_face", "material_face")] + n_threads=1, start=True, **tangent_kwargs(tm), condense=c) for c in ("spatial_face", "material_face")] for a, b in zip(*out): np.testing.assert_array_equal(a, b) diff --git a/simcoon-python-builder/test/test_core/test_umat_temperature.py b/simcoon-python-builder/test/test_core/test_umat_temperature.py index cc866f5f0..07ffca2ce 100644 --- a/simcoon-python-builder/test/test_core/test_umat_temperature.py +++ b/simcoon-python-builder/test/test_core/test_umat_temperature.py @@ -66,14 +66,14 @@ def test_sma_closest_point_tangent_with_phase_dependent_expansion(): sigma, statev, wm, _ = sim.umat("SMADI", _col(etot), _col(de), np.empty(0), np.empty(0), sigma, DR, _col(props), statev, float(i), 0.01, wm, temp=np.array([T_a + (T_b - T_a) * i / n]), DT=np.array([(T_b - T_a) / n]), - n_threads=1, start=(i == 0), tangent_mode=3) + n_threads=1, start=(i == 0), integrator="closest_point") etot = etot + de assert statev[1, 0] > 0.1 # transforming def call(De): s, _, _, Lt = sim.umat("SMADI", _col(etot), _col(De), np.empty(0), np.empty(0), sigma.copy(order="F"), DR, _col(props), statev.copy(order="F"), 1., 0.01, wm.copy(order="F"), - temp=np.array([T_b]), DT=np.array([0.5]), n_threads=1, start=False, tangent_mode=3) + temp=np.array([T_b]), DT=np.array([0.5]), n_threads=1, start=False, integrator="closest_point") return s[:, 0], Lt[:, :, 0] De0, h = np.array([2e-4, -6e-5, -6e-5, 1e-4, 0., 0.]), 1e-7 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 46e9c6e5d..564a492ce 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -43,12 +43,8 @@ target_sources(simcoon PRIVATE Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp Continuum_mechanics/Umat/Finite/saint_venant.cpp - # Continuum Mechanics - UMAT Mechanical Combined - Continuum_mechanics/Umat/Mechanical/Combined/Prony_Nfast_Plastic.cpp - # Continuum Mechanics - UMAT Mechanical Damage Continuum_mechanics/Umat/Mechanical/Damage/damage_LLD_0.cpp - Continuum_mechanics/Umat/Mechanical/Damage/damage_weibull.cpp # Continuum Mechanics - UMAT Mechanical Elasticity @@ -111,6 +107,7 @@ target_sources(simcoon PRIVATE Simulation/Geometry/layer.cpp # Simulation - Maths + Simulation/Maths/eig_sym3.cpp Simulation/Maths/lagrange.cpp Simulation/Maths/num_solve.cpp Simulation/Maths/random.cpp diff --git a/src/Continuum_mechanics/Functions/constitutive.cpp b/src/Continuum_mechanics/Functions/constitutive.cpp index e7beacd05..6e7863c2b 100755 --- a/src/Continuum_mechanics/Functions/constitutive.cpp +++ b/src/Continuum_mechanics/Functions/constitutive.cpp @@ -22,19 +22,20 @@ along with simcoon. If not, see . #include #include #include +#include #include #include #include #include #include #include +#include using namespace std; using namespace arma; namespace simcoon{ -//Returns the fourth order identity tensor written in Voigt notation Ireal mat Ireal(){ mat Ireal = eye(6,6); @@ -44,7 +45,6 @@ mat Ireal(){ return Ireal; } -//Returns the volumic of the identity tensor Ireal written in Voigt notation mat Ivol(){ mat Ivol = zeros(6,6); @@ -56,13 +56,29 @@ mat Ivol(){ return Ivol; } -//Returns the deviatoric of the identity tensor Ireal written in Voigt notation +// a*Ivol + b*X entry by entry: one allocation, same rounding as the expression +static mat iso_combination(const double &a, const double &b, const double &X_shear) { + const double v = a*(1./3.); // a Ivol_ij on the normal block + const double d_ii = b*(1.-1./3.); // b X_ii, normal block + const double d_ij = b*(0.-1./3.); // b X_ij, normal block, i != j + const double d_ss = b*X_shear; // b X_ii, shear block + const double n_ii = v + d_ii; + const double n_ij = v + d_ij; + mat L(6, 6, fill::zeros); + for (int i=0; i<3; i++) { + for (int j=0; j<3; j++) { + L(i,j) = (i == j) ? n_ii : n_ij; + } + L(i+3,i+3) = d_ss; + } + return L; +} + mat Idev(){ - return Ireal() - Ivol(); + return iso_combination(0., 1., 0.5); } -//Returns the fourth order identity tensor Iˆ written in Voigt notation mat Ireal2(){ mat Ireal2 = eye(6,6); @@ -72,13 +88,11 @@ mat Ireal2(){ return Ireal2; } -//Returns the deviatoric of the identity tensor Iˆ written in Voigt notation mat Idev2(){ - return Ireal2() - Ivol(); + return iso_combination(0., 1., 2.); } -//Returns the expansion vector vec Ith(){ vec Ith = zeros(6); @@ -88,7 +102,6 @@ vec Ith(){ return Ith; } -//Returns the stress 2 strain operator vec Ir2(){ vec Ir2 = ones(6); @@ -98,7 +111,6 @@ vec Ir2(){ return Ir2; } -//Returns the strain 2 stress operator vec Ir05(){ vec Ir05 = ones(6); @@ -108,10 +120,6 @@ vec Ir05(){ return Ir05; } -//Provides the elastic stiffness tensor for an isotropic material. -//The two first arguments are a couple of Lamé coefficients. The third argument specify which couple has been provided and the order of coefficients. -//Exhaustive list of possible third argument : -// ‘Enu’,’nuE,’Kmu’,’muK’, ‘KG’, ‘GK’, ‘lambdamu’, ‘mulambda’, ‘lambdaG’, ‘Glambda’. mat L_iso(const double &C1, const double &C2, const std::string &conv) { double K = 0.; @@ -154,16 +162,12 @@ mat L_iso(const double &C1, const double &C2, const std::string &conv) { mu = C1; } else { - cout << "ERROR : Please use a valid couple of elastic constants"; + throw std::invalid_argument("L_iso: unknown convention '" + conv + "'; expected Enu, nuE, Kmu/KG, muK/GK, lambdamu/lambdaG, mulambda/Glambda"); } - return 3.*K*Ivol() + 2.*mu*Idev(); + return iso_combination(3.*K, 2.*mu, 0.5); // 3K Ivol + 2mu Idev } -//Provides the elastic compliance tensor for an isotropic material. -//The two first arguments are a couple of Lamé coefficients. The third argument specify which couple has been provided and the order of coefficients. -//Exhaustive list of possible third argument : -//‘Enu’,’nuE,’Kmu’,’muK’, ‘KG’, ‘GK’, ‘lambdamu’, ‘mulambda’, ‘lambdaG’, ‘Glambda’. mat M_iso(const double &C1, const double &C2, const string &conv) { double K = 0.; @@ -206,15 +210,12 @@ mat M_iso(const double &C1, const double &C2, const string &conv) { mu = C1; } else { - cout << "ERROR : Please use a valid couple of elastic constants"; - exit(0); + throw std::invalid_argument("M_iso: unknown convention '" + conv + "'; expected Enu, nuE, Kmu/KG, muK/GK, lambdamu/lambdaG, mulambda/Glambda"); } - return 1/(3.*K)*Ivol() + 1/(2.*mu)*Idev2(); + return iso_combination(1/(3.*K), 1/(2.*mu), 2.); // 1/(3K) Ivol + 1/(2mu) Idev2 } -//Returns the elastic stiffness tensor for a cubic material. -//Arguments are the stiffness coefficients C11, C12 and C44, or E, nu and G mat L_cubic(const double &C1, const double &C2, const double &C4, const string &conv){ mat L = zeros(6,6); @@ -255,15 +256,12 @@ mat L_cubic(const double &C1, const double &C2, const double &C4, const string & L(5,5) = C44; } else { - cout << "ERROR : Please use a valid couple of elastic constants"; - exit(0); + throw std::invalid_argument("L_cubic: unknown convention '" + conv + "'; expected Cii, EnuG"); } return L; } -//Returns the elastic compliance tensor for an isotropic material. -//Arguments are the stiffness coefficients C11, C12 and C44, or E, nu and G. mat M_cubic(const double &C1, const double &C2, const double &C4, const string &conv){ mat M = zeros(6,6); @@ -301,15 +299,65 @@ mat M_cubic(const double &C1, const double &C2, const double &C4, const string & M(5,5) = 1./G; } else { - cout << "ERROR : Please use a valid couple of elastic constants"; - exit(0); + throw std::invalid_argument("M_cubic: unknown convention '" + conv + "'; expected Cii, EnuG"); } return M; } -//Returns the elastic stiffness tensor for an orthotropic material. -//Arguments are the stiffness coefficients Cii or E and nu's +bool cubic_coefficients(const mat &A, double &a, double &b, double &c, const double &tol) { + if (A.n_rows != 6 || A.n_cols != 6) { + return false; + } + a = A(0,0); + b = A(0,1); + c = A(3,3); + const double thr = (tol > 0.) ? tol*abs(A).max() : 0.; + for (int j=0; j<6; j++) { + for (int i=0; i<6; i++) { + const double expected = (i < 3 && j < 3) ? ((i == j) ? a : b) : ((i == j) ? c : 0.); + if (!(std::fabs(A(i,j) - expected) <= thr)) { // a NaN fails too + return false; + } + } + } + return true; +} + +bool is_isotropic(const mat &L, const Tensor4Type &type, const double &tol) { + double a, b, c, row, col; + mandel_factors(type, row, col); // isotropic: Mandel shear c row col = a - b + return cubic_coefficients(L, a, b, c, tol) && std::fabs(c*row*col - (a - b)) <= tol*abs(L).max(); +} + +// closed form: eigenvalues a+2b, a-b of the normal block, shear 1/c +mat inv_cubic(const mat &A) { + + double a = 0., b = 0., c = 0.; + if (!cubic_coefficients(A, a, b, c)) { + mat A_inv; + if (!inv(A_inv, A)) { + throw simcoon::exception_inv("Error in inv function inside inv_cubic."); + } + return A_inv; + } + const double l_vol = a + 2.*b; + const double l_dev = a - b; + if (l_vol == 0. || l_dev == 0. || c == 0. || !std::isfinite(l_vol*l_dev*c)) { + throw simcoon::exception_inv("Error in inv function inside inv_cubic (singular matrix)."); + } + const double a_inv = (1./l_vol + 2./l_dev)/3.; + const double b_inv = (1./l_vol - 1./l_dev)/3.; + mat A_inv(6, 6, fill::zeros); + for (int i=0; i<3; i++) { + for (int j=0; j<3; j++) { + A_inv(i,j) = (i == j) ? a_inv : b_inv; + } + A_inv(i+3,i+3) = 1./c; + } + return A_inv; +} + mat L_ortho(const double &C11, const double &C12, const double &C13, const double &C22, const double &C23, const double &C33, const double &C44, const double &C55, const double &C66, const string &conv){ mat L = zeros(6,6); @@ -359,15 +407,12 @@ mat L_ortho(const double &C11, const double &C12, const double &C13, const doubl L(5,5) = G23; } else { - cout << "ERROR : Please use a valid couple of elastic constants"; - exit(0); + throw std::invalid_argument("L_ortho: unknown convention '" + conv + "'; expected Cii, EnuG"); } return L; } -//Returns the elastic compliance tensor for an orthotropic material. -//Arguments are the stiffness coefficients Cii or E and nu's mat M_ortho(const double &C11, const double &C12, const double &C13, const double &C22, const double &C23, const double &C33, const double &C44, const double &C55, const double &C66, const string &conv){ mat M = zeros(6,6); @@ -420,15 +465,12 @@ mat M_ortho(const double &C11, const double &C12, const double &C13, const doubl M(5,5) = 1./G23; } else { - cout << "ERROR : Please use a valid couple of elastic constants"; - exit(0); + throw std::invalid_argument("M_ortho: unknown convention '" + conv + "'; expected Cii, EnuG"); } return M; } -//Returns the elastic stiffness tensor for an isotropic transverse material. -//Arguments are longitudinal Young modulus EL, transverse young modulus, Poisson’s ratio for loading along the longitudinal axis nuTL, Poisson’s ratio for loading along the transverse axis nuTT, shear modulus GLT and the axis of symmetry. mat L_isotrans(const double &EL, const double &ET, const double &nuTL, const double &nuTT, const double &GLT, const int &axis){ mat L = zeros(6,6); @@ -481,8 +523,7 @@ mat L_isotrans(const double &EL, const double &ET, const double &nuTL, const dou break; } default : { - cout << "ERROR : Please use a valid couple of elastic constants"; - exit(0); + throw std::invalid_argument("L_isotrans: the axis must be 1, 2 or 3; got " + std::to_string(axis)); } } @@ -491,8 +532,6 @@ mat L_isotrans(const double &EL, const double &ET, const double &nuTL, const dou return L; } -//Returns the elastic compliance tensor for an isotropic transverse material. -//Arguments are longitudinal Young modulus EL, transverse young modulus, Poisson’s ratio for loading along the longitudinal axis nuTL, Poisson’s ratio for loading along the transverse axis nuTT, shear modulus GLT and the axis of symmetry. mat M_isotrans(const double &EL, const double &ET, const double &nuTL, const double &nuTT, const double &GLT, const int &axis){ mat M = zeros(6,6); @@ -543,25 +582,19 @@ mat M_isotrans(const double &EL, const double &ET, const double &nuTL, const dou break; } default : { - cout << "ERROR : Please use a valid couple of elastic constants"; - exit(0); + throw std::invalid_argument("M_isotrans: the axis must be 1, 2 or 3; got " + std::to_string(axis)); } } return M; } -//Provides the viscous tensor H an isotropic material. -//The two first arguments are a couple of viscous coefficients (the first is bulk, the second is shear). mat H_iso(const double &etaB, const double &etaS) { - return 3.*etaB*Ivol() + 2.*etaS*Idev(); + return iso_combination(3.*etaB, 2.*etaS, 0.5); // 3 etaB Ivol + 2 etaS Idev } -// Condensed stress states (ndi < 3), Abaqus ordering: the stress keeps the first ndi direct -// components and the first nshr shears (12, 13, 23); every other component is stress-free. Its -// strain is free and eliminated: Q = L_KK - L_KC L_CC^-1 L_CK on the kept set K. ndi = 3 is the -// full or plane-strain-like state, whose missing shears are zero strain, not zero stress. -// nshr < 0 picks Abaqus' default for ndi: truss (1, 0), plane stress (2, 1), 3D (3, 3). +// ndi < 3 (Abaqus order): first ndi direct + first nshr shears kept, the rest stress-free; +// Q = L_KK - L_KC L_CC^-1 L_CK. nshr < 0: Abaqus default for ndi static vec condensed_pred(const mat &L, const vec &E, const int &ndi, const int &nshr) { const int ns = (nshr < 0) ? ((ndi == 1) ? 0 : 1) : nshr; @@ -587,7 +620,6 @@ vec el_pred(const vec &sigma_start, const mat &L, const vec &DE_el, const int &n return sigma_start + condensed_pred(L, DE_el, ndi, nshr); } -//Provides the elastic prediction, providing the stiffness tensor and the trial elastic strain vec el_pred(const mat &L, const vec &E_el, const int &ndi, const int &nshr) { if (ndi == 3) diff --git a/src/Continuum_mechanics/Functions/contimech.cpp b/src/Continuum_mechanics/Functions/contimech.cpp index 645f6989f..89b604283 100755 --- a/src/Continuum_mechanics/Functions/contimech.cpp +++ b/src/Continuum_mechanics/Functions/contimech.cpp @@ -116,7 +116,6 @@ mat deta_stress(const vec &v) { return (Pm - eta*eta.t())/sigeq; } -//This function determines the strain flow (direction) from a stress tensor, according to the Voigt convention for strains vec eta_norm_stress(const vec &v) { assert(v.size()==6); @@ -494,37 +493,22 @@ mat sym_dyadic_operator(const mat &A, const mat &B) { return C; } -mat linearop_eigsym(const vec &b_i, const vec &b_j) { - - mat Bij = b_i*b_j.t(); - mat BBBB = zeros(6,6); +void add_linearop_eigsym(mat::fixed<6,6> &acc, const double &c, const vec::fixed<3> &b_i, const vec::fixed<3> &b_j) { + static const int Id[3][3] = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + double Bij[3][3]; + for (int i=0; i<3; i++) + for (int j=0; j<3; j++) + Bij[i][j] = b_i(i)*b_j(j); + for (int i=0; i<3; i++) + for (int j=i; j<3; j++) + for (int k=0; k<3; k++) + for (int l=k; l<3; l++) + acc(Id[i][j], Id[k][l]) += c*(0.5*(Bij[i][j]*Bij[k][l] + Bij[i][j]*Bij[l][k])); +} - int ij=0; - int kl=0; - - umat Id(3,3); - Id(0,0) = 0; - Id(0,1) = 3; - Id(0,2) = 4; - Id(1,0) = 3; - Id(1,1) = 1; - Id(1,2) = 5; - Id(2,0) = 4; - Id(2,1) = 5; - Id(2,2) = 2; - - for (int i=0; i<3; i++) { - for (int j=i; j<3; j++) { - ij = Id(i,j); - for (int k=0; k<3; k++) { - for (int l=k; l<3; l++) { - kl = Id(k,l); - BBBB(ij,kl) = 0.5*(Bij(i,j)*Bij(k,l) + Bij(i,j)*Bij(l,k)); - } - } - } - } - +mat linearop_eigsym(const vec &b_i, const vec &b_j) { + mat::fixed<6,6> BBBB(fill::zeros); + add_linearop_eigsym(BBBB, 1., b_i, b_j); return BBBB; } diff --git a/src/Continuum_mechanics/Functions/criteria.cpp b/src/Continuum_mechanics/Functions/criteria.cpp index cf471fafb..e403b07dd 100755 --- a/src/Continuum_mechanics/Functions/criteria.cpp +++ b/src/Continuum_mechanics/Functions/criteria.cpp @@ -38,7 +38,6 @@ using namespace arma; namespace simcoon { - // This function returns the Drucker equivalent stress. double Drucker_stress(const vec &v, const double &b, const double &n) { assert(v.size() == 6); @@ -69,7 +68,6 @@ namespace simcoon return temp; } - // This function returns the derivative of the Drucker equivalent stress. vec dDrucker_stress(const vec &v, const double &b, const double &n) { assert(v.size() == 6); @@ -116,7 +114,6 @@ namespace simcoon return temp; } - // This function returns the Drucker equivalent stress. double Tresca_stress(const vec &v) { mat sigma = v2t_stress(v); @@ -126,16 +123,13 @@ namespace simcoon return lambda(2) - lambda(0); } - // This function returns the Tresca equivalent stress. vec dTresca_stress(const vec &v) { return eta_stress(v); } namespace { - // Hessian of f(sigma) (1 + b J3/J2^1.5)^(1/n) for a base equivalent stress f with gradient - // df and Hessian ddf (von Mises or DFA). Rows strain-typed, columns per stress component - // (the convention of deta_stress / ddEq_stress_P). + // rows strain-typed, columns per stress component (as deta_stress / ddEq_stress_P) mat drucker_hessian(const vec &v, const double &f, const vec &df, const mat &ddf, const double &b, const double &n) { if (f <= simcoon::iota) return zeros(6, 6); // same floor as the value and the gradient @@ -194,7 +188,6 @@ namespace simcoon return drucker_hessian(v, DFA_stress(v, params), dDFA_stress(v, params), ddDFA_stress(v, params), b, n); } - // This function returns the combination of the Drucker equivalent stress by replacing VM by DFA double Drucker_ani_stress(const vec &v, const vec ¶ms, const double &b, const double &n) { assert(v.size() == 6); @@ -227,7 +220,6 @@ namespace simcoon return temp; } - // This function returns the derivative of the Drucker equivalent stress. vec dDrucker_ani_stress(const vec &v, const vec ¶ms, const double &b, const double &n) { assert(v.size() == 6); @@ -299,12 +291,7 @@ namespace simcoon { assert(params.n_elem == 6); // F,G,H,L,M,N mat P = zeros(6, 6); - // param(0) = F - // param(1) = G - // param(2) = H - // param(3) = L - // param(4) = M - // param(5) = N + // params = (F, G, H, L, M, N) P(0, 0) = params(1) + params(2); // P_11 = G+H P(1, 1) = params(0) + params(2); // P_22 = F+H P(2, 2) = params(0) + params(1); // P_33 = F+G @@ -325,13 +312,7 @@ namespace simcoon { // Deshpande–Fleck–Ashby assert(params.n_elem == 7); // F,G,H,L,M,N,K mat P = zeros(6, 6); - // param(0) = F - // param(1) = G - // param(2) = H - // param(3) = L - // param(4) = M - // param(5) = N - // param(6) = K + // params = (F, G, H, L, M, N, K) P(0, 0) = params(1) + params(2) + params(6) / 9.; // P_11 = G+H+K/9 P(1, 1) = params(0) + params(2) + params(6) / 9.; // P_22 = F+H+K/9 P(2, 2) = params(0) + params(1) + params(6) / 9.; // P_33 = F+G+K/9 @@ -468,9 +449,7 @@ namespace simcoon } else { - // throw, do NOT exit(0): an invalid criterion string must be - // reportable by the caller (an exit kills the host process, - // e.g. the Python interpreter). + // throw, not exit(0): an exit kills the host process (e.g. Python) throw std::invalid_argument("Eq_stress: unknown criterion '" + eq_type + "'"); } diff --git a/src/Continuum_mechanics/Functions/damage.cpp b/src/Continuum_mechanics/Functions/damage.cpp index 721510356..71ca37740 100755 --- a/src/Continuum_mechanics/Functions/damage.cpp +++ b/src/Continuum_mechanics/Functions/damage.cpp @@ -15,7 +15,7 @@ */ -///@file damage.hpp +///@file damage.cpp ///@brief Functions that computes damage evolution laws ///@version 1.0 @@ -35,7 +35,6 @@ using namespace arma; namespace simcoon{ -//This function returns damage evolution (/dt) considering a Weibull damage law double damage_weibull(const vec &stress, const double &damage, const double &alpha, const double &beta, const double &DTime, const string &criterion) { if ((criterion == "Mises")) { @@ -53,7 +52,6 @@ double damage_weibull(const vec &stress, const double &damage, const double &alp } } -//This function returns damage evolution (/dt) considering Kachanov's creep damage law double damage_kachanov(const vec &stress, const vec &strain, const double &damage, const double &A0, const double &r, const string &criterion) { // Use Ith() = {1,1,1,0,0,0} to add identity only to normal components @@ -74,13 +72,11 @@ double damage_kachanov(const vec &stress, const vec &strain, const double &damag } } -//This function returns the constant damage evolution (/dN) considering Woehler- Miner's damage law double damage_miner(const double &S_max, const double &S_mean, const double &S_ult, const double &b, const double &B0, const double &beta, const double &Sl_0) { return (S_max - (S_mean + Sl_0*(1.-b*S_mean)))/(S_ult-S_max)*pow((S_max-S_mean)/(B0*(1.-b*S_mean)),beta); } -//This function returns the constant damage evolution (/dN) considering Coffin-Manson's damage law double damage_manson(const double &S_amp, const double &C2, const double &gamma2) { return pow(S_amp/C2,gamma2); diff --git a/src/Continuum_mechanics/Functions/hyperelastic.cpp b/src/Continuum_mechanics/Functions/hyperelastic.cpp index fc171f476..d09eb6999 100755 --- a/src/Continuum_mechanics/Functions/hyperelastic.cpp +++ b/src/Continuum_mechanics/Functions/hyperelastic.cpp @@ -229,9 +229,7 @@ vec beta_coefs(const vec &dWdlambda_bar, const vec &lambda_bar) { mat gamma_coefs(const vec &dWdlambda_bar, const mat &dW2dlambda_bar2, const vec &lambda_bar) { mat gamma = zeros(3,3); - // g_ab = lambda_a lambda_b W_ab + delta_ab lambda_a W_a (the W_a term is DIAGONAL, - // not an outer product): gamma = P.g.P with P the deviatoric projector, i.e. - // gamma_ab = d beta_a / d ln(lambda_b) + // g_ab = lam_a lam_b W_ab + delta_ab lam_a W_a (W_a term diagonal); gamma = P.g.P = d beta_a / d ln lam_b mat factor = dW2dlambda_bar2%(lambda_bar*lambda_bar.t()) + diagmat(dWdlambda_bar%lambda_bar); vec factor_sum_col = ((-1./3.)*sum(factor,0)).t(); // sum along dim 0 returns row vec, transpose to col vec factor_sum_row = (-1./3.)*sum(factor,1); @@ -478,9 +476,7 @@ mat L_iso_hyper_pstretch(const vec &dWdlambda_bar, const mat &dW2dlambda_bar2, c c += (gamma(i,j) - 2*delta(i,j)*beta(i))*dyadic_4vectors_sym(n_pvectors.col(i), n_pvectors.col(j), "aabb") ; } - // shear term, summed over ordered pairs a!=b: the full 1/2-symmetrized dyadic - // 2*(abab(n_i,n_j) + abab(n_j,n_i)) collapses to the rank-1 outer product s*s^T - // with the symmetric dyadic of (n_i n_j^T + n_j n_i^T) + // ordered pairs a!=b: the symmetrized dyadic collapses to s s^T, s = sym dyadic of (n_i n_j^T + n_j n_i^T) for(unsigned int j=i+1; j<3; j++) { if(pow(lambda_bar(j),2.)-pow(lambda_bar(i),2.) < 1.E-6) { factor = pow(lambda_bar(i), 2.) * pow(lambda_bar(j), -2.) * (0.5*gamma(j,j) - beta(j)) - 0.5*gamma(i,j); @@ -515,14 +511,8 @@ mat L_iso_hyper_pstretch(const vec &dWdlambda_bar, const mat &dW2dlambda_bar2, c return L_iso_hyper_pstretch(dWdlambda_bar, dW2dlambda_bar2, lambda_bar, n_pvectors, J); } -// The two box operators of ANY symmetric A whose convected rate is -// l*A + A*l^T - (2/3) tr(d) A, hence whose trace obeys d(tr A)/dt = 2 dev(A) : d. -// b_bar satisfies this (tr = I_1_bar) and so does a fibre family's dispersed structure -// tensor (tr = I*_4_bar): sharing them is what lets the anisotropic tangent reuse the -// isotropic I_1 algebra verbatim. -// They take the deviator and trace already held by the caller rather than A itself: both -// call sites have them, and recomputing dev(A) here once per operator put six extra -// allocations per call on the isotropic path that the inline form did not pay. +// box operators of any symmetric A with convected rate l A + A l^T - 2/3 tr(d) A (b_bar, fibre +// structure tensors): the anisotropic tangent reuses the I_1 algebra verbatim static mat gamma_linear(const mat &dev_A, const double &tr_A, const mat &I_dev, const mat &Id) { return (4./3.)*(tr_A*I_dev - (sym_dyadic(dev_A,Id)+sym_dyadic(Id,dev_A))); } @@ -542,40 +532,50 @@ mat L_iso_hyper_invariants(const double &dWdI_1_bar, const double &dWdI_2_bar, c } } mat b_bar = pow(J,-2./3.)*b; - - mat b_bar2; - try { - b_bar2 = powmat(b_bar,2); - } catch (const std::runtime_error &e) { - cerr << "Error in det: " << e.what() << endl; - throw simcoon::exception_powmat("Error in powmat function inside L_iso_hyper_invariants."); - } - - mat dev_b_bar = dev(b_bar); - mat dev_b_bar2 = dev(b_bar2); + mat dev_b_bar = dev(b_bar); vec I_bar = isochoric_invariants(b,J); mat Id = eye(3,3); - mat H_bar = auto_sym_dyadic_operator(b_bar); - - mat devdevbb2 = I_bar(0)*dev(b_bar) - dev(b_bar2); - // Plain locals on purpose: a function-local static of an armadillo type registers a - // destructor that runs at DLL unload, and on Windows the unload order of the extension - // module, the BLAS/LAPACK DLLs and the CRT is not defined — freeing the matrix after its - // allocator is gone aborts the process once the tests are over. These are 6x6 identities, - // negligible next to the six dyadic products this function already builds per call. - mat I_real = Ireal(); - mat I_vol = Ivol(); + // no static arma object: its destructor runs at DLL unload (Windows) mat I_dev = Idev(); - mat gamma_1 = gamma_linear(dev_b_bar, I_bar(0), I_dev, Id); - mat gamma_2 = (8./3.)*(I_bar(1)*(I_real - 2.*I_vol) - I_bar(0)*(sym_dyadic(dev_b_bar,Id)+sym_dyadic(Id,dev_b_bar)) - + (sym_dyadic(dev_b_bar2,Id)+sym_dyadic(Id,dev_b_bar2))) + 4*(auto_sym_dyadic(b_bar)-H_bar); - mat gamma_11 = gamma_quadratic(dev_b_bar); - mat gamma_22 = 4.*auto_sym_dyadic(devdevbb2); - mat gamma_12 = 4.*(sym_dyadic(dev_b_bar,devdevbb2)+sym_dyadic(devdevbb2,dev_b_bar)); + // terms of a zero potential derivative skipped (exact zeros) + mat Lt = gamma_linear(dev_b_bar, I_bar(0), I_dev, Id)*dWdI_1_bar; + if ((dWdI_2_bar != 0.) || (dW2dI_12_bar != 0.) || (dW2dI_22_bar != 0.)) { + mat b_bar2; + try { + b_bar2 = powmat(b_bar,2); + } catch (const std::runtime_error &e) { + cerr << "Error in det: " << e.what() << endl; + throw simcoon::exception_powmat("Error in powmat function inside L_iso_hyper_invariants."); + } + mat dev_b_bar2 = dev(b_bar2); + mat devdevbb2 = I_bar(0)*dev(b_bar) - dev(b_bar2); + if (dWdI_2_bar != 0.) { + mat I_real = Ireal(); + mat I_vol = Ivol(); + mat H_bar = auto_sym_dyadic_operator(b_bar); + mat gamma_2 = (8./3.)*(I_bar(1)*(I_real - 2.*I_vol) - I_bar(0)*(sym_dyadic(dev_b_bar,Id)+sym_dyadic(Id,dev_b_bar)) + + (sym_dyadic(dev_b_bar2,Id)+sym_dyadic(Id,dev_b_bar2))) + 4*(auto_sym_dyadic(b_bar)-H_bar); + Lt += gamma_2*dWdI_2_bar; + } + if (dW2dI_11_bar != 0.) { + Lt += gamma_quadratic(dev_b_bar)*dW2dI_11_bar; + } + if (dW2dI_12_bar != 0.) { + mat gamma_12 = 4.*(sym_dyadic(dev_b_bar,devdevbb2)+sym_dyadic(devdevbb2,dev_b_bar)); + Lt += gamma_12*dW2dI_12_bar; + } + if (dW2dI_22_bar != 0.) { + mat gamma_22 = 4.*auto_sym_dyadic(devdevbb2); + Lt += gamma_22*dW2dI_22_bar; + } + } + else if (dW2dI_11_bar != 0.) { + Lt += gamma_quadratic(dev_b_bar)*dW2dI_11_bar; + } - return (1./J)*(gamma_1*dWdI_1_bar+gamma_2*dWdI_2_bar+gamma_11*dW2dI_11_bar+gamma_12*dW2dI_12_bar+gamma_22*dW2dI_22_bar); + return (1./J)*Lt; } mat L_vol_hyper(const double &dUdJ, const double &dU2dJ2, const mat &b, const double &mJ) { @@ -808,20 +808,12 @@ hyper_invariants_dW hyper_potential_derivatives(const HyperPotential &potential, for (uword i = 0; i < n_fam; i++) { // tr(A_i) IS the fibre pseudo-invariant: no separate channel for it const double E_bar = trace(A[i]) - 1.; - // The fibre TERM is inactive below the switch (note this is a condition on - // the pseudo-invariant, not on fibre compression: with kappa_d > 0 a - // compressed fibre can still have I*_4 > 1). The threshold is iota, not 0: - // psi'' jumps from 0 to k1 across it, so an exact comparison would let - // round-off in I*_4 decide a FINITE tangent. At I*_4 = 1 exactly (any - // unstretched fibre, e.g. pure dilatation) the two routes to b differ in - // the last bits and would otherwise disagree by k1. + // switch on I*_4 (not on fibre compression), threshold iota not 0: psi'' jumps 0 -> k1 across it, + // so round-off at I*_4 = 1 must not select a finite tangent if (E_bar <= simcoon::iota) { continue; } - // Finite-arithmetic guard only: exp overflows to inf near arg 709, and a - // Newton trial iterate can overshoot the fibre stretch far past anything - // physical. inf here becomes NaN stress and tangent, which the solver - // cannot step-cut its way out of; a large finite value it can. + // finite-arithmetic guard: inf here becomes a NaN state the solver cannot step-cut out of const double e = exp(std::min(k_2*E_bar*E_bar, 350.)); dW.dWdI_a_bar(i) = k_1*E_bar*e; dW.dW2dI_aa_bar(i) = k_1*(1. + 2.*k_2*E_bar*E_bar)*e; @@ -845,27 +837,18 @@ void hyper_invariants_response(const hyper_invariants_dW &dW, const mat &b, cons + std::to_string(dW.dWdI_a_bar.n_elem) + " fibre derivatives"); } - // Kirchhoff throughout: tau is what the potential differentiates to per REFERENCE volume, - // and it is what every consumer on the finite route wants. Cauchy is tau/J, produced at the - // output boundary (solver_sink, the python wrapper), never on the route. + // Kirchhoff throughout: Cauchy = tau/J only at the output boundary mat m_tau_iso = tau_iso_hyper_invariants(dW.dWdI_1_bar, dW.dWdI_2_bar, b, J); mat m_tau_vol = tau_vol_hyper(dW.dUdJ, b, J); mat m_tau = m_tau_iso + m_tau_vol; - // These two builders return the spatial elasticity c = (1/J) d(L_v tau)/dD -- NOT - // d(L_v sigma)/dD, which differs from it by sigma (x) I. The single J that turns c into the - // Kirchhoff-Lie tangent is applied once, below, where it is visible. - // The J is written ASYMMETRICALLY inside them (L_iso carries an explicit 1/J, L_vol carries - // none and is J-free by cancellation), so they must be scaled together as a sum and never - // "tidied" one at a time. + // builders return c = (1/J) d(L_v tau)/dD (not d(L_v sigma)/dD), with J written asymmetrically + // (1/J in L_iso, none in L_vol): scale them together as a sum, never one at a time mat Lt_iso = L_iso_hyper_invariants(dW.dWdI_1_bar, dW.dWdI_2_bar, dW.dW2dI_11_bar, dW.dW2dI_12_bar, dW.dW2dI_22_bar, b, J); mat Lt_vol = L_vol_hyper(dW.dUdJ, dW.dU2dJ2, b, J); mat Lt_spatial = Lt_iso + Lt_vol; - // Fibre terms: the I_1 algebra with b_bar replaced by the family's structure tensor. - // kappa_d is already folded into A upstream, which is what keeps that substitution exact. - // Both derivatives are exactly 0 for a family below the tension switch -- the routine - // case, not an edge case -- so skip rather than assemble six 6x6 dyadics times zero. + // fibre terms: I_1 algebra with b_bar -> structure tensor (kappa_d folded into A); zero below the switch if (!A.empty()) { mat Id = eye(3,3); mat I_dev = Idev(); diff --git a/src/Continuum_mechanics/Functions/kinematics.cpp b/src/Continuum_mechanics/Functions/kinematics.cpp index ad0551dcd..fe69e0666 100755 --- a/src/Continuum_mechanics/Functions/kinematics.cpp +++ b/src/Continuum_mechanics/Functions/kinematics.cpp @@ -21,6 +21,7 @@ #include #include #include +#include #include using namespace std; @@ -28,7 +29,6 @@ using namespace arma; namespace simcoon{ -//This function returns F (in a vectorized), from E (Green-Lagrange strain) and R (Rotation matrix), according to a RU decomposition mat ER_to_F(const mat &E, const mat &R) { assert(E.n_cols == 3); @@ -36,17 +36,14 @@ mat ER_to_F(const mat &E, const mat &R) { assert(R.n_cols == 3); assert(R.n_rows == 3); - //From E we compute C : E = 1/2 (C-I) --> C = U^2 = 2E+I - //symmatu() removes any numerical asymmetry so a mathematically-SPD C is not - //rejected by sqrtmat_sympd's symmetry check (the input E is symmetric by construction). + //C = 2E + I; symmatu() so the symmetry check of sqrtmat_sympd accepts it mat C = symmatu(2.*E+eye(3,3)); - try { - return (R*sqrtmat_sympd(C)); - } catch (const std::runtime_error &e) { - cerr << "Error in sqrtmat_sympd : " << e.what() << endl; + mat::fixed<3,3> U; + if (!sqrtmat_sympd_3x3(U, C)) { throw simcoon::exception_sqrtmat_sympd("Error in sqrtmat_sympd function inside ER_to_F."); - } + } + return R*U; } mat eR_to_F(const mat &e, const mat &R) { @@ -57,23 +54,17 @@ mat eR_to_F(const mat &e, const mat &R) { assert(R.n_rows == 3); //From e we compute V : e = 1/2 (C-I) --> C = U^2 = 2E+I - mat V; - try { - V = expmat_sym(e); - } catch (const std::runtime_error &e) { - cerr << "Error in expmat_sym : " << e.what() << endl; + mat::fixed<3,3> V; + if (!expmat_sym_3x3(V, e)) { throw simcoon::exception_expmat_sym("Error in expmat_sym function inside eR_to_F."); - } - + } return (V*R); } -//This function computes the gradient of displacement (Lagrangian) from the deformation gradient tensor mat G_UdX(const mat &F) { return F - eye(3,3); } -//This function computes the gradient of displacement (Eulerian) from the deformation gradient tensor mat G_Udx(const mat &F) { try { @@ -84,55 +75,54 @@ mat G_Udx(const mat &F) { } } -//This function computes the Right Cauchy-Green C mat R_Cauchy_Green(const mat &F) { return F.t()*F; } -//This function computes the Left Cauchy-Green B mat L_Cauchy_Green(const mat &F) { return F*F.t(); } -//Provides the RU decomposition of the transformation gradient F -void RU_decomposition(mat &R, mat &U, const mat &F) { - mat U2 = F.t()*F; - - try { - U = sqrtmat_sympd(U2); - } catch (const std::runtime_error &e) { - cerr << "Error in sqrtmat_sympd : " << e.what() << endl; +//Right polar decomposition F = R U from one eigen-solve of C = F^T F = N diag(c) N^T +void RU_spectral(mat::fixed<3,3> &R, vec::fixed<3> &c, mat::fixed<3,3> &N, const mat &F) { + const mat::fixed<3,3> F_(F); + // same failure modes as sqrtmat_sympd(C) then inv(U): a negative (or non-finite) spectrum, + // then a singular stretch + if (!eig_sym_3x3(c, N, F_.t()*F_) || c(0) < 0.) { throw simcoon::exception_sqrtmat_sympd("Error in sqrtmat_sympd function inside RU_decomposition."); - } - - try { - R = F*inv(U); - } catch (const std::runtime_error &e) { - cerr << "Error in inv: " << e.what() << endl; + } + if (!(c(0) > 0.)) { throw simcoon::exception_inv("Error in inv function inside RU_decomposition."); - } + } + vec::fixed<3> inv_s; + for (int k = 0; k < 3; k++) inv_s(k) = 1./std::sqrt(c(k)); + R = F_*spectral_compose_3x3(N, inv_s); } -//Provides the VR decomposition of the transformation gradient F -void VR_decomposition(mat &V, mat &R, const mat &F) { - mat V2 = F*F.t(); +void RU_decomposition(mat &R, mat &U, const mat &F) { + mat::fixed<3,3> R_, N; + vec::fixed<3> c; + RU_spectral(R_, c, N, F); + R = R_; + U = spectral_compose_3x3(N, sqrt(c)); +} - try { - V = sqrtmat_sympd(V2); - } catch (const std::runtime_error &e) { - cerr << "Error in sqrtmat_sympd : " << e.what() << endl; +void VR_decomposition(mat &V, mat &R, const mat &F) { + const mat::fixed<3,3> F_(F); + vec::fixed<3> b; + mat::fixed<3,3> N; + if (!eig_sym_3x3(b, N, F_*F_.t()) || b(0) < 0.) { throw simcoon::exception_sqrtmat_sympd("Error in sqrtmat_sympd function inside VR_decomposition."); - } - - try { - R = inv(V)*F; - } catch (const std::runtime_error &e) { - cerr << "Error in inv: " << e.what() << endl; - throw simcoon::exception_inv("Error in inv function inside RU_decomposition."); - } + } + if (!(b(0) > 0.)) { + throw simcoon::exception_inv("Error in inv function inside VR_decomposition."); + } + vec::fixed<3> inv_s; + for (int k = 0; k < 3; k++) inv_s(k) = 1./std::sqrt(b(k)); + V = spectral_compose_3x3(N, sqrt(b)); + R = spectral_compose_3x3(N, inv_s)*F_; } -//This function computes the common Right (or Left) Cauchy-Green invariants vec Inv_X(const mat &X) { vec I = zeros(3); @@ -142,7 +132,6 @@ vec Inv_X(const mat &X) { return I; } -//This function computes the Cauchy deformation tensor b mat Cauchy(const mat &F) { try { return inv(L_Cauchy_Green(F)); @@ -152,27 +141,25 @@ mat Cauchy(const mat &F) { } } -//This function computes the Green-Lagrange finite strain tensor E mat Green_Lagrange(const mat &F) { return 0.5*(R_Cauchy_Green(F) - eye(3,3)); } -//This function computes the Euler-Almansi finite strain tensor e mat Euler_Almansi(const mat &F) { return 0.5*(eye(3,3) - Cauchy(F)); } -//This function computes the Euler-Almansi finite strain tensor h mat Log_strain(const mat &F) { - try { - return 0.5*logmat_sympd(L_Cauchy_Green(F)); - } catch (const std::runtime_error &e) { - cerr << "Error in logmat_sympd: " << e.what() << endl; + const mat::fixed<3,3> F_(F); + vec::fixed<3> b; + mat::fixed<3,3> N; + if (!eig_sym_3x3(b, N, F_*F_.t()) || !(b(0) > 0.)) { throw simcoon::exception_logmat_sympd("Error in logmat_sympd function inside Log_strain."); - } + } + for (int k = 0; k < 3; k++) b(k) = 0.5*std::log(b(k)); + return spectral_compose_3x3(N, b); } -//This function computes the velocity difference mat finite_L(const mat &F0, const mat &F1, const double &DTime) { if (DTime <= simcoon::iota) { return zeros(3,3); // a rate is undefined at DTime = 0 (see finite_rotation) @@ -187,13 +174,11 @@ mat finite_L(const mat &F0, const mat &F1, const double &DTime) { } } -//This function computes the deformation rate D mat finite_D(const mat &F0, const mat &F1, const double &DTime) { const mat L = finite_L(F0, F1, DTime); return 0.5*(L+L.t()); } -//This function computes the spin tensor W (correspond to Jaumann rate) mat finite_W(const mat &F0, const mat &F1, const double &DTime) { const mat L = finite_L(F0, F1, DTime); @@ -201,12 +186,11 @@ mat finite_W(const mat &F0, const mat &F1, const double &DTime) { } -//This function computes the exact relative polar rotation and its midpoint spin -// Note : here R is the rigid body rotation in the polar decomposition of the deformation gradient F void finite_rotation(const mat &F0, const mat &F1, const double &DTime, mat &DR, mat &Omega) { - mat R0, U0, R1, U1; - RU_decomposition(R0, U0, F0); - RU_decomposition(R1, U1, F1); + mat::fixed<3,3> R0, R1, N; + vec::fixed<3> c; + RU_spectral(R0, c, N, F0); + RU_spectral(R1, c, N, F1); // Exact and DTime-independent: DR stays the true relative rotation even // when the time increment degenerates (the spin, a rate, does not). DR = R1*R0.t(); @@ -216,10 +200,7 @@ void finite_rotation(const mat &F0, const mat &F1, const double &DTime, mat &DR, return; } try { - // Inverse Cayley: exactly skew for an orthogonal DR, and its - // Hughes-Winget (Cayley) update recovers DR exactly. Singular at a - // 180-degree increment (Cayley chart limit) and for improper inputs - // (det F < 0) -> exception_inv, the solver's step-cut signal. + // singular at a 180-degree increment or det F < 0 -> exception_inv (solver step-cut signal) Omega = (2./DTime)*(DR-eye(3,3))*inv(DR+eye(3,3)); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; @@ -227,16 +208,12 @@ void finite_rotation(const mat &F0, const mat &F1, const double &DTime, mat &DR, } } -//This function computes the spin tensor Omega (correspond to Green-Naghdi rate) mat finite_Omega(const mat &F0, const mat &F1, const double &DTime) { mat DR, Omega; finite_rotation(F0, F1, DTime, DR, Omega); return Omega; } -//This function computes the increment of finite rotation -// (generalized two-spin midpoint; with Omega0 == Omega1 the two Cayley factors -// commute and finite_DQ(W, W, dt) == Hughes_Winget(W, dt) exactly) mat finite_DQ(const mat &Omega0, const mat &Omega1, const double &DTime) { try { diff --git a/src/Continuum_mechanics/Functions/objective_rates.cpp b/src/Continuum_mechanics/Functions/objective_rates.cpp index 194f44f83..c253e51ac 100755 --- a/src/Continuum_mechanics/Functions/objective_rates.cpp +++ b/src/Continuum_mechanics/Functions/objective_rates.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -128,14 +129,62 @@ static inline Fastor::Tensor apply_jaumann_correction( return result; } +// Spectral decomposition of B = F F^T (eigenvalues bi, eigenvectors the columns of Bi) with the +// fixed-size 3x3 solver; exception_eig_sym on failure, as arma::eig_sym did. +static inline void eig_B(vec::fixed<3> &bi, mat::fixed<3,3> &Bi, const mat &F, const char *where) { + const mat::fixed<3,3> F_(F); + if (!eig_sym_3x3(bi, Bi, F_*F_.t())) { + throw simcoon::exception_eig_sym(std::string("Error in eig_sym function inside ") + where + "."); + } +} + +// Spin coefficients of the logarithmic rate (Xiao-Bruhns-Meyers), f(r) of r = b_i/b_j +static inline double log_spin_f(const double &bi, const double &bj) { + const double s = bi/bj - 1.; + if (fabs(s) > 1.e-4) { + return (1.+(bi/bj))/(1.-(bi/bj))+2./log(bi/bj); + } + // Taylor expansion: f(r) = -s/6 + s^2/12 + O(s^3) where s = r - 1 + return s*(-1./6. + s/12.); +} + +// g(r) = (1 - sqrt r)/(1 + sqrt r), r = b_i/b_j +static inline double log_spin_g(const double &bi, const double &bj) { + const double s = bi/bj - 1.; + if (fabs(s) > 1.e-4) { + return (1.-(pow(bi/bj,0.5)))/(1.+(pow(bi/bj,0.5))); + } + // Taylor expansion: g(r) = -s/4 + 3s^2/16 + O(s^3) where s = r - 1 + return s*(-1./4. + 3.*s/16.); +} + +// t/sinh(t), t = 1/2 ln(b_i/b_j): the A^R (log_R) Daleckii-Krein kernel +static inline double kappa_R(const double &bi, const double &bj) { + const double t = 0.5*log(bi/bj); // ln(lambda_i/lambda_j); 0 on the diagonal + if (fabs(t) > 1.e-4) { + return t/sinh(t); + } + return 1. - t*t/6. + 7.*t*t*t*t/360.; // Taylor of t/sinh(t) +} + +// N_1 and N_2 of the logarithmic rates on the eigenbasis (bi, Bi) of B +static inline void log_spin_N1_N2(mat &N_1, mat &N_2, const vec::fixed<3> &bi, const mat::fixed<3,3> &Bi, const mat &D) { + mat::fixed<3,3> f, g; + for (int i = 0; i < 3; i++) { + for (int j = 0; j < 3; j++) { + f(i, j) = (i != j) ? log_spin_f(bi(i), bi(j)) : 0.; + g(i, j) = (i != j) ? log_spin_g(bi(i), bi(j)) : 0.; + } + } + N_1 = spectral_congruence(Bi, f, D); + N_2 = spectral_congruence(Bi, g, D); +} + void Jaumann(mat &DR, mat &D, mat &W, const double &DTime, const mat &F0, const mat &F1) { mat L = zeros(3,3); // DTime <= iota: D = W = 0, DR = I (rate undefined, see finite_rotation) if(DTime > simcoon::iota) { try { - // 2nd-order centered velocity gradient: L = Fdot F^-1 with Fdot=(F1-F0)/dt and F at - // the MID configuration (F0+F1)/2 -> L = (2/dt)(F1-F0)(F0+F1)^-1 (the end-config form - // (F1-F0)F1^-1 is only 1st order). The SAME estimate is used by every rate function so - // that D=sym(L) is identical across them (enforced by Tobjective_rates.all_rates_same_D). + // 2nd-order midpoint L = (2/dt)(F1-F0)(F0+F1)^-1, shared by every rate so D = sym(L) agrees L = (2./DTime)*(F1-F0)*inv(F0+F1); } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; @@ -166,13 +215,14 @@ void Green_Naghdi(mat &DR, mat &D, mat &Omega, const double &DTime, const mat &F //decomposition of L D = 0.5*(L+L.t()); - // Exact relative polar rotation DR = R1 R0^T and its inverse-Cayley - // midpoint spin in one polar pass (Hughes_Winget(Omega) == DR exactly, - // so no reconstruction round trip is needed). + // DR = R1 R0^T exactly; Hughes_Winget(Omega) == DR, no reconstruction needed finite_rotation(F0, F1, DTime, DR, Omega); } -void logarithmic_R(mat &DR, mat &N_1, mat &N_2, mat &D, mat &Omega, const double &DTime, const mat &F0, const mat &F1) { +namespace { + +// one eigen-solve of C0 and one of C1 +void logarithmic_R_kinematics(mat &DR, mat &D, mat &Omega, vec::fixed<3> &b1, mat::fixed<3,3> &Nb, const double &DTime, const mat &F0, const mat &F1) { mat L = zeros(3,3); if(DTime > simcoon::iota) { try { @@ -188,56 +238,41 @@ void logarithmic_R(mat &DR, mat &N_1, mat &N_2, mat &D, mat &Omega, const double D = 0.5*(L+L.t()); // Exact relative polar rotation + inverse-Cayley spin (see Green_Naghdi): // the exact DR is what makes the log_R tangent transport equivariant. - finite_rotation(F0, F1, DTime, DR, Omega); - - //Logarithmic - mat B = L_Cauchy_Green(F1); - - vec bi = zeros(3); - mat Bi; - bool success_eig_sym = eig_sym(bi, Bi, B); - if (!success_eig_sym) { - throw simcoon::exception_eig_sym("Error in eig_sym function inside logarithmic_R."); + mat::fixed<3,3> R0, R1, N0, N1; + vec::fixed<3> c0; + RU_spectral(R0, c0, N0, F0); + RU_spectral(R1, b1, N1, F1); + DR = R1*R0.t(); + Nb = R1*N1; // B1 = R1 C1 R1^T + + if (DTime <= simcoon::iota) { + Omega = zeros(3,3); + return; } - - std::vector Bi_proj(3); - Bi_proj[0] = Bi.col(0)*(Bi.col(0)).t(); - Bi_proj[1] = Bi.col(1)*(Bi.col(1)).t(); - Bi_proj[2] = Bi.col(2)*(Bi.col(2)).t(); - - N_1 = zeros(3,3); - for (unsigned int i=0; i<3; i++) { - for (unsigned int j=0; j<3; j++) { - if (i!=j) { - double f_z = 0.; - double s = bi(i)/bi(j) - 1.; - if (fabs(s) > 1.e-4) { - f_z = (1.+(bi(i)/bi(j)))/(1.-(bi(i)/bi(j)))+2./log(bi(i)/bi(j)); - } else { - // Taylor expansion: f(r) = -s/6 + s^2/12 + O(s^3) where s = r - 1 - f_z = s*(-1./6. + s/12.); - } - N_1 += f_z*Bi_proj[i]*D*Bi_proj[j]; - } - } + // Inverse Cayley, as finite_rotation + mat::fixed<3,3> DRpI = DR + eye(3,3), DRpI_inv; + if (!inv(DRpI_inv, DRpI)) { + throw simcoon::exception_inv("Error in inv function inside finite_rotation."); } + Omega = (2./DTime)*(DR-eye(3,3))*DRpI_inv; +} - N_2 = zeros(3,3); - for (unsigned int i=0; i<3; i++) { - for (unsigned int j=0; j<3; j++) { - if (i!=j) { - double g_z = 0.; - double s = bi(i)/bi(j) - 1.; - if (fabs(s) > 1.e-4) { - g_z = (1.-(pow(bi(i)/bi(j),0.5)))/(1.+(pow(bi(i)/bi(j),0.5))); - } else { - // Taylor expansion: g(r) = -s/4 + 3s^2/16 + O(s^3) where s = r - 1 - g_z = s*(-1./4. + 3.*s/16.); - } - N_2 += g_z*Bi_proj[i]*D*Bi_proj[j]; - } - } - } +// De = A^R : D on the eigenbasis (b, Nb) of B, by congruence (t/sinh t kernel) +mat::fixed<3,3> A_R_D(const vec::fixed<3> &b, const mat::fixed<3,3> &Nb, const mat &D) { + mat::fixed<3,3> K; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + K(i, j) = kappa_R(b(i), b(j)); + return spectral_congruence(Nb, K, D); +} + +} // namespace + +void logarithmic_R(mat &DR, mat &N_1, mat &N_2, mat &D, mat &Omega, const double &DTime, const mat &F0, const mat &F1) { + vec::fixed<3> bi; + mat::fixed<3,3> Bi; + logarithmic_R_kinematics(DR, D, Omega, bi, Bi, DTime, F0, F1); + log_spin_N1_N2(N_1, N_2, bi, Bi, D); } void logarithmic_F(mat &DF, mat &N_1, mat &N_2, mat &D, mat &L, const double &DTime, const mat &F0, const mat &F1) { @@ -257,52 +292,10 @@ void logarithmic_F(mat &DF, mat &N_1, mat &N_2, mat &D, mat &L, const double &DT D = 0.5*(L+L.t()); //Logarithmic - mat B = L_Cauchy_Green(F1); - - vec bi = zeros(3); - mat Bi; - bool success_eig_sym = eig_sym(bi, Bi, B); - if (!success_eig_sym) { - throw simcoon::exception_eig_sym("Error in eig_sym function inside logarithmic_F."); - } - std::vector Bi_proj(3); - Bi_proj[0] = Bi.col(0)*(Bi.col(0)).t(); - Bi_proj[1] = Bi.col(1)*(Bi.col(1)).t(); - Bi_proj[2] = Bi.col(2)*(Bi.col(2)).t(); - - N_1 = zeros(3,3); - for (unsigned int i=0; i<3; i++) { - for (unsigned int j=0; j<3; j++) { - if (i!=j) { - double f_z = 0.; - double s = bi(i)/bi(j) - 1.; - if (fabs(s) > 1.e-4) { - f_z = (1.+(bi(i)/bi(j)))/(1.-(bi(i)/bi(j)))+2./log(bi(i)/bi(j)); - } else { - // Taylor expansion: f(r) = -s/6 + s^2/12 + O(s^3) where s = r - 1 - f_z = s*(-1./6. + s/12.); - } - N_1 += f_z*Bi_proj[i]*D*Bi_proj[j]; - } - } - } - - N_2 = zeros(3,3); - for (unsigned int i=0; i<3; i++) { - for (unsigned int j=0; j<3; j++) { - if (i!=j) { - double g_z = 0.; - double s = bi(i)/bi(j) - 1.; - if (fabs(s) > 1.e-4) { - g_z = (1.-(pow(bi(i)/bi(j),0.5)))/(1.+(pow(bi(i)/bi(j),0.5))); - } else { - // Taylor expansion: g(r) = -s/4 + 3s^2/16 + O(s^3) where s = r - 1 - g_z = s*(-1./4. + 3.*s/16.); - } - N_2 += g_z*Bi_proj[i]*D*Bi_proj[j]; - } - } - } + vec::fixed<3> bi; + mat::fixed<3,3> Bi; + eig_B(bi, Bi, F1, "logarithmic_F"); + log_spin_N1_N2(N_1, N_2, bi, Bi, D); DF = Hughes_Winget(L, DTime); } @@ -346,28 +339,14 @@ mat Hughes_Winget(const mat &Omega, const double &DTime) { } mat get_BBBB(const mat &F1) { - mat B = L_Cauchy_Green(F1); - - vec bi = zeros(3); - mat Bi; - bool success_eig_sym = eig_sym(bi, Bi, B); - if (!success_eig_sym) { - throw simcoon::exception_eig_sym("Error in eig_sym function inside get_BBBB."); - } - mat BBBB = zeros(6,6); - - double f_z = 0.; + vec::fixed<3> bi; + mat::fixed<3,3> Bi; + eig_B(bi, Bi, F1, "get_BBBB"); + mat::fixed<6,6> BBBB(fill::zeros); for (unsigned int i=0; i<3; i++) { for (unsigned int j=0; j<3; j++) { if (i!=j) { - double s = bi(i)/bi(j) - 1.; - if (fabs(s) > 1.e-4) { - f_z = (1.+(bi(i)/bi(j)))/(1.-(bi(i)/bi(j)))+2./log(bi(i)/bi(j)); - } else { - // Taylor expansion: f(r) = -s/6 + s^2/12 + O(s^3) where s = r - 1 - f_z = s*(-1./6. + s/12.); - } - BBBB = BBBB + f_z*linearop_eigsym(Bi.col(i),Bi.col(j)); + add_linearop_eigsym(BBBB, log_spin_f(bi(i), bi(j)), Bi.col(i), Bi.col(j)); } } } @@ -375,22 +354,15 @@ mat get_BBBB(const mat &F1) { } mat get_BBBB_GN(const mat &F1) { - mat B = L_Cauchy_Green(F1); - - vec bi = zeros(3); - mat Bi; - bool success_eig_sym = eig_sym(bi, Bi, B); - if (!success_eig_sym) { - throw simcoon::exception_eig_sym("Error in eig_sym function inside get_BBBB_GN."); - } - mat BBBB = zeros(6,6); - - double f_z = 0.; + vec::fixed<3> bi; + mat::fixed<3,3> Bi; + eig_B(bi, Bi, F1, "get_BBBB_GN"); + mat::fixed<6,6> BBBB(fill::zeros); for (unsigned int i=0; i<3; i++) { for (unsigned int j=0; j<3; j++) { if (i!=j) { - f_z = (sqrt(bi(j)) - sqrt(bi(i)))/(sqrt(bi(j)) + sqrt(bi(i))); - BBBB = BBBB + f_z*linearop_eigsym(Bi.col(i),Bi.col(j)); + const double f_z = (sqrt(bi(j)) - sqrt(bi(i)))/(sqrt(bi(j)) + sqrt(bi(i))); + add_linearop_eigsym(BBBB, f_z, Bi.col(i), Bi.col(j)); } } } @@ -400,44 +372,25 @@ mat get_BBBB_GN(const mat &F1) { // A^R: log_R-frame strain-concentration tensor, De = A^R:D (full doc in objective_rates.hpp). // t/sinh(t) geometric-mean Daleckii-Krein kernel on the eigenbasis of B = F F^T. mat A_R(const mat &F) { - mat B = L_Cauchy_Green(F); - vec bi = zeros(3); - mat Bi; - bool success_eig_sym = eig_sym(bi, Bi, B); - if (!success_eig_sym) { - throw simcoon::exception_eig_sym("Error in eig_sym function inside A_R."); - } - mat AR = zeros(6,6); + vec::fixed<3> bi; + mat::fixed<3,3> Bi; + eig_B(bi, Bi, F, "A_R"); + mat::fixed<6,6> AR(fill::zeros); for (unsigned int i=0; i<3; i++) { for (unsigned int j=0; j<3; j++) { - double t = 0.5*log(bi(i)/bi(j)); // ln(lambda_i/lambda_j); 0 on the diagonal - double c; - if (fabs(t) > 1.e-4) { - c = t/sinh(t); - } else { - c = 1. - t*t/6. + 7.*t*t*t*t/360.; // Taylor of t/sinh(t) - } - AR = AR + c*linearop_eigsym(Bi.col(i),Bi.col(j)); + add_linearop_eigsym(AR, kappa_R(bi(i), bi(j)), Bi.col(i), Bi.col(j)); } } AR.rows(3,5) *= 2.0; // tensor -> engineering strain-concentration convention: A^R(I)=I, read De with v2t_strain return AR; } -// A^F: log_F-frame (convected) strain-concentration tensor (full doc in objective_rates.hpp). -// Kernel t*coth(t), t = 1/2 ln(b_i/b_j) -> diagonal = 1, so A^F:D recovers ln V (like A^R:D) in -// the convected F-frame. The cosh(t) factor vs A_R's t/sinh(t) compensates the convected (inv DF) -// transport. (An earlier "-1/2 ln(b_i b_j)" term made it deliberately indefinite past lambda=sqrt(e); -// that corrupted the diagonal to 1-2 lnλ and was the bug -- A^F MUST integrate to ln V, as A^R does.) +// kernel t*coth(t): unit diagonal, so A^F:D integrates to ln V like A^R:D mat A_F(const mat &F) { - mat B = L_Cauchy_Green(F); - vec bi = zeros(3); - mat Bi; - bool success_eig_sym = eig_sym(bi, Bi, B); - if (!success_eig_sym) { - throw simcoon::exception_eig_sym("Error in eig_sym function inside A_F."); - } - mat AF = zeros(6,6); + vec::fixed<3> bi; + mat::fixed<3,3> Bi; + eig_B(bi, Bi, F, "A_F"); + mat::fixed<6,6> AF(fill::zeros); for (unsigned int i=0; i<3; i++) { for (unsigned int j=0; j<3; j++) { double t = 0.5*log(bi(i)/bi(j)); @@ -448,7 +401,7 @@ mat A_F(const mat &F) { tcoth = 1. + t*t/3. - t*t*t*t/45.; // Taylor of t*coth(t) } double c = tcoth; // t*coth(t): F-frame strain-concentration kernel (diagonal -> 1, recovers ln V) - AF = AF + c*linearop_eigsym(Bi.col(i),Bi.col(j)); + add_linearop_eigsym(AF, c, Bi.col(i), Bi.col(j)); } } AF.rows(3,5) *= 2.0; // tensor -> engineering strain-concentration convention: A^F(I)=I, read De with v2t_strain @@ -473,35 +426,14 @@ void logarithmic(mat &DR, mat &D, mat &Omega, const double &DTime, const mat &F0 mat W = 0.5*(L-L.t()); //Logarithmic - mat B = L_Cauchy_Green(F1); - - vec bi = zeros(3); - mat Bi; - bool success_eig_sym = eig_sym(bi, Bi, B); - if (!success_eig_sym) { - throw simcoon::exception_eig_sym("Error in eig_sym function inside logarithmic."); - } - std::vector Bi_proj(3); - Bi_proj[0] = Bi.col(0)*(Bi.col(0)).t(); - Bi_proj[1] = Bi.col(1)*(Bi.col(1)).t(); - Bi_proj[2] = Bi.col(2)*(Bi.col(2)).t(); - - mat N = zeros(3,3); - for (unsigned int i=0; i<3; i++) { - for (unsigned int j=0; j<3; j++) { - if (i!=j) { - double f_z = 0.; - double s = bi(i)/bi(j) - 1.; - if (fabs(s) > 1.e-4) { - f_z = (1.+(bi(i)/bi(j)))/(1.-(bi(i)/bi(j)))+2./log(bi(i)/bi(j)); - } else { - // Taylor expansion: h(r) = -s/6 + s^2/12 + O(s^3) where s = r - 1 - f_z = s*(-1./6. + s/12.); - } - N += f_z*Bi_proj[i]*D*Bi_proj[j]; - } - } - } + vec::fixed<3> bi; + mat::fixed<3,3> Bi; + eig_B(bi, Bi, F1, "logarithmic"); + mat::fixed<3,3> f; + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) + f(i, j) = (i != j) ? log_spin_f(bi(i), bi(j)) : 0.; + const mat::fixed<3,3> N = spectral_congruence(Bi, f, D); Omega = W + N; DR = Hughes_Winget(Omega, DTime); } @@ -512,9 +444,7 @@ mat Delta_log_strain(const mat &D, const mat &Omega, const double &DTime) { } mat Delta_log_strain_F(const mat &D, const mat &L, const double &DTime) { - // Naive log_F midpoint increment: same form as Delta_log_strain, but the frame - // increment DF = Hughes_Winget(L, dt) is non-orthogonal, so the rotated term is - // the push-forward DF*D*inv(DF) -- inverse, NOT transpose. + // DF is non-orthogonal: the rotated term is DF*D*inv(DF), not DF*D*DF^T const mat DF = Hughes_Winget(L, DTime); return 0.5*(D+(DF*D*inv(DF)))*DTime; } @@ -523,11 +453,13 @@ mat Delta_log_strain_F(const mat &D, const mat &L, const double &DTime) { // chosen objective rate. Single source of truth for the solver's control_type ladders. void corate_kinematics(const int &corate_type, mat &DR, mat &D, mat &Omega, const mat &F0, const mat &F1, const double &DTime) { mat N_1 = zeros(3,3), N_2 = zeros(3,3); + vec::fixed<3> b1; + mat::fixed<3,3> Nb; switch (corate_type) { case 0: Jaumann(DR, D, Omega, DTime, F0, F1); break; case 1: Green_Naghdi(DR, D, Omega, DTime, F0, F1); break; case 2: logarithmic(DR, D, Omega, DTime, F0, F1); break; - case 3: logarithmic_R(DR, N_1, N_2, D, Omega, DTime, F0, F1); break; // DR = R-rotation + case 3: logarithmic_R_kinematics(DR, D, Omega, b1, Nb, DTime, F0, F1); break; // DR = R-rotation (N_1, N_2 unused) case 4: Truesdell(DR, D, Omega, DTime, F0, F1); break; // DR = DF, Omega = L case 5: logarithmic_F(DR, N_1, N_2, D, Omega, DTime, F0, F1); break; // DR = DF, Omega = L case corate_type_total: Jaumann(DR, D, Omega, DTime, F0, F1); DR.eye(3,3); Omega.zeros(3,3); break; // strain space: no frame increment @@ -535,25 +467,57 @@ void corate_kinematics(const int &corate_type, mat &DR, mat &D, mat &Omega, cons } } +// One-step kinematics plus the strain increment, sharing the spectral work (doc in objective_rates.hpp) +void corate_kinematics(const int &corate_type, mat &DR, mat &D, mat &Omega, mat &De, const mat &F0, const mat &F1, const double &DTime) { + if (corate_type == 3) { // log_R: the eigen-solve of C1 gives R1, the eigenbasis of B1 and A^R + vec::fixed<3> b1; + mat::fixed<3,3> Nb; + logarithmic_R_kinematics(DR, D, Omega, b1, Nb, DTime, F0, F1); + const mat::fixed<3,3> Dh = A_R_D(b1, Nb, D); + De = 0.5*(Dh + DR*Dh*DR.t())*DTime; + return; + } + corate_kinematics(corate_type, DR, D, Omega, F0, F1, DTime); + De = Delta_log_strain_corate(F0, F1, DR, D, Omega, DTime, corate_type); +} + +namespace { + +// 1/2 ln of the SPD matrix A (a Cauchy-Green tensor) +mat::fixed<3,3> half_logmat(const mat::fixed<3,3> &A, const char *where) { + mat::fixed<3,3> H; + if (!logmat_sympd_3x3(H, A)) { + throw simcoon::exception_logmat_sympd(std::string("Error in logmat_sympd function inside ") + where + "."); + } + return 0.5*H; +} + +} // namespace + // Dispatch documented in objective_rates.hpp. mat Delta_log_strain_corate(const mat &F0, const mat &F1, const mat &DR, const mat &D, const mat &Omega, const double &DTime, const int &corate_type) { if (corate_type == corate_type_total) { // total formulation: H = 1/2 ln C is a state function of F, reference components - return 0.5*logmat_sympd(R_Cauchy_Green(F1)) - 0.5*logmat_sympd(R_Cauchy_Green(F0)); + const mat::fixed<3,3> F0_(F0), F1_(F1); + return half_logmat(F1_.t()*F1_, "Delta_log_strain_corate") - half_logmat(F0_.t()*F0_, "Delta_log_strain_corate"); } if (corate_type == 5) { // log_F: convected A^F:D rate (Omega carries the velocity gradient L) return Delta_log_strain_F(v2t_strain(A_F(F1)*t2v_strain(D)), Omega, DTime); } if (corate_type == 2) { // XBM: exact closed-form spatial log-strain difference -> etot = ln V1 - mat lnV0 = 0.5*logmat_sympd(L_Cauchy_Green(F0)); - mat lnV1 = 0.5*logmat_sympd(L_Cauchy_Green(F1)); + const mat::fixed<3,3> F0_(F0), F1_(F1); + const mat::fixed<3,3> lnV0 = half_logmat(F0_*F0_.t(), "Delta_log_strain_corate"); + const mat::fixed<3,3> lnV1 = half_logmat(F1_*F1_.t(), "Delta_log_strain_corate"); return lnV1 - DR*lnV0*DR.t(); // orthogonal (DR^T) transport } - // corate 3 (log_R): A^R:D = the R-corotational (Green-Naghdi) rate of ln V, integrated in the - // natural frame -> recovers ln V (~2e-4) and is frame-indifferent under rigid rotation. The - // log_R spin is carried by sv_M->DR (logarithmic_R); the solver must NOT also apply the DR_N - // natural-basis rotation -- stacking both double-counts the log_R correction (undershoots ln V - // by ~11% under large open shear). A^R:D alone is the correct, self-consistent formulation. - if (corate_type == 3) return Delta_log_strain(v2t_strain(A_R(F1)*t2v_strain(D)), Omega, DTime); // log_R: A^R:D + // log_R spin is carried by sv_M->DR: the solver must not also apply DR_N (double count) + // frame increment is DR itself (inverse Cayley of Omega) + if (corate_type == 3) { // log_R: A^R:D + vec::fixed<3> b1; + mat::fixed<3,3> Nb; + eig_B(b1, Nb, F1, "Delta_log_strain_corate"); + const mat::fixed<3,3> Dh = A_R_D(b1, Nb, D); + return 0.5*(Dh + DR*Dh*DR.t())*DTime; + } if (corate_type == 4) { // Truesdell: e_A(F1) = DF^-T e_A(F0) DF^-1 + De, exactly, with DR = DF return 0.5*(eye(3,3) - inv_sympd(DR*DR.t())); } @@ -574,50 +538,29 @@ double Delta_work_conjugacy(const vec &tau_start, const vec &tau_start_tr, const - 0.5*dot(tau_start_tr + tau, Detot); } -// --------------------------------------------------------------------------- -// EXACT log-box <-> material tangent maps (contract and derivation: see the -// DtauDe_2_DSDE / DSDE_2_DtauDe Doxygen in objective_rates.hpp). Both -// directions differentiate S(E) = U^-1 R^T tau(ln V) R U^-1 with -// Daleckii-Krein spectral derivatives and are mutual algebraic inverses by -// construction — which keeps the hyperelastic bake-transport round trip -// exact with no coordinated re-derivation. -// The kernel box Lt lives in the SPATIAL frame (argument ln V = R ln U R^T), -// while the spectral machinery differentiates the material-frame ln U: the -// box must therefore be applied as R^T (Lt : R dh R^T) R. Skipping that -// conjugation is exact only for isotropic Lt or R = I — with a plastified -// (anisotropic) box tangent under accumulated rotation it leaves an -// O(||Lt_dev|| * theta_R) tangent error that grows with rotation and -// degrades Newton (the "history transport" finding: ~5e-3 at gamma = 0.3 -// simple shear on EPICP, conjugated map ~1e-9). -// --------------------------------------------------------------------------- +// Exact log-box <-> material tangent maps (contract: DtauDe_2_DSDE / DSDE_2_DtauDe) namespace { -// Spectral machinery on C = F^T F shared by the two exact maps above. The -// Daleckii-Krein derivative is applied in congruence form, -// dY = N (G % N^T dX N) N^T (N = eigenvector matrix), which is the same map -// as the sum over eigenprojector sandwiches at a fraction of the products. +// Daleckii-Krein in congruence form: dY = N (G % N^T dX N) N^T struct dk_spectral { - arma::vec lam; // eigenvalues of C (SPD) - arma::mat N; // eigenvectors of C (columns) - arma::mat G_h; // DK coefficients of h(C) = 1/2 ln C - arma::mat G_iU; // DK coefficients of C^{-1/2} - arma::mat U; // C^{1/2} - arma::mat invU; // C^{-1/2} - arma::mat R; // polar rotation F U^{-1} - arma::mat tau_hat; // corotational Kirchhoff stress R^T tau R + arma::vec::fixed<3> lam; // eigenvalues of C (SPD) + arma::mat::fixed<3,3> N; // eigenvectors of C (columns) + arma::mat::fixed<3,3> G_h; // DK coefficients of h(C) = 1/2 ln C + arma::mat::fixed<3,3> G_iU; // DK coefficients of C^{-1/2} + arma::mat::fixed<3,3> U; // C^{1/2} + arma::mat::fixed<3,3> invU; // C^{-1/2} + arma::mat::fixed<3,3> R; // polar rotation F U^{-1} + arma::mat::fixed<3,3> tau_hat; // corotational Kirchhoff stress R^T tau R void build(const arma::mat &F, const arma::mat &tau) { - const bool success_eig_sym = arma::eig_sym(lam, N, R_Cauchy_Green(F)); + const arma::mat::fixed<3,3> F_(F); + const bool success_eig_sym = eig_sym_3x3(lam, N, F_.t()*F_); if (!success_eig_sym) { throw simcoon::exception_eig_sym( "Error in eig_sym function inside the exact DtauDe<->DSDE map."); } - // Near-singular F: lam(min) -> 0 would push log/1/sqrt to NaN/Inf - // tangents with NO exception. Throw the exception the solver's - // step-cut catch ladder already handles (exception_inv: F is not - // usably invertible), preserving the recoverable failure mode of - // the previous inv(F)-based implementation. + // near-singular F: throw exception_inv, the solver step-cut signal, rather than NaN tangents if (!lam.is_finite() || lam.min() <= simcoon::iota) { throw simcoon::exception_inv( "Near-singular F inside the exact DtauDe<->DSDE map (eigenvalue of C <= iota)."); @@ -626,15 +569,13 @@ struct dk_spectral { [](double l) { return 0.5 / l; }); G_iU = gmat([](double l) { return 1.0 / std::sqrt(l); }, [](double l) { return -0.5 * std::pow(l, -1.5); }); - U = N * diagmat(sqrt(lam)) * N.t(); - invU = N * diagmat(1.0 / sqrt(lam)) * N.t(); - R = F * invU; - tau_hat = R.t() * tau * R; + U = spectral_compose_3x3(N, sqrt(lam)); + invU = spectral_compose_3x3(N, 1.0 / sqrt(lam)); + R = F_ * invU; + tau_hat = R.t() * arma::mat::fixed<3,3>(tau) * R; } - // Divided-difference coefficient matrix for f, symmetric; f' at the - // midpoint on the coincidence guard (2nd-order limit; same conservative - // 1e-4 relative threshold as get_BBBB / A_R in this file). + // f' at the midpoint under the 1e-4 relative coincidence guard (as get_BBBB / A_R) template arma::mat gmat(Ff f, Fdf df) const { const arma::vec fl = {f(lam(0)), f(lam(1)), f(lam(2))}; @@ -653,20 +594,57 @@ struct dk_spectral { }; +// Fixed-size Voigt helpers of the 6-column tangent maps below: per column, 3x3 products on the +// stack and the 6x6 box applied by hand (a BLAS gemv call per column otherwise). +typedef arma::mat::fixed<3,3> m33; + +// engineering-Voigt unit strain e_j as a tensor (v2t_strain of the j-th basis vector) +inline m33 unit_strain_tensor(const int j) { + static const int ia[6] = {0, 1, 2, 0, 0, 1}, ib[6] = {0, 1, 2, 1, 2, 2}; + m33 e(arma::fill::zeros); + const double v = (j < 3) ? 1. : 0.5; + e(ia[j], ib[j]) = v; + e(ib[j], ia[j]) = v; + return e; +} + +// y = Lt t2v_strain(e) +inline void box_apply(const arma::mat::fixed<6,6> &Lt, const m33 &e, double y[6]) { + const double x[6] = {e(0,0), e(1,1), e(2,2), e(0,1) + e(1,0), e(0,2) + e(2,0), e(1,2) + e(2,1)}; + for (int i = 0; i < 6; ++i) { + double yi = 0.; + for (int k = 0; k < 6; ++k) yi += Lt(i, k)*x[k]; + y[i] = yi; + } +} + +inline m33 v2t_stress_arr(const double v[6]) { + m33 S; + S(0,0) = v[0]; S(1,1) = v[1]; S(2,2) = v[2]; + S(0,1) = S(1,0) = v[3]; + S(0,2) = S(2,0) = v[4]; + S(1,2) = S(2,1) = v[5]; + return S; +} + +// column j of a 6x6 Voigt map, as t2v_stress(S) +inline void set_stress_col(arma::mat &M, const int j, const m33 &S) { + M(0, j) = S(0,0); M(1, j) = S(1,1); M(2, j) = S(2,2); + M(3, j) = 0.5*(S(0,1) + S(1,0)); + M(4, j) = 0.5*(S(0,2) + S(2,0)); + M(5, j) = 0.5*(S(1,2) + S(2,1)); +} + } // namespace namespace { -// Total formulation (strain space). The box is posed on H = f(C), f = 1/2 ln, in reference -// components and returns the dual stress T; with That = N^T T N in the eigenbasis of C: -// S = 2 Df(C)[T] = 2 N (G % That) N^T, G_ab = f[c_a, c_b] -// tau = F S F^T = R N (kappa % That) N^T R^T, kappa = 2 lam_a lam_b G_ab -// dS = 2 Df(C)[dT] + 2 D2f(C)[T, dC], D2f(C)[T, dC]_ab = sum_k Gam_akb (That_ak dC_kb + dC_ak That_kb) -// with Gam_abc = f[c_a, c_b, c_c]. kappa(t) = t/sinh(t), t = ln(lam_a/lam_b), is A_R's kernel. +// S = 2 N (G % That) N^T, tau = R N (kappa % That) N^T R^T, That = N^T T N, G_ab = f[c_a, c_b], +// f = 1/2 ln, kappa = 2 lam_a lam_b G_ab = t/sinh(t) (A_R kernel) struct logU_spectral { - arma::vec c; // eigenvalues of C (SPD) - arma::mat N; // eigenvectors of C (columns) - arma::mat R; // polar rotation F U^{-1} + arma::vec::fixed<3> c; // eigenvalues of C (SPD) + arma::mat::fixed<3,3> N; // eigenvectors of C (columns) + arma::mat::fixed<3,3> R; // polar rotation F U^{-1} arma::mat::fixed<3,3> kappa; // t/sinh(t) arma::mat::fixed<3,3> G; // first divided differences of 1/2 ln double Gam[3][3][3]; // second divided differences of 1/2 ln @@ -679,10 +657,7 @@ struct logU_spectral { static double dd1(const double &x, const double &y) { return kappa_of(x, y)/(2.*std::sqrt(x*y)); } - // f[x, y, z]. Separated: difference of the two first differences over the WIDEST gap, so two - // coalescing arguments cost nothing. Clustered (spread below 1e-4): Taylor about the mean m, - // f[..] = sum_k f^(k+2)(m)/(k+2)! h_k(d), h_k the complete homogeneous polynomials of the - // offsets d (h_1 = 0 about the mean); truncated after h_4, exact limit -1/(4 m^2). + // f[x,y,z]: separated -> difference over the widest gap; clustered (< 1e-4) -> Taylor about the mean to h_4 static double dd2(double x, double y, double z) { if (x > y) std::swap(x, y); if (y > z) std::swap(y, z); @@ -699,7 +674,8 @@ struct logU_spectral { // tangent = false: the stress maps need kappa only void build(const arma::mat &F, const bool &tangent) { - const bool success_eig_sym = arma::eig_sym(c, N, R_Cauchy_Green(F)); + const arma::mat::fixed<3,3> F_(F); + const bool success_eig_sym = eig_sym_3x3(c, N, F_.t()*F_); if (!success_eig_sym) { throw simcoon::exception_eig_sym("Error in eig_sym function inside the total-formulation (strain-space) maps."); } @@ -707,7 +683,7 @@ struct logU_spectral { if (!c.is_finite() || c.min() <= simcoon::iota) { throw simcoon::exception_inv("Near-singular F inside the total-formulation (strain-space) maps (eigenvalue of C <= iota)."); } - R = F*N*diagmat(1.0/sqrt(c))*N.t(); + R = F_*spectral_compose_3x3(N, 1.0/sqrt(c)); for (int a = 0; a < 3; ++a) { for (int b = 0; b < 3; ++b) { kappa(a, b) = kappa_of(c(a), c(b)); @@ -795,22 +771,22 @@ mat DtauDe_2_DSDE(const mat &Lt, const mat &F, const mat &tau){ dk_spectral sp; sp.build(F, tau); + const arma::mat::fixed<6,6> Lt_(Lt); mat DSDE(6, 6); - vec ej(6); + double y[6]; for (int j = 0; j < 6; ++j) { - ej.zeros(); ej(j) = 1.; - const mat dC = 2. * v2t_strain(ej); // engineering Voigt basis - const mat dCp = sp.N.t() * dC * sp.N; // shared eigenbasis transform - const mat dh = sp.N * (sp.G_h % dCp) * sp.N.t(); // d(1/2 ln C) = d(ln U) + const m33 dC = 2. * unit_strain_tensor(j); // engineering Voigt basis + const m33 dCp = sp.N.t() * dC * sp.N; // shared eigenbasis transform + const m33 dh = sp.N * (sp.G_h % dCp) * sp.N.t(); // d(1/2 ln C) = d(ln U) // The box acts in the spatial frame (d ln V = R d(ln U) R^T at fixed R): // conjugate in, apply Lt, conjugate back to the corotational frame. - const mat dtau_sp = v2t_stress(Lt * t2v_strain(sp.R * dh * sp.R.t())); - const mat dtau_hat = sp.R.t() * dtau_sp * sp.R; - const mat dinvU = sp.N * (sp.G_iU % dCp) * sp.N.t(); // d(C^{-1/2}) - const mat dS = sp.invU * dtau_hat * sp.invU + box_apply(Lt_, sp.R * dh * sp.R.t(), y); + const m33 dtau_hat = sp.R.t() * v2t_stress_arr(y) * sp.R; + const m33 dinvU = sp.N * (sp.G_iU % dCp) * sp.N.t(); // d(C^{-1/2}) + const m33 dS = sp.invU * dtau_hat * sp.invU + dinvU * sp.tau_hat * sp.invU + sp.invU * sp.tau_hat * dinvU; - DSDE.col(j) = t2v_stress(dS); + set_stress_col(DSDE, j, dS); } return DSDE; } @@ -889,13 +865,6 @@ mat DsigmaDe_JaumannDD_2_DSDE(const mat &Lt, const mat &F, const mat &sigma){ mat DtauDe_GreenNaghdiDD_2_DSDE(const mat &Lt, const mat &F, const mat &tau){ - // The PLAIN Green-Naghdi corotational strain (corate 1, integral of - // R^T D R) is a PATH integral — not a state function of C — so the exact - // spectral map above does not apply: this conversion stays the rate - // identity (GN box -> Lie via the GN spin correction, then pull-back), - // the exact inverse of DSDE_2_Dtau_GreenNaghdiDD. NB log_R (corate 3) is - // NOT in this family: A^R:D accumulates exactly ln U, so the corate - // dispatchers route it to the exact map. mat invF; try { invF = inv(F); @@ -938,32 +907,25 @@ mat DsigmaDe_2_DtauDe(const mat &Lt, const double &J) { mat DSDE_2_DtauDe(const mat &DSDE, const mat &F, const mat &tau) { - // Exact algebraic inverse of DtauDe_2_DSDE (see the block comment there): - // every step of the composition is inverted — the Daleckii-Krein map is - // diagonal in the eigenprojector basis, so its inverse is the entrywise - // reciprocal of the coefficient matrix. dk_spectral sp; sp.build(F, tau); - const mat G_h_inv = 1.0 / sp.G_h; // elementwise; G_h > 0 (1/2 ln strictly increasing) - const mat G_comp = sp.G_iU % G_h_inv; // composed d(C^{-1/2}) o [d(1/2 ln C)]^{-1} + const m33 G_h_inv = 1.0 / sp.G_h; // elementwise; G_h > 0 (1/2 ln strictly increasing) + const m33 G_comp = sp.G_iU % G_h_inv; // composed d(C^{-1/2}) o [d(1/2 ln C)]^{-1} + const arma::mat::fixed<6,6> DSDE_(0.5 * DSDE); mat Lt(6, 6); - vec ej(6); + double y[6]; for (int j = 0; j < 6; ++j) { - ej.zeros(); ej(j) = 1.; - // The box argument is the SPATIAL log strain: pull the basis - // perturbation d(ln V) back to the material frame (d ln U at fixed R) - // before inverting the spectral steps, and push the corotational - // stress response forward again — the exact inverse of the forward - // map's R conjugation. - const mat dh = sp.R.t() * v2t_strain(ej) * sp.R; - const mat dhp = sp.N.t() * dh * sp.N; // eigenframe - const mat dC = sp.N * (G_h_inv % dhp) * sp.N.t(); // inverse of d(1/2 ln C) - const mat dS = v2t_stress(0.5 * (DSDE * t2v_strain(dC))); - const mat dinvU = sp.N * (G_comp % dhp) * sp.N.t(); - const mat dtau_hat = + // box argument is ln V: pull dh back by R (inverse of the forward R conjugation) + const m33 dh = sp.R.t() * unit_strain_tensor(j) * sp.R; + const m33 dhp = sp.N.t() * dh * sp.N; // eigenframe + const m33 dC = sp.N * (G_h_inv % dhp) * sp.N.t(); // inverse of d(1/2 ln C) + box_apply(DSDE_, dC, y); + const m33 dS = v2t_stress_arr(y); + const m33 dinvU = sp.N * (G_comp % dhp) * sp.N.t(); + const m33 dtau_hat = sp.U * (dS - dinvU * sp.tau_hat * sp.invU - sp.invU * sp.tau_hat * dinvU) * sp.U; - Lt.col(j) = t2v_stress(sp.R * dtau_hat * sp.R.t()); + set_stress_col(Lt, j, sp.R * dtau_hat * sp.R.t()); } return Lt; } @@ -1071,12 +1033,9 @@ mat DSDE_2_DtauDe_corate(const mat &DSDE, const int &corate_type, const mat &F, case 4: return DSDE_2_Dtau_LieDD(DSDE, F); // Truesdell: the convected box IS the Lie tangent case 5: return DSDE_2_Dtau_JaumannDD(DSDE, F, tau)*inv(A_F(F)); // log_F: De = A^F:D dt, stress carried by sym(DF X DF^-1) case corate_type_total: return DSDE_2_logU_box(DSDE, F, tau); // strain space: dT/dH in reference components + case corate_type_material_tangent: return DSDE; // the material tangent itself case 2: // XBM (logarithmic) - case 3: // log_R: A^R:D accumulates EXACTLY ln U in the - // R frame (Hoger/Miehe d(ln U)/dC in rate form), - // a state function of C -> exact map applies; the - // R transport even cancels without the isotropy - // argument XBM needs (no residual rotation). + case 3: // log_R: A^R:D accumulates exactly ln U (a function of C) -> exact map default: return DSDE_2_DtauDe(DSDE, F, tau); } } @@ -1094,7 +1053,7 @@ mat dH_dE(const mat &C) { for (int j = 0; j < 6; j++) { vec e = zeros(6); e(j) = 1.; - P.col(j) = t2v_strain(N*(2.*(g % (N.t()*v2t_strain(e)*N)))*N.t()); + P.col(j) = t2v_strain(spectral_congruence(N, 2.*g, v2t_strain(e))); } return P; } @@ -1117,6 +1076,7 @@ mat DtauDe_corate_2_DSDE(const mat &Lt, const int &corate_type, const mat &F, co case 4: return Dtau_LieDD_2_DSDE(Lt, F); // Truesdell: convected box = Lie tangent case 5: return DtauDe_JaumannDD_2_DSDE(Lt*A_F(F), F, tau); // log_F: inverse of the forward map case corate_type_total: return logU_box_2_DSDE(Lt, F, tau); // strain space: P^T Lt P + T:L + case corate_type_material_tangent: return Lt; // already dS/dE case 2: // XBM and... case 3: // log_R: exact map (see DSDE_2_DtauDe_corate) default: return DtauDe_2_DSDE(Lt, F, tau); @@ -1124,21 +1084,40 @@ mat DtauDe_corate_2_DSDE(const mat &Lt, const int &corate_type, const mat &F, co } mat Dtau_LieDD_2_DtauDe_corate(const mat &Dtau_LieDD, const int &corate_type, const mat &F, const mat &tau) { - // Mirrors DSDE_2_DtauDe_corate above, one step earlier in the chain: from the SPATIAL - // (Lie/Oldroyd) tangent, which is what a hyperelastic potential closes on, straight to the - // box of the requested rate. switch (corate_type) { case 0: return Dtau_LieDD_Dtau_JaumannDD(Dtau_LieDD, tau); case 1: return Dtau_LieDD_Dtau_GreenNaghdiDD(Dtau_LieDD, F, tau); case 4: return Dtau_LieDD; // Truesdell: the convected box IS the Lie tangent case 5: return Dtau_LieDD_Dtau_JaumannDD(Dtau_LieDD, tau)*inv(A_F(F)); // log_F: chain rule through De = A^F:D dt case corate_type_total: return DSDE_2_logU_box(Dtau_LieDD_2_DSDE(Dtau_LieDD, F), F, tau); // strain space, through dS/dE + case corate_type_material_tangent: return Dtau_LieDD_2_DSDE(Dtau_LieDD, F); // the pull-back alone case 2: // XBM and... case 3: // log_R: exact spectral map default: return Dtau_LieDD_Dtau_logarithmicDD(Dtau_LieDD, F, tau); } } +mat Dtau_LieDD_2_DtauDe_stretch(const mat &Dtau_LieDD, const vec::fixed<3> &e, const mat::fixed<3,3> &N, const mat &tau) { + m33 H; + for (int a = 0; a < 3; ++a) { + for (int b = 0; b < 3; ++b) { + const double d = e(a) - e(b); + H(a, b) = (std::fabs(d) > 1.e-4) ? std::tanh(d)/d : 1. - d*d/3. + 2.*d*d*d*d/15.; // Taylor of tanh(d)/d + } + } + const arma::mat::fixed<6,6> Jc(Dtau_LieDD); + const m33 tau_(tau); + mat Lt(6, 6); + double y[6]; + for (int j = 0; j < 6; ++j) { + const m33 dt = spectral_congruence(N, H, unit_strain_tensor(j)); + box_apply(Jc, dt, y); + const m33 dtau = v2t_stress_arr(y) + dt * tau_ + tau_ * dt; + set_stress_col(Lt, j, dtau); + } + return Lt; +} + // Canonical box tangent Lt = d(tau_hat)/d(De) (full doc in objective_rates.hpp). // From the material tangent dS/dE: mat box_DtauDe_from_dSdE(const mat &dSdE, const mat &F, const vec &sigma) { @@ -1172,11 +1151,7 @@ mat Dtau_LieDD_Dtau_GreenNaghdiDD(const mat &Dtau_LieDD, const mat &F, const mat mat Dtau_LieDD_Dtau_logarithmicDD(const mat &Dtau_LieDD, const mat &F, const mat &tau) { - // For the XBM logarithmic rate the tangent in-rate IS the box tangent - // d(tau_hat)/dDe: route through the exact spectral pair so the whole - // corate-2 family shares one core and composes as mutual inverses - // (the former frozen-spin correction left this leg first-order while - // the box<->DSDE legs were exact). + // XBM in-rate tangent = box tangent: route through the exact spectral pair return DSDE_2_DtauDe(Dtau_LieDD_2_DSDE(Dtau_LieDD, F), F, tau); } diff --git a/src/Continuum_mechanics/Functions/recovery_props.cpp b/src/Continuum_mechanics/Functions/recovery_props.cpp index ee685379d..ed1f45be8 100755 --- a/src/Continuum_mechanics/Functions/recovery_props.cpp +++ b/src/Continuum_mechanics/Functions/recovery_props.cpp @@ -35,12 +35,7 @@ namespace simcoon{ void check_symetries(const mat &L, std::string &umat_type, int &axis, vec &props, int &maj_sym, const double &tol) { - // Frobenius, not the spectral norm: norm(X,2) on a matrix is a full SVD - // (LAPACK dgesdd), which reads out of bounds in the CLAPACK build shipped - // in the Windows wheel. These are deviation measures compared against a - // tolerance, and the deviations are either rounding-level or of the order - // of the stiffness itself, so the (bounded) change of scale between the - // two norms does not move any classification. See real_logmat_3x3. + //Frobenius, not norm(X,2): its SVD (dgesdd) reads out of bounds in the Windows-wheel CLAPACK double max_tol_sim = simcoon::limit; if(tol > simcoon::limit) { max_tol_sim = tol; @@ -60,9 +55,7 @@ void check_symetries(const mat &L, std::string &umat_type, int &axis, vec &props Mat type_sym(3,3); type_sym.zeros(); - //Everything is checked based on a "symmtrized" matrix - //Check reflexions - //reflexion around the z axis - symmetry plane xy + //checks on the symmetrized matrix; reflexion about z (symmetry plane xy) Q = { {1,0,0}, {0,1,0}, {0,0,-1} }; L_test = rotate_stiffness(L_sym, Q); check_sym(2,0) = norm(L_sym - L_test,"fro"); @@ -130,25 +123,17 @@ void check_symetries(const mat &L, std::string &umat_type, int &axis, vec &props } } - //Check orthotropic - //Orthotropic: three mutually orthogonal planes of reflection. Symmetry. a!=b=c, alpha=beta=gamma=90 Number of independent coefficients: 9 - //Symmetry transformations: reflections about all three orthogonal planes + //orthotropic: reflections about the three orthogonal planes (9 constants) if(type_sym(0,0) + type_sym(1,0) + type_sym(2,0) == 3) { umat_type = "ELORT"; } - //Check cubic - //three mutually orthogonal planes of reflection symmetry - //Plus 90deg rotation symmetry with respect to those planes. a=b=c, alpha=beta=gamma=90 Number of independent coefficients: 3 - //Symmetry transformations: reflections and 90 rotations about all three orthogonal planes + //cubic: orthotropic + 90deg rotations about the three axes (3 constants) if((type_sym(0,0) + type_sym(1,0) + type_sym(2,0) == 3) && (type_sym(0,1) + type_sym(1,1) + type_sym(2,1) == 3)) { umat_type = "ELCUB"; } - //Check Transversely isotropic - //Transversely isotropic: The physical properties are symmetric about an axis that is normal to a plane of isotropy (xy-plane in the figure). - //Three mutually orthogonal planes of reflection symmetry and axial symmetry with respect to z-axis. Number of independent coefficients: 5 - //Symmetry transformations: reflections about all three orthogonal planes plus all rotations about z- axis, x- axis or y - axis + //transversely isotropic: orthotropic + every rotation about one axis (5 constants) if((type_sym(0,0) + type_sym(1,0) + type_sym(2,0) == 3) && (type_sym(0,1) + type_sym(1,1) + type_sym(2,1) == 1) && (type_sym(0,2) + type_sym(1,2) + type_sym(2,2) == 1)) { if(type_sym(0,1) + type_sym(0,2) == 2) { umat_type = "ELIST"; @@ -168,25 +153,11 @@ void check_symetries(const mat &L, std::string &umat_type, int &axis, vec &props } } - //Check Isotropic - //Isotropic: The physical properties are symmetric about all axis - //Three mutually orthogonal planes of reflection symmetry and axial symmetry with respect to z-axis. Number of independent coefficients: 3 - //Symmetry transformations: reflections about all three orthogonal planes plus all rotations about z- axis, x- axis and y - axis + //isotropic: orthotropic + every rotation about the three axes (2 constants) if((type_sym(0,0) + type_sym(1,0) + type_sym(2,0) == 3) && (type_sym(0,1) + type_sym(1,1) + type_sym(2,1) == 3) && (type_sym(0,2) + type_sym(1,2) + type_sym(2,2) == 3)) { umat_type = "ELISO"; } - //Here is the full list of possibilities - //Triclinic -- fully anisotropic - 21 independant parameters - //Monoclinic - xy - 15 independant parameters - //Monoclinic - xz - 15 independant parameters - //Monoclinic - yz - 15 independant parameters - //Orthotropic - 9 independant parameters - //Cubic - 3 independant parameters - //Transversely isotropic x - 5 independant parameters - //Transversely isotropic y - 5 independant parameters - //Transversely isotropic z - 5 independant parameters - //Isotropic - 2 independant parameters if (umat_type == "ELISO") { props = L_iso_props(L_sym); diff --git a/src/Continuum_mechanics/Functions/stress.cpp b/src/Continuum_mechanics/Functions/stress.cpp index 326b29325..105173bf6 100755 --- a/src/Continuum_mechanics/Functions/stress.cpp +++ b/src/Continuum_mechanics/Functions/stress.cpp @@ -30,7 +30,6 @@ using namespace arma; namespace simcoon{ -//This function returns the first Piola-Kirchoff stress tensor from the Cauchy stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat Cauchy2PKI(const mat &sigma, const mat &F, const double &mJ) { double J=mJ; @@ -56,7 +55,6 @@ mat Cauchy2PKI(const mat &sigma, const mat &F, const double &mJ) { } } -//This function returns the Biot stress tensor from the Cauchy stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat Cauchy2Biot(const mat &sigma, const mat &F, const mat &mR, const double &mJ) { mat PKI = Cauchy2PKI(sigma, F, mJ); @@ -82,7 +80,6 @@ mat Kirchoff2Biot(const mat &tau, const mat &F, const mat &mR) { return 0.5*(R.t()*PKI + PKI.t()*R); } -//This function returns the second Piola-Kirchoff stress tensor from the Cauchy stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat Cauchy2PKII(const mat &sigma, const mat &F, const double &mJ) { double J=mJ; @@ -108,7 +105,6 @@ mat Cauchy2PKII(const mat &sigma, const mat &F, const double &mJ) { } } -//This function returns the Kirchoff stress tensor from the Cauchy stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat Cauchy2Kirchoff(const mat &sigma, const mat &F, const double &mJ) { double J=mJ; @@ -130,7 +126,6 @@ mat Cauchy2Kirchoff(const mat &sigma, const mat &F, const double &mJ) { } -//This function returns the Kirchoff stress tensor from the Cauchy stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) vec Cauchy2Kirchoff(const vec &sigma, const mat &F, const double &mJ) { double J=mJ; @@ -152,7 +147,6 @@ vec Cauchy2Kirchoff(const vec &sigma, const mat &F, const double &mJ) { } -//This function returns the Cauchy stress tensor from the Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat Kirchoff2Cauchy(const mat &tau, const mat &F, const double &mJ) { double J=mJ; @@ -173,7 +167,6 @@ mat Kirchoff2Cauchy(const mat &tau, const mat &F, const double &mJ) { } } -//This function returns the Cauchy stress tensor from the Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) vec Kirchoff2Cauchy(const vec& tau, const mat& F, const double& mJ) { double J = mJ; @@ -195,7 +188,6 @@ vec Kirchoff2Cauchy(const vec& tau, const mat& F, const double& mJ) { } -//This function returns the first Piola-Kirchoff stress tensor from the Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat Kirchoff2PKI(const mat &tau, const mat &F, const double &mJ) { UNUSED(mJ); @@ -207,7 +199,6 @@ mat Kirchoff2PKI(const mat &tau, const mat &F, const double &mJ) { } } -//This function returns the second Piola-Kirchoff stress tensor from the Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat Kirchoff2PKII(const mat &tau, const mat &F, const double &mJ) { UNUSED(mJ); @@ -232,21 +223,18 @@ vec Kirchoff2PKII(const vec &tau, const mat &F, const double &mJ) { } -//This function returns the Kirchoff stress tensor from the first Piola-Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat PKI2Kirchoff(const mat &Sigma, const mat &F, const double &mJ) { UNUSED(mJ); return Sigma*F.t(); } -//This function returns the Kirchoff stress tensor from the second Piola-Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat PKII2Kirchoff(const mat &S, const mat &F, const double &mJ) { UNUSED(mJ); return F*S*F.t(); } -//This function returns the Cauchy stress tensor from the second Piola-Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat PKI2Cauchy(const mat &Sigma, const mat &F, const double &mJ) { double J=mJ; @@ -267,7 +255,6 @@ mat PKI2Cauchy(const mat &Sigma, const mat &F, const double &mJ) { } } -//This function returns the Cauchy stress tensor from the second Piola-Kirchoff stress tensor, the transformation gradient F and its determinant (optional, if not indicated, it will be computed) mat PKII2Cauchy(const mat &S, const mat &F, const double &mJ) { double J=mJ; diff --git a/src/Continuum_mechanics/Functions/tensor.cpp b/src/Continuum_mechanics/Functions/tensor.cpp index cc02b3187..77fd14ceb 100644 --- a/src/Continuum_mechanics/Functions/tensor.cpp +++ b/src/Continuum_mechanics/Functions/tensor.cpp @@ -57,11 +57,7 @@ static Tensor4Type parse_tensor4_type(const std::string &s) { "Expected: stiffness, compliance, strain_concentration, stress_concentration, generic"); } -// Helper: validate that a secondary operand in a batch op is either broadcast -// (n_slices == 1) or matches the primary batch size. Without this check, a -// mismatched count silently reads past the owned cube buffer in release builds -// (ARMA_NO_DEBUG), producing garbage output and, under OpenMP, non-deterministic -// crashes. +// secondary operand: broadcast (1) or matched (N); a mismatch reads past the cube buffer under ARMA_NO_DEBUG static void check_batch_broadcast(arma::uword n_secondary, arma::uword N_primary, const char *fn, @@ -86,10 +82,7 @@ static arma::mat::fixed<3,3> inv33(const arma::mat::fixed<3,3> &F) { return result; } -// Helper: select the 3x3 contraction kernel for tensor4 push/pull on all 4 indices. -// forward=true (push): stiffness/generic → F, compliance → F^{-T} -// forward=false (pull): stiffness/generic → F^{-1}, compliance → F^T -// Concentration types mix covariant and contravariant indices — not implemented. +// push: stiffness/generic -> F, compliance -> F^-T; pull: F^-1, F^T (concentration: not implemented) static arma::mat::fixed<3,3> tensor4_kernel(const arma::mat::fixed<3,3> &F, Tensor4Type type, bool forward) { switch (type) { @@ -108,20 +101,12 @@ static arma::mat::fixed<3,3> tensor4_kernel(const arma::mat::fixed<3,3> &F, return F; // unreachable } -// ============================================================================ -// Kelvin-Mandel convention -// ============================================================================ -// -// tensor4 stores its 6x6 in Mandel. Boundaries: engineering <-> Mandel per-type -// congruence eng_to_mandel/mandel_to_eng (tensor.hpp); Mandel <-> full-index via the -// type-free mandel_to_fastor4/fastor4_to_mandel (fastor_bridge.hpp). +// ==== Kelvin-Mandel: tensor4 stores Mandel (eng<->Mandel: tensor.hpp; Mandel<->full-index: fastor_bridge.hpp) ==== static const double SQ2 = std::sqrt(2.0); -// Mandel rotation operator: sigma_hat' = R6 * sigma_hat reproduces Q*sigma*Q^T for symmetric -// tensors. R6 is orthogonal, so one congruence R6 * X * R6^T rotates every Tensor4Type exactly -// (~3x cheaper than the full-index einsum route, no _fastor cache build). -// Entry: R6(I,J) = (c_I/c_J) * (Q_ik Q_jl + (1-delta_kl) Q_il Q_jk), c = (1,1,1,sqrt2,sqrt2,sqrt2). +// R6 is orthogonal: R6 X R6^T rotates every Tensor4Type exactly +// R6(I,J) = (c_I/c_J)(Q_ik Q_jl + (1-delta_kl) Q_il Q_jk), c = (1,1,1,sqrt2,sqrt2,sqrt2) static arma::mat::fixed<6,6> mandel_rotation(const arma::mat::fixed<3,3> &Q) { static const int pi[6] = {0,1,2,0,0,1}; // Voigt order [11,22,33,12,13,23] static const int pj[6] = {0,1,2,1,2,2}; @@ -139,9 +124,7 @@ static arma::mat::fixed<6,6> mandel_rotation(const arma::mat::fixed<3,3> &Q) { return R6; } -// Scalar 6x6 products for the Mandel congruences: arma Mat*Mat dispatches to BLAS dgemm, -// which at this size costs more than the flops and contends (Accelerate) when the batch -// loops run one call per item across threads. +// scalar 6x6 products: dgemm at this size costs more than the flops and contends across threads static arma::mat::fixed<6,6> mul66(const arma::mat::fixed<6,6> &A, const arma::mat::fixed<6,6> &B) { arma::mat::fixed<6,6> C; for (int j = 0; j < 6; ++j) @@ -174,9 +157,7 @@ static arma::mat::fixed<6,6> base_volumetric() { return m; } -// ============================================================================ -// tensor2 implementation -// ============================================================================ +// ==== tensor2 implementation ==== tensor2::tensor2() : _mat(arma::fill::zeros), _vtype(Tensor2Type::stress) {} @@ -534,9 +515,7 @@ tensor2 dev(const tensor2 &t) { double Mises(const tensor2 &t) { if (t.vtype() == Tensor2Type::none) throw std::runtime_error("Mises not defined for Tensor2Type::none"); - // From the 3x3 deviator (convention-free): stress -> sqrt(3/2 dev:dev), - // strain -> sqrt(2/3 dev:dev). accu(dev % dev) is the double contraction (off-diagonals - // counted twice via symmetry), so no engineering shear factors are needed. + // on the 3x3 deviator: accu(dev % dev) is the full contraction, no engineering shear factors arma::mat::fixed<3,3> d = t.mat(); double tr = (d(0,0) + d(1,1) + d(2,2)) / 3.0; d(0,0) -= tr; d(1,1) -= tr; d(2,2) -= tr; @@ -546,9 +525,7 @@ double Mises(const tensor2 &t) { return std::sqrt(1.5 * dd); } -// Gradient of a Drucker-Prager-type surface s_eq + coeff*tr(sigma): (3/2) dev(sigma)/s_eq + coeff*I. -// Deviator and s_eq are formed inline (no Voigt round-trip). s_eq <= iota is the cone vertex: -// the deviatoric normal is undefined there, so it is dropped and only the volumetric part remains. +// grad(s_eq + coeff tr sigma); at s_eq <= iota (cone vertex) only the volumetric part is kept static tensor2 dp_surface_normal(const tensor2 &sigma, double coeff) { arma::mat::fixed<3,3> n = sigma.mat(); double tr3 = (n(0,0) + n(1,1) + n(2,2)) / 3.0; @@ -596,9 +573,7 @@ tensor2 trans(const tensor2 &t) { return tensor2(arma::mat::fixed<3,3>(t.mat().t()), t.vtype()); } -// ============================================================================ -// tensor4 implementation -// ============================================================================ +// ==== tensor4 implementation ==== tensor4::tensor4() : _mandel(arma::fill::zeros), _type(Tensor4Type::stiffness) {} @@ -746,18 +721,14 @@ tensor4 tensor4::push_forward(const arma::mat::fixed<3,3> &F, CoRate rate, if (rate == CoRate::lie) return push_forward(F, metric); - // The B-correction recipe is defined on a Truesdell-rate Kirchhoff tangent - // (stress-stress, contravariant on all four indices). It does not apply to - // compliance or concentration tangents. + // the B-correction is defined on a Truesdell-rate stiffness tangent only if (_type != Tensor4Type::stiffness && _type != Tensor4Type::generic) { throw std::runtime_error( "tensor4::push_forward(F, CoRate, tau): corotational-rate " "correction is only defined for stiffness/generic tangents"); } - // Step 1: Truesdell push-forward (F-transport) WITHOUT metric, giving the - // Lie-rate Kirchhoff tangent. B-correction is applied at the Kirchhoff - // level before metric scaling. + // Truesdell push-forward without metric (Lie-rate Kirchhoff); B-correction before metric scaling auto F_fastor = arma_to_fastor2(F, false); _ensure_fastor(); Fastor::Tensor lie_full = push_forward_4(*_fastor, F_fastor); @@ -765,9 +736,7 @@ tensor4 tensor4::push_forward(const arma::mat::fixed<3,3> &F, CoRate rate, arma::mat Lt_v = fastor4_to_voigt(lie_full, _type); const arma::mat::fixed<3,3> tau_mat = tau.mat(); - // Step 2: Apply the corotational rate correction through the solver's own per-corate - // dispatch (Dtau_LieDD_2_DtauDe_corate), so a tangent built through this API is exactly the - // box tangent the solver integrates the stress against -- one kernel, no duplicated switch. + // corate correction through the solver's own Dtau_LieDD_2_DtauDe_corate: one kernel int corate_type = 2; switch (rate) { case CoRate::jaumann: corate_type = 0; break; @@ -797,9 +766,7 @@ tensor4 tensor4::push_forward(const arma::mat &F, CoRate rate, } tensor4 tensor4::rotate(const Rotation &R, bool active) const { - // One orthogonal rotation applied to all four indices, valid for every type: exact Mandel - // congruence R6 * X * R6^T (see mandel_rotation) -- no per-type Voigt kernel, no full-index - // einsum, no _fastor cache build. + // one Mandel congruence R6 X R6^T, valid for every type arma::mat::fixed<3,3> Q = R.as_matrix(); if (!active) Q = arma::mat::fixed<3,3>(Q.t()); const arma::mat::fixed<6,6> R6 = mandel_rotation(Q); @@ -807,10 +774,7 @@ tensor4 tensor4::rotate(const Rotation &R, bool active) const { } tensor4 tensor4::inverse() const { - // In Mandel the tensor inverse is a plain matrix inverse (no convention bookkeeping). - // Hand-rolled Gauss-Jordan with partial pivoting on the fixed 6x6: arma::inv on a - // dynamic mat goes through heap + LAPACK per call, which serializes the parallel - // batch loops (and costs more than the ~400 flops even in serial). + // Gauss-Jordan on the fixed 6x6: arma::inv (heap + LAPACK) serializes the parallel batch loops arma::mat::fixed<6,6> A = _mandel; arma::mat::fixed<6,6> Inv(arma::fill::eye); for (int col = 0; col < 6; ++col) { @@ -929,9 +893,7 @@ tensor4 auto_dyadic(const tensor2 &a) { return dyadic(a, a); } -// ============================================================================ -// Batch operations -// ============================================================================ +// ==== Batch operations ==== Tensor2Type infer_contraction_vtype(Tensor4Type t4type) { switch (t4type) { @@ -952,11 +914,8 @@ Tensor4Type infer_inverse_type(Tensor4Type t4type) { return t4type; } -// Batch loops below use simcoon_parallel_for_safe (parallel.hpp): exception-safe GCD/OpenMP, -// so a singular slice raises a catchable error instead of terminating the process. -// -// IMPORTANT: never call Cube::slice(i) inside these loops (see slice_view.hpp): these helpers -// copy through slice_memptr() into fixed-size matrices instead. +// simcoon_parallel_for_safe: a singular slice raises a catchable error +// never call Cube::slice(i) in these loops (slice_view.hpp): copy through slice_memptr() template static arma::mat::fixed slice_fixed(const arma::cube &c, arma::uword s) { @@ -972,7 +931,7 @@ static arma::vec::fixed<6> col_fixed6(const arma::mat &m, arma::uword j) { } arma::mat batch_rotate(const arma::mat &voigt, Tensor2Type vtype, - const arma::cube &rot_matrices, bool active) { + const arma::cube &rot_matrices, bool active, unsigned int n_threads) { int N = voigt.n_cols; check_batch_broadcast(rot_matrices.n_slices, N, "batch_rotate", "rot_matrices", "voigt.n_cols"); @@ -983,7 +942,7 @@ arma::mat batch_rotate(const arma::mat &voigt, Tensor2Type vtype, tensor2 t = tensor2::from_voigt(col_fixed6(voigt, i), vtype); Rotation R = Rotation::from_matrix(slice_fixed<3,3>(rot_matrices, broadcast ? 0 : i)); result.col(i) = t.rotate(R, active).voigt(); - }); + }, simcoon_parallel_cutoff, n_threads); return result; } @@ -1047,9 +1006,6 @@ arma::mat batch_contract(const arma::cube &t4, Tensor4Type t4type, int N4 = t4.n_slices; int N2 = t2.n_cols; int N = std::max(N4, N2); - // Both operands must be 1 (broadcast) or N (matched). Without these guards - // the general-path loop reads past the owned buffer when the two non- - // broadcast sizes disagree. check_batch_broadcast(N4, N, "batch_contract", "t4", "max(t4.n_slices,t2.n_cols)"); check_batch_broadcast(N2, N, "batch_contract", "t2", "max(t4.n_slices,t2.n_cols)"); bool bc4 = (N4 == 1); @@ -1075,7 +1031,7 @@ arma::mat batch_contract(const arma::cube &t4, Tensor4Type t4type, } arma::cube batch_rotate(const arma::cube &t4, Tensor4Type t4type, - const arma::cube &rot_matrices, bool active) { + const arma::cube &rot_matrices, bool active, unsigned int n_threads) { int N = t4.n_slices; check_batch_broadcast(rot_matrices.n_slices, N, "batch_rotate", "rot_matrices", "t4.n_slices"); @@ -1087,13 +1043,11 @@ arma::cube batch_rotate(const arma::cube &t4, Tensor4Type t4type, Rotation R = Rotation::from_matrix(slice_fixed<3,3>(rot_matrices, broadcast ? 0 : i)); const arma::mat::fixed<6,6> out = L.rotate(R, active).mat(); std::memcpy(result.slice_memptr(i), out.memptr(), 36*sizeof(double)); - }); + }, simcoon_parallel_cutoff, n_threads); return result; } -// Concentration tensors mix covariant/contravariant indices, so push_forward/pull_back are -// undefined for them (also rejected per-element in tensor4_kernel). The batch loops must reject -// BEFORE their OpenMP parallel region: an exception escaping an active parallel region is UB. +// concentration tensors have no push/pull; reject BEFORE the OpenMP region (an escaping exception is UB) static void reject_concentration_transport(Tensor4Type type, const char *op) { if (type == Tensor4Type::strain_concentration || type == Tensor4Type::stress_concentration) throw std::runtime_error(std::string("tensor4::") + op + " not implemented for " diff --git a/src/Continuum_mechanics/Homogenization/cylinder_multi.cpp b/src/Continuum_mechanics/Homogenization/cylinder_multi.cpp index d7376e9f2..d36fd6753 100755 --- a/src/Continuum_mechanics/Homogenization/cylinder_multi.cpp +++ b/src/Continuum_mechanics/Homogenization/cylinder_multi.cpp @@ -41,9 +41,6 @@ namespace simcoon{ //=====Public methods for cylinder_multi==================================== -/*! - \brief default constructor -*/ //------------------------------------------------------------- cylinder_multi::cylinder_multi() : phase_multi(), T_loc(6,6), T(6,6), A_loc(6,6), B_loc(6,6) @@ -53,9 +50,6 @@ cylinder_multi::cylinder_multi() : phase_multi(), T_loc(6,6), T(6,6), A_loc(6,6) } -/*! - \brief Constructor with parameters -*/ //------------------------------------------------------------- cylinder_multi::cylinder_multi(const mat &mA, const mat &mA_start, const mat &mB, const mat &mB_start, const vec &mA_in, const mat &mA_loc, const mat &mB_loc, const mat &mT_loc, const mat &mT) : phase_multi(mA, mA_start, mB, mB_start, mA_in), T_loc(6,6), T(6,6), A_loc(6,6), B_loc(6,6) @@ -68,10 +62,6 @@ cylinder_multi::cylinder_multi(const mat &mA, const mat &mA_start, const mat &mB B_loc = mB_loc; } -/*! - \brief Copy constructor - \param s cylinder_multi object to duplicate -*/ //------------------------------------------------------ cylinder_multi::cylinder_multi(const cylinder_multi& pc) : phase_multi(pc), T_loc(6,6), T(6,6), A_loc(6,6), B_loc(6,6) @@ -84,19 +74,11 @@ cylinder_multi::cylinder_multi(const cylinder_multi& pc) : phase_multi(pc), T_lo B_loc = pc.B_loc; } -/*! - \brief Destructor - - Deletes phase_multi (the arma::mat). -*/ //------------------------------------- cylinder_multi::~cylinder_multi() {} //------------------------------------- -/*! - \brief Standard operator = for phase_multi -*/ //---------------------------------------------------------------------- diff --git a/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp b/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp index f87f77cd7..8b1ac8dac 100755 --- a/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp +++ b/src/Continuum_mechanics/Homogenization/ellipsoid_multi.cpp @@ -24,9 +24,11 @@ #include #include #include +#include #include #include #include +#include #include #include #include @@ -37,23 +39,10 @@ using namespace arma; namespace simcoon{ -//Definition of the static variables -int ellipsoid_multi::mp; -int ellipsoid_multi::np; -// Never destroyed on purpose (see the header): the OS reclaims them at exit. -vec& ellipsoid_multi::x = *new vec(); -vec& ellipsoid_multi::wx = *new vec(); -vec& ellipsoid_multi::y = *new vec(); -vec& ellipsoid_multi::wy = *new vec(); - - //=====Private methods for ellipsoid_multi=================================== //=====Public methods for ellipsoid_multi==================================== -/*! - \brief default constructor -*/ //------------------------------------------------------------- ellipsoid_multi::ellipsoid_multi() : phase_multi(), S_loc(6,6), P_loc(6,6), T_loc(6,6), T(6,6), T_in_loc(6,6), T_in(6,6) @@ -63,9 +52,6 @@ ellipsoid_multi::ellipsoid_multi() : phase_multi(), S_loc(6,6), P_loc(6,6), T_lo } -/*! - \brief Constructor with parameters -*/ //------------------------------------------------------------- ellipsoid_multi::ellipsoid_multi(const mat &mA, const mat &mA_start, const mat &mB, const mat &mB_start, const vec &mA_in, const mat &mS_loc, const mat &mP_loc, const mat &mT_loc, const mat &mT, const mat &mT_in_loc, const mat &mT_in) : phase_multi(mA, mA_start, mB, mB_start, mA_in), S_loc(6,6), P_loc(6,6), T_loc(6,6), T(6,6), T_in_loc(6,6), T_in(6,6) @@ -79,10 +65,6 @@ ellipsoid_multi::ellipsoid_multi(const mat &mA, const mat &mA_start, const mat & T_in = mT_in; } -/*! - \brief Copy constructor - \param s ellipsoid_multi object to duplicate -*/ //------------------------------------------------------ ellipsoid_multi::ellipsoid_multi(const ellipsoid_multi& pc) : phase_multi(pc), S_loc(6,6), P_loc(6,6), T_loc(6,6), T(6,6), T_in_loc(6,6), T_in(6,6) @@ -96,19 +78,11 @@ ellipsoid_multi::ellipsoid_multi(const ellipsoid_multi& pc) : phase_multi(pc), S T_in = pc.T_in; } -/*! - \brief Destructor - - Deletes phase_multi (the arma::mat). -*/ //------------------------------------- ellipsoid_multi::~ellipsoid_multi() {} //------------------------------------- -/*! - \brief Standard operator = for phase_multi -*/ //------------------------------------- const mat* eshelby_memo::find(const mat &Lt_local, const double &a1, const double &a2, const double &a3) const @@ -138,9 +112,43 @@ void eshelby_memo::clear() entries.clear(); } -// Eshelby tensor of ell in the medium Lt_local (inclusion frame): the inclusion's own last -// tensor when the medium has not changed, else the pass memo when given, else integrated. -mat ellipsoid_multi::eshelby_memoized(eshelby_memo *memo, const mat &Lt_local, const ellipsoid &ell) +namespace { + +// per-thread ring of 32 integrated tensors, exact key: outlives the phase objects +struct eshelby_cache_entry { + arma::mat::fixed<6,6> Lt; + double a1, a2, a3; + const eshelby_quadrature *quad; + arma::mat::fixed<6,6> S; +}; +const std::size_t eshelby_cache_size = 32; +thread_local std::vector eshelby_cache; +thread_local std::size_t eshelby_cache_next = 0; + +const arma::mat::fixed<6,6> *eshelby_cache_find(const mat &Lt_local, const ellipsoid &ell, const eshelby_quadrature &quad) +{ + for (const eshelby_cache_entry &e : eshelby_cache) { + if (e.quad == &quad && e.a1 == ell.a1 && e.a2 == ell.a2 && e.a3 == ell.a3 + && std::memcmp(e.Lt.memptr(), Lt_local.memptr(), 36 * sizeof(double)) == 0) + return &e.S; + } + return nullptr; +} + +void eshelby_cache_store(const mat &Lt_local, const ellipsoid &ell, const eshelby_quadrature &quad, const mat &S) +{ + eshelby_cache_entry e{arma::mat::fixed<6,6>(Lt_local), ell.a1, ell.a2, ell.a3, &quad, arma::mat::fixed<6,6>(S)}; + if (eshelby_cache.size() < eshelby_cache_size) { + eshelby_cache.push_back(e); + } else { + eshelby_cache[eshelby_cache_next] = e; + eshelby_cache_next = (eshelby_cache_next + 1) % eshelby_cache_size; + } +} + +} // namespace + +mat ellipsoid_multi::eshelby_memoized(eshelby_memo *memo, const mat &Lt_local, const ellipsoid &ell, const eshelby_quadrature &quad) { if (const mat *own = S_loc_memo.find(Lt_local, ell.a1, ell.a2, ell.a3)) return *own; @@ -149,7 +157,14 @@ mat ellipsoid_multi::eshelby_memoized(eshelby_memo *memo, const mat &Lt_local, c if (hit != nullptr) { S = *hit; } else { - S = Eshelby(Lt_local, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); + if (quad.closed_form && Eshelby_closed_form(Lt_local, ell.a1, ell.a2, ell.a3, S)) { + //isotropic medium, sphere or spheroid: no integration + } else if (const arma::mat::fixed<6,6> *cached = eshelby_cache_find(Lt_local, ell, quad)) { + S = *cached; + } else { + S = Eshelby(Lt_local, ell.a1, ell.a2, ell.a3, quad.x, quad.wx, quad.y, quad.wy, quad.mp, quad.np); + eshelby_cache_store(Lt_local, ell, quad, S); + } if (memo != nullptr) memo->store(Lt_local, ell.a1, ell.a2, ell.a3, S); } S_loc_memo.clear(); // one entry: the current medium @@ -158,33 +173,47 @@ mat ellipsoid_multi::eshelby_memoized(eshelby_memo *memo, const mat &Lt_local, c } //------------------------------------- -void ellipsoid_multi::fillS_loc(const mat& Lt_m, const ellipsoid &ell) +void ellipsoid_multi::fillS_loc(const mat& Lt_m, const ellipsoid &ell, const eshelby_quadrature &quad) //------------------------------------- { Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); mat Ltm_local_geom = rot.apply_stiffness(Lt_m, false); - S_loc = Eshelby(Ltm_local_geom, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); + if (!(quad.closed_form && Eshelby_closed_form(Ltm_local_geom, ell.a1, ell.a2, ell.a3, S_loc))) + S_loc = Eshelby(Ltm_local_geom, ell.a1, ell.a2, ell.a3, quad.x, quad.wx, quad.y, quad.wy, quad.mp, quad.np); } //------------------------------------- -void ellipsoid_multi::fillP_loc(const mat& Lt_m, const ellipsoid &ell) +void ellipsoid_multi::fillP_loc(const mat& Lt_m, const ellipsoid &ell, const eshelby_quadrature &quad) //------------------------------------- { Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); mat Ltm_local_geom = rot.apply_stiffness(Lt_m, false); - P_loc = T_II(Ltm_local_geom, ell.a1, ell.a2, ell.a3, x, wx, y, wy, mp, np); + P_loc = T_II(Ltm_local_geom, ell.a1, ell.a2, ell.a3, quad.x, quad.wx, quad.y, quad.wy, quad.mp, quad.np); } //------------------------------------- -void ellipsoid_multi::fillT(const mat& Lt_m, const mat& Lt, const ellipsoid &ell, eshelby_memo *memo) -//This method correspond to the classical Eshelby method -//------------------------------------- +void ellipsoid_multi::fillT(const mat& Lt_m, const mat& Lt, const ellipsoid &ell, const eshelby_quadrature &quad, eshelby_memo *memo) { - Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); - mat Lt_m_local_geom = rot.apply_stiffness(Lt_m, false); - S_loc = eshelby_memoized(memo, Lt_m_local_geom, ell); - mat Lt_local_geom = rot.apply_stiffness(Lt, false); + const double geometry[6] = {ell.a1, ell.a2, ell.a3, ell.psi_geom, ell.theta_geom, ell.phi_geom}; + if (T_memo.valid && T_memo.quad == &quad + && std::memcmp(T_memo.geometry, geometry, sizeof(geometry)) == 0 + && std::memcmp(T_memo.Lt_m.memptr(), Lt_m.memptr(), 36 * sizeof(double)) == 0 + && std::memcmp(T_memo.Lt.memptr(), Lt.memptr(), 36 * sizeof(double)) == 0) { + S_loc = T_memo.S_loc; + T_loc = T_memo.T_loc; + T = T_memo.T; + return; + } + + //A geometry frame that is the global one needs no rotation (from_euler(0,0,0) is the identity) + const bool global_frame = (ell.psi_geom == 0. && ell.theta_geom == 0. && ell.phi_geom == 0.); + Rotation rot = global_frame ? Rotation::identity() : Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); + //An isotropic medium is not rotated either: the rotation would only add round-off, and the + //same medium in every frame shares one Eshelby tensor between orientations (memo, cache) + mat Lt_m_local_geom = (global_frame || is_isotropic(Lt_m, Tensor4Type::stiffness)) ? Lt_m : rot.apply_stiffness(Lt_m, false); + S_loc = eshelby_memoized(memo, Lt_m_local_geom, ell, quad); + mat Lt_local_geom = global_frame ? Lt : rot.apply_stiffness(Lt, false); try { T_loc = inv(eye(6,6) + S_loc*inv(Lt_m_local_geom)*(Lt_local_geom - Lt_m_local_geom)); @@ -192,17 +221,24 @@ void ellipsoid_multi::fillT(const mat& Lt_m, const mat& Lt, const ellipsoid &ell cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside ellipsoid_multi::fillT."); } - T = rot.apply_strain_concentration(T_loc, true); + T = global_frame ? T_loc : rot.apply_strain_concentration(T_loc, true); + + T_memo.Lt_m = Lt_m; + T_memo.Lt = Lt; + std::memcpy(T_memo.geometry, geometry, sizeof(geometry)); + T_memo.quad = &quad; + T_memo.S_loc = S_loc; + T_memo.T_loc = T_loc; + T_memo.T = T; + T_memo.valid = true; } //------------------------------------- -void ellipsoid_multi::fillT_iso(const mat& Lt_m, const mat& Lt, const ellipsoid &ell, eshelby_memo *memo) -//This method corresponf to the isotropization method -//------------------------------------- +void ellipsoid_multi::fillT_iso(const mat& Lt_m, const mat& Lt, const ellipsoid &ell, const eshelby_quadrature &quad, eshelby_memo *memo) { Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); mat Lt_m_iso = Isotropize(Lt_m); - S_loc = eshelby_memoized(memo, Lt_m_iso, ell); + S_loc = eshelby_memoized(memo, Lt_m_iso, ell, quad); mat Lt_local_geom = rot.apply_stiffness(Lt, false); try { @@ -216,14 +252,11 @@ void ellipsoid_multi::fillT_iso(const mat& Lt_m, const mat& Lt, const ellipsoid } //------------------------------------- -void ellipsoid_multi::fillT_mec_in(const mat& L_m, const mat& L, const ellipsoid &ell, eshelby_memo *memo) -//This method corresponds to the Eshelby inhomogeneous inhomogeneity - inelasticity problem. It calculates -//the interaction tensors T for the elastic and the inelastic part. -//------------------------------------- +void ellipsoid_multi::fillT_mec_in(const mat& L_m, const mat& L, const ellipsoid &ell, const eshelby_quadrature &quad, eshelby_memo *memo) { Rotation rot = Rotation::from_euler(ell.psi_geom, ell.theta_geom, ell.phi_geom, "zxz"); mat L_m_local_geom = rot.apply_stiffness(L_m, false); - S_loc = eshelby_memoized(memo, L_m_local_geom, ell); + S_loc = eshelby_memoized(memo, L_m_local_geom, ell, quad); mat L_local_geom = rot.apply_stiffness(L, false); try { diff --git a/src/Continuum_mechanics/Homogenization/eshelby.cpp b/src/Continuum_mechanics/Homogenization/eshelby.cpp index 3bed46a83..8572eac34 100755 --- a/src/Continuum_mechanics/Homogenization/eshelby.cpp +++ b/src/Continuum_mechanics/Homogenization/eshelby.cpp @@ -21,6 +21,13 @@ ///@version 1.0 #include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -121,14 +128,7 @@ mat Eshelby_penny(const double &nu) { mat S = zeros(6,6); - // Limit values as ar -> 0 (penny-shaped crack / flat disc) - // S_1111 -> 1 - // S_1122 = S_1133 -> nu/(1-nu) - // S_2211 = S_3311 -> 0 - // S_2222 = S_3333 -> 0 - // S_2233 = S_3322 -> 0 - // S_2323 -> 0 - // S_1212 = S_1313 -> 1/2 + // ar -> 0 limits: S_1111 = 1, S_1122 = S_1133 = nu/(1-nu), S_1212 = S_1313 = 1/2, all others 0 S(0,0) = 1.; S(0,1) = nu/(1.-nu); S(0,2) = nu/(1.-nu); @@ -138,14 +138,13 @@ mat Eshelby_penny(const double &nu) { S(2,0) = 0.; S(2,1) = 0.; S(2,2) = 0.; - S(3,3) = 0.; - S(4,4) = 2.*(1./2.); // S_1313 in Voigt: factor 2 for shear - S(5,5) = 2.*(1./2.); // S_1212 in Voigt: factor 2 for shear + S(3,3) = 2.*(1./2.); // S_1212 (Voigt index 3 = 12), factor 2 for the engineering shear + S(4,4) = 2.*(1./2.); // S_1313 (index 4 = 13) + S(5,5) = 0.; // S_2323 (index 5 = 23): in-plane shear of the crack, unconstrained return S; } -//This methods is using the Voigt notations for the tensors. void calG(const double &pt, const double &a1, const double &a2, const double &a3, const double &x3, const Mat &Id, const mat &Lt, mat &G) { @@ -200,35 +199,97 @@ void calG(const double &pt, const double &a1, const double &a2, const double &a3 } -void Gauss(Mat &Id, const mat &Lt, mat &G, const double &a1, const double &a2, const double &a3, const vec &x, const vec &wx, const vec &y, const vec &wy, const int &mp, const int &np) // To check +//Voigt index of the pair (i,j): the table that Eshelby and T_II hand to Gauss as Id +static const int Voigt_index[3][3] = {{0,3,4},{3,1,5},{4,5,2}}; + +// sum of calG in the per-entry operation order of the point loop (bitwise); sin/cos hoisted +void Gauss(Mat &Id, const mat &Lt, mat &G, const double &a1, const double &a2, const double &a3, const vec &x, const vec &wx, const vec &y, const vec &wy, const int &mp, const int &np) { - double x3 = 0.; - double pt=0.; - mat H = zeros(6,6); - mat rg = zeros(6,6); - - for (int l=0; l cos_y(np), sin_y(np); + for (int i=0; i ring(9*np); + double *X[3] = {&ring[0], &ring[np], &ring[2*np]}; + double *R00 = &ring[3*np], *R01 = &ring[4*np], *R02 = &ring[5*np]; + double *R11 = &ring[6*np], *R12 = &ring[7*np], *R22 = &ring[8*np]; + const double *R[3][3] = {{R00, R01, R02}, {nullptr, R11, R12}, {nullptr, nullptr, R22}}; + + for (int l=0; lmp_asked == mp && last->np == np && last->rule == rule && last->closed_form == closed_form) + return *last; + + // never destroyed: no arma destructor at DLL unload (Windows) + typedef std::tuple key_type; + static std::mutex &tables_mutex = *new std::mutex(); + static std::map &tables = *new std::map(); + std::lock_guard lock(tables_mutex); + const key_type key(mp, np, rule, closed_form); + auto it = tables.find(key); + if (it == tables.end()) { + it = tables.emplace(key, new eshelby_quadrature(mp, np, rule, closed_form)).first; + } + last = it->second; + return *last; +} + +bool Eshelby_closed_form(const mat &L, const double &a1, const double &a2, const double &a3, mat &S) +{ + if (a2 != a3) + return false; + const double ar = a1/a2; + if (ar != 1. && fabs(ar - 1.) < 1.E-2) + return false; + const double lambda = L(0,1); + const double mu = L(3,3); + const double tol = 1.E-10*abs(L).max(); + for (int j=0; j<6; j++) { + for (int i=0; i<6; i++) { + double L_iso_ij = 0.; + if (i < 3 && j < 3) + L_iso_ij = (i == j) ? lambda + 2.*mu : lambda; + else if (i == j) + L_iso_ij = mu; + if (!(fabs(L(i,j) - L_iso_ij) <= tol)) + return false; + } + } + const double nu = lambda/(2.*(lambda + mu)); + if (ar == 1.) + S = Eshelby_sphere(nu); + else if (ar > 1.) + S = Eshelby_prolate(nu, ar); + else + S = Eshelby_oblate(nu, ar); + return true; } } //namespace simcoon diff --git a/src/Continuum_mechanics/Homogenization/layer_multi.cpp b/src/Continuum_mechanics/Homogenization/layer_multi.cpp index 40acb9a0c..020ca83c7 100755 --- a/src/Continuum_mechanics/Homogenization/layer_multi.cpp +++ b/src/Continuum_mechanics/Homogenization/layer_multi.cpp @@ -35,9 +35,6 @@ namespace simcoon{ //=====Public methods for layer_multi==================================== -/*! - \brief default constructor -*/ //------------------------------------------------------------- layer_multi::layer_multi() : phase_multi(), Dnn(3,3), Dnt(3,3), dXn(3,3), dXt(3,3), sigma_hat(3), dzdx1(3) @@ -46,9 +43,6 @@ layer_multi::layer_multi() : phase_multi(), Dnn(3,3), Dnt(3,3), dXn(3,3), dXt(3, } -/*! - \brief Constructor with parameters -*/ //------------------------------------------------------------- layer_multi::layer_multi(const mat &mA, const mat &mA_start, const mat &mB, const mat &mB_start, const vec &mA_in, const mat &mDnn, const mat &mDnt, const mat &mdXn, const mat &mdXt, const vec &msigma_hat, const vec &mdzdx1) : phase_multi(mA, mA_start, mB, mB_start, mA_in), Dnn(3,3), Dnt(3,3), dXn(3,3), dXt(3,3), sigma_hat(3), dzdx1(3) @@ -62,10 +56,6 @@ layer_multi::layer_multi(const mat &mA, const mat &mA_start, const mat &mB, cons dzdx1 = mdzdx1; } -/*! - \brief Copy constructor - \param s phase_multi object to duplicate -*/ //------------------------------------------------------ layer_multi::layer_multi(const layer_multi& pc) : phase_multi(pc) @@ -79,19 +69,11 @@ layer_multi::layer_multi(const layer_multi& pc) : phase_multi(pc) dzdx1 = pc.dzdx1; } -/*! - \brief Destructor - - Deletes phase_multi (the arma::mat). -*/ //------------------------------------- layer_multi::~layer_multi() {} //------------------------------------- -/*! - \brief Standard operator = for phase_multi -*/ //---------------------------------------------------------------------- layer_multi& layer_multi::operator = (const layer_multi& pc) diff --git a/src/Continuum_mechanics/Homogenization/phase_multi.cpp b/src/Continuum_mechanics/Homogenization/phase_multi.cpp index 0efb25511..468797734 100755 --- a/src/Continuum_mechanics/Homogenization/phase_multi.cpp +++ b/src/Continuum_mechanics/Homogenization/phase_multi.cpp @@ -35,9 +35,6 @@ namespace simcoon{ //=====Public methods for phase_multi==================================== -/*! - \brief default constructor -*/ //------------------------------------------------------------- phase_multi::phase_multi() : A(6,6), A_start(6,6), B(6,6), B_start(6,6), A_in(6) @@ -46,9 +43,6 @@ phase_multi::phase_multi() : A(6,6), A_start(6,6), B(6,6), B_start(6,6), A_in(6) } -/*! - \brief Constructor with parameters -*/ //------------------------------------------------------------- phase_multi::phase_multi(const mat &mA, const mat &mA_start, const mat &mB, const mat &mB_start, const vec &mA_in) : A(6,6), A_start(6,6), B(6,6), B_start(6,6), A_in(6) @@ -63,10 +57,6 @@ phase_multi::phase_multi(const mat &mA, const mat &mA_start, const mat &mB, cons A_in = mA_in; } -/*! - \brief Copy constructor - \param s phase_multi object to duplicate -*/ //------------------------------------------------------ phase_multi::phase_multi(const phase_multi& pc) : A(6,6), A_start(6,6), B(6,6), B_start(6,6), A_in(6) @@ -81,19 +71,11 @@ phase_multi::phase_multi(const phase_multi& pc) : A(6,6), A_start(6,6), B(6,6), A_in = pc.A_in; } -/*! - \brief Destructor - - Deletes phase_multi (the arma::mat). -*/ //------------------------------------- phase_multi::~phase_multi() {} //------------------------------------- -/*! - \brief Standard operator = for phase_multi -*/ //------------------------------------------------------------- void phase_multi::to_start() diff --git a/src/Continuum_mechanics/Material/variant.cpp b/src/Continuum_mechanics/Material/variant.cpp index d4c87678d..c933f1c9a 100755 --- a/src/Continuum_mechanics/Material/variant.cpp +++ b/src/Continuum_mechanics/Material/variant.cpp @@ -35,10 +35,6 @@ namespace simcoon{ //=====Public methods for variant==================================== -/*! - \brief default constructor -*/ - //------------------------------------------------------------- variant::variant() : n(3), m(3), R(3,3), ETn(6) //------------------------------------------------------------- @@ -52,12 +48,6 @@ variant::variant() : n(3), m(3), R(3,3), ETn(6) } -/*! - \brief Constructor with parameters - \n\n - \f$ \textbf{Examples :} \f$ \n -*/ - //------------------------------------------------------------- variant::variant(vec mn, vec mm, double mg) : n(3), m(3), R(3,3), ETn(6) //------------------------------------------------------------- @@ -72,11 +62,6 @@ variant::variant(vec mn, vec mm, double mg) : n(3), m(3), R(3,3), ETn(6) ETn = g*Schmid_v(n, m); } -/*! - \brief Copy constructor - \param s variant object to duplicate -*/ - //------------------------------------------------------ variant::variant(const variant& sv) : n(3), m(3), R(3,3), ETn(6) //------------------------------------------------------ @@ -88,12 +73,6 @@ variant::variant(const variant& sv) : n(3), m(3), R(3,3), ETn(6) ETn = sv.ETn; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ - //------------------------------------- variant::~variant() //------------------------------------- @@ -101,10 +80,6 @@ variant::~variant() } -/*! - \brief Standard operator = for variant -*/ - //------------------------------------------------------------- void variant::build(const double& g) //------------------------------------------------------------- @@ -113,6 +88,7 @@ void variant::build(const double& g) assert (m.size() == 3); assert (g > 0.); + this->g = g; R = Schmid(n, m); ETn = g*Schmid_v(n, m); } diff --git a/src/Continuum_mechanics/Micromechanics/multiphase.cpp b/src/Continuum_mechanics/Micromechanics/multiphase.cpp index d615da410..1792eeff5 100755 --- a/src/Continuum_mechanics/Micromechanics/multiphase.cpp +++ b/src/Continuum_mechanics/Micromechanics/multiphase.cpp @@ -26,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -56,6 +57,21 @@ int self_consistent_start(const phase_characteristics &phase) { return (props.n_elem > 3) ? static_cast(props(3)) : 1; } +mean_field_options mean_field_options_of(const phase_characteristics &phase) { + const std::string &name = phase.sptr_matprops->umat_name; + const vec &props = phase.sptr_matprops->props; + mean_field_options opt; + //first option slot: after n_matrix (MIMTN), after start (MISCN) + const uword first = (name == "MIMTN") ? 3 : ((name == "MISCN") ? 4 : props.n_elem); + if (props.n_elem > first) opt.closed_form = static_cast(props(first)); + if (props.n_elem > first + 1) opt.rule = static_cast(props(first + 1)); + if (name == "MISCN") { + if (props.n_elem > first + 2) opt.anderson = static_cast(props(first + 2)); + if (props.n_elem > first + 3) opt.rtol = props(first + 3); + } + return opt; +} + void check_sub_phases(const phase_characteristics &phase) { const std::string &name = phase.sptr_matprops->umat_name; const vec &props = phase.sptr_matprops->props; @@ -64,15 +80,33 @@ void check_sub_phases(const phase_characteristics &phase) { throw std::invalid_argument(name + " needs its sub-phases: pass them (phases=) to the solver or to L_eff."); } - //The props are the scheme's settings only. An exact length turns the pre-2.0 layout - //[nphases, file number, mp, np, n_matrix] into an error instead of a misread n_matrix. + //props = the scheme's settings [mp, np, n_matrix, ...] + mean_field_options; the length check + //turns the pre-2.0 layout [nphases, file number, mp, np, n_matrix] into an error const bool self_consistent = (name == "MISCN"); const uword n_min = (name == "MIPLN") ? 0 : ((name == "MIHEN") ? 2 : 3); - const uword n_max = self_consistent ? 4 : n_min; + const uword n_max = self_consistent ? 8 : ((name == "MIMTN") ? 5 : n_min); if (props.n_elem < n_min || props.n_elem > n_max) { throw std::invalid_argument(name + " takes props = " + std::string(name == "MIPLN" ? "[]" : (name == "MIHEN" ? "[mp, np]" - : (self_consistent ? "[mp, np, n_matrix] or [mp, np, n_matrix, start]" : "[mp, np, n_matrix]"))) - + ", got " + std::to_string(props.n_elem) + " values (the leading [nphases, file number] slots no longer exist)"); + : (self_consistent ? "[mp, np, n_matrix, (start, closed_form, rule, anderson, rtol)]" : "[mp, np, n_matrix, (closed_form, rule)]"))) + + " (trailing slots optional), got " + std::to_string(props.n_elem) + " values (the leading [nphases, file number] slots no longer exist)"); + } + //options: comparisons written so that NaN fails them + const uword first_option = self_consistent ? 4 : 3; + for (uword k = first_option; k < props.n_elem; k++) { + const uword slot = k - first_option; + const double v = props(k); + if (slot <= 1 && !(v == 0. || v == 1.)) { + throw std::invalid_argument(name + ": " + (slot == 0 ? "closed_form" : "rule") + " (props[" + std::to_string(k) + + "]) must be 0 or 1, got " + std::to_string(v) + + " (the leading [nphases, file number] slots of the pre-2.0 layout no longer exist)"); + } + if (slot == 2 && !(v >= 0. && v <= 10. && v == std::floor(v))) { + throw std::invalid_argument(name + ": anderson (props[" + std::to_string(k) + "]), the depth of the Anderson " + "acceleration, must be an integer in [0, 10], got " + std::to_string(v)); + } + if (slot == 3 && !(v >= 0. && v < 1.)) { + throw std::invalid_argument(name + ": rtol (props[" + std::to_string(k) + "]) must be in [0, 1), got " + std::to_string(v)); + } } //comparisons written so that NaN fails them; the casts to int come after if (n_min >= 2 && !(props(0) >= 1. && props(0) <= 10000. && props(1) >= 1. && props(1) <= 10000.)) { @@ -107,27 +141,14 @@ void umat_multi(phase_characteristics &phase, const mat &DR, const double &Time, shared_ptr umat_phase_M = std::dynamic_pointer_cast(phase.sptr_sv_local); //shared_ptr on state variables of the rve shared_ptr umat_sub_phases_M; //shared_ptr on state variables - //1 - Quadrature points of the Eshelby integrals (ellipsoidal schemes only) - if (start && (sub_phase_shape(phase.sptr_matprops->umat_name) == 2)) { - ellipsoid_multi::mp = phase.sptr_matprops->props(0); - ellipsoid_multi::np = phase.sptr_matprops->props(1); - ellipsoid_multi::x.set_size(ellipsoid_multi::mp); - ellipsoid_multi::wx.set_size(ellipsoid_multi::mp); - ellipsoid_multi::y.set_size(ellipsoid_multi::np); - ellipsoid_multi::wy.set_size(ellipsoid_multi::np); - points(ellipsoid_multi::x, ellipsoid_multi::wx, ellipsoid_multi::y, ellipsoid_multi::wy,ellipsoid_multi::mp, ellipsoid_multi::np); - } - + // Lt cleared by the solver before a block's priming call: reseed it, do not invert it + const bool sc_seed = start || umat_phase_M->Lt.is_zero(); + //Initialization if (start) { for (int i=0; itangent_mode = simcoon::tangent_continuum; //Run the appropriate constitutive model select_umat_M(phase.sub_phases[i], DR, Time, DTime, ndi, nshr, start, solver_type, tnew_dt); @@ -161,13 +182,15 @@ void umat_multi(phase_characteristics &phase, const mat &DR, const double &Time, break; } case 102: { + //Isotropized Mori-Tanaka (Doghri & Ouaar): no UMAT name maps to code 102 yet + //(umat_smart), so this scheme is not reachable from the solver nor from L_eff int n_matrix = phase.sptr_matprops->props(2); DE_Mori_Tanaka_iso(phase, n_matrix); break; } case 103: { int n_matrix = phase.sptr_matprops->props(2); - DE_Self_Consistent(phase, n_matrix, start, self_consistent_start(phase)); + DE_Self_Consistent(phase, n_matrix, sc_seed, self_consistent_start(phase)); break; } case 104: { @@ -180,13 +203,9 @@ void umat_multi(phase_characteristics &phase, const mat &DR, const double &Time, for (unsigned int i=0; ito_start(); - //Theta method for the tangent modulus //mat Lt_start = umat_sub_phases_M->Lt select_umat_M(phase.sub_phases[i], DR, Time, DTime, ndi, nshr, start, solver_type, tnew_dt); - //Theta method for the tangent modulus - //umat_sub_phases_M = std::dynamic_pointer_cast(r.sptr_sv_global); - //Lt* = (1 - (2./3.))*Lt_start + 2./3.*Lt; } error = 0.; @@ -217,7 +236,7 @@ void umat_multi(phase_characteristics &phase, const mat &DR, const double &Time, } case 103: { int n_matrix = phase.sptr_matprops->props(2); - Lt_Self_Consistent(phase, n_matrix, start, self_consistent_start(phase)); + Lt_Self_Consistent(phase, n_matrix, sc_seed, self_consistent_start(phase)); break; } case 104: { diff --git a/src/Continuum_mechanics/Micromechanics/schemes.cpp b/src/Continuum_mechanics/Micromechanics/schemes.cpp index 7978d0cdd..d77b4466f 100755 --- a/src/Continuum_mechanics/Micromechanics/schemes.cpp +++ b/src/Continuum_mechanics/Micromechanics/schemes.cpp @@ -40,11 +40,19 @@ #include // #include #include +#include using namespace std; using namespace arma; namespace simcoon{ + +// read from the phase at each pass: nested schemes and concurrent solves keep their own +static const eshelby_quadrature &scheme_quadrature(const phase_characteristics &phase) { + const mean_field_options opt = mean_field_options_of(phase); + return eshelby_quadrature_points(static_cast(phase.sptr_matprops->props(0)), static_cast(phase.sptr_matprops->props(1)), + opt.rule, opt.closed_form == 1); +} void Lt_Homogeneous_E(phase_characteristics &phase) { @@ -61,6 +69,7 @@ void DE_Homogeneous_E(phase_characteristics &phase) { //Compute the strain concentration tensor A for(auto r : phase.sub_phases) { + sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); r.sptr_multi->A = eye(6,6); sv_r->DEtot = r.sptr_multi->A*sv_eff->DEtot; //Recall that the global coordinates of subphases is the local coordinates of the generic phase } @@ -81,6 +90,7 @@ void Lt_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; + const eshelby_quadrature &quad = scheme_quadrature(phase); for(auto r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); @@ -91,7 +101,7 @@ void Lt_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { if (r.sptr_matprops->number == n_matrix) elli_multi->T = eye(6,6); else - elli_multi->fillT(sv_0->Lt, sv_r->Lt, *elli, &memo); + elli_multi->fillT(sv_0->Lt, sv_r->Lt, *elli, quad, &memo); //Compute the normalization interaction tensir sumT sumT += elli->concentration*elli_multi->T; @@ -126,6 +136,7 @@ void Lt_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; + const eshelby_quadrature &quad = scheme_quadrature(phase); for(auto r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); @@ -136,7 +147,7 @@ void Lt_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { if (r.sptr_matprops->number == n_matrix) elli_multi->T = eye(6,6); else - elli_multi->fillT_iso(sv_0->Lt, sv_r->Lt, *elli, &memo); + elli_multi->fillT_iso(sv_0->Lt, sv_r->Lt, *elli, quad, &memo); //Compute the normalization interaction tensir sumT sumT += elli->concentration*elli_multi->T; @@ -171,6 +182,7 @@ void DE_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; + const eshelby_quadrature &quad = scheme_quadrature(phase); for(auto r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); @@ -180,7 +192,7 @@ void DE_Mori_Tanaka(phase_characteristics &phase, const int &n_matrix) { if (r.sptr_matprops->number == n_matrix) elli_multi->T = eye(6,6); else - elli_multi->fillT(sv_0->Lt, sv_r->Lt, *elli, &memo); + elli_multi->fillT(sv_0->Lt, sv_r->Lt, *elli, quad, &memo); //Compute the normalization interaction tensir sumT sumT += elli->concentration*elli_multi->T; @@ -218,6 +230,7 @@ void DE_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; + const eshelby_quadrature &quad = scheme_quadrature(phase); for(auto r : phase.sub_phases) { elli_multi = std::dynamic_pointer_cast(r.sptr_multi); elli = std::dynamic_pointer_cast(r.sptr_shape); @@ -227,7 +240,7 @@ void DE_Mori_Tanaka_iso(phase_characteristics &phase, const int &n_matrix) { if (r.sptr_matprops->number == n_matrix) elli_multi->T = eye(6,6); else - elli_multi->fillT_iso(sv_0->Lt, sv_r->Lt, *elli, &memo); + elli_multi->fillT_iso(sv_0->Lt, sv_r->Lt, *elli, quad, &memo); //Compute the normalization interaction tensir sumT sumT += elli->concentration*elli_multi->T; @@ -284,6 +297,7 @@ void Lt_Self_Consistent(phase_characteristics &phase, const int &n_matrix, const //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; + const eshelby_quadrature &quad = scheme_quadrature(phase); for(unsigned int i=0; i(phase.sub_phases[i].sptr_multi); elli = std::dynamic_pointer_cast(phase.sub_phases[i].sptr_shape); @@ -293,7 +307,7 @@ void Lt_Self_Consistent(phase_characteristics &phase, const int &n_matrix, const if (phase.sub_phases[i].sptr_matprops->number == n_matrix) elli_multi->T = eye(6,6); else { - elli_multi->fillT(sv_eff->Lt, sv_r->Lt, *elli, &memo); + elli_multi->fillT(sv_eff->Lt, sv_r->Lt, *elli, quad, &memo); sumA += elli->concentration*elli_multi->T; } @@ -344,6 +358,7 @@ void DE_Self_Consistent(phase_characteristics &phase, const int &n_matrix, const //Compute the Eshelby tensor and the interaction tensor for each phase //(one Eshelby integration per distinct inclusion geometry in this pass: eshelby_memo) eshelby_memo memo; + const eshelby_quadrature &quad = scheme_quadrature(phase); for(unsigned int i=0; i(phase.sub_phases[i].sptr_multi); elli = std::dynamic_pointer_cast(phase.sub_phases[i].sptr_shape); @@ -353,7 +368,7 @@ void DE_Self_Consistent(phase_characteristics &phase, const int &n_matrix, const if (phase.sub_phases[i].sptr_matprops->number == n_matrix) elli_multi->T = eye(6,6); else { - elli_multi->fillT(sv_eff->Lt, sv_r->Lt, *elli, &memo); + elli_multi->fillT(sv_eff->Lt, sv_r->Lt, *elli, quad, &memo); sumA += elli->concentration*elli_multi->T; } @@ -421,8 +436,9 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { for(auto r : phase.sub_phases) { lay_multi = std::dynamic_pointer_cast(r.sptr_multi); lay = std::dynamic_pointer_cast(r.sptr_shape); - sumDnn += lay->concentration*inv(lay_multi->Dnn); - sumcDsig += lay->concentration*inv(lay_multi->Dnn)*lay_multi->sigma_hat; + const mat inv_Dnn = inv(lay_multi->Dnn); + sumDnn += lay->concentration*inv_Dnn; + sumcDsig += lay->concentration*inv_Dnn*lay_multi->sigma_hat; } } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; @@ -435,20 +451,8 @@ void dE_Periodic_Layer(phase_characteristics &phase, const int &nbiter) { } catch (const std::runtime_error &e) { cerr << "Error in inv: " << e.what() << endl; throw simcoon::exception_inv("Error in inv function inside dE_Periodic_Layer."); - } - - try { - for(auto r : phase.sub_phases) { - lay_multi = std::dynamic_pointer_cast(r.sptr_multi); - lay = std::dynamic_pointer_cast(r.sptr_shape); - sumDnn += lay->concentration*inv(lay_multi->Dnn); - sumcDsig += lay->concentration*inv(lay_multi->Dnn)*lay_multi->sigma_hat; - } - } catch (const std::runtime_error &e) { - cerr << "Error in inv: " << e.what() << endl; - throw simcoon::exception_inv("Error in inv function inside dE_Periodic_Layer."); - } - + } + for(auto r : phase.sub_phases) { sv_r = std::dynamic_pointer_cast(r.sptr_sv_global); diff --git a/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp b/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp index 75575dbbc..0cc5067fe 100755 --- a/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp +++ b/src/Continuum_mechanics/Umat/Finite/Neo_hookean_incomp.cpp @@ -15,7 +15,7 @@ */ -///@file elastic_isotropic.cpp +///@file Neo_hookean_incomp.cpp ///@brief User subroutine for Isotropic elastic materials in 3D case ///@version 1.0 @@ -40,13 +40,6 @@ using namespace arma; namespace simcoon{ -///@brief The elastic UMAT requires 2 constants: -///@brief props[0] : Young modulus -///@brief props[1] : Poisson ratio -///@brief props[2] : CTE - -///@brief No statev is required for thermoelastic constitutive law - void umat_neo_hookean_incomp(const string &umat_name, const vec &etot, const vec &Detot, const mat &F0, const mat &F1, vec &sigma, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &corate_type, const int &tangent_mode) { @@ -108,18 +101,13 @@ void umat_neo_hookean_incomp(const string &umat_name, const vec &etot, const vec } mat I = eye(3,3); - //Compute the PKII stress and then the Cauchy stress. - // S = 2 dW/dC for W = C_10*(I1_bar-3) + (1/D_1)*(J-1)^2, with I1_bar = J^(-2/3) tr(C): - // S_iso = 2 C_10 J^(-2/3) I - (2/3) C_10 I1_bar invC ; S_vol = (2/D_1)(J-1) J invC. - // (The previous deviatoric I-term carried a stray 1/J -> J^(-5/3) instead of J^(-2/3), - // a ~ (1-1/J) stress error; fixed so stress and the rebuilt tangent both match dS/dE.) + // S = 2 dW/dC: S_iso = 2 C_10 J^(-2/3) I - (2/3) C_10 I1_bar invC, S_vol = (2/D_1)(J-1) J invC mat S = (-2./3.)*C_10*I1_bar*invC + 2.*C_10*pow(J,-2./3.)*I + (2./D_1)*(J-1)*J*invC; // Kirchhoff-native: the route stress IS tau, Cauchy is tau/J formed at the output boundary. mat tau_t = PKII2Kirchoff(S, F1, J); sigma = t2v_stress(tau_t); - // Tangent from the invariant machinery (spatial Lie tangent of the same potential), - // converted to the box tangent of corate_type, as generic_hyper_invariants. + // spatial Lie tangent of the same potential, mapped to the box of corate_type mat b = L_Cauchy_Green(F1); double dWdI_1_bar = C_10; // dW/dI1_bar; dW/dI2_bar = 0 (no I2 term), all 2nd deviatoric derivs = 0 double dUdJ = (2./D_1)*(J-1.); diff --git a/src/Continuum_mechanics/Umat/Finite/generic_hyper_invariants.cpp b/src/Continuum_mechanics/Umat/Finite/generic_hyper_invariants.cpp index ea095f2e4..792600327 100755 --- a/src/Continuum_mechanics/Umat/Finite/generic_hyper_invariants.cpp +++ b/src/Continuum_mechanics/Umat/Finite/generic_hyper_invariants.cpp @@ -38,11 +38,6 @@ using namespace arma; namespace simcoon{ -///@brief props: the parameters of the potential (see HyperPotential, hyperelastic.hpp), then an -///@brief optional selector of the volumetric term U(J) (see VolumetricPotential) - -///@brief No statev is required for a hyperelastic constitutive law - void umat_generic_hyper_invariants(const std::string &umat_name, const vec &etot, const vec &Detot, const mat &F0, const mat &F1, vec &sigma, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &corate_type, const int &tangent_mode) { @@ -79,8 +74,7 @@ void umat_generic_hyper_invariants(const std::string &umat_name, const vec &etot throw std::invalid_argument("The choice of hyperelastic potential could not be found in the simcoon library: " + umat_name); } - // An anisotropic potential reads its fibre pseudo-invariants off the structure - // tensors (tr A_i); an isotropic one gets an empty A and ignores it. + // fibre pseudo-invariants from the structure tensors; empty A for an isotropic potential const hyper_anisotropy an = hyper_potential_anisotropy(it_potential->second, props); const std::vector A = structure_tensors_push_forward(F1, an.a0, an.kappa_d, J); const hyper_invariants_dW dW = hyper_potential_derivatives(it_potential->second, props, I_bar, J, A); @@ -103,9 +97,7 @@ void umat_generic_hyper_invariants(const std::string &umat_name, const vec &etot L = Lt; } - //Computation of the mechanical and thermal work quantities. - // Kirchhoff work per reference volume: tau:d(lnV). Both ends are ALREADY tau -- the kernel - // is Kirchhoff-native and the stored state is tau_n -- so no J enters here any more. + // Kirchhoff work per reference volume, tau : d(lnV): no J Wm += 0.5*sum((sigma_start + sigma)%Detot); Wm_r += 0.5*sum((sigma_start + sigma)%Detot); Wm_ir += 0.; diff --git a/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp b/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp index 59fbb472e..276a1d99c 100755 --- a/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp +++ b/src/Continuum_mechanics/Umat/Finite/generic_hyper_pstretch.cpp @@ -115,17 +115,13 @@ void umat_generic_hyper_pstretch(const std::string &umat_name, const vec &etot, Wm_d = 0.; } - // Kirchhoff-native: the route stress IS tau, Cauchy is tau/J formed at the output boundary. - // The 3-argument tau_iso_hyper_pstretch needs no J at all. + // the route stress is tau; tau_iso_hyper_pstretch needs no J mat m_tau_iso = tau_iso_hyper_pstretch(dWdlambda_bar, lambda_bar, N_projectors); mat m_tau_vol = tau_vol_hyper(dUdJ, b, J); mat m_tau = m_tau_iso + m_tau_vol; sigma = t2v_stress(m_tau); - // These return the spatial elasticity c = (1/J) d(L_v tau)/dD, not d(L_v sigma)/dD (they - // differ by sigma (x) I). The J that turns c into the Kirchhoff-Lie tangent is applied once, - // below. L_iso carries an explicit 1/J and L_vol is J-free by cancellation, so they must be - // scaled as a sum -- see the note on L_vol_hyper. + // c = (1/J) d(L_v tau)/dD: J is applied once to the sum (see L_vol_hyper) mat Lt_iso = L_iso_hyper_pstretch(dWdlambda_bar, dW2dlambda_bar2, lambda_bar, n_pvectors, J); mat Lt_vol = L_vol_hyper(dUdJ, dU2dJ2, b, J); mat Lt_spatial = Lt_iso + Lt_vol; @@ -137,16 +133,7 @@ void umat_generic_hyper_pstretch(const std::string &umat_name, const vec &etot, L = Lt; } -/* cout << "L = " << L << endl; - cout << "Lt = " << Lt << endl; - cout << "Lt_iso = " << Lt_iso << endl; - cout << "Lt_vol = " << Lt_vol << endl; - cout << "eig(Lt)" << eig_sym(Lt); -*/ - - //Computation of the mechanical and thermal work quantities. - // Kirchhoff work per reference volume: tau:d(lnV). Both ends are ALREADY tau -- the kernel - // is Kirchhoff-native and the stored state is tau_n -- so no J enters here any more. + // Kirchhoff work per reference volume, tau : d(lnV): no J Wm += 0.5*sum((sigma_start + sigma)%Detot); Wm_r += 0.5*sum((sigma_start + sigma)%Detot); Wm_ir += 0.; diff --git a/src/Continuum_mechanics/Umat/Finite/hypoelastic_orthotropic.cpp b/src/Continuum_mechanics/Umat/Finite/hypoelastic_orthotropic.cpp index 82abbd070..b63760423 100755 --- a/src/Continuum_mechanics/Umat/Finite/hypoelastic_orthotropic.cpp +++ b/src/Continuum_mechanics/Umat/Finite/hypoelastic_orthotropic.cpp @@ -15,7 +15,7 @@ */ -///@file Elastic_orthotropic.cpp +///@file hypoelastic_orthotropic.cpp ///@brief User subroutine for ortothropic elastic materials in 3D case #include @@ -88,8 +88,6 @@ void umat_hypoelasticity_ortho(const string &umat_name, const vec &Etot, const v vec DEel = DEtot - alpha*DT; sigma = el_pred(sigma_start, L, DEel); - // Kirchhoff rate: sigma here is tau, so L is already the box d(tau_hat)/dDe, and in-rate - // whatever corate_type is (the increment arrives corotated). UNUSED(F0); UNUSED(F1); UNUSED(corate_type); diff --git a/src/Continuum_mechanics/Umat/Finite/saint_venant.cpp b/src/Continuum_mechanics/Umat/Finite/saint_venant.cpp index 33b080227..20c44fc13 100755 --- a/src/Continuum_mechanics/Umat/Finite/saint_venant.cpp +++ b/src/Continuum_mechanics/Umat/Finite/saint_venant.cpp @@ -15,7 +15,7 @@ */ -///@file elastic_isotropic.cpp +///@file saint_venant.cpp ///@brief User subroutine for Isotropic elastic materials in 3D case ///@version 1.0 @@ -39,13 +39,6 @@ using namespace arma; namespace simcoon{ -///@brief The elastic UMAT requires 2 constants: -///@brief props[0] : Young modulus -///@brief props[1] : Poisson ratio -///@brief props[2] : CTE - -///@brief No statev is required for thermoelastic constitutive law - void umat_saint_venant(const string &umat_name, const vec &etot, const vec &Detot, const mat &F0, const mat &F1, vec &sigma, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &corate_type, const int &tangent_mode) { @@ -87,18 +80,10 @@ void umat_saint_venant(const string &umat_name, const vec &etot, const vec &Deto mat S = v2t_stress(el_pred(L, Eel)); sigma = t2v_stress(PKII2Kirchoff(S, F1)); - // Box tangent in the canonical convention Lt = d(tau_hat)/d(De), IN the solver's corate. - // For SVK the elastic stiffness L IS the material tangent dS/dE, so one map does it. - // NOT box_DtauDe_from_dSdE: that helper rebuilds tau as det(F)*its stress argument, i.e. - // it expects CAUCHY, and this kernel's stress is now tau -- feeding it here would square - // the J. (A wrong tangent does not move converged values, so the regression baseline - // cannot see this class of error; only the FD tangent tests can.) + // not box_DtauDe_from_dSdE: it expects Cauchy, and would square J on tau Lt = DSDE_2_DtauDe_corate(L, corate_type, F1, v2t_stress(sigma)); - //Computation of the mechanical and thermal work quantities. - // Wm is the Kirchhoff work per REFERENCE volume: tau:d(lnV), conjugate to the log-strain - // increment Detot. Both ends are ALREADY tau -- the kernel is Kirchhoff-native and the - // stored state is tau_n -- so no J enters here any more. + // Kirchhoff work per reference volume, tau : d(lnV): no J Wm += 0.5*sum((sigma_start + sigma)%Detot); Wm_r += 0.5*sum((sigma_start + sigma)%Detot); Wm_ir += 0.; diff --git a/src/Continuum_mechanics/Umat/Mechanical/Combined/Prony_Nfast_Plastic.cpp b/src/Continuum_mechanics/Umat/Mechanical/Combined/Prony_Nfast_Plastic.cpp deleted file mode 100755 index 69f782456..000000000 --- a/src/Continuum_mechanics/Umat/Mechanical/Combined/Prony_Nfast_Plastic.cpp +++ /dev/null @@ -1,406 +0,0 @@ -///@file Prony_Nfast_PLastic.cpp -///@brief User subroutine for Prony series viscoelastic model in 3D case -///@author Chemisky -///@version 1.0 - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std; -using namespace arma; - -///@brief The viscoelastic Prony series model requires 4+N*4 constants: -// ------------------- -///@brief props(0) = E0 - Thermoelastic Young's modulus -///@brief props(1) = nu0 - Thermoelastic Poisson's ratio -///@brief props(2) = alpha_iso - Thermoelastic CTE -///@brief props(3) = N_prony - Number of Prony series -///@brief props(4+i*4) = E_visco(i) - Viscoelastic Young modulus of Prony branch i -///@brief props(4+i*4+1) = nu_visco(i) - Viscoelastic Poisson ratio of Prony branch i -///@brief props(4+i*4+2) = etaB_visco - Viscoelastic Bulk viscosity of Prony branch i -///@brief props(4+i*4+3) = etaS_visco - Viscoelastic Bulk viscosity of Prony branch i - -///@brief Number of statev required for thermoelastic constitutive law : 7+N*7 - -namespace simcoon { - -void umat_prony_Nfast_plastic(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) -{ - - UNUSED(umat_name); - UNUSED(nprops); - UNUSED(nstatev); - UNUSED(Time); - UNUSED(tnew_dt); - - //From the props to the material properties - double E0 = props(0); - double nu0 = props(1); - double alpha_iso = props(2); - int N_prony = int(props(3)); - int N_mec = N_prony+1; - - double sigmaY = props(4); - double k=props(5); - double m=props(6); - - vec E_visco = zeros(N_prony); - vec nu_visco = zeros(N_prony); - vec etaB_visco = zeros(N_prony); - vec etaS_visco = zeros(N_prony); - - for (int i=0; i EV_i(N_prony); - vec v = zeros(N_prony); - - for (int i=0; i L_i(N_prony); - std::vector H_i(N_prony); - std::vector invH_i(N_prony); - - vec stress_start = stress; - if(start) { //Initialization - T_init = T; - p = 0.; - EP = zeros(6); - EV_tilde = zeros(6); - for (int i=0; i simcoon::iota) { - dHpdp = m*k*pow(p, m-1); - Hp = k*pow(p, m); - } - else { - dHpdp = 0.; - Hp = 0.; - } - - vec EP_start = EP; - double A_p_start = -Hp; - std::vector DEV_i(N_prony); - std::vector A_v(N_prony); - std::vector A_v_start(N_prony); - - //Specific viscoelastic Variables values at the start of the increment - for (int i=0; i dPhidEv(N_prony); - std::vector dPhi_idv_temp(N_prony); - for (int i=0; i kappa_j(N_mec); - kappa_j[0] = L0*Lambdap; - - std::vector flow_visco(N_prony); - std::vector Lambdav(N_prony); - for (int i=0; i simcoon::precision_umat)); compteur++) { - - p = s_j(0); - for (int i=0; i simcoon::iota) { - dHpdp = m*k*pow(p, m-1); - Hp = k*pow(p, m); - } - else { - dHpdp = 0.; - Hp = 0.; - } - dPhidsigma = eta_stress(stress); - dPhidp = -1.*dHpdp; - - //compute Phi and the derivatives - Phi(0) = Mises_stress(stress) - Hp - sigmaY; - Lambdap = eta_stress(stress); - kappa_j[0] = L0*Lambdap; - K(0,0) = -1.*sum(dPhidsigma%kappa_j[0]) + dPhidp; - - //Viscoelastic part - for (int i=0; i simcoon::iota) { - Phi(i+1) = norm_strain(flow_visco[i]) - Ds_j(i+1)/DTime; - dPhidv[i] = -1.*sum((dPhi_idv_temp[i])%(L_i[i]*Lambdav[i]))-1./DTime; - } - else { - //No time, no flow: the branch is INACTIVE. The stationary condition - //Phi = ||flow|| has root EV = eps, committed as relaxed by the zero-time probe. - Phi(i+1) = 0.; - dPhidv[i] = -1.; - } - kappa_j[i+1] = L_i[i]*Lambdav[i]; // assign: this runs once per local - K(i+1,i+1) = dPhidv[i]; // Newton iteration, and both fed - // the tangent scaled by the count - } - - B = zeros(N_mec,N_mec); - B(0, 0) = K(0,0); - - Y_crit(0) = sigmaY; - for (int i=0; i P_epsilon(N_mec); - - Lt = L0; - for (int i=0; i simcoon::iota) - op(i) = 1.; - - for (int j = 0; j Y_22_u): held there, d12 keeps its own law constexpr double d_broken = 0.99; -// The fibre frame (fibres along 1), damaged: the compliance of the 22-33 block and of G23 scaled -// by 1/(1 - d22), of G12 and G13 by 1/(1 - d12); nuTL is the minor Poisson ratio, as in L_isotrans. -// The stiffness is the inverse of the 11-22-33 block plus the reciprocal shears. +// fibre frame (fibres along 1); nuTL is the minor Poisson ratio, as in L_isotrans struct damaged_elasticity { double EL, ET, nuTL, nuTT, GLT; @@ -268,9 +266,7 @@ void umat_damage_LLD_0(const string &umat_name, const vec &Etot, const vec &DEto d_12 = d(1); // ---- 3. Tangent of the two-stage update (plasticity, then damage) ---- - // dEel/deps = I - Lambda_p (x) L0 Lambda_p / h_p when plasticity is active (the direction held - // fixed: continuum); the active damage mechanisms then solve dPhi_l = 0 with - // dsigma = L~ dEel - L~ Lambda_j dd_j + // plastic flow direction held fixed (continuum), then the active damage set linearized exactly mat A = eye(6, 6); if (Dp(0) > simcoon::iota) { const double h_p = sum(Lambda_p % (L0*Lambda_p)) + dhardening(p_ts); diff --git a/src/Continuum_mechanics/Umat/Mechanical/Damage/damage_weibull.cpp b/src/Continuum_mechanics/Umat/Mechanical/Damage/damage_weibull.cpp deleted file mode 100755 index 5b0018b56..000000000 --- a/src/Continuum_mechanics/Umat/Mechanical/Damage/damage_weibull.cpp +++ /dev/null @@ -1,223 +0,0 @@ -/* This file is part of Simcoon private. - - Only part of Simcoon is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - This file is not be distributed under the terms of the GNU GPL 3. - It is a proprietary file, copyrighted by the authors - */ - -///@file damage_weibull.cpp -///@brief Isotropic damage with a weibull distribution of damage along a static loading - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using namespace std; -using namespace arma; - -namespace simcoon { - -///@brief Material properties -///@param props(0) : E Young's modulus -///@param props(1) : nu Poisson's ration -///@param props(2) : alpha CTE -///@param props(3) : alphaD Damage evolution parameter alpha -///@param props(4) : betaD Damage evolution parameter beta -///@param props(5) : lambdaD Damage evolution parameter lambda -///@param props(6) : deltaD Damage evolution parameter delta - -void umat_damage_weibull(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) { - - UNUSED(umat_name); - UNUSED(nprops); - UNUSED(nstatev); - UNUSED(Time); - UNUSED(DTime); - UNUSED(tnew_dt); - UNUSED(DR); - - //From the props to the material properties - double E = props(0); - double nu = props(1); - double alpha_iso = props(2); - double alpha_weibull = props(3); - double beta_weibull = props(4); - - //Set the Lagrange multipliers coefficients - double c_lambda = props(5); - double p0_lambda = props(6); - double n_lambda = props(7); - double alpha_lambda = props(8); - - //definition of the CTE tensor - vec alpha = alpha_iso*Ith(); - - ///@brief Temperature initialization - double T_init = statev(0); - ///@brief Damage variable - double Damage = statev(1); - - //Elastic stiffness tensors - L = L_iso(E, nu, "Enu"); - mat M = M_iso(E, nu, "Enu"); - - ///@brief Initialization - if(start) - { - T_init = T; - Damage = simcoon::precision_umat; - stress = zeros(6); - - Wm = 0.; - Wm_r = 0.; - Wm_ir = 0.; - Wm_d = 0.; - } - - vec sigma_eff = zeros(6); - - double lambda_D = lagrange_pow_1(Damage, c_lambda, p0_lambda, n_lambda, alpha_lambda); - double dlambda_D = dlagrange_pow_1(Damage, c_lambda, p0_lambda, n_lambda, alpha_lambda); - - double A_Damage = 0.; - double Rv = 0.; - if (Mises_stress(stress) > simcoon::iota) { - Rv = (2./3.)*(1.+nu)+3.*(1.-2.*nu)*pow(tr(stress)/(3.*Mises_stress(stress)),2.); - A_Damage = pow(Mises_stress(stress),2.)*Rv/(2.*E*pow(1.-Damage,2.)); - } - double A_Damage_start = A_Damage; - - double Weibull = 0.; - double dWeibulldsigma_eq = 0.; - - //Variables values at the start of the increment - vec stress_start = stress; - - //Variables required for the loop - vec s_j = zeros(1); - s_j(0) = Damage; - vec Ds_j = zeros(1); - vec ds_j = zeros(1); - - ///Elastic prediction - Accounting for the thermal prediction - vec Eel = Etot + DEtot - alpha*(T+DT-T_init); - mat L_tilde = (1.-Damage)*L; - stress = el_pred(L_tilde, Eel); - sigma_eff = el_pred(L, Eel); - - //Define the Damage function and the stress - vec Phi = zeros(1); - mat B = zeros(1,1); - vec Y_crit = ones(1); - - vec Lambda_Damage = zeros(6); - mat dStildedDamage = zeros(6,6); - std::vector kappa_j(1); - kappa_j[0] = zeros(6); - mat K = zeros(1,1); - - mat B_conc = L*inv(L_tilde); //stress "localization factor" in damage - - double dPhidDamage=0.; - vec dPhidsigma = zeros(6); - vec dPhidsigma_eff = zeros(6); - - //Loop parameters - int compteur = 0; - double error = 1.; - - - //Loop - for (compteur = 0; ((compteur < simcoon::maxiter_umat) && (error > simcoon::precision_umat)); compteur++) { - - Damage = s_j(0); - L_tilde = (1.-s_j(0))*L; - - Weibull = cumul_distrib_weibull(Mises_stress(sigma_eff), alpha_weibull, beta_weibull); - dWeibulldsigma_eq = proba_distrib_weibull(Mises_stress(sigma_eff), alpha_weibull, beta_weibull); - - lambda_D = lagrange_pow_1(Damage, c_lambda, p0_lambda, n_lambda, alpha_lambda); - dlambda_D = dlagrange_pow_1(Damage, c_lambda, p0_lambda, n_lambda, alpha_lambda); - - dPhidsigma_eff = dWeibulldsigma_eq*eta_stress(sigma_eff); - dPhidDamage = -1. - dlambda_D; - //dPhidDamage = -1.; - - //compute Phi and the derivatives - Phi(0) = Weibull - Damage - lambda_D; - //Phi(0) = Weibull - Damage; - - //Compute the explicit "damage direction" and flow direction - dStildedDamage = 1./(pow(1.-Damage,2.))*M; - Lambda_Damage = dStildedDamage*stress; - kappa_j[0] = L_tilde*Lambda_Damage; - - K(0,0) = dPhidDamage; - B(0, 0) = -1.*sum(dPhidsigma%kappa_j[0]) + K(0,0); - //B(0, 0) = K(0,0); - Y_crit(0) = 1.; - - Fischer_Burmeister_m(Phi, Y_crit, B, Ds_j, ds_j, error); - - s_j(0) += ds_j(0); - - //the stress is now computed using the relationship stress = L(E-Ep) - Eel = Etot + DEtot - alpha*(T + DT - T_init); - stress = el_pred(L_tilde, Eel); - sigma_eff = el_pred(L, Eel); - } - - //Computation of the increments of variables - vec Dsigma = stress - stress_start; - double DDamage = Ds_j[0]; - - dPhidsigma = dWeibulldsigma_eq*(B_conc.t()*eta_stress(sigma_eff)); - dStildedDamage = 1./(pow(1.-Damage,2.))*M; - Lambda_Damage = dStildedDamage*stress; - kappa_j[0] = L_tilde*Lambda_Damage; - - //Computation of the tangent modulus — continuum operator via shared helper (doc §7.4). - mat Bhat = zeros(1, 1); - Bhat(0, 0) = sum(dPhidsigma%kappa_j[0]) - K(0,0); - - const std::vector dPhidsigma_l = { dPhidsigma }; - const ContinuumTangent ct = assemble_continuum_tangent(Bhat, kappa_j, dPhidsigma_l, Ds_j, L_tilde); - Lt = ct.Lt; - const std::vector& P_epsilon = ct.P_epsilon; - //Lt = L_tilde; - - if (Mises_stress(stress) > simcoon::iota) { - Rv = (2./3.)*(1.+nu)+3.*(1.-2.*nu)*pow(tr(stress)/(3.*Mises_stress(stress)),2.); - A_Damage = pow(Mises_stress(stress),2.)*Rv/(2.*E*pow(1.-Damage,2.)); - } - double Dgamma_loc = 0.5*(A_Damage_start + A_Damage)*DDamage; - - //Computation of the mechanical and thermal work quantities - Wm += 0.5*sum((stress_start+stress)%DEtot); - Wm_r += 0.5*sum((stress_start+stress)%(DEtot)) - 0.5*(A_Damage_start + A_Damage)*DDamage; - Wm_ir += 0.; - Wm_d += Dgamma_loc; - - ///@brief statev evolving variables - //statev - statev(0) = T_init; - statev(1) = Damage; -} - -} //namespace simcoon diff --git a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp index 9d3397961..82c6e07b7 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_chaboche.cpp @@ -39,33 +39,6 @@ using namespace arma; namespace simcoon { -///@brief The mechanical elastic-plastic UMAT with kinematic + isotropic hardening requires 7 constants: - -///@brief props[0] : Young modulus -///@brief props[1] : Poisson ratio -///@brief props[2] : CTE -///@brief props[3] : J2 equivalent yield stress limit : sigmaY -///@brief props[4] : hardening parameter k -///@brief props[5] : exponent m -///@brief props[6] : linear kinematical hardening h - -///@brief The elastic-plastic UMAT with kinematic + isotropic hardening requires 14 statev: -///@brief statev[0] : T_init : Initial temperature -///@brief statev[1] : Accumulative plastic parameter: p -///@brief statev[2] : Plastic strain 11: EP(0,0) -///@brief statev[3] : Plastic strain 22: EP(1,1) -///@brief statev[4] : Plastic strain 33: EP(2,2) -///@brief statev[5] : Plastic strain 12: EP(0,1) (*2) -///@brief statev[6] : Plastic strain 13: EP(0,2) (*2) -///@brief statev[7] : Plastic strain 23: EP(1,2) (*2) -///@brief statev[8] : Backstress 11: X(0,0) -///@brief statev[9] : Backstress 11: X(1,1) -///@brief statev[10] : Backstress 11: X(2,2) -///@brief statev[11] : Backstress 11: X(0,1) -///@brief statev[12] : Backstress 11: X(0,2) -///@brief statev[13] : Backstress 11: X(1,2) - - void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) { @@ -86,8 +59,6 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve double D_1 = props(7); double C_2 = props(8); double D_2 = props(9); - //double X_0 = props(10); - //double ep_eq0 = props(11); //definition of the CTE tensor vec alpha = alpha_iso*Ith(); @@ -137,15 +108,12 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve X_2(4) = statev(30); X_2(5) = statev(31); - // Isotropic Voce term Hp = R(p) = Q (1 - exp(-b p)) through the modular block: the isotropic - // state is p, statev(32) is its output (the loop used to accumulate Hp by explicit Euler of - // dHp = b (Q - Hp) dp, a step-size dependent approximation of the same law). + // Voce R(p) in closed form from p; statev(32) is an output only VoceHardening hardening; { int off = 0; hardening.configure(vec{Q, b}, off); } double Hp = hardening.R(p); - //Rotation of internal variables (tensors): the back-strains are strain-like, the - //back-stresses stress-like + // back-strains rotate strain-like, back-stresses stress-like EP = rotate_strain(EP, DR); a_1 = rotate_strain(a_1, DR); a_2 = rotate_strain(a_2, DR); @@ -226,10 +194,7 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve int compteur = 0; double error = 1.; - // Closest-point projection (tangent_closest_point): the modular von Mises + Voce + Chaboche - // row on this kernel's props and state (backward-Euler state and total derivatives in - // PlasticityMechanism::refresh_state). Condensed states keep the loop. Bhat / K of the tail - // only feed the cutting-plane tangent. + // Bhat / K below only feed the cutting-plane tangent, not the closest-point branch ReturnMappingResult rm; const bool use_cpp = (tangent_mode == simcoon::tangent_closest_point); if (use_cpp) { @@ -270,10 +235,8 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve dHpdp = hardening.dR_dp(p); dPhidsigma = eta_stress(stress-X); dPhidp = -1.*dHpdp; -// dPhidp = -1.*dHpdp; dPhida_1 = -1.*(2./3.)*C_1*(eta_stress(stress-X)%Ir05()); dPhida_2 = -1.*(2./3.)*C_2*(eta_stress(stress-X)%Ir05()); -// dPhida = 0.*(eta_stress(stress - X)%Ir05()); //compute Phi and the derivatives Phi(0) = Mises_stress(stress-X) - Hp - sigmaY; @@ -313,8 +276,7 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve vec Da_1 = a_1 - a_1start; vec Da_2 = a_2 - a_2start; - //Computation of the tangent modulus — continuum elastic-plastic operator - //assembled via the shared leading-mechanism helper (doc §7.4). + // tangent via the shared leading-mechanism helper (doc §7.4) mat Bhat = zeros(1, 1); Bhat(0, 0) = sum(dPhidsigma%kappa_j[0]) - K(0,0); @@ -322,9 +284,7 @@ void umat_plasticity_chaboche(const string &umat_name, const vec &Etot, const ve const ContinuumTangent ct = rm.converged ? cpp_consistent_tangent(rm, L) : compute_tangent_operator( tangent_mode, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // Simo-Hughes algorithmic tangent on the cutting-plane state. J2 flow on the effective - // stress (sigma-X): dLambda_eps/dsigma = deta_stress(stress-X); the backstress state - // coupling is only in the closest-point branch (tangent_closest_point). + // J2 flow on sigma-X; the backstress coupling is only in the closest-point branch const std::vector dLambda_dsigma_l = { deta_stress(stress - X) }; return dLambda_dsigma_l; }); diff --git a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp index 3dc54030b..f64765a91 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_isotropic.cpp @@ -41,51 +41,7 @@ using namespace arma; namespace simcoon { -///@brief The elastic-plastic UMAT with isotropic hardening requires 6 constants for a mechanical coupling: - -///@brief props[0] : Young modulus -///@brief props[1] : Poisson ratio -///@brief props[2] : CTE -///@brief props[3] : J2 equivalent yield stress limit : sigmaY -///@brief props[4] : hardening parameter k -///@brief props[5] : exponent m - -///@brief The elastic-plastic UMAT with isotropic hardening requires 8 statev: -///@brief statev[0] : T_init : Initial temperature -///@brief statev[1] : Accumulative plastic parameter: p -///@brief statev[2] : Plastic strain 11: EP(0,0) -///@brief statev[3] : Plastic strain 22: EP(1,1) -///@brief statev[4] : Plastic strain 33: EP(2,2) -///@brief statev[5] : Plastic strain 12: EP(0,1) (*2) -///@brief statev[6] : Plastic strain 13: EP(0,2) (*2) -///@brief statev[7] : Plastic strain 23: EP(1,2) (*2) - -/// The constitutive model has one lead mechanism, that contains two internal variables : p and EP -/// The Gibbs free energy is: - -/* - - \begin{eqnarray} - G\left(\b{\sigma},\theta,\b{V}_1\right)&=& G^{\textrm{r}}\left(\b{\sigma},\theta, \b{\varepsilon}^{\textrm{p}}\right)+G^{\textrm{ir}} \left(\theta,p\right), - \EqnCont - G^{\textrm{r}}\left(\b{\sigma},\theta, \b{\varepsilon}^{\textrm{p}}\right) - - = - \f{1}{2} \b{\sigma} \dcon \b{\mathcal{S}} \dcon \b{\sigma} + {c_0}\left[ [\theta-\theta_0] - -\theta\ln\left(\f{\theta}{\theta_0}\right)\right]-\eta_0\theta+{E}_0 - \b{\sigma} : \b{\varepsilon}^{\textrm{p}} - - \b\sigma : \b{\alpha} \left(\theta - \theta_0 \right), - \EqnCont - G^{\textrm{ir}}\left(\theta,p\right)&=&F(\theta,p) - \label{eq:elastoplastic_Gibbs_potential} - \end{eqnarray} - - So the following quantitites are : (A_x = \pfg{G}{x}) - A_sigma = \varepsilon - A_p = - A_EP = - A_theta = - */ - -void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) +void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode, const int &ncp_code) { UNUSED(umat_name); @@ -94,6 +50,8 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE UNUSED(Time); UNUSED(DTime); + const NCPMethod ncp = ncp_method_of(ncp_code, "EPICP"); + //From the props to the material properties double E = props(0); double nu= props(1); @@ -141,10 +99,7 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE double Hp=0.; double dHpdp=0.; - // Power-law hardening k p^m through the modular block: for m < 1 the exact slope m k p^(m-1) - // is infinite at the onset and the cutting-plane Newton cycles on the first plastic increment - // (committed off-surface, silently); PowerLawHardening is exact for p >= 1e-6 and a C1 - // quadratic blend below. + // PowerLawHardening: C1 blend below p = 1e-6 (slope of k p^m infinite at onset for m < 1) PowerLawHardening hardening; int hardening_offset = 0; hardening.configure(vec{k, m}, hardening_offset); @@ -185,10 +140,7 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE int compteur = 0; double error = 1.; - // Closest-point projection (tangent_closest_point): the modular von Mises + power-law row on - // this kernel's props and state — radial return, the same converged state as the loop below - // to the local tolerance, with the helper's exact operator. Condensed states keep the loop. - // Bhat / K of the tail only feed the cutting-plane tangent. + // Bhat / K below only feed the cutting-plane tangent, not the closest-point branch ReturnMappingResult rm; const bool use_cpp = (tangent_mode == simcoon::tangent_closest_point); if (use_cpp) { @@ -212,6 +164,7 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE } } else { + ncp_step local_step(ncp); //Loop for (compteur = 0; ((compteur < simcoon::maxiter_umat) && (error > simcoon::precision_umat)); compteur++) { @@ -230,8 +183,15 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE K(0,0) = dPhidp; B(0, 0) = -1.*sum(dPhidsigma%kappa_j[0]) + K(0,0); Y_crit(0) = sigmaY; - - Fischer_Burmeister_m(Phi, Y_crit, B, Ds_j, ds_j, error); + + if (compteur == 0 && Phi(0) < 0. && sqrt(pow(Phi(0),2.)) + Phi(0) == 0.) { + // admissible trial: FB residual exactly 0, skip the solve + ds_j.zeros(); + error = 0.; + } + else { + local_step(Phi, Y_crit, B, Ds_j, ds_j, error); + } s_j(0) += ds_j(0); EP = EP + ds_j(0)*Lambdap; @@ -247,23 +207,38 @@ void umat_plasticity_iso(const string &umat_name, const vec &Etot, const vec &DE vec DEP = EP - EP_start; double Dp = Ds_j[0]; - //Computation of the tangent modulus — continuum elastic-plastic operator - //assembled via the shared leading-mechanism helper (doc §7.4). + // tangent via the shared leading-mechanism helper (doc §7.4) mat Bhat = zeros(1, 1); Bhat(0, 0) = sum(dPhidsigma%kappa_j[0]) - K(0,0); const std::vector dPhidsigma_l = { dPhidsigma }; - const ContinuumTangent ct = rm.converged ? cpp_consistent_tangent(rm, L) : compute_tangent_operator( - tangent_mode, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, - [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // Simo-Hughes algorithmic (consistent) tangent. J2 associated flow - // Lambda_eps = eta_stress(sigma), so dLambda_eps/dsigma = d2Phi/dsigma2 = deta_stress(sigma). - // Isotropic hardening: the direction has no internal-variable coupling, so this single - // dLambda/dsigma term is the complete consistent correction (Q-quadratic). + // lazy: needed in algorithmic mode with an active multiplier only + HessianProvider dLambda_dsigma = nullptr; + if (Ds_j(0) > simcoon::iota) { + dLambda_dsigma = [&]() -> std::vector { + // J2 associated flow: dLambda/dsigma = deta_stress, complete (no internal-variable coupling) const std::vector dLambda_dsigma_l = { deta_stress(stress) }; return dLambda_dsigma_l; - }); - Lt = ct.Lt; + }; + } + if (!use_cpp && tangent_mode == simcoon::tangent_algorithmic && Ds_j(0) > simcoon::iota) { + // closed-form radial-return tangent (formula in the header) + const double G = E/(2.*(1.+nu)); + const double K_b = E/(3.*(1.-2.*nu)); + const double q_tr = Mises_stress(stress) + 3.*G*Dp; + const double theta = 1. - 3.*G*Dp/q_tr; + const double theta_bar = 1./(1. + hardening.dR_dp(p)/(3.*G)) - (1. - theta); + const vec s_dev = dev(stress); + const double s_norm = sqrt(s_dev(0)*s_dev(0) + s_dev(1)*s_dev(1) + s_dev(2)*s_dev(2) + + 2.*(s_dev(3)*s_dev(3) + s_dev(4)*s_dev(4) + s_dev(5)*s_dev(5))); + const vec N = s_dev/s_norm; + Lt = 3.*K_b*Ivol() + 2.*G*theta*Idev() - 2.*G*theta_bar*(N*N.t()); + } + else { + const ContinuumTangent ct = rm.converged ? cpp_consistent_tangent(rm, L) : compute_tangent_operator( + tangent_mode, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, dLambda_dsigma); + Lt = ct.Lt; + } std::vector P_theta(1); P_theta[0] = dPhidtheta - sum(dPhidsigma%(L*alpha)); diff --git a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp index 01d20dd4f..4e1ea827b 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Plasticity/plastic_johnson_cook_ccp.cpp @@ -93,9 +93,7 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, Wm_d = 0.; } - // Thermal softening factor f_T = 1 - g(T*), g = T*^m C2-regularized near T* = 0 for m < 2 - // (johnson_cook_softening, the same law as the thermomechanical kernel), T* clamped to [0, 1) - // (defined below T_ref, never at the singular melting point), at the start and end temperatures + // f_T = 1 - g(T*), T* clamped to [0, 1): defined below T_ref, never at the melting point auto homologous = [&](const double &theta) { double Tstar = (theta - T_ref) / (T_melt - T_ref); if (Tstar < 0.) Tstar = 0.; @@ -110,8 +108,7 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, const double thermal_factor_start = 1. - softening(T); const double thermal_factor = 1. - softening(T + DT); - // Strain hardening Hp = B p^n, C1-regularized at the onset for n < 1 (the modular - // power-law block carries the blend; same law, no second implementation) + // Hp = B p^n through PowerLawHardening (C1 onset blend for n < 1) PowerLawHardening hardening; int hardening_offset = 0; hardening.configure(vec{B_jc, n_jc}, hardening_offset); @@ -150,12 +147,7 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, std::vector kappa_j(1); mat K = zeros(1,1); - // Branch selection at the rate kink x0 = edot0*DTime, where Phi(Dp) changes from the - // quasi-static branch (rate factor 1) to the convex logarithmic one. A Newton started at - // Dp = 0 with the quasi-static slope overshoots the kink and the log branch throws it back - // below zero: a 2-cycle to maxiter_umat at small DTime. So the branch is decided first from - // Phi at the kink (radial estimate), the loop starts AT the kink on the log branch with the - // right derivative of the rate factor, and Newton is then monotone (convex branch). + // branch decided from Phi at the rate kink x0 = edot0*DTime, else Newton 2-cycles (header) bool log_branch = false; if (DTime > simcoon::iota) { const double x0 = edot0*DTime; @@ -227,14 +219,12 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, sigma = el_pred(L, Eel); } - // The loop reads p at the top and corrects s_j at the bottom: bring p and the hardening to - // the converged iterate (EP and sigma already are) + // the loop reads p at the top and corrects s_j at the bottom p = s_j(0); Hp = hardening.R(p); dHpdp = hardening.dR_dp(p); - // A loop that leaves at maxiter_umat has not met the yield surface: the state would be - // committed off-surface, so ask the solver for a step cut instead + // unconverged at maxiter_umat: step cut rather than commit off-surface if (error > simcoon::precision_umat) { tnew_dt = 0.5; } @@ -249,9 +239,7 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, edot_p_out = Dp / DTime; } - //Computation of the tangent modulus via the shared leading-mechanism helper (doc 7.4). - //The rate term sits in K, hence in Bhat: the algorithmic operator is the exact Jacobian of - //the discrete map at fixed DTime. tangent_none returns L (explicit integration). + // the rate term sits in K, hence in Bhat (exact Jacobian at fixed DTime) mat Bhat = zeros(1, 1); Bhat(0, 0) = sum(dPhidsigma%kappa_j[0]) - K(0,0); @@ -259,15 +247,13 @@ void umat_plasticity_johnson_cook_CCP(const string &umat_name, const vec &Etot, const ContinuumTangent ct = compute_tangent_operator( tangent_mode, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // J2 associated flow: dLambda/dsigma = deta_stress(sigma), the complete - // Simo-Hughes correction for a single isotropic mechanism + // J2 associated flow: dLambda/dsigma = deta_stress, complete for one isotropic mechanism const std::vector dLambda_dsigma_l = { deta_stress(sigma) }; return dLambda_dsigma_l; }); Lt = ct.Lt; - // Energy split: stored G^ir = f_T B p^(n+1)/(n+1), so A_p = -f_T B p^n and the dissipation - // f_T [A f_rate + B p^n (f_rate - 1)] Dp stays non-negative at every temperature and rate + // A_p = -f_T B p^n (not -B p^n): keeps the dissipation non-negative at every temperature double A_p = -thermal_factor*Hp; double Dgamma_loc = 0.5*sum((sigma_start+sigma)%DEP) + 0.5*(A_p_start + A_p)*Dp; diff --git a/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp b/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp index c09c72731..d29208543 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/SMA/SMA_mono.cpp @@ -15,7 +15,7 @@ */ -///@file sma_mono.cpp +///@file SMA_mono.cpp ///@brief User subroutine for monocrystalline SMA with multiple elastic symmetry options ///@author Chemisky @@ -185,8 +185,6 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v //Rotation of internal variables (tensors) ET = rotate_strain(ET, DR); - // Note: Elastic stiffness tensor L is already constructed based on umat_name - ///@brief Initialization if(start) { @@ -344,8 +342,6 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v if(fabs(det(dPhidxi)) > 0.) dxin = -1.*inv(dPhidxi)*Phi; else { - //pnewdt = 0.1; -// Phi = zeros(nactive); dxin = zeros(nactive); } } catch (const std::runtime_error &e) { @@ -359,7 +355,6 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v dxi += dxin(i); xin(active[i]) += dxin(i); -// Amortissement(xin(active[i]), dxin(i), xin_start(active[i])); } @@ -375,7 +370,6 @@ void umat_sma_mono(const string &umat_name, const vec &Etot, const vec &DEtot, v xi += xin(i); } -// Amortissement(xi, dxi, xi_start); for(int i=0; i 0.) { Lt = L + ((L*lambda)*(inv(dPhidxi))*(dPhidsigma*L)); diff --git a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp index 12d7f161c..ca059cb95 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_T.cpp @@ -17,16 +17,6 @@ ///@file unified_T.cpp ///@brief Unified SMA model supporting multiple elastic symmetries and transformation criteria -///@brief Based on constitutive model of D. Chatziathanasiou Ph.D Thesis -///@brief Implemented by Y. Chemisky and D. Chatziathanasiou -///@brief -///@brief The elastic symmetry and criteria type is determined by umat_name: -///@brief SMADI (isotropic elasticity, Drucker criteria): props(0)=flagT, props(1)=EA, props(2)=EM, props(3)=nuA, props(4)=nuM, then common params -///@brief SMADC (cubic elasticity, Drucker criteria): props(0)=flagT, props(1-3)=C11A,C12A,C44A, props(4-6)=C11M,C12M,C44M, then common params -///@brief SMAAI (isotropic elasticity, anisotropic Drucker criteria): same as SMADI + DFA params at end -///@brief SMAAC (cubic elasticity, anisotropic Drucker criteria): same as SMADC + DFA params at end -///@brief -///@brief Implemented in 1D-2D-3D #include #include @@ -95,14 +85,14 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt if (!cubic_elasticity) { // Isotropic elasticity: EA, EM, nuA, nuM - M_A = inv(L_iso(props(1), props(3), "Enu")); - M_M = inv(L_iso(props(2), props(4), "Enu")); + M_A = M_iso(props(1), props(3), "Enu"); + M_M = M_iso(props(2), props(4), "Enu"); offset = 5; } else { // Cubic elasticity: EA, EM, nuA, nuM, GA, GM - M_A = inv(L_cubic(props(1), props(3), props(5), "EnuG")); - M_M = inv(L_cubic(props(2), props(4), props(6), "EnuG")); + M_A = M_cubic(props(1), props(3), props(5), "EnuG"); + M_M = M_cubic(props(2), props(4), props(6), "EnuG"); offset = 7; } @@ -183,7 +173,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt // Reuss mixing on compliance tensor mat DM = M_M - M_A; mat M_eff = xi*M_M + (1. - xi)*M_A; - L = inv(M_eff); + L = inv_cubic(M_eff); //definition of the CTE tensor vec alpha = (alphaM_iso*xi + alphaA_iso*(1.-xi))*Ith(); @@ -380,11 +370,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt double dPhiFdxiF = 0.; double dPhiFdxiR = 0.; - //Relative to reverse transformation. The K(1,\cdot ) chain rule (built below - //through ETMean as the natural intermediate variable) does not need - //dPhihatRdET, dYtRdET, dPhihatRdxi or dPhiRdxi / dPhiRdET — those would - //carry \sigma /\xi -divergent terms that cancel analytically but lose ~12 digits - //in floating point. Only the finite \sigma -gradients survive here. + //Relative to reverse transformation: only the finite sigma-gradients (K(1,.) goes through ETMean, see header) vec dPhihatRdsigma = zeros(6); vec dA_xiRdsigma = zeros(6); double dA_xiRdxiF = 0.; @@ -403,13 +389,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt int compteur = 0; double error = 1.; - // tangent_closest_point: the closest-point projection replaces the CCP loop. - // L(xi) and alpha(xi) are FROZEN at xi_n: since the Reuss mixture M(xi) and alpha(xi) are - // LINEAR in xi, the effective flows Lambda_eff^F = lambdaTF + DM*sigma + Dalpha*(T+DT-T_init) - // and Lambda_eff^R = -ETMean - DM*sigma - Dalpha*(T+DT-T_init) make the frozen-L residual the - // EXACT discrete system (M(xi)sigma = M(xi_n)sigma + Dxi*DM*sigma is an identity). The state - // is rebuilt from the start values at every iterate (ETMean in closed form) and the callbacks - // return the analytic TOTAL derivatives of that composed map (linearise below). + // closest point: L(xi), alpha(xi) frozen at xi_n, exact since both are linear in xi (see header) ReturnMappingResult rm; const bool use_cpp = (tangent_mode == simcoon::tangent_closest_point); const mat L_cpp = use_cpp ? L : mat(); // frozen stiffness used consistently by the helper and the tangent @@ -427,10 +407,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt xi = xi_n + Dl(0) - Dl(1); Hcur = Hmin + (Hmax - Hmin)*(1. - exp(-1.*k1*std::max(Mises_stress(sig) - sigmacrit, 0.))); lambdaTF = sma_transformation_flow(sig, sigmacrit, Hmin, Hmax, k1, aniso_criteria, DFA_params, prager_b, prager_n); - // ETMean = dev(ET)/xi with ET = ET_n + Dl_F lambdaTF - Dl_R ETMean has the closed form - // ETMean (xi + Dl_R) = dev(ET_n) + Dl_F dev(lambdaTF) (the martensite present before - // the reversal); the sweep form of this fixed point diverges when Dl_R > xi. The loop's - // guard (no martensite, no transformation strain -> lambdaTF) is kept. + // closed form ETMean (xi + Dl_R) = dev(ET_n) + Dl_F dev(lambdaTF): the sweep diverges when Dl_R > xi const double xi_before = xi + Dl(1); ETMean_is_flow = !(xi_before > simcoon::precision_umat); ETMean = ETMean_is_flow ? lambdaTF : vec((dev(ET_start) + Dl(0)*dev(lambdaTF))/xi_before); @@ -440,10 +417,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt ETMean = lambdaTF; ET = ET_start + Dl(0)*lambdaTF - Dl(1)*ETMean; } - // Smooth hardening functions at xi clamped to [0,1]: an iterate can leave the bounds, - // where pow(xi, n) is NaN for the fractional exponents; the slope of Hf is infinite - // there (the loop uses 1e12), so a C1 continuation is a model change, not a numerical - // one. The Lagrange penalties are already C1-continued outside [0,1]. + // Hf at xi clamped to [0,1]: pow(xi, n) is NaN outside for fractional n const double xi_h = std::min(std::max(xi, 0.), 1.); if ((n1 == 1.) && (n2 == 1.)) { HfF = a1*xi_h + a3; } else { HfF = 0.5*a1*(1. + pow(xi_h, n1) - pow(1. - xi_h, n2)) + a3; } @@ -466,18 +440,10 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt const double AxR = -1.*rhoDs0*(T + DT) + rhoDE0 - 0.5*sum(sig%DMsig) - sum(sig%Dalpha)*thp + HfR; return -1.*sum(sig%ETMean) + AxR + lambda0 - (Y0t + D*sum(sig%ETMean)); }; - // Effective flows (stiffness/CTE mixture variation folded in, see header comment). + // effective flows: stiffness/CTE mixture variation folded in auto LamF_val = [&](const vec &sig) { return vec(lambdaTF + DM*sig + Dalpha*thp); }; auto LamR_val = [&](const vec &sig) { return vec(-1.*ETMean - DM*sig - Dalpha*thp); }; - // Analytic total derivatives at the refreshed state (sig, Dl_cur), built once per iterate. - // lambdaTF = Hcur g, g = dDrucker: dlambdaTF/dsigma = g (x) dHcur + Hcur ddDrucker - // ETMean (xi_n + Dl_F) = dev(ET_n) + Dl_F dev(lambdaTF) (independent of Dl_R): - // dETMean/dsigma = Dl_F/(xi_n + Dl_F) dev(dlambdaTF/dsigma), dETMean/dDl_F = (dev(lambdaTF) - ETMean)/(xi_n + Dl_F) - // (guard branch ETMean = lambdaTF: dETMean/dsigma = dlambdaTF/dsigma, no multiplier dependence) - // Phi_F = Hcur Drucker + A_xiF(sigma, xi) - lambda1(xi) - Y0t - D Hcur sigma_eq - // Phi_R = -(1 + D) sigma:ETMean + A_xiR(sigma, xi) + lambda0(xi) - Y0t - // K_lj = dPhi_l/dDl_j; the flux chain c_j = sum_k Dl_k dLambda_k/dDl_j is -Dl_R dETMean/dDl_F for - // the forward row and zero for the reverse one. + // analytic total derivatives at (sig, Dl_cur), once per iterate (derivation in header) mat dev_op = eye(6, 6); // dev() of a Voigt vector dev_op.submat(0, 0, 2, 2) -= ones(3, 3)/3.; vec dPhiF_ds = zeros(6), dPhiR_ds = zeros(6), c_F = zeros(6); @@ -539,11 +505,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt hooks.K = [&](const vec &sig, const vec &) { linearise(sig); return K_cpp; }; hooks.flow_state_coupling = [&](const vec &sig, const vec &) { linearise(sig); return std::vector{c_F, zeros(6)}; }; vec Ycrit_cpp(2); - // FB normalisation scales. Both YtF = Y0t + D*Hcur*Mises and YtR = Y0t + D*sigma:ETMean - // degenerate to ~0 at low-stress/virgin states (Y0t may be 0, Hcur -> Hmin): a vanishing - // normaliser blows the relative FB error up for physically negligible residuals - // (~1e-10 MPa) and defeats the deactivation snap. Floor both to an ABSOLUTE material - // scale built from the calibration point. + // FB scales floored at an absolute material scale: both thresholds vanish in virgin states const double Yfloor = 0.1*std::max(Y0t + D*Hmax*sigmacaliber, 1.e-2); Ycrit_cpp(0) = std::max(std::fabs(Y0t) + std::fabs(D*Hcur*Mises_stress(stress)), Yfloor); Ycrit_cpp(1) = std::max(std::fabs(Y0t) + std::fabs(D*sum(stress%ETMean)), Yfloor); @@ -555,20 +517,16 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt s_j += Ds_j; xiF = s_j(0); xiR = s_j(1); - // xi, ET, ETMean, lambdaTF, HfF/R, lambda0/1, Hcur are inner-consistent already. DETF = Ds_j(0)*lambdaTF; DETR = Ds_j(1)*ETMean; - // Refresh the mixture stiffness at the converged xi for the OUTPUT L (the tangent - // uses the frozen L_cpp consistently through cpp_consistent_tangent below). + // output L at the converged xi; the tangent keeps the frozen L_cpp M_eff = xi*M_M + (1. - xi)*M_A; - L = inv(M_eff); - // Thermodynamic forces at the converged state (feed the work quantities below). + L = inv_cubic(M_eff); A_xiF = rhoDs0*(T + DT) - rhoDE0 + 0.5*sum(stress%(DM*stress)) + sum(stress%Dalpha)*thp - HfF; A_xiR = -1.*rhoDs0*(T + DT) + rhoDE0 - 0.5*sum(stress%(DM*stress)) - sum(stress%Dalpha)*thp + HfR; } else { - // step cut, never a silent cutting-plane fallback; the start state back (Ds_j, DETF, - // DETR are still zero, xiF / xiR / s_j untouched) + // step cut, never a silent cutting-plane fallback tnew_dt = 0.5; stress = stress_start; refresh_state(stress, zeros(2)); @@ -580,7 +538,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt // Reuss mixing on compliance tensor M_eff = xi*M_M + (1. - xi)*M_A; - L = inv(M_eff); + L = inv_cubic(M_eff); DM_sig = DM*stress; Dalpha_T = Dalpha*(T+DT-T_init); @@ -727,21 +685,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt dYtRdsigma = D*ETMean; dPhiRdsigma = -1.*dPhihatRdsigma + dA_xiRdsigma + dlambda0dsigma - dYtRdsigma; - // K(1,\cdot ) via \Lambda_ETMean^j chain rule — avoids the floating-point cancellation - // between dPhiRdxiF (\propto +(1+D)\cdot \sigma :ETMean/\xi ) and sum(dPhiRdET\cdot lambdaTF) - // (\propto -(1+D)\cdot \sigma :lambdaTF/\xi ) which are analytically equal-and-opposite at - // small \xi when ETMean fallback is lambdaTF, but lose ~12 significant - // digits in IEEE 754 when \xi \approx 1e-12. Reformulation through the natural - // intermediate variable ETMean = dev(\varepsilon^T)/\xi : - // K(1, j) = \partial \Phi^R/\partial ETMean \cdot \Lambda_ETMean^j + \partial \Phi^R/\partial \xi |finite \cdot d\xi /d\xi_j - // \partial \Phi^R/\partial ETMean = -(1+D)\cdot \sigma (finite) - // \Lambda_ETMean^F = (lambdaTF - ETMean)/\xi (finite — zero - // when ETMean fallback = lambdaTF) - // \Lambda_ETMean^R = 0 (reverse removes - // martensite proportionally; mean unchanged) - // \partial \Phi^R/\partial \xi |finite = dHfR + d\lambda_0/d\xi (hardening only; - // the \sigma :ETMean/\xi part of dPhihatRdxi is absorbed - // into the \Lambda_ETMean chain-rule contribution) + // K(1,.) through ETMean: avoids a ~12-digit cancellation at small xi (derivation in header) const vec dPhiRdETMean = -(1. + D) * stress; vec Lambda_ETMean_F = zeros(6); if (Mises_strain(lambdaTF - ETMean) > simcoon::iota) { @@ -800,10 +744,7 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt double DxiF = Ds_j[0]; double DxiR = Ds_j[1]; - //Computation of the tangent modulus — continuum SMA operator - //assembled via the shared 2-mechanism leading-mechanism helper (doc §7.4); - //closest-point branch: the exact operator of the converged solve, with the frozen L_cpp it - //was solved with (elastic placeholder on a step cut). + // closest point: exact operator of the converged solve with the frozen L_cpp (elastic on a step cut) ContinuumTangent ct; if (use_cpp) { ct = cpp_consistent_tangent(rm, L_cpp); @@ -816,25 +757,13 @@ void umat_sma_unified_T(const string &umat_name, const vec &Etot, const vec &DEt Bhat(1,1) = sum(dPhiRdsigma%kappa_j[1]) - K(1,1); const std::vector dPhidsigma_l = { dPhiFdsigma, dPhiRdsigma }; - // tangent_algorithmic is clamped to continuum for the SMA kernels: the - // finite-difference transformation-flow Hessian makes the local system - // near-singular on the transformation plateau (rcond ~ 1e-17; whether the - // backend flags it is LAPACK-dependent — see the thermomechanical twin, - // which crashed CI through this path). Re-enable together with the exact - // CPP Hessian rework. + // algorithmic clamped to continuum: the FD flow Hessian is near-singular on the plateau const int tangent_mode_eff = (tangent_mode == tangent_algorithmic || tangent_mode == tangent_closest_point) ? tangent_continuum : tangent_mode; ct = compute_tangent_operator( tangent_mode_eff, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // Simo-Hughes algorithmic tangent (closest-point). The forward transformation strain-flow - // Lambda_eps^F = Hcur(sigma)*dDrucker(sigma) + DM*sigma + Dalpha_T couples to stress through - // Hcur and the (non-quadratic) Drucker direction. We form d(Hcur*dDrucker)/dsigma by central - // finite difference of that flow (robust; avoids the Drucker J2/J3 Hessian), add the analytic - // linear stiffness-difference term DM, and hold ETMean (transformation state) fixed. - // Lambda_eps^R = -ETMean - DM*sigma - Dalpha_T -> dLambda^R/dsigma = -DM. - // The transformation-state coupling (dLambda/dETMean . dETMean/dsigma, dxi/dsigma) is the - // deferred state-coupling term (closest-point/CPP rework, future release). + // Simo-Hughes: dLambda^F/dsigma by central FD + DM, dLambda^R/dsigma = -DM, ETMean held fixed auto lambdaTF_at = [&](const vec &s) -> vec { return sma_transformation_flow(s, sigmacrit, Hmin, Hmax, k1, aniso_criteria, DFA_params, diff --git a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp index ac8f3e1f2..e82a1a9c3 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/SMA/unified_TR.cpp @@ -17,16 +17,6 @@ ///@file unified_TR.cpp ///@brief Unified SMA model with transformation AND martensite reorientation -///@brief Based on Chatziathanasiou Ph.D Thesis (2016) -///@brief Implemented by Y. Chemisky and D. Chatziathanasiou -///@brief -///@brief The variant is determined by umat_name: -///@brief SMRDI: isotropic elasticity, Drucker criteria -///@brief SMRDC: cubic elasticity, Drucker criteria -///@brief SMRAI: isotropic elasticity, anisotropic Drucker (DFA) criteria -///@brief SMRAC: cubic elasticity, anisotropic Drucker (DFA) criteria -///@brief -///@brief Implemented in 1D-2D-3D. #include #include @@ -91,13 +81,13 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE mat M_M; if (!cubic_elasticity) { - M_A = inv(L_iso(props(1), props(3), "Enu")); - M_M = inv(L_iso(props(2), props(4), "Enu")); + M_A = M_iso(props(1), props(3), "Enu"); + M_M = M_iso(props(2), props(4), "Enu"); offset = 5; } else { - M_A = inv(L_cubic(props(1), props(3), props(5), "EnuG")); - M_M = inv(L_cubic(props(2), props(4), props(6), "EnuG")); + M_A = M_cubic(props(1), props(3), props(5), "EnuG"); + M_M = M_cubic(props(2), props(4), props(6), "EnuG"); offset = 7; } @@ -173,7 +163,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE // ###################### Elastic compliance and stiffness ################################# mat DM = M_M - M_A; mat M_eff = xi*M_M + (1. - xi)*M_A; - L = inv(M_eff); + L = inv_cubic(M_eff); vec alpha = (alphaM_iso*xi + alphaA_iso*(1.-xi))*Ith(); vec Dalpha = (alphaM_iso - alphaA_iso)*Ith(); @@ -226,11 +216,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE Y0t = 0.5*rhoDs0*(MsSmooth - AfSmooth) - a3; } - // Rotate strain-like internal variables. The back-strain accumulator areo - // store the same flow direction as the transformation/reorientation flow \Lambda_\bullet - // (gradient of Prager/Drucker stress potential), which has engineering shear - // convention — they MUST rotate as strain (legacy SmartPlus used rotate_stress - // by mistake, producing factor-2 errors on shears after any finite rotation). + // areo carries engineering shears like the flows: rotate as strain, not stress ET = rotate_strain(ET, DR); EReo = rotate_strain(EReo, DR); areo = rotate_strain(areo, DR); @@ -239,11 +225,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE vec stress_start = stress; vec ET_start = ET; double xiF_start = xiF, xiR_start = xiR; - // xi_start is the martensite volume fraction available for reorientation at the - // start of this increment. It is the "existing" martensite that can be reoriented; - // any new martensite formed by forward transformation in this increment forms - // already aligned with \sigma and does not need reorienting. xi_start is constant - // during the local Newton loop -> no chain-rule contributions to K(2,F) or K(2,R). + // martensite available for reorientation, constant over the local loop: K(2,F) = K(2,R) = 0 const double xi_start = xi; // xi-floor for divisions @@ -275,10 +257,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE vec lambdaTR = -1.*ETMean; - // Back-stress + reorientation flow. - // Per Chatziathanasiou §4.3, the only kinematic back-strain in the model - // is v^re (= `areo`). aF/aR were SmartPlus legacy artifacts and have been - // removed: forward and reverse transformation use isotropic hardening only. + // back-stress from v^re (areo) only, Chatziathanasiou §4.3 vec X = HReo * (areo % Ir05()); double a_eq = Mises_strain(areo); @@ -377,9 +356,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE vec dPhiRdsigma = zeros(6); double dPhiRdpTR = 0.; - // Reorientation — aF/aR are not part of the Chatziathanasiou framework - // (only v^re = areo). dPhiReoda{F,R} and dPhiReodxi{F,R} are zero by - // construction with the thesis-aligned X = HReo\cdot areo\cdot Ir05. + // Reorientation double dlambda1Reo_arg = 0.; // d\lambda_1Reo/d(a_eq/ETRmax) vec dPhiReodsigma = zeros(6); vec dPhiReodareo = zeros(6); @@ -391,13 +368,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE int compteur = 0; double error = 1.; - // tangent_closest_point: the closest-point projection replaces the CCP loop, with the same - // frozen-mixture construction as unified_T — L(xi_n), alpha(xi_n) and the effective flows - // Lambda_F = lambdaTF + DM sigma + Dalpha theta, Lambda_R = -ETMean - DM sigma - Dalpha theta - // (exact since M(xi), alpha(xi) are linear in xi), Lambda_re = xi_start lambdaReo_raw. The state - // (xi, ET, ETMean, areo, X, lambda1Reo, Hf, lambda0/1) is rebuilt from the start values at every - // iterate — the back-strain by a Newton on its backward-Euler equation, ETMean in closed form — - // and the callbacks return the analytic TOTAL derivatives of that composed map (linearise below). + // closest point: frozen-mixture construction of unified_T (see header) ReturnMappingResult rm; const bool use_cpp = (tangent_mode == simcoon::tangent_closest_point); const mat L_cpp = use_cpp ? L : mat(); // frozen stiffness used consistently by the helper and the tangent @@ -411,8 +382,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE bool lin_stale = true; // the linearisation below is not the one of the refreshed state const vec T_shear = Ir05(); // engineering -> tensorial shear - // Reorientation direction r = dDrucker(sigma_eff), its Hessian, and d sigma_eff / d areo for - // sigma_eff = sig - (1 + lambda1Reo(a_eq/ETRmax)) HReo T areo (d a_eq/d areo = eta_strain % T). + // r = dDrucker(sigma_eff), its Hessian, and dsigma_eff/dareo auto reo_direction = [&](const vec &seff) -> vec { if (!(Mises_stress(seff) > simcoon::iota)) return zeros(6); return aniso_criteria ? dDrucker_ani_stress(seff, DFA_params, prager_b, prager_n) @@ -444,10 +414,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE // Hcur, forward flow (explicit in sigma) Hcur = Hmin + (Hmax - Hmin)*(1. - exp(-1.*k1*std::max(Mises_stress(sig) - sigmacrit, 0.))); lambdaTF = build_lambdaTF(sig); - // Back-strain v^re: Newton on R(areo) = areo - areo_n - Dl_re r(sigma_eff(areo)) = 0, - // Jacobian I - Dl_re ddDrucker(sigma_eff) dsigma_eff/dareo, started from the start value - // (a sweep on the same equation contracts only while Dl_re HReo (1 + lambda1Reo) / - // Mises(sigma_eff) < 1). No root within the budget rejects the trial (the helper halves it). + // back-strain by Newton (a sweep does not contract); no root rejects the trial areo = areo_n; set_sigma_eff(sig); lambdaReo_raw = reo_direction(sigma_eff); @@ -466,9 +433,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE } lambdaReo = xi_start * lambdaReo_raw; EReo = EReo_n + Dl(2)*lambdaReo; - // ETMean = dev(ET)/xi with ET = ET_n + Dl_F lambdaTF - Dl_R ETMean + Dl_re lambdaReo: closed - // form ETMean (xi + Dl_R) = dev(ET_n) + Dl_F dev(lambdaTF) + Dl_re dev(lambdaReo) (see - // unified_T; the sweep form diverges when Dl_R > xi), with the loop's guard. + // ETMean in closed form: the sweep diverges when Dl_R > xi const double xi_before = xi + Dl(1); ETMean_is_flow = !(xi_before > simcoon::precision_umat); ETMean = ETMean_is_flow ? lambdaTF @@ -479,8 +444,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE ETMean = lambdaTF; ET = ET_start + Dl(0)*lambdaTF - Dl(1)*ETMean + Dl(2)*lambdaReo; } - // Hardening at xi clamped to [0,1] (an iterate can leave the bounds, where pow(xi, n) is - // NaN; the slope is infinite there, see unified_T); the penalties are C1-continued. + // Hf at xi clamped to [0,1]: pow(xi, n) is NaN outside for fractional n compute_Hf(std::min(std::max(xi, 0.), 1.), HfF, HfR); lambda1 = lagrange_pow_1(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); lambda0 = -1.*lagrange_pow_0(xi, c_lambda, p0_lambda, n_lambda, alpha_lambda); @@ -504,15 +468,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE auto LamF_val = [&](const vec &sig) { return vec(lambdaTF + DM*sig + Dalpha*thp); }; auto LamR_val = [&](const vec &sig) { return vec(-1.*ETMean - DM*sig - Dalpha*thp); }; auto LamReo_val = [&](const vec &) { return lambdaReo; }; - // Analytic total derivatives at the refreshed state (sig, Dl_cur), built once per iterate - // (forward / reverse rows: see unified_T). Reorientation, with r = dDrucker(sigma_eff), - // G = ddDrucker(sigma_eff), A = dsigma_eff/dareo and J = I - Dl_re G A (the Jacobian of the - // back-strain equation): - // dareo/dsigma = J^-1 Dl_re G, dareo/dDl_re = J^-1 r - // dsigma_eff/dsigma = I + A dareo/dsigma, dsigma_eff/dDl_re = A dareo/dDl_re - // Phi_re = Drucker(sigma_eff) - YReo, Lambda_re = xi_start r - // ETMean (xi_n + Dl_F) = dev(ET_n) + Dl_F dev(lambdaTF) + Dl_re dev(Lambda_re) - // K_lj = dPhi_l/dDl_j and the flux chain c_j = sum_k Dl_k dLambda_k/dDl_j. + // analytic total derivatives at (sig, Dl_cur), once per iterate (derivation in header) mat dev_op = eye(6, 6); // dev() of a Voigt vector dev_op.submat(0, 0, 2, 2) -= ones(3, 3)/3.; vec dPhiF_ds = zeros(6), dPhiR_ds = zeros(6), dPhiReo_ds = zeros(6), c_F = zeros(6), c_Reo = zeros(6); @@ -604,19 +560,17 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE Ds_j = rm.Dlambda; s_j += Ds_j; xiF = s_j(0); xiR = s_j(1); pTR = s_j(2); - // xi, ET, EReo, areo, ETMean, flows, Hf, penalties are inner-consistent already. DETF = Ds_j(0)*lambdaTF; DETR = Ds_j(1)*ETMean; DEReo = Ds_j(2)*lambdaReo; M_eff = xi*M_M + (1. - xi)*M_A; // output L at the converged xi (the tangent keeps L_cpp) - L = inv(M_eff); + L = inv_cubic(M_eff); DM_sig = DM*stress; A_xiF = rhoDs0*(T + DT) - rhoDE0 + 0.5*sum(stress%DM_sig) + sum(stress%Dalpha)*thp - HfF; A_xiR = -1.*rhoDs0*(T + DT) + rhoDE0 - 0.5*sum(stress%DM_sig) - sum(stress%Dalpha)*thp + HfR; } else { - // step cut (error stays 1, the guard below requests it), never a cutting-plane fallback; - // the start state back (Ds_j, DETF, DETR, DEReo still zero, xiF / xiR / pTR / s_j untouched) + // step cut (error stays 1, the guard below requests it), never a cutting-plane fallback stress = stress_start; refresh_state(stress, zeros(3)); } @@ -626,7 +580,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE // Reuss mixing M_eff = xi*M_M + (1. - xi)*M_A; - L = inv(M_eff); + L = inv_cubic(M_eff); DM_sig = DM*stress; Dalpha_T = Dalpha*(T+DT-T_init); @@ -640,17 +594,9 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE lambdaTF = build_lambdaTF(stress); lambdaTR = -1. * ETMean; - // Back-stress for the reorientation surface — see the framework - // rationale at the first computation of X above. X = HReo * (areo % Ir05()); a_eq = Mises_strain(areo); - // Saturation argument is the *intrinsic* (per-unit-martensite) ratio - // ||v^re|| / ETRmax, NOT ||v^re|| / (\xi \cdot ETRmax). v^re is the per-unit back-strain - // (its evolution \dot{v}^re = \dot{p}^re \cdot \Lambda_V^re is intrinsic per the thesis); the - // macroscopic \varepsilon^re reads \xi \cdot \bar{\varepsilon}^re. Putting \xi in the denominator of the - // saturation argument (legacy SmartPlus form) makes the Lagrange penalty - // blow up when \xi -> 0 even if ||v^re|| is tiny, producing astronomical - // \sigma_eff = \sigma - (1+1e20)\cdot X garbage. + // intrinsic saturation argument ||v^re||/ETRmax, no xi: the penalty would blow up as xi -> 0 lambda1Reo = lagrange_pow_1(a_eq/ETRmax, c_lambdaReo, p0_lambdaReo, n_lambdaReo, alpha_lambdaReo); sigma_eff = stress - (1. + lambda1Reo) * X; @@ -665,16 +611,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE } else { lambdaReo_raw = zeros(6); } - // Thesis §4 (eq 503): the macroscopic flow contributing to \varepsilon^T from - // reorientation is \xi -scaled — \Lambda_\varepsilon^re = \xi \cdot \bar{\Lambda}^re — because it only - // moves the variant orientation in the pre-existing martensite of - // volume \xi . The back-strain flow \Lambda_V^re = \bar{\Lambda}^re is NOT \xi -scaled: the - // back-stress tracks the intrinsic reorientation history within the - // martensite domain, evolving per unit of \dot{p}^re regardless of how much - // martensite there is. Criterion gradient \partial Phi^re/\partial \sigma also stays - // un-scaled (it's just the stress gradient of the yield function). - // This asymmetric structure keeps B(2,2) finite at \xi =0 through the - // K(2,2) entry from the back-strain channel. + // only the macroscopic flow is xi-scaled (thesis §4 eq. 503, see header) lambdaReo = xi_start * lambdaReo_raw; // macroscopic flow into \varepsilon^T etaReo = lambdaReo_raw; // \partial Phi^re/\partial \sigma — un-scaled lambda_areo = lambdaReo_raw; // back-strain v^re evolution — un-scaled @@ -701,17 +638,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE double YtR = Y0t + D*PhihatR; Phi(1) = -1.*PhihatR + A_xiR + lambda0 - YtR; - // Reorientation criterion (Chatziathanasiou thesis eq 664, Form B): - // Phi^re = Drucker(\sigma - (1+\lambda_1Reo)\cdot X) - Y^re (UN-scaled in \xi ) - // No \xi multiplier on the criterion — the yield condition is per unit - // martensite volume. The \xi -scaling lives in the macroscopic flow - // (\Lambda_\varepsilon^re = \xi \cdot \bar{\Lambda}^re, applied above to lambdaReo and kappa_j[2]). - // At \xi_start = 0, \kappa_2 = 0 so Phi^re cannot be driven down through \sigma , - // but it can still be driven down by growing v^re (the un-scaled - // back-strain channel) — which is the correct physical statement: - // the kinematic back-strain tracks the intrinsic reorientation - // history within the martensite domain even when no macroscopic strain - // is yet produced. B(2,2) stays finite via the back-strain entry in K(2,2). + // Reorientation criterion, per unit martensite (thesis eq. 664, Form B) double Prager_sigma_eff = aniso_criteria ? Drucker_ani_stress(sigma_eff, DFA_params, prager_b, prager_n) : Drucker_stress(sigma_eff, prager_b, prager_n); @@ -758,63 +685,30 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE dA_xiRdxiR = -dHfR; dPhiRdpTR = 0.; - // Phi_R partial w.r.t. ETMean is well-defined and finite: - // Phi_R = -\sigma :ETMean + A_xiR + \lambda_0 - YtR, with YtR = Y0t + D\cdot \sigma :ETMean. - // => \partial Phi_R/\partial ETMean = -(1+D)\cdot \sigma + // K(1,.) through ETMean: avoids a ~12-digit cancellation at small xi (see unified_T.hpp) const vec dPhiRdETMean = -(1. + D) * stress; - // "Finite" Phi_R partials w.r.t. \xi (only the hardening + Lagrange-penalty - // pieces; the \sigma :dev(\varepsilon^T)/\xi = \sigma :ETMean parts that would otherwise blow up - // as \xi ->0 are absorbed into the \Lambda_ETMean chain-rule contributions below). - const double dPhiRdxi_finite_F = dA_xiRdxiF + dlambda0dxiF; // +dHfR - d\lambda_0/d\xi - const double dPhiRdxi_finite_R = dA_xiRdxiR + dlambda0dxiR; // -dHfR + d\lambda_0/d\xi - // The legacy dPhiRdET and dPhihatRdxiF symbols are no longer assembled - // here — K(1,\cdot ) is built directly from dPhiRdETMean \cdot \Lambda_ETMean^j below - // to avoid the +(1+D)\cdot \sigma :ETMean/\xi - (1+D)\cdot \sigma :\Lambda_\varepsilon^j/\xi = (finite) cancellation - // that loses ~12 digits at \xi \approx 1e-12. - - // ---- Reorientation derivatives (Form B, consistent linearisation) ---- - // Saturation argument: ||v^re||/ETRmax (intrinsic, no \xi ). + const double dPhiRdxi_finite_F = dA_xiRdxiF + dlambda0dxiF; // +dHfR - d\lambda_0/d\xi + const double dPhiRdxi_finite_R = dA_xiRdxiR + dlambda0dxiR; // -dHfR + d\lambda_0/d\xi + + // ---- Reorientation derivatives (Form B) ---- dlambda1Reo_arg = dlagrange_pow_1(a_eq/ETRmax, c_lambdaReo, p0_lambdaReo, n_lambdaReo, alpha_lambdaReo); const double eta_dot_X = sum(etaReo % X); // \eta_Reo : X (tensor double contraction) - // Gradient of a_eq = Mises_strain(areo): eta_strain is the strain-like direction - // (engineering shears), its gradient carries half shears, hence % Ir05. + // eta_strain has engineering shears; the gradient of a_eq carries half shears, hence % Ir05 const vec eta_strain_areo = (a_eq > simcoon::iota) ? vec(eta_strain(areo) % Ir05()) : vec(zeros(6)); - // d\lambda_1Reo/dareo = d\lambda_1Reo/d(arg) \cdot (1/ETRmax) \cdot \eta_strain(areo) const vec dlambda1Reo_dareo = dlambda1Reo_arg * (1./ETRmax) * eta_strain_areo; dPhiReodsigma = etaReo; - - // \partial \Phi^re/\partial areo = -(1+\lambda_1)\cdot H^Reo\cdot (\eta_Reo \odot Ir05) - (\eta_Reo:X)\cdot d\lambda_1Reo/dareo dPhiReodareo = -(1. + lambda1Reo) * HReo * (etaReo % Ir05()) - eta_dot_X * dlambda1Reo_dareo; - - // Saturation argument no longer depends on \xi => d\lambda_1Reo/d\xi_{F,R} = 0. dPhiReodpTR = 0.; - // ---- Assemble K (state-coupling block, excluding the \sigma -flow term) ---- - // mech 0 (forward) evolves \xi^F (+1 in \xi ), \varepsilon^F via lambdaTF - // mech 1 (reverse) evolves \xi^R (-1 in \xi ), \varepsilon^R via lambdaTR - // mech 2 (reorient.) evolves pTR (+1), \varepsilon^re_macro = \xi \cdot \bar{\Lambda}^re into \varepsilon^T, - // EReo via lambdaReo (= \xi \cdot \bar{\Lambda}^re), areo via \bar{\Lambda}^re - // \varepsilon^T is shared by all mechanisms; the chain rule for row 1 routes - // through ETMean as the natural intermediate (see K(1,\cdot ) below). + // ---- K: state-coupling block, without the sigma-flow term ---- K(0,0) = dPhiFdxiF; K(0,1) = dPhiFdxiR; K(0,2) = dPhiFdpTR; - // ---- K(1,\cdot ) via \Lambda_ETMean^j chain rule (no \sigma /\xi blow-up) ---- - // For a mechanism j with kinetic flow \Lambda_\varepsilon^j and \xi -growth rate \Lambda_\xi^j, - // the rate of change of ETMean = dev(\varepsilon^T)/\xi is - // \Lambda_ETMean^j = (dev(\Lambda_\varepsilon^j) - \Lambda_\xi^j \cdot ETMean) / \xi - // Closed forms: - // \Lambda_ETMean^F = (\Lambda_\varepsilon^F - ETMean)/\xi (\Lambda_\varepsilon^F deviatoric, \Lambda_\xi^F = +1) - // \Lambda_ETMean^R = 0 (\Lambda_\varepsilon^R = -ETMean, \Lambda_\xi^R = -1: cancels) - // \Lambda_ETMean^re = \bar{\Lambda}^re (\Lambda_\varepsilon^re_macro = \xi \cdot \bar{\Lambda}^re, \Lambda_\xi^re = 0) - // The ETMean fallback (= lambdaTF when \varepsilon^T and \xi are both small) makes - // (\Lambda_\varepsilon^F - ETMean) = 0 identically there, so \Lambda_ETMean^F = 0/0 -> 0 in - // floating point as long as we don't actually divide by \xi when both - // numerator and \xi are below their respective floors. + // K(1,.) via Lambda_ETMean^j (header); no division when the fallback ETMean = lambdaTF is active vec Lambda_ETMean_F = zeros(6); if (Mises_strain(lambdaTF - ETMean) > simcoon::iota) { Lambda_ETMean_F = (lambdaTF - ETMean) / xi_div; @@ -825,9 +719,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE K(1,1) = dPhiRdxi_finite_R + sum(dPhiRdETMean % Lambda_ETMean_R); K(1,2) = dPhiRdpTR + sum(dPhiRdETMean % Lambda_ETMean_re); - // K(2,F) = K(2,R) = 0 by construction: \Phi^re depends on internal state - // only through areo (via X) and \sigma ; aF and aR are not in the model, and - // \xi no longer appears in the saturation argument after the v^re fix. + // Phi^re depends on the state only through areo: K(2,F) = K(2,R) = 0 K(2,0) = 0.; K(2,1) = 0.; K(2,2) = dPhiReodpTR + sum(dPhiReodareo % lambda_areo); @@ -846,9 +738,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE Y_crit(1) = std::max(fabs(Y0t) + fabs(D*PhihatR), simcoon::iota); Y_crit(2) = std::max(YReo, simcoon::iota); // guard the FB /Y_crit normalization against a (degenerate) YReo = 0 - // Active set: a mechanism with Phi < 0 and B(j,j) = 0 has a singular FB row and - // ds_j = 0 anyway (the reorientation row at zero effective stress): see the - // active-set note in unified_TR.hpp. The reduced solve equals the full one otherwise. + // active set: drop a mechanism with Phi < 0 and B(j,j) = 0 (singular FB row, see header) const arma::uvec active = arma::find((Phi >= 0.) + (arma::abs(B.diag()) >= simcoon::iota)); vec Ds_active = Ds_j(active); // the helper accumulates Dp += dp in place vec ds_active(active.n_elem); @@ -864,7 +754,6 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE // State updates ET = ET + ds_j(0)*lambdaTF + ds_j(1)*lambdaTR + ds_j(2)*lambdaReo; EReo = EReo + ds_j(2)*lambdaReo; - // aF and aR are not updated (legacy accumulators kept at zero). areo = areo + ds_j(2)*lambda_areo; xiF = s_j(0); @@ -887,14 +776,12 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE } } - // If the local Newton did not converge within maxiter_umat, request a sub-step - // (tnew_dt < 1) from the global solver rather than committing a non-converged increment. + // non-converged local solve: request a sub-step, never commit it if (error > simcoon::precision_umat) { tnew_dt = 0.5; } - // ---- Tangent assembly via shared 3-mechanism helper; closest-point branch: the exact - // operator of the converged solve with the frozen L_cpp (elastic placeholder on a step cut) ---- + // closest point: exact operator of the converged solve with the frozen L_cpp (elastic on a step cut) ContinuumTangent ct; if (use_cpp) { ct = cpp_consistent_tangent(rm, L_cpp); @@ -909,22 +796,13 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE } const std::vector dPhidsigma_l = { dPhiFdsigma, dPhiRdsigma, dPhiReodsigma }; - // tangent_algorithmic is clamped to continuum for the SMA kernels: the - // finite-difference transformation-flow Hessian makes the local system - // near-singular on the transformation plateau (rcond ~ 1e-17; whether the - // backend flags it is LAPACK-dependent — see the thermomechanical twin, - // which crashed CI through this path). Re-enable together with the exact - // CPP Hessian rework. + // algorithmic clamped to continuum: the FD flow Hessian is near-singular on the plateau const int tangent_mode_eff = (tangent_mode == tangent_algorithmic || tangent_mode == tangent_closest_point) ? tangent_continuum : tangent_mode; ct = compute_tangent_operator( tangent_mode_eff, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // Simo-Hughes algorithmic tangent (closest-point), 3-mechanism. dLambda/dsigma by central - // FD of the transformation flows: forward Hcur(sigma)*dDrucker(sigma); reorientation - // xi_start*dDrucker(sigma_eff), sigma_eff = sigma-(1+lambda1Reo)*X; plus the analytic linear - // DM term. ETMean, X, lambda1Reo held fixed -> transformation/back-strain state-coupling is - // the deferred term (closest-point/CPP rework, future release). + // Simo-Hughes: dLambda/dsigma by central FD + DM; ETMean, X, lambda1Reo held fixed auto lambdaTF_at = [&](const vec &s) -> vec { return sma_transformation_flow(s, sigmacrit, Hmin, Hmax, k1, aniso_criteria, DFA_params, @@ -954,11 +832,7 @@ void umat_sma_unified_TR(const string &umat_name, const vec &Etot, const vec &DE } Lt = ct.Lt; - // ---- Energy partition (SMA: Wm_ir is always 0; everything is either recoverable or dissipated) ---- - // Back-strain accumulators (aF, aR, areo) are kinematic *history* variables that only - // shift the reorientation criterion — they do NOT store energy in the free energy \psi , so - // there are no X:\Delta a contributions. Partition mirrors umat_sma_unified_T extended with - // the reorientation strain increment. + // energy partition: Wm_ir = 0, the back-strain stores no energy (see header) double DxiF = Ds_j[0]; double DxiR = Ds_j[1]; diff --git a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Prony_Nfast.cpp b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Prony_Nfast.cpp index 8d7fefd64..3eb9ffdda 100644 --- a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Prony_Nfast.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Prony_Nfast.cpp @@ -1,4 +1,21 @@ -///@file Prony.hpp +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file Prony_Nfast.cpp ///@brief User subroutine for Prony series viscoelastic model in 3D case ///@author Chemisky, Chatzigeorgiou ///@version 1.0 @@ -16,19 +33,6 @@ using namespace std; using namespace arma; -///@brief The viscoelastic Prony series model requires 4+N*4 constants: -// ------------------- -///@brief props(0) = E0 - Thermoelastic Young's modulus -///@brief props(1) = nu0 - Thermoelastic Poisson's ratio -///@brief props(2) = alpha_iso - Thermoelastic CTE -///@brief props(3) = N_prony - Number of Prony series -///@brief props(4+i*4) = E_visco(i) - Viscoelastic Young modulus of Prony branch i -///@brief props(4+i*4+1) = nu_visco(i) - Viscoelastic Poisson ratio of Prony branch i -///@brief props(4+i*4+2) = etaB_visco - Viscoelastic Bulk viscosity of Prony branch i -///@brief props(4+i*4+3) = etaS_visco - Viscoelastic Bulk viscosity of Prony branch i - -///@brief Number of statev required for thermoelastic constitutive law : 7+N*7 - namespace simcoon { void umat_prony_Nfast(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) @@ -98,8 +102,6 @@ void umat_prony_Nfast(const string &umat_name, const vec &Etot, const vec &DEtot Wm_d = 0.; } - // Implicit (backward-Euler) step of the Maxwell branches in closed form: the exact solution of - // the discrete equations and its consistent tangent (linear_viscoelastic.hpp) const std::vector EV_i_start = EV_i; // start strain from the start state, not Etot: condensed callers do not know every component vec sigma_EV = stress_start; @@ -126,7 +128,6 @@ void umat_prony_Nfast(const string &umat_name, const vec &Etot, const vec &DEtot Wm_ir += 0.; Wm_d += Dgamma_loc; - //Return the statev; statev(0) = T_init; //From the statev to the internal variables statev(1) = EV_tilde(0); diff --git a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_Nfast.cpp b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_Nfast.cpp index 3a94f6a70..1c800f323 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_Nfast.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_Nfast.cpp @@ -1,4 +1,21 @@ -///@file Zener_fast.hpp +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file Zener_Nfast.cpp ///@brief User subroutine for Zener viscoelastic model with N viscoelastic Kelvin branches in series in 3D case, with thermoelastic effect ///@brief This implementation uses a single scalar internal variable for the evaluation of the viscoelastic strain increment ///@author Chemisky, Chatzigeorgiou @@ -17,21 +34,6 @@ using namespace std; using namespace arma; -///@brief The viscoelastic burger model requires 4+N*4 constants: -// ------------------- -// -///@brief -///@brief props(0) = E0 - Thermoelastic Young's modulus -///@brief props(1) = nu0 - Thermoelastic Poisson's ratio -///@brief props(2) = alpha_iso - Thermoelastic CTE -///@brief props(3) = N_kelvin - Number of Kelvin branches -///@brief props(4+i*4) = E_visco(i) - Viscoelastic Young modulus of Zener branch i -///@brief props(4+i*4+1) = nu_visco(i) - Viscoelastic Poisson ratio of Zener branch i -///@brief props(4+i*4+2) = etaB_visco - Viscoelastic Bulk viscosity of Zener branch i -///@brief props(4+i*4+3) = etaS_visco - Viscoelastic Bulk viscosity of Zener branch i - -///@brief Number of statev required for thermoelastic constitutive law : 7+N*7 - namespace simcoon { void umat_zener_Nfast(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) @@ -100,8 +102,6 @@ void umat_zener_Nfast(const string &umat_name, const vec &Etot, const vec &DEtot Wm_d = 0.; } - // Implicit (backward-Euler) step of the Kelvin branches in closed form: the exact solution of - // the discrete equations and its consistent tangent (linear_viscoelastic.hpp) const std::vector EV_i_start = EV_i; const vec eps_e = Etot + DEtot - alpha*(T + DT - T_init); const LinearViscoStep st = kelvin_series_step(L0, L_i, H_i, EV_i_start, eps_e, alpha, DTime); @@ -127,7 +127,6 @@ void umat_zener_Nfast(const string &umat_name, const vec &Etot, const vec &DEtot Wm_ir += 0.; Wm_d += Dgamma_loc; - //Return the statev; statev(0) = T_init; //From the statev to the internal variables statev(1) = EV(0); diff --git a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp index cc1320319..3124a1d1a 100755 --- a/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp +++ b/src/Continuum_mechanics/Umat/Mechanical/Viscoelasticity/Zener_fast.cpp @@ -1,5 +1,22 @@ -///@file Zener_fast.hpp -///@brief User subroutine for Zener viscoelastic model in 3D case, with thermoelastic effect(Poynting-Thomson model) +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file Zener_fast.cpp +///@brief Standard linear solid, Poynting-Thomson (Kelvin) form (ZENER) ///@brief This implementation uses a single scalar internal variable for the evaluation of the viscoelastic strain increment ///@author Chemisky, Chatzigeorgiou ///@version 1.0 @@ -17,18 +34,6 @@ using namespace std; using namespace arma; -///@brief The viscoelastic Zener model requires 10 constants: -// ------------------- -///@brief props(0) = E0 - Thermoelastic Young's modulus -///@brief props(1) = nu0 - Thermoelastic Poisson's ratio -///@brief props(2) = alpha_iso - Thermoelastic CTE -///@brief props(3) = E1 - Viscoelastic Young modulus of Zener branch i -///@brief props(4) = nu1 - Viscoelastic Poisson ratio of Zener branch i -///@brief props(5) = etaB1 - Viscoelastic Bulk viscosity of Zener branch i -///@brief props(6) = etaS1 - Viscoelastic Bulk viscosity of Zener branch i - -///@brief Number of statev required for thermoelastic constitutive law - namespace simcoon { void umat_zener_fast(const string &umat_name, const vec &Etot, const vec &DEtot, vec &stress, mat &Lt, mat &L, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT, const double &Time, const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) @@ -94,8 +99,7 @@ void umat_zener_fast(const string &umat_name, const vec &Etot, const vec &DEtot, vec EV1_start = EV1; vec A_v_start = stress_start - L1*EV1; - // Implicit (backward-Euler) step of the Kelvin branch in closed form: the exact solution of - // the discrete equations and its consistent tangent (linear_viscoelastic.hpp) + // backward Euler in closed form, with its consistent tangent (linear_viscoelastic.hpp) const vec eps_e = Etot + DEtot - alpha*(T + DT - T_init); const LinearViscoStep st = kelvin_series_step(L0, {L1}, {H1}, {EV1_start}, eps_e, alpha, DTime); EV1 = st.EV_i[0]; @@ -113,7 +117,6 @@ void umat_zener_fast(const string &umat_name, const vec &Etot, const vec &DEtot, Wm_ir += 0.; Wm_d += Dgamma_loc; - //Return the statev; statev(0) = T_init; statev(1) = v; statev(2) = EV1(0); diff --git a/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp b/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp index 7270596c8..89c3bbde6 100644 --- a/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp +++ b/src/Continuum_mechanics/Umat/Modular/damage_mechanism.cpp @@ -49,14 +49,6 @@ DamageMechanism::DamageMechanism(DamageType type) // ========== Configuration ========== void DamageMechanism::configure(const arma::vec& props, int& offset) { - // Props layout for damage: - // props[offset]: damage_type (if not set in constructor) - // props[offset+1]: Y_0 (damage threshold) - // props[offset+2]: Y_c (critical damage driving force) - // props[offset+3]: D_c (critical damage value, optional, default 0.99) - // props[offset+4]: A or n (damage parameter, depending on type) - - // Read parameters Y_0_ = props(offset); Y_c_ = props(offset + 1); offset += 2; @@ -102,9 +94,7 @@ void DamageMechanism::register_variables() { // ========== Constitutive Computations ========== double DamageMechanism::compute_driving_force(const arma::vec& sigma_eff, const arma::mat& /*S*/) const { - // Y = -dpsi/dD = psi_0, the UNDAMAGED energy, on the effective stress: exact for a linear - // block (M = the tangent compliance of the undamaged block, an approximation of psi_0 for a - // hyperelastic one). + // Y = psi_0 on the effective stress: exact for a linear block, tangent-compliance approximation if hyperelastic return 0.5 * arma::dot(sigma_eff, (M_cached_t_ * stress(sigma_eff)).to_arma_voigt()); } @@ -157,10 +147,7 @@ void DamageMechanism::compute_constraints( D_current_ = ivc_.get("D").scalar(); double Y_max = ivc_.get("Y_max").scalar(); - // Cache the compliance (raw + typed), keyed on the stiffness it inverts: L - // is the tangent of the current elastic state, which a state-dependent - // (hyperelastic) block moves at every refresh, while a linear block keeps - // it fixed and pays a single inversion. + // compliance cache keyed on L: a hyperelastic block moves L at every refresh, a linear one inverts once if (!M_cached_valid_ || !arma::approx_equal(L, L_cached_, "absdiff", 0.0)) { L_cached_ = L; M_cached_ = arma::inv(L); @@ -212,12 +199,7 @@ void DamageMechanism::compute_jacobian_contribution( arma::mat& B, int row_offset ) const { - // Unit diagonal for the history-type damage row. Phi = Y - Y_max is - // integrated explicitly (see compute_constraints / update), so the damage - // multiplier is not solved implicitly; a unit slope keeps the FB system - // well-conditioned without steering the (self-satisfying) damage row. - // Cross-mechanism coupling (plasticity <-> damage) still flows through the - // off-diagonal dPhi_dsigma . kappa terms assembled by the orchestrator. + // unit diagonal: the damage row is explicit; coupling flows through the off-diagonal dPhi . kappa terms B(row_offset, row_offset) = 1.0; } @@ -267,10 +249,7 @@ void DamageMechanism::update( const arma::vec& /*ds*/, int /*offset*/ ) { - // Fixed point on D (the FB multiplier increment is unused): D = f(max(Y_max at step - // start, Y of this iterate)). The history is the one of the START of the step, never the - // running maximum over iterates, whose overshooting first iterate would freeze too much - // damage; the orchestrator iterates until consistency_residual() vanishes. + // history from the step START, never the running max over iterates (an overshooting iterate freezes damage) double& Y_max = ivc_.get("Y_max").scalar(); Y_max = std::max(ivc_.get("Y_max").scalar_start(), Y_current_); double& D = ivc_.get("D").scalar(); diff --git a/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp b/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp index 71c43abee..991572003 100644 --- a/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp +++ b/src/Continuum_mechanics/Umat/Modular/elasticity_module.cpp @@ -26,6 +26,8 @@ along with simcoon. If not, see . #include #include #include +#include +#include #include #include @@ -121,9 +123,7 @@ void ElasticityModule::configure_transverse_isotropic(double EL, double ET, doub + std::to_string(static_cast(conv)) + " (valid: 0)"); } - // Validate BEFORE calling the library builders: L_isotrans terminates the - // process (exit(0)!) on an invalid axis — a throw here keeps the failure - // catchable and attributable. + // validate first, with the module's own message if (axis < 1 || axis > 3) { throw std::invalid_argument( "ElasticityModule: transverse-isotropic axis must be 1, 2 or 3, got " @@ -188,19 +188,7 @@ void ElasticityModule::configure_hyper_invariants(HyperPotential potential, cons alpha_ = alpha_scalar * Ith(); - // At the stress-free ground state (b = I, J = 1, U'(1) = 0) the invariant - // builders reduce to the isotropic K = U''(1), mu = 2 (W_1 + W_2). Closed - // form rather than a full evaluate(): the module is rebuilt on every UMAT - // call. refresh_tensors() rejects a ground state that is not positive - // definite, a genuine admissibility check on the parameters. - // A fibre family is unstretched at the ground state: tr(A) = 3 kappa_d + (1 - 3 kappa_d) - // = 1, so its derivatives vanish there (the tension-only switch) and mu0 is the matrix - // one. L_ is therefore the ISOTROPIC ground state even for an anisotropic potential -- - // a fact, not an approximation, and exact for the fibre-inactive state it describes. - // Nothing consumes it as an anisotropic stiffness: plasticity and damage are handed - // L_cur_, the current tangent. (The Prony branches ARE isotropic for an anisotropic - // potential, but because viscoelastic_mechanism.cpp builds each as L_iso(E_i, nu_i), - // not because of this L_ -- it enters there only as the compliance inv(L_0).) + // closed-form ground state, isotropic and exact even for an anisotropic potential (see header) const arma::mat I3 = arma::eye(3, 3); an_ = hyper_potential_anisotropy(potential, params); // parsed ONCE, read by evaluate() const hyper_invariants_dW dW0 = hyper_potential_derivatives( @@ -216,9 +204,7 @@ void ElasticityModule::configure_hyper_invariants(HyperPotential potential, cons } void ElasticityModule::configure(ElasticityType type, const arma::vec& props, int& offset) { - // Every linear block starts with the convention slot; the conv_string - // helpers (and the isotrans guard) validate the code inside the - // configure_* call. + // the conv_string helpers validate the convention slot inside configure_* switch (type) { case ElasticityType::ISOTROPIC: { // props: conv, C1, C2, alpha @@ -313,12 +299,16 @@ void ElasticityModule::evaluate(const arma::vec& eps_el, int ndi, int nshr, "ElasticityModule: a hyperelastic block supports ndi = 3 only (got " + std::to_string(ndi) + ")"); } - // eps_el -> V_el = exp(eps_el) -> b_el = exp(2 eps_el). Passing V_el where - // the response expects F is what makes the returned box tangent - // d(tau)/d(eps_el): the frame is corotational here, so R = I. + // V_el = exp(eps_el) passed where F is expected: the returned box tangent is d(tau)/d(eps_el), R = I const arma::mat eps_t = v2t_strain(eps_el); - const arma::mat V_el = eR_to_F(eps_t, arma::eye(3, 3)); - const arma::mat b_el = V_el * V_el; + arma::vec::fixed<3> e; + arma::mat::fixed<3,3> N; + if (!eig_sym_3x3(e, N, eps_t)) { + throw simcoon::exception_expmat_sym("Error in expmat_sym function inside ElasticityModule::evaluate."); + } + const arma::vec::fixed<3> exp_e = arma::exp(e); + const arma::mat V_el = spectral_compose_3x3(N, exp_e); + const arma::mat b_el = spectral_compose_3x3(N, exp_e % exp_e); const double J_el = std::exp(arma::trace(eps_t)); // An anisotropic potential carries reference fibre directions: push them forward with @@ -331,6 +321,13 @@ void ElasticityModule::evaluate(const arma::vec& eps_el, int ndi, int nshr, isochoric_invariants(b_el, J_el), J_el, A); // tau = dpsi/deps_el per reference volume (isotropic psi); corate 3, never the solver's. const bool total = (kinematic_form_ == KinematicForm::total); + if (!total && A.empty()) { + // isotropic: closed-form box from the Lie tangent (one decomposition); Truesdell box = Lie tangent + arma::mat Lt_Lie; + hyper_invariants_response(dW, b_el, J_el, V_el, 4, sigma, Lt_Lie, A); + Lt = Dtau_LieDD_2_DtauDe_stretch(Lt_Lie, e, N, v2t_stress(sigma)); + return; + } hyper_invariants_response(dW, b_el, J_el, V_el, total ? corate_type_total : 3, sigma, Lt, A); if (total) { // dual stress of H_el: equal to tau when coaxial, and what makes a fibre potential exact diff --git a/src/Continuum_mechanics/Umat/Modular/hardening.cpp b/src/Continuum_mechanics/Umat/Modular/hardening.cpp index 80b192fb7..650aeb219 100644 --- a/src/Continuum_mechanics/Umat/Modular/hardening.cpp +++ b/src/Continuum_mechanics/Umat/Modular/hardening.cpp @@ -29,23 +29,14 @@ along with simcoon. If not, see . namespace simcoon { namespace { -// Conjugacy: X = (2/3) C α (chapter 6 backstress↔back-strain relation). -// File-local helper so the convention has one canonical implementation across -// Prager / Armstrong-Frederick / Chaboche. -// -// X is a STRESS-like tensor: it shifts the stress in the yield function -// (σ - X) and pairs with the strain-like flow direction n in X:n. The -// back-strain α is stored strain-like (factor-2 shear); the product carries -// that convention, so the result is re-tagged as stress (via the 3x3, which -// is convention-free) — otherwise σ - X and X:n double-count the shear terms. +// X = (2/3) C alpha, re-tagged STRESS through the 3x3: alpha is strain-typed (factor-2 shear), +// so a strain-typed X would double-count the shear terms in sigma - X and X:n inline tensor2 backstress_t(double C, const tensor2& alpha) { return stress(arma::mat::fixed<3,3>(((2.0 / 3.0) * C * alpha).mat())); } } // namespace -// ============================================================================ -// ISOTROPIC HARDENING IMPLEMENTATIONS -// ============================================================================ +// ==== ISOTROPIC HARDENING IMPLEMENTATIONS ==== std::unique_ptr IsotropicHardening::create(IsoHardType type, int N) { switch (type) { @@ -70,10 +61,7 @@ void LinearHardening::configure(const arma::vec& props, int& offset) { offset += 1; } -// PowerLawHardening — onset regularization for m < 1 (rationale and the SMA -// lagrange_pow analogy are in the header Doxygen). Implementation notes only: -// the p <= 0 branch extends R linearly (slope a_reg_) so a transient negative -// FB iterate meets a finite, consistent slope instead of pow(negative, m). +// p <= 0: R extended linearly (slope a_reg_), so a negative FB iterate meets a finite slope namespace { constexpr double p_reg_powerlaw = 1.0e-6; ///< onset regularization cutoff (see header) } @@ -156,9 +144,7 @@ double CombinedVoceHardening::dR_dp(double p) const { return result; } -// ============================================================================ -// KINEMATIC HARDENING IMPLEMENTATIONS -// ============================================================================ +// ==== KINEMATIC HARDENING IMPLEMENTATIONS ==== std::unique_ptr KinematicHardening::create(KinHardType type, int N) { switch (type) { @@ -192,10 +178,7 @@ void PragerHardening::compute_backstresses(const InternalVariableCollection& ivc double PragerHardening::hardening_modulus(const tensor2& n, const std::vector& /*X_i*/) const { - // Exact slope n : dX/dp = (2/3) C (tensorial contraction; = C for a - // von Mises normal, = 3/2). The former (2/3)C underestimated it by - // C/3, driving a systematic Newton overshoot (frozen FB loop) and a wrong - // continuum tangent via H_eff. + // exact slope n : dX/dp = (2/3) C (= C for a von Mises normal, = 3/2) return (2.0 / 3.0) * C_ * arma::accu(n.mat() % n.mat()); } @@ -237,10 +220,7 @@ void ArmstrongFrederickHardening::compute_backstresses( double ArmstrongFrederickHardening::hardening_modulus( const tensor2& n, const std::vector& X_i) const { - // Exact slope n : dX/dp with dX/dp = (2/3)C(n − D α): - // H_kin = (2/3) C − D (X : n) (tensorial = 3/2 for von Mises, - // giving the classical C − D(X:n)). X:n as stress-Voigt · engineering- - // strain-Voigt is the work-conjugate pairing of the full contraction. + // H_kin = (2/3) C - D (X:n); X:n = stress-Voigt . engineering-strain-Voigt const double nn = arma::accu(n.mat() % n.mat()); return (2.0 / 3.0) * C_ * nn - D_ * arma::dot(X_i[0].to_arma_voigt(), n.to_arma_voigt()); } @@ -287,9 +267,7 @@ void ChabocheHardening::register_variables(InternalVariableCollection& ivc) { void ChabocheHardening::compute_backstresses(const InternalVariableCollection& ivc, std::vector& X_i) const { - // X_i = (2/3) C_i α_i, STRESS-typed (backstress_t re-tags through the 3x3; - // a strain-typed carrier would put the factor-2 shear back — wrong under - // any loading with shear). + // X_i stress-typed (backstress_t): a strain-typed carrier would put the factor-2 shear back X_i.resize(static_cast(N_), tensor2(Tensor2Type::stress)); for (int i = 0; i < N_; ++i) { X_i[static_cast(i)] = backstress_t(C_(i), ivc.get(a_keys_[i]).as_tensor2()); diff --git a/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp b/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp index a7eb62a40..ec7dab910 100644 --- a/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp +++ b/src/Continuum_mechanics/Umat/Modular/internal_variable.cpp @@ -232,17 +232,13 @@ void InternalVariable::rotate(const Rotation& R) { case IVarType::SCALAR: break; case IVarType::VECTOR_6: { - // Route through tensor2 so the rotation kernel (apply_strain vs - // apply_stress, and the future generic path) is chosen once, inside - // tensor2::rotate, from the variable's authoritative Tensor2Type. + // kernel (strain vs stress) chosen inside tensor2::rotate from the Tensor2Type const tensor2 t = tensor2::from_voigt(vec_value_, vtype_).rotate(R); vec_value_ = t.to_arma_voigt(); break; } case IVarType::MATRIX_6x6: - // Typed congruence: correct for stiffness- AND compliance-like - // storage (the Mandel rotation is one orthogonal congruence; the - // Tensor4Type carries the engineering factors). + // typed Mandel congruence: correct for stiffness- and compliance-like storage mat_value_ = tensor4(mat_value_, t4type_).rotate(R).mat(); break; } diff --git a/src/Continuum_mechanics/Umat/Modular/internal_variable_collection.cpp b/src/Continuum_mechanics/Umat/Modular/internal_variable_collection.cpp index 3faa744ec..f2c8de32a 100644 --- a/src/Continuum_mechanics/Umat/Modular/internal_variable_collection.cpp +++ b/src/Continuum_mechanics/Umat/Modular/internal_variable_collection.cpp @@ -88,6 +88,14 @@ const InternalVariable& InternalVariableCollection::get(const std::string& name) return variables_[it->second]; } +size_t InternalVariableCollection::index_of(const std::string& name) const { + auto it = name_to_index_.find(name); + if (it == name_to_index_.end()) { + throw std::runtime_error("InternalVariableCollection: variable '" + name + "' not found"); + } + return it->second; +} + bool InternalVariableCollection::has(const std::string& name) const { return name_to_index_.find(name) != name_to_index_.end(); } diff --git a/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp b/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp index cbdacfc9a..02a14c976 100644 --- a/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp +++ b/src/Continuum_mechanics/Umat/Modular/legacy_adapters.cpp @@ -31,11 +31,7 @@ namespace simcoon { namespace { -// Modular props stream (configure_from_props): -// [el_type, conv, el params..., alphas..., n_mech, mech blocks...] -// Plasticity block: [0, yield, iso, kin, N_iso, N_kin, sigma_Y, -// yield params, iso params, kin params] -// Viscoelastic block: [1, N, (E_i, nu_i, etaB_i, etaS_i) x N] +// MODUL props stream: see ModularUMAT::configure_from_props (modular_umat.hpp) vec translate_ELISO(const vec& p) { // legacy: E nu alpha @@ -115,11 +111,7 @@ vec translate_EPCHG(const vec& p) { const int n_crit = n_crit_map[crit_legacy]; const uword legacy_crit_off = 8 + 2 * static_cast(N_iso + N_kin); - // The legacy N-term isotropic hardening couples every term through a - // SINGLE Hp (dHp/dp = sum_i b_i (Q_i - Hp)) — mathematically ONE - // effective Voce with b_eff = sum(b_i), Q_eff = sum(b_i Q_i)/sum(b_i), - // NOT the standard combined-Voce sum (proven by the Phase-1 equivalence - // test at 2.6e-4). Map accordingly. + // legacy N-term hardening shares ONE Hp: an effective Voce, b_eff = sum b_i, Q_eff = sum(b_i Q_i)/b_eff double b_eff = 0.0, bq = 0.0; for (int i = 0; i < N_iso; ++i) { const double Q_i = p(8 + 2 * static_cast(i)); @@ -206,7 +198,7 @@ void umat_legacy_modular( const double& T, const double& DT, const double& Time, const double& DTime, double& Wm, double& Wm_r, double& Wm_ir, double& Wm_d, const int& ndi, const int& nshr, const bool& start, double& tnew_dt, - const int& tangent_mode) { + const int& tangent_mode, const int& ncp) { const auto it = registry().find(umat_name); if (it == registry().end()) { @@ -219,7 +211,7 @@ void umat_legacy_modular( static_cast(props_mod.n_elem), props_mod, nstatev, statev, T, DT, Time, DTime, Wm, Wm_r, Wm_ir, Wm_d, - ndi, nshr, start, tnew_dt, tangent_mode); + ndi, nshr, start, tnew_dt, tangent_mode, KinematicForm::rate, ncp); } catch (const std::logic_error& e) { // a configuration error, named after the legacy law; numerical failures pass through // with their own type (the step-cut policy) diff --git a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp index d4de1d2d0..6e82b574e 100644 --- a/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp +++ b/src/Continuum_mechanics/Umat/Modular/modular_umat.cpp @@ -31,8 +31,11 @@ along with simcoon. If not, see . #include #include #include +#include +#include #include #include +#include namespace simcoon { @@ -94,6 +97,14 @@ DamageMechanism& ModularUMAT::add_damage( return static_cast(*mechanisms_.back()); } +void ModularUMAT::set_ncp(NCPMethod ncp) { + if (ncp == NCPMethod::min_fb_fallback && nonlinear_kinematic_) { + throw std::invalid_argument("MODUL: ncp='min' serves isotropic and linear (Prager) kinematic " + "hardening only, not Armstrong-Frederick back stresses"); + } + ncp_ = ncp; +} + void ModularUMAT::configure_from_props(const arma::vec& props, int offset) { // Read elasticity type int el_type = static_cast(props(offset)); @@ -105,6 +116,7 @@ void ModularUMAT::configure_from_props(const arma::vec& props, int offset) { // Read number of mechanisms int num_mech = static_cast(props(offset)); offset += 1; + nonlinear_kinematic_ = false; // an Armstrong-Frederick back stress (see set_ncp) // Configure each mechanism for (int i = 0; i < num_mech; ++i) { @@ -120,6 +132,8 @@ void ModularUMAT::configure_from_props(const arma::vec& props, int offset) { int N_iso = static_cast(props(offset + 3)); int N_kin = static_cast(props(offset + 4)); offset += 5; + nonlinear_kinematic_ = nonlinear_kinematic_ + || (kin_type != static_cast(KinHardType::NONE) && kin_type != static_cast(KinHardType::PRAGER)); add_plasticity( static_cast(yield_type), @@ -150,15 +164,11 @@ void ModularUMAT::configure_from_props(const arma::vec& props, int offset) { } } - // An anisotropic elastic potential composed with an inelastic mechanism is accepted on - // purpose; the fibre-convection approximation that entails is documented on - // structure_tensors_push_forward (hyperelastic.hpp). + // anisotropic potential + inelastic mechanism accepted: fibre convection approximate (structure_tensors_push_forward) } void ModularUMAT::initialize(int nstatev, arma::vec& statev) { - // Each mechanism registers its variables into its OWN collection, then - // receives a base offset into the shared statev layout: - // statev = [T_init | mech 0 | mech 1 | ...] (composition order) + // statev = [T_init | mech 0 | mech 1 | ...] (composition order) for (auto& mech : mechanisms_) { mech->register_variables(); } @@ -175,11 +185,7 @@ void ModularUMAT::initialize(int nstatev, arma::vec& statev) { ") < required (" + std::to_string(required) + ")"); } - // required_nstatev() is a hand-counted pre-initialize estimate (callers - // size their statev array with it). Guard it against silent drift from the - // authoritative count that register_variables() just produced — any - // mechanism that gains/loses a variable without updating required_nstatev() - // fails here on the first initialize, not with a corrupted statev later. + // required_nstatev() is hand-counted: catch its drift from register_variables() here, not as corrupted statev if (required_nstatev() != required) { throw std::logic_error( "ModularUMAT: required_nstatev() (" + std::to_string(required_nstatev()) + @@ -193,9 +199,7 @@ void ModularUMAT::initialize(int nstatev, arma::vec& statev) { mech->unpack(statev); } - // Cache the composition invariants (constant for the rest of the - // object's lifetime): per-mechanism constraint-row offsets and the - // drift-guard arming decision (see the member doc). + // composition invariants: per-mechanism row offsets and the drift-guard arming mech_offset_.assign(mechanisms_.size(), 0); int acc = 0; int n_guarded_rows = 0; @@ -203,9 +207,7 @@ void ModularUMAT::initialize(int nstatev, arma::vec& statev) { mech_offset_[m] = acc; acc += mechanisms_[m]->num_constraints(); if (mechanisms_[m]->guarded_constraints()) { - // Count ROWS, not mechanisms: a single future mechanism carrying - // several coupled surfaces (SMA forward/reverse transformation) - // is already the multi-surface configuration the guard watches. + // count rows, not mechanisms: one mechanism with coupled surfaces is already multi-surface n_guarded_rows += mechanisms_[m]->num_constraints(); } } @@ -283,14 +285,19 @@ void ModularUMAT::run( } initialize(nstatev, statev); } + else { + // Reused instance (umat_modular keeps one per configuration): the state + // comes from statev, exactly as initialize() unpacks it on a fresh one. + for (auto& mech : mechanisms_) { + mech->unpack(statev); + } + } // Store initial temperature T_init_ = statev(0); if (start) { T_init_ = T; - // Legacy contract: the cumulative work accumulators are RESET on - // the start increment (every legacy kernel zeroes them in its - // if(start) block); stale caller-supplied values must not leak in. + // legacy contract: the cumulative work accumulators are reset on the start increment Wm = 0.0; Wm_r = 0.0; Wm_ir = 0.0; @@ -332,18 +339,7 @@ void ModularUMAT::run( arma::vec Ds_total = arma::zeros(n_total); const bool converged = return_mapping(Etot, DEtot, sigma, T_init_, T, DT, DTime, ndi, nshr, Ds_total, tangent_mode); - // Reject unusable committed states with a step cut: non-finite stress - // (true divergence), a runaway COMMITTED multiplier (multiplier_cap — - // checked on the state, not the raw FB row, which can be smaller after - // clipped excursions), or a state that drifted from the flow rule - // (state_drift) — a spurious CONVERGED root of an oscillating FB Newton, - // invisible to every residual check (arming policy: see - // drift_guard_armed_). Thresholds are per-mechanism, infinity = opt out. - // (Negative multipliers are handled at the source: PlasticityMechanism - // projects the state update onto p >= p_start, and PowerLawHardening is - // C1-regularized at onset, so no negative-Ds check is needed here.) - // The closest-point branch reports its own non-convergence (no commit-at-maxiter - // semantics there) and satisfies the flow rule by construction (drift guard moot). + // rejection policy: see run() in modular_umat.hpp bool reject = !converged || !sigma.is_finite(); for (size_t m = 0; !reject && m < mechanisms_.size(); ++m) { const double drift_tol = mechanisms_[m]->drift_tolerance(); @@ -353,20 +349,10 @@ void ModularUMAT::run( mechanisms_[m]->state_drift(sigma_eff_) > drift_tol); } if (reject) { - // Ask the global solver to halve the increment and retry. statev is - // left at its incoming values (pack_all is skipped) so the retry - // restarts from the correct state; sigma is reset to its incoming - // value and Lt to elastic so the rejected-step output stays a - // self-consistent triple — the solver's inforce-at-Dn_mini branch - // commits this output WITHOUT re-running the UMAT, and a rejected - // stress paired with the un-updated statev would silently corrupt - // the rest of the history. + // statev untouched (no pack_all), sigma and Lt back to the start state: a self-consistent triple sigma = sigma_start_; tnew_dt = 0.5; - // Lt is the START state's elastic tangent too: restore the mechanisms - // from the untouched statev (with the same rotation as above) and - // evaluate the elastic block there, so neither the rejected iterate's - // damage nor its elastic strain leaks into the committed tangent. + // elastic tangent of the start state: neither the rejected damage nor its elastic strain leaks in for (auto& mech : mechanisms_) { mech->unpack(statev); if (rotate) mech->rotate(DR); @@ -380,15 +366,10 @@ void ModularUMAT::run( // Compute consistent tangent (it seeds Lt itself) compute_tangent(sigma, Ds_total, Lt, tangent_mode); - // Work quantities — CUMULATIVE in/out, the legacy UMAT contract - // (Wm += increment each call; the solver reports the path integral). - // Total mechanical work increment: trapezoidal sigma:dE on the total - // strain increment, exactly as the legacy kernels. + // cumulative work (legacy contract): trapezoidal sigma:dE on the total strain increment const double Wm_inc = 0.5 * arma::dot(sigma_start_ + sigma, DEtot); - // Stored/dissipated increments from the mechanisms; the recoverable part - // closes the energy balance (guarantees Wm = Wm_r + Wm_ir + Wm_d and - // reduces to the legacy elastic trapezoid when no mechanism is present). + // Wm_r closes the balance Wm = Wm_r + Wm_ir + Wm_d double Wm_ir_inc = 0.0, Wm_d_inc = 0.0; for (const auto& mech : mechanisms_) { double Wm_r_m = 0.0, Wm_ir_m = 0.0, Wm_d_m = 0.0; @@ -442,9 +423,7 @@ bool ModularUMAT::return_mapping( n_total += mech->num_constraints(); } - // Elastic prediction. With no constraints there is no inelastic strain and - // no damage, so this same call IS the whole elastic response — only the - // constraint machinery below is skipped. + // with no constraints this elastic call is the whole response const arma::vec Etot_end = Etot + DEtot; for (auto& mech : mechanisms_) { mech->predict(Etot_end, DTime); // closed-form parts first (viscoelastic branches) @@ -461,11 +440,8 @@ bool ModularUMAT::return_mapping( evaluate_constraints(Etot_end, DTime, Phi, Y_crit); Ds_total.zeros(n_total); - // Admissible trial state: the whole answer for every integrator. The FB solve would return - // ds = 0 and error = 0 exactly (phi(Phi, 0) = |Phi| + Phi = 0 for Phi <= 0) and the zero - // update is the identity for a multiplier row; only the rows without multipliers (damage) - // commit their fixed point here, after which the stress is refreshed. With no closest-point - // solve, cpp_consistent_tangent() returns the elastic operator. + // admissible trial: the answer for every integrator (FB would return ds = 0); only the rows without + // multipliers (damage) commit their fixed point here bool elastic_pass = false; // the admissible-trial pass counted as the loop's first iterate if (Phi.max() <= 0.0) { bool inert_rows = false; @@ -548,6 +524,7 @@ void ModularUMAT::return_mapping_ccp( ReturnMappingControl control; control.maxiter = maxiter_; control.precision = precision_; + control.ncp = ncp_; cutting_plane_return_mapping(Phi, Y_crit, Ds_total, hooks, control, iter0); } @@ -678,15 +655,8 @@ void ModularUMAT::assemble_jacobian( double DT, arma::mat& B ) { - // Phase 2: cross-mechanism off-diagonal Jacobian entries per theory eq - // B_{lj} = -dPhi^l/dsigma · kappa^j + K^{lj}. - // Mechanisms whose Phi is strain-form (viscoelastic) return - // empty dPhi_dsigma → no rows contributed here, matching the - // decoupled-from-stress structure. - // The dot is taken on engineering Voigt components — the - // work-conjugate pairing for strain-typed dPhi with - // stress-typed kappa (and the established numerics for the - // damage strain-typed kappa; see DamageMechanism::kappa). + // Phase 2: B_lj = -dPhi^l/dsigma . kappa^j + K^lj (engineering Voigt dot); strain-form Phi + // (viscoelastic) returns an empty dPhi_dsigma and contributes no row B.zeros(); // Fetch each mechanism's kappa list once — the const-refs stay valid // for the whole phase (no compute_constraints call in between). @@ -756,9 +726,7 @@ void ModularUMAT::compute_tangent( // mechanisms see sigma_eff_; damage enters last through its stress_map(). const arma::vec& sigma_eff = sigma_eff_; - // Mechanisms opting into the algorithmic assembly: stress-dependent Phi - // AND an analytic flow Hessian. Others (viscoelastic, damage, Hessian-less - // criteria) keep their continuum tangent_contribution in every mode. + // algorithmic assembly only for stress-dependent Phi with an analytic flow Hessian std::vector algo; if (tangent_mode == tangent_closest_point) { // Exact operator of the closest-point solve; every multiplier-carrying row is inside it. @@ -767,10 +735,7 @@ void ModularUMAT::compute_tangent( } Lt = cpp_consistent_tangent(cpp_result_, L_cur_).Lt; } else if (tangent_mode == tangent_algorithmic && arma::any(Ds_total > simcoon::iota)) { - // With no active multiplier the assembly masks every mechanism (same Ds > iota - // threshold, tangent_assembly.cpp) and returns L: skip the Hessians, which the - // opt-in predicate below would evaluate before the assembly could; the continuum - // contributions are no-ops at Ds <= iota. + // no active multiplier: the assembly masks everything and returns L, skip the Hessians for (size_t m = 0; m < mechanisms_.size(); ++m) { if (mechanisms_[m]->dLambda_dsigma(sigma_eff) != nullptr && !mechanisms_[m]->dPhi_dsigma(sigma_eff).empty()) { @@ -780,11 +745,7 @@ void ModularUMAT::compute_tangent( } if (tangent_mode == tangent_algorithmic && !algo.empty()) { - // Rebuild the local Jacobian at the converged state (the mechanism - // caches were refreshed by the last compute_constraints call) and - // hand the opted-in sub-block to the Simo-Hughes assembly. - // Sign: modular B = -dPhi·kappa + K, tangent_assembly Bhat = dPhi·kappa - K, - // hence Bhat = -B. + // sign: modular B = -dPhi.kappa + K, tangent_assembly Bhat = dPhi.kappa - K, hence Bhat = -B int n_total = 0; for (const auto& mech : mechanisms_) { n_total += mech->num_constraints(); @@ -852,6 +813,59 @@ void ModularUMAT::apply_total_strain_maps(arma::mat& Lt) const { // ========== Standalone UMAT Function ========== +namespace { + +// configured instances of this thread, most recent first, keyed on (props bitwise, nstatev, kinematic form); +// the state is unpacked from statev at every call, so a reused instance returns a fresh one's result +class modular_umat_cache { +public: + static constexpr std::size_t capacity = 8; + + /// the instance of (props, nstatev, kinematic_form), created on a miss; least recently used evicted + ModularUMAT& acquire(const arma::vec& props, int nstatev, KinematicForm kinematic_form) { + for (auto it = entries_.begin(); it != entries_.end(); ++it) { + if (it->matches(props, nstatev, kinematic_form)) { + std::rotate(entries_.begin(), it, it + 1); + return *entries_.front().umat; + } + } + if (entries_.size() == capacity) { + entries_.pop_back(); + } + entry e; + e.props.assign(props.begin(), props.end()); + e.nstatev = nstatev; + e.kinematic_form = kinematic_form; + e.umat = std::make_unique(); + e.umat->set_kinematic_form(kinematic_form); + entries_.insert(entries_.begin(), std::move(e)); + return *entries_.front().umat; + } + + /// Forget the instance returned by the last acquire(). + void drop_most_recent() { + if (!entries_.empty()) { + entries_.erase(entries_.begin()); + } + } + +private: + struct entry { + std::vector props; + int nstatev = 0; + KinematicForm kinematic_form = KinematicForm::rate; + std::unique_ptr umat; + + bool matches(const arma::vec& p, int n, KinematicForm kf) const { + return nstatev == n && kinematic_form == kf && props.size() == p.n_elem + && (props.empty() || std::memcmp(props.data(), p.memptr(), props.size() * sizeof(double)) == 0); + } + }; + std::vector entries_; +}; + +} // namespace + void umat_modular( const std::string& umat_name, const arma::vec& Etot, @@ -877,22 +891,25 @@ void umat_modular( const bool& start, double& tnew_dt, const int& tangent_mode, - const KinematicForm& kinematic_form + const KinematicForm& kinematic_form, + const int& ncp ) { - // Create a fresh instance each call. Configuration is re-parsed from props - // and state is restored from statev, ensuring correctness for multi-point - // simulations. The cost of re-parsing props is negligible compared to the - // Newton iteration in return_mapping. - ModularUMAT mumat; - mumat.set_kinematic_form(kinematic_form); - - mumat.run( - umat_name, Etot, DEtot, sigma, Lt, L, DR, - nprops, props, nstatev, statev, - T, DT, Time, DTime, - Wm, Wm_r, Wm_ir, Wm_d, - ndi, nshr, start, tnew_dt, tangent_mode - ); + thread_local modular_umat_cache cache; + ModularUMAT& mumat = cache.acquire(props, nstatev, kinematic_form); + try { + mumat.set_ncp(ncp_method_of(ncp, "MODUL")); + mumat.run( + umat_name, Etot, DEtot, sigma, Lt, L, DR, + nprops, props, nstatev, statev, + T, DT, Time, DTime, + Wm, Wm_r, Wm_ir, Wm_d, + ndi, nshr, start, tnew_dt, tangent_mode + ); + } catch (...) { + // a failed configuration or initialization must not be reused + cache.drop_most_recent(); + throw; + } } } // namespace simcoon diff --git a/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp b/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp index 5a92bd68e..270ed1597 100644 --- a/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp +++ b/src/Continuum_mechanics/Umat/Modular/plasticity_mechanism.cpp @@ -23,6 +23,7 @@ along with simcoon. If not, see . #include #include #include +#include #include #include #include @@ -31,11 +32,7 @@ along with simcoon. If not, see . namespace simcoon { namespace { -// Absolute strain floor of the state_drift denominator (see state_drift). -// 1e-4: at near-zero committed Dp, rejection then requires a flow-rule -// error above tolerance * floor = 5e-5 strain (~10 MPa) — two orders below -// the measured pathology (~1e-2) and comfortably above the EP residue a -// benign overshoot/walk-back leaves behind (~1e-5). +// state_drift denominator floor: rejection at near-zero Dp needs a flow-rule error > tol * 1e-4 strain constexpr double drift_scale_floor = 1.0e-4; } // namespace @@ -89,18 +86,10 @@ PlasticityMechanism::PlasticityMechanism( // ========== Configuration ========== void PlasticityMechanism::configure(const arma::vec& props, int& offset) { - // Props layout: - // 1. sigma_Y (initial yield stress) - // 2. Yield criterion parameters (if needed) - // 3. Isotropic hardening parameters - // 4. Kinematic hardening parameters sigma_Y_ = props(offset); offset += 1; - // Configure yield criterion from props if not already done - // (von Mises and Tresca are configured in the constructor since they - // need no parameters; others read their parameters from props here) if (!yield_->is_configured()) { yield_->configure(yield_type_, props, offset); } @@ -114,6 +103,9 @@ void PlasticityMechanism::register_variables() { ivc_.add_scalar("p", 0.0); ivc_.add_vec("EP", arma::zeros(6), true); kin_hard_->register_variables(ivc_); + // resolved once: the return-mapping loop reads them through ivc_.at() + p_index_ = ivc_.index_of("p"); + EP_index_ = ivc_.index_of("EP"); } // ========== Yield-side queries ========== @@ -130,7 +122,7 @@ double PlasticityMechanism::equivalent_stress(const tensor2& sigma) const { } double PlasticityMechanism::yield_function(const arma::vec& sigma) const { - const double p = ivc_.get("p").scalar(); + const double p = ivc_.at(p_index_).scalar(); return equivalent_stress(sigma) - iso_hard_->R(p) - sigma_Y_; } @@ -160,12 +152,9 @@ void PlasticityMechanism::compute_constraints( arma::vec& Y_crit ) const { // Get internal variables - double p = ivc_.get("p").scalar(); + double p = ivc_.at(p_index_).scalar(); - // Shifted stress (sigma - X); skip the subtraction under pure isotropic - // hardening. The branch backstresses X_i are built ONCE here and reused - // for hardening_modulus below — they are only valid within this FB - // iteration (the back-strains move in update()). + // X_i built once here and reused by hardening_modulus: valid within this FB iteration only const bool has_kin = kin_hard_->num_backstresses() > 0; double sigma_eq; if (has_kin) { @@ -237,7 +226,7 @@ bool PlasticityMechanism::refresh_state( const arma::vec T = Ir05(); // engineering -> tensorial shear const bool has_kin = kin_hard_->num_backstresses() > 0; - auto& p_var = ivc_.get("p"); + auto& p_var = ivc_.at(p_index_); p_var.scalar() = p_var.scalar_start() + dp; // Newton on the n <-> X coupling (class note in the header). @@ -264,11 +253,11 @@ bool PlasticityMechanism::refresh_state( kin_hard_->backward_euler_factors(dp, n, ivc_, gamma, beta); const arma::mat H = yield_->flow_hessian(xi); const arma::mat A = arma::eye(6, 6) + gamma * (H.each_row() % T.t()); // I + gamma H T - arma::vec dn; - if (!arma::solve(dn, A, -G, arma::solve_opts::fast)) { + small_lu<6> lu_A; // on the stack: a LAPACK 6x6 solve per step otherwise + if (!lu_A.factor(A)) { return false; } - n += dn; + n -= lu_A.solve(G); } if (!converged) { return false; @@ -276,7 +265,7 @@ bool PlasticityMechanism::refresh_state( kin_hard_->backward_euler_factors(dp, n, ivc_, gamma, beta); } - auto& EP_var = ivc_.get("EP"); + auto& EP_var = ivc_.at(EP_index_); EP_var.raw_voigt() = EP_var.raw_voigt_start() + dp * n; // Total derivatives at this state (class note in the header). @@ -293,33 +282,29 @@ bool PlasticityMechanism::refresh_state( return true; } const arma::mat HT = H.each_row() % T.t(); - arma::mat Ainv; - if (!arma::inv(Ainv, arma::eye(6, 6) + gamma * HT)) { + small_lu<6> lu_A; // A = I + gamma H T on the stack: solves instead of a LAPACK inverse + if (!lu_A.factor(arma::eye(6, 6) + gamma * HT)) { return false; } const arma::vec Tn = T % n; - out.dLambda_dsigma = Ainv * H; + out.dLambda_dsigma = lu_A.solve(H); out.dPhi_dsigma = n - gamma * (out.dLambda_dsigma.t() * Tn); - out.K = -arma::dot(Tn, Ainv * beta) - dR_dp; - out.dLambda_dDs = -(Ainv * (HT * beta)); + out.K = -arma::dot(Tn, lu_A.solve(beta)) - dR_dp; + out.dLambda_dDs = -lu_A.solve(arma::vec(HT * beta)); return true; } double PlasticityMechanism::state_drift(const arma::vec& sigma) const { // Committed Dp from the (projected) internal variable, not the raw FB // row — update() enforces p >= p_start, the state is the truth. - const double dp = ivc_.get("p").delta_scalar(); - const arma::vec dEP = ivc_.get("EP").delta_vec(); + const double dp = ivc_.at(p_index_).delta_scalar(); + const arma::vec dEP = ivc_.at(EP_index_).delta_vec(); if (dp < simcoon::iota && norm_strain(dEP) < simcoon::iota) { // inert increment: avoid evaluating the flow direction at a // possibly-zero stress return 0.0; } - // norm_strain (tensorial norm of an engineering-Voigt strain): arma::norm - // would weight the shear entries sqrt(2) too heavy against the cap. - // The absolute denominator floor keeps the relative measure from - // amplifying a numerically negligible EP residue at near-zero Dp (see - // the constant above for its calibration). + // norm_strain, not arma::norm (sqrt2-heavy shears); the floor keeps a negligible EP residue at Dp ~ 0 inert const arma::vec n = flow_direction(sigma); return norm_strain(dEP - dp * n) / std::max(dp * norm_strain(n), drift_scale_floor); @@ -336,7 +321,7 @@ double PlasticityMechanism::multiplier_cap() const { } double PlasticityMechanism::committed_multiplier() const { - return ivc_.get("p").delta_scalar(); + return ivc_.at(p_index_).delta_scalar(); } void PlasticityMechanism::compute_jacobian_contribution( @@ -345,9 +330,7 @@ void PlasticityMechanism::compute_jacobian_contribution( arma::mat& B, int row_offset ) const { - // B = -dPhi/dsigma : kappa + K with K = dPhi/dp = -H_total (hardening - // REDUCES Phi as Dp grows) — the plastic_isotropic convention, so the - // mode-1 assembly's Bhat = -B = n:kappa + H_total holds exactly. + // K = dPhi/dp = -H_total, so the mode-1 assembly Bhat = -B = n:kappa + H_total holds exactly // dPhi/dsigma * L * dPhi/dsigma (engineering Voigt dot = n : L : n) double dPhi_L_dPhi = arma::dot(flow_dir_, kappa_); @@ -356,37 +339,29 @@ void PlasticityMechanism::compute_jacobian_contribution( } arma::vec PlasticityMechanism::inelastic_strain() const { - return ivc_.get("EP").raw_voigt(); + return ivc_.at(EP_index_).raw_voigt(); } void PlasticityMechanism::update( const arma::vec& ds, int offset ) { - // Apply the FB correction PROJECTED onto Dp >= 0: negative per-iteration - // corrections walk back an overshoot (gating on dp > iota froze the state - // after any overshoot — maxiter spins with a ~1 MPa yield violation and a - // 12x slowdown), but the multiplier never drops below its start-of- - // increment value. Without the projection a tiny negative Dp is a SPURIOUS - // Fischer-Burmeister root whenever Phi > 0 is large (phi(Phi, Dp) ~ Dp for - // |Dp| << Phi) — reached in practice for near-singular hardening slopes, - // e.g. power-law R = k p^m with m < 1 at first yield (p < 0, Phi -> +100 - // MPa, mechanism inert). + // projected onto Dp >= 0 (see update() in the header) double dp = ds(offset); if (dp == 0.0) { return; // the update below is exactly the identity for dp = 0 } // Update accumulated plastic strain (projected: p never below p_start) - double& p = ivc_.get("p").scalar(); - const double p_start = ivc_.get("p").scalar_start(); + double& p = ivc_.at(p_index_).scalar(); + const double p_start = ivc_.at(p_index_).scalar_start(); if (p + dp < p_start) { dp = p_start - p; } p += dp; // Update plastic strain - arma::vec& EP = ivc_.get("EP").raw_voigt(); + arma::vec& EP = ivc_.at(EP_index_).raw_voigt(); EP += dp * flow_dir_; // Update kinematic hardening variables @@ -403,9 +378,7 @@ void PlasticityMechanism::tangent_contribution( double Dp = Ds(offset); if (Dp > simcoon::iota) { - // Consistent tangent for plasticity: - // Lt = L - (L * n) ⊗ (L * n) / H_eff - // where H_eff = n : L : n + H_total + // Lt = L - (L n) x (L n) / H_eff, H_eff = n:L:n + H_total double n_L_n = arma::dot(flow_dir_, kappa_); double H_eff = n_L_n + H_total_; @@ -428,8 +401,8 @@ void PlasticityMechanism::compute_work( double& Wm_d ) const { // Get increments - double Dp = ivc_.get("p").delta_scalar(); - arma::vec DEP = ivc_.get("EP").delta_vec(); + double Dp = ivc_.at(p_index_).delta_scalar(); + arma::vec DEP = ivc_.at(EP_index_).delta_vec(); if (Dp == 0.0 && DEP.is_zero()) { Wm_r = Wm_ir = Wm_d = 0.0; // both dots below would be (signed) zeros return; diff --git a/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp b/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp index a17c77479..9f37b0f63 100644 --- a/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp +++ b/src/Continuum_mechanics/Umat/Modular/viscoelastic_mechanism.cpp @@ -94,9 +94,7 @@ void ViscoelasticMechanism::compute_constraints( arma::vec& Phi, arma::vec& Y_crit ) const { - // The branches are not solved by the FB system: predict() took their backward-Euler step - // in closed form. Their rows stay in the system (uniform bookkeeping) but are always - // satisfied: Phi = -Y_crit gives Delta s = 0. + // branches solved in closed form by predict(): rows kept but always satisfied (Phi = -Y_crit, Delta s = 0) Phi.set_size(N_prony_); Y_crit.set_size(N_prony_); Y_crit.fill(1.0); @@ -121,9 +119,7 @@ const std::vector& ViscoelasticMechanism::kappa( } arma::vec ViscoelasticMechanism::inelastic_strain() const { - // EV_tilde = sum_i (M_0 · L_i) · EV_i (Prony_Nfast form). - // M0_L_i_ is pre-multiplied in set_reference_stiffness to avoid a 6x6·6x6 - // matmul on every FB iteration. + // EV_tilde = sum_i (M_0 L_i) EV_i (Prony_Nfast form); M0_L_i_ precomputed in set_reference_stiffness arma::vec EV_tilde = arma::zeros(6); for (int i = 0; i < N_prony_; ++i) { EV_tilde += M0_L_i_[i] * ivc_.get(ev_key_[i]).raw_voigt(); @@ -139,9 +135,7 @@ void ViscoelasticMechanism::update( } void ViscoelasticMechanism::predict(const arma::vec& E_total_end, double DTime) { - // Closed-form backward-Euler step from the (rotated) start state: the branches see the - // total strain only, so it is final before the other mechanisms' return mapping starts. - // A zero time increment leaves them inactive (C_i = 0). + // closed-form backward Euler from the rotated start, final before the other mechanisms; DTime = 0 -> inactive std::vector EV_start(N_prony_); for (int i = 0; i < N_prony_; ++i) { EV_start[i] = ivc_.get(ev_key_[i]).raw_voigt_start(); @@ -182,11 +176,8 @@ void ViscoelasticMechanism::compute_work( double& Wm_ir, double& Wm_d ) const { - // Dissipation of the dashpots (Prony_Nfast lines 274-288): trapezoidal - // W_d = sum_i 0.5 (A_i_start + A_i_end) . DEV_i - // on the BRANCH stress A_i = L_i (eps - EV_i), the one the dashpot carries, - // not the total stress (that overcounts by L_0/L_i and made Wm_d exceed Wm). - // The recoverable part is closed by the orchestrator (Wm - Wm_ir - Wm_d). + // W_d = sum_i 1/2 (A_i_start + A_i_end) . DEV_i on the BRANCH stress A_i = L_i (eps - EV_i) + // (the total stress overcounts by L_0/L_i); Wm_r is closed by the orchestrator Wm_r = 0.0; Wm_ir = 0.0; Wm_d = 0.0; diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.cpp index 19498ca11..68ed9a2c7 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_isotropic.cpp @@ -32,13 +32,6 @@ using namespace arma; namespace simcoon{ -///@brief The elastic UMAT requires 2 constants: -///@brief props[0] : Young modulus -///@brief props[1] : Poisson ratio -///@brief props[2] : CTE - -///@brief No statev is required for thermoelastic constitutive law - void umat_elasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) { diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.cpp index d281546ec..b2e356da7 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_orthotropic.cpp @@ -15,7 +15,7 @@ */ -///@file Elastic_orthotropic.cpp +///@file elastic_orthotropic.cpp ///@brief User subroutine for ortothropic elastic materials in 3D case #include @@ -31,18 +31,6 @@ using namespace arma; namespace simcoon{ -///@brief The thermomechanical elastic orthoptropic UMAT requires 13 constants: -///@brief props[0] : density -///@brief props[1] : specific heat capacity -///@brief props[2-4] : 3 Young modulus -///@brief props[5-7] : 3 Poisson ratio -///@brief props[8-10] : 3 Shear ratio ?? -///@brief props[11] : CTE x -///@brief props[12] : CTE y -///@brief props[13] : CTE z - -///@brief No statev is required for thermoelastic constitutive law - void umat_elasticity_ortho_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) { diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.cpp index 836b18e1b..662c37361 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Elasticity/elastic_transverse_isotropic.cpp @@ -15,7 +15,7 @@ */ -///@file elastic_transversely_isotropic.cpp +///@file elastic_transverse_isotropic.cpp ///@brief User subroutine for transversely isotropic elastic materials in 3D case ///@version 1.0 @@ -32,20 +32,6 @@ using namespace arma; namespace simcoon{ -///@brief The thermomechanical elastic istropic transverse UMAT requires 10 constants: -///@brief props[0] : density -///@brief props[1] : specific heat capacity -///@brief props[2] : Axe of the longitudinal direction -///@brief props[3] : Longitudinal Young modulus -///@brief props[4] : Transverse Young modulus -///@brief props[5] : Longitudinal-Transverse Poisson ratio -///@brief props[6] : Transverse-Transverse Poisson ratio -///@brief props[7] : Shear Modulus -///@brief props[8] : Longitudinal thermal expansion coeficient -///@brief props[9] : Transverse thermal expansion coeficient - -///@brief No statev is required for thermoelastic constitutive law - void umat_elasticity_trans_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) { diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp index e538a1334..1777ab4d3 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_isotropic.cpp @@ -39,52 +39,6 @@ using namespace arma; namespace simcoon{ -///@brief The elastic-plastic UMAT with isotropic hardening requires 8 constants for a full thermomechanical coupling: - -///@brief props[0] : density -///@brief props[1] : specific heat capacity -///@brief props[2] : Young modulus -///@brief props[3] : Poisson ratio -///@brief props[4] : CTE -///@brief props[5] : J2 equivalent yield stress limit : sigmaY -///@brief props[6] : hardening parameter k -///@brief props[7] : exponent m - -///@brief The elastic-plastic UMAT with isotropic hardening requires 8 statev: -///@brief statev[0] : T_init : Initial temperature -///@brief statev[1] : Accumulative plastic parameter: p -///@brief statev[2] : Plastic strain 11: EP(0,0) -///@brief statev[3] : Plastic strain 22: EP(1,1) -///@brief statev[4] : Plastic strain 33: EP(2,2) -///@brief statev[5] : Plastic strain 12: EP(0,1) (*2) -///@brief statev[6] : Plastic strain 13: EP(0,2) (*2) -///@brief statev[7] : Plastic strain 23: EP(1,2) (*2) - -/// The constitutive model has one lead mechanism, that contains two internal variables : p and EP -/// The Gibbs free energy is: - - /* - - \begin{eqnarray} - G\left(\b{\sigma},\theta,\b{V}_1\right)&=& G^{\textrm{r}}\left(\b{\sigma},\theta, \b{\varepsilon}^{\textrm{p}}\right)+G^{\textrm{ir}} \left(\theta,p\right), - \EqnCont - G^{\textrm{r}}\left(\b{\sigma},\theta, \b{\varepsilon}^{\textrm{p}}\right) - - = - \f{1}{2} \b{\sigma} \dcon \b{\mathcal{S}} \dcon \b{\sigma} + {c_0}\left[ [\theta-\theta_0] - -\theta\ln\left(\f{\theta}{\theta_0}\right)\right]-\eta_0\theta+{E}_0 - \b{\sigma} : \b{\varepsilon}^{\textrm{p}} - - \b\sigma : \b{\alpha} \left(\theta - \theta_0 \right), - \EqnCont - G^{\textrm{ir}}\left(\theta,p\right)&=&F(\theta,p) - \label{eq:elastoplastic_Gibbs_potential} - \end{eqnarray} - - So the following quantitites are : (A_x = \pfg{G}{x}) - A_sigma = \varepsilon - A_p = - A_EP = - A_theta = - */ - void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) { @@ -149,10 +103,7 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double double Hp=0.; double dHpdp=0.; - // Power-law hardening k p^m through the modular block: for m < 1 the exact slope m k p^(m-1) - // is infinite at the onset and the cutting-plane Newton cycles on the first plastic increment - // (committed off-surface, silently); PowerLawHardening is exact for p >= 1e-6 and a C1 - // quadratic blend below. + // PowerLawHardening: C1 blend below p = 1e-6 (slope of k p^m infinite at onset for m < 1) PowerLawHardening hardening; int hardening_offset = 0; hardening.configure(vec{k, m}, hardening_offset); @@ -193,9 +144,7 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double int compteur = 0; double error = 1.; - // Closest-point projection (tangent_closest_point): the modular von Mises + power-law row on - // this kernel's props and state (same branch as the mechanical kernel). Condensed states keep - // the loop; the thermal tail reads dPhidsigma / Lambdap / kappa_j / dHpdp. + // the thermal tail reads dPhidsigma / Lambdap / kappa_j / dHpdp from either branch ReturnMappingResult rm; const bool use_cpp = (tangent_mode == simcoon::tangent_closest_point); if (use_cpp) { @@ -259,25 +208,18 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double vec DEP = EP - EP_start; double Dp = Ds_j[0]; - //Computation of the tangent modulus — continuum operator via shared helper (doc §7.4). + // tangent via the shared leading-mechanism helper (doc §7.4) mat Bhat = zeros(1, 1); Bhat(0, 0) = sum(dPhidsigma%kappa_j[0]) - K(0,0); const std::vector dPhidsigma_l = { dPhidsigma }; - // tangent_none must NOT zero P_epsilon/invBhat here: these sensitivities - // feed the PHYSICAL heat source r and its linearization (drdE/drdT), not - // just the Newton operator. Explicit-integration callers get the continuum - // operator in the thermomechanical kernels (the mechanical-only kernels - // honor tangent_none). + // tangent_none -> continuum: P_epsilon / invBhat feed the physical heat source r const int tangent_mode_eff = (tangent_mode == tangent_none) ? tangent_continuum : tangent_mode; const ContinuumTangent ct = rm.converged ? cpp_consistent_tangent(rm, L) : compute_tangent_operator( tangent_mode_eff, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // Simo-Hughes algorithmic tangent (J2): dLambda_eps/dsigma = deta_stress(sigma). - // NOTE: only the mechanical block dSdE is algorithmically corrected. The thermal - // cross-tangents below (dSdT/drdE/drdT) are assembled from the raw kappa_j and L - // (continuum form) in every mode. + // J2: dLambda/dsigma = deta_stress; only dSdE is corrected, thermal blocks stay continuum const std::vector dLambda_dsigma_l = { deta_stress(sigma) }; return dLambda_dsigma_l; }); @@ -285,10 +227,7 @@ void umat_plasticity_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double const std::vector& P_epsilon = ct.P_epsilon; std::vector P_theta(1); - // Consistency relation: P_theta = invBhat * (dPhi/dtheta - dPhi/dsigma:L:alpha) - // (units 1/K; cf. the SMA thermomechanical kernel). invBhat is active-set - // masked, so elastic steps correctly give P_theta = 0 — the previous form - // omitted invBhat (units 1/MPa missing) and polluted elastic steps. + // P_theta = invBhat (dPhi/dtheta - dPhi/dsigma:L:alpha), 1/K; invBhat masked so elastic gives 0 P_theta[0] = ct.invBhat(0, 0) * (dPhidtheta - sum(dPhidsigma%(L*alpha))); dSdT = -1.*L*alpha - (kappa_j[0]*P_theta[0]); diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.cpp index 9384c4860..8f7bede85 100644 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_johnson_cook_ccp.cpp @@ -102,9 +102,7 @@ void umat_plasticity_johnson_cook_CCP_T(const vec &Etot, const vec &DEtot, vec & //Additional parameters double c_0 = rho*c_p; - // Thermal softening factor f_T = 1 - g(T*), g = T*^m C2-regularized near T* = 0 for m < 2 - // (johnson_cook_softening), and its temperature derivatives; T* clamped to [0, 1) (defined - // below T_ref, never at the singular melting point), flat at the melting clamp + // f_T = 1 - g(T*), T* clamped to [0, 1); derivatives zero at the melting clamp const double DTm = T_melt - T_ref; auto homologous = [&](const double &theta) { double Tstar = (theta - T_ref) / DTm; @@ -124,15 +122,13 @@ void umat_plasticity_johnson_cook_CCP_T(const vec &Etot, const vec &DEtot, vec & const double dthermal_dT_end = melt ? 0. : -dg / DTm; const double d2thermal_dT2_end = melt ? 0. : -d2g / (DTm * DTm); - // Strain hardening Hp = B p^n, C1-regularized at the onset for n < 1 (the modular - // power-law block carries the blend; same law, no second implementation) + // Hp = B p^n through PowerLawHardening (C1 onset blend for n < 1) PowerLawHardening hardening; int hardening_offset = 0; hardening.configure(vec{B_jc, n_jc}, hardening_offset); double Hp = hardening.R(p); double dHpdp = hardening.dR_dp(p); - // Stored energy of the hardening, G^ir = f_T B p^(n+1)/(n+1) (the blend below p_reg - // contributes O(p_reg^(n+1)), neglected in the integral) + // G^ir / f_T = B p^(n+1)/(n+1); the onset blend's O(p_reg^(n+1)) share is neglected auto stored_hardening = [&](const double &pp) { return (pp > 0.) ? B_jc * pow(pp, n_jc + 1.) / (n_jc + 1.) : 0.; }; @@ -171,12 +167,7 @@ void umat_plasticity_johnson_cook_CCP_T(const vec &Etot, const vec &DEtot, vec & std::vector kappa_j(1); mat K = zeros(1,1); - // Branch selection at the rate kink x0 = edot0*DTime, where Phi(Dp) changes from the - // quasi-static branch (rate factor 1) to the convex logarithmic one. A Newton started at - // Dp = 0 with the quasi-static slope overshoots the kink and the log branch throws it back - // below zero: a 2-cycle to maxiter_umat at small DTime. So the branch is decided first from - // Phi at the kink (radial estimate), the loop starts AT the kink on the log branch with the - // right derivative of the rate factor, and Newton is then monotone (convex branch). + // branch decided from Phi at the rate kink x0 = edot0*DTime, else Newton 2-cycles (mechanical header) bool log_branch = false; if (DTime > simcoon::iota) { const double x0 = edot0*DTime; @@ -246,14 +237,12 @@ void umat_plasticity_johnson_cook_CCP_T(const vec &Etot, const vec &DEtot, vec & sigma = el_pred(L, Eel); } - // The loop reads p at the top and corrects s_j at the bottom: bring p and the hardening to - // the converged iterate (EP and sigma already are) + // the loop reads p at the top and corrects s_j at the bottom p = s_j(0); Hp = hardening.R(p); dHpdp = hardening.dR_dp(p); - // A loop that leaves at maxiter_umat has not met the yield surface: the state would be - // committed off-surface, so ask the solver for a step cut instead + // unconverged at maxiter_umat: step cut rather than commit off-surface if (error > simcoon::precision_umat) { tnew_dt = 0.5; } @@ -271,21 +260,18 @@ void umat_plasticity_johnson_cook_CCP_T(const vec &Etot, const vec &DEtot, vec & // dPhi/dT = -(A + Hp) f_rate df_T/dT > 0 (softening); zero where the clamped factor is flat dPhidtheta = -(A_jc + Hp) * rate_factor * dthermal_dT_end; - //Computation of the tangent modulus via the shared leading-mechanism helper (doc 7.4). + // tangent via the shared leading-mechanism helper (doc §7.4) mat Bhat = zeros(1, 1); Bhat(0, 0) = sum(dPhidsigma%kappa_j[0]) - K(0,0); const std::vector dPhidsigma_l = { dPhidsigma }; - // tangent_none must NOT zero P_epsilon/invBhat here: these sensitivities feed the - // PHYSICAL heat source r and its linearization, not just the Newton operator - // (same rule as the other thermomechanical kernels). + // tangent_none -> continuum: P_epsilon / invBhat feed the physical heat source r const int tangent_mode_eff = (tangent_mode == tangent_none) ? tangent_continuum : tangent_mode; const ContinuumTangent ct = compute_tangent_operator( tangent_mode_eff, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // J2 associated flow: dLambda/dsigma = deta_stress(sigma). Only dSdE is - // algorithmically corrected; the thermal cross-tangents keep the continuum form. + // J2: dLambda/dsigma = deta_stress; only dSdE is corrected, thermal blocks stay continuum const std::vector dLambda_dsigma_l = { deta_stress(sigma) }; return dLambda_dsigma_l; }); @@ -293,8 +279,7 @@ void umat_plasticity_johnson_cook_CCP_T(const vec &Etot, const vec &DEtot, vec & const std::vector& P_epsilon = ct.P_epsilon; std::vector P_theta(1); - // P_theta = invBhat (dPhi/dtheta - dPhi/dsigma:L:alpha); invBhat is active-set masked, - // so elastic steps give P_theta = 0 + // P_theta = invBhat (dPhi/dtheta - dPhi/dsigma:L:alpha); invBhat masked so elastic gives 0 P_theta[0] = ct.invBhat(0, 0) * (dPhidtheta - sum(dPhidsigma%(L*alpha))); dSdT = -1.*L*alpha - (kappa_j[0]*P_theta[0]); @@ -315,16 +300,13 @@ void umat_plasticity_johnson_cook_CCP_T(const vec &Etot, const vec &DEtot, vec & double Deta_r = eta_r - eta_r_start; double Deta_ir = eta_ir - eta_ir_start; - // Hardening force A_p = -f_T B p^n (the dissipation f_T [A f_rate + B p^n (f_rate - 1)] Dp - // is then non-negative at every temperature and rate) and its derivatives + // A_p = -f_T B p^n (energy split of the mechanical header) double A_p = -thermal_factor*Hp; double dA_pdp = -thermal_factor*dHpdp; double dA_pdtheta = -dthermal_dT_end*Hp; - // Dissipation increment over the step (the quantity Wm_d accumulates) double Dgamma_loc = 0.5*sum((sigma_start+sigma)%DEP) + 0.5*(A_p_start + A_p)*Dp; - // its derivatives through dDp = P and dDEP = Lambdap P (flow direction held fixed over the - // increment), plus the explicit softening of A_p + // flow direction held fixed over the increment, plus the explicit softening of A_p const double f_p = 0.5*(sum((sigma_start+sigma)%Lambdap) + A_p_start + A_p + dA_pdp*Dp); const vec dDgamma_dE = 0.5*dSdE.t()*DEP + f_p*P_epsilon[0]; const double dDgamma_dT = 0.5*sum(dSdT%DEP) + f_p*P_theta[0] + 0.5*dA_pdtheta*Dp; diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp index 3e50e2aeb..4f2fc978b 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Plasticity/plastic_kin_iso.cpp @@ -39,35 +39,6 @@ using namespace arma; namespace simcoon{ -///@brief The thermomechanical elastic-plastic UMAT with kinematic + isotropic hardening requires 9 constants: - -///@brief props[0] : density -///@brief props[1] : specific heat capacity -///@brief props[2] : Young modulus -///@brief props[3] : Poisson ratio -///@brief props[4] : CTE -///@brief props[5] : J2 equivalent yield stress limit : sigmaY -///@brief props[6] : hardening parameter k -///@brief props[7] : exponent m -///@brief props[8] : linear kinematical hardening h - -///@brief The elastic-plastic UMAT with kinematic + isotropic hardening requires 14 statev: -///@brief statev[0] : T_init : Initial temperature -///@brief statev[1] : Accumulative plastic parameter: p -///@brief statev[2] : Plastic strain 11: EP(0,0) -///@brief statev[3] : Plastic strain 22: EP(1,1) -///@brief statev[4] : Plastic strain 33: EP(2,2) -///@brief statev[5] : Plastic strain 12: EP(0,1) (*2) -///@brief statev[6] : Plastic strain 13: EP(0,2) (*2) -///@brief statev[7] : Plastic strain 23: EP(1,2) (*2) -///@brief statev[8] : Backstress 11: X(0,0) -///@brief statev[9] : Backstress 11: X(1,1) -///@brief statev[10] : Backstress 11: X(2,2) -///@brief statev[11] : Backstress 11: X(0,1) -///@brief statev[12] : Backstress 11: X(0,2) -///@brief statev[13] : Backstress 11: X(1,2) - - void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) { @@ -142,17 +113,12 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do double c_0 = rho*c_p; vec X = kX*(a%Ir05()); - double Hp=0.; - double dHpdp=0.; - - if (p > simcoon::iota) { - dHpdp = m*k*pow(p, m-1); - Hp = k*pow(p, m); - } - else { - dHpdp = 0.; - Hp = 0.; - } + // PowerLawHardening: C1 blend below p = 1e-6 (slope of k p^m infinite at onset for m < 1) + PowerLawHardening hardening; + int hardening_offset = 0; + hardening.configure(vec{k, m}, hardening_offset); + double Hp = hardening.R(p); + double dHpdp = hardening.dR_dp(p); //Variables values at the start of the increment vec sigma_start = sigma; @@ -194,10 +160,7 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do int compteur = 0; double error = 1.; - // Closest-point projection (tangent_closest_point): the modular von Mises + power-law + - // Prager row on this kernel's props and state; X = kX T a = (2/3) C a with C = 3 kX / 2. - // Condensed states keep the loop; the thermal tail reads dPhidsigma / Lambdap / Lambdaa / - // kappa_j / dHpdp. + // Prager C = 3 kX / 2 so that X = kX T a = (2/3) C a ReturnMappingResult rm; const bool use_cpp = (tangent_mode == simcoon::tangent_closest_point); if (use_cpp) { @@ -234,14 +197,8 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do for (compteur = 0; ((compteur < simcoon::maxiter_umat) && (error > simcoon::precision_umat)); compteur++) { p = s_j(0); - if (p > simcoon::iota) { - dHpdp = m*k*pow(p, m-1); - Hp = k*pow(p, m); - } - else { - dHpdp = 0.; - Hp = 0.; - } + Hp = hardening.R(p); + dHpdp = hardening.dR_dp(p); dPhidsigma = eta_stress(sigma-X); dPhidp = -1.*dHpdp; dPhida = -1.*kX*(eta_stress(sigma - X)%Ir05()); @@ -276,25 +233,18 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do double Dp = Ds_j[0]; vec Da = a - a_start; - //Computation of the tangent modulus — continuum operator via shared helper (doc §7.4). + // tangent via the shared leading-mechanism helper (doc §7.4) mat Bhat = zeros(1, 1); Bhat(0, 0) = sum(dPhidsigma%kappa_j[0]) - K(0,0); const std::vector dPhidsigma_l = { dPhidsigma }; - // tangent_none must NOT zero P_epsilon/invBhat here: these sensitivities - // feed the PHYSICAL heat source r and its linearization (drdE/drdT), not - // just the Newton operator. Explicit-integration callers get the continuum - // operator in the thermomechanical kernels (the mechanical-only kernels - // honor tangent_none). + // tangent_none -> continuum: P_epsilon / invBhat feed the physical heat source r const int tangent_mode_eff = (tangent_mode == tangent_none) ? tangent_continuum : tangent_mode; const ContinuumTangent ct = rm.converged ? cpp_consistent_tangent(rm, L) : compute_tangent_operator( tangent_mode_eff, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // Simo-Hughes algorithmic tangent on the cutting-plane state, J2 flow on (sigma-X); - // the backstress state coupling is only in the closest-point branch. - // NOTE: only the mechanical block dSdE is algorithmically corrected. The thermal - // cross-tangents below (dSdT/drdE/drdT) keep the continuum form (raw kappa_j, L). + // J2 flow on sigma-X; only dSdE is corrected, thermal blocks stay continuum const std::vector dLambda_dsigma_l = { deta_stress(sigma - X) }; return dLambda_dsigma_l; }); @@ -302,10 +252,7 @@ void umat_plasticity_kin_iso_T(const vec &Etot, const vec &DEtot, vec &sigma, do const std::vector& P_epsilon = ct.P_epsilon; std::vector P_theta(1); - // Consistency relation: P_theta = invBhat * (dPhi/dtheta - dPhi/dsigma:L:alpha) - // (units 1/K; cf. the SMA thermomechanical kernel). invBhat is active-set - // masked, so elastic steps correctly give P_theta = 0 — the previous form - // omitted invBhat (units 1/MPa missing) and polluted elastic steps. + // P_theta = invBhat (dPhi/dtheta - dPhi/dsigma:L:alpha), 1/K; invBhat masked so elastic gives 0 P_theta[0] = ct.invBhat(0, 0) * (dPhidtheta - sum(dPhidsigma%(L*alpha))); dSdT = -1.*L*alpha - (kappa_j[0]*P_theta[0]); diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp index 87bc0ba83..1119a5178 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/SMA/unified_T.cpp @@ -17,16 +17,6 @@ ///@file unified_T.cpp ///@brief Unified thermomechanical SMA model supporting multiple elastic symmetries and transformation criteria -///@brief Based on constitutive model of D. Chatziathanasiou Ph.D Thesis -///@brief Implemented by Y. Chemisky and D. Chatziathanasiou -///@brief -///@brief The elastic symmetry and criteria type is determined by umat_name: -///@brief SMADI (isotropic elasticity, Drucker criteria): props(0-3)=rho,c_pA,c_pM,flagT, props(4-7)=EA,EM,nuA,nuM, then common params -///@brief SMADC (cubic elasticity, Drucker criteria): props(0-3)=rho,c_pA,c_pM,flagT, props(4-9)=C11A,C12A,C44A,C11M,C12M,C44M, then common params -///@brief SMAAI (isotropic elasticity, anisotropic Drucker criteria): same as SMADI + DFA params at end -///@brief SMAAC (cubic elasticity, anisotropic Drucker criteria): same as SMADC + DFA params at end -///@brief -///@brief Implemented in 1D-2D-3D #include #include @@ -97,14 +87,14 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D if (!cubic_elasticity) { // Isotropic elasticity: EA, EM, nuA, nuM - M_A = inv(L_iso(props(4), props(6), "Enu")); - M_M = inv(L_iso(props(5), props(7), "Enu")); + M_A = M_iso(props(4), props(6), "Enu"); + M_M = M_iso(props(5), props(7), "Enu"); offset = 8; } else { // Cubic elasticity: EA, EM, nuA, nuM, GA, GM - M_A = inv(L_cubic(props(4), props(6), props(8), "EnuG")); - M_M = inv(L_cubic(props(5), props(7), props(9), "EnuG")); + M_A = M_cubic(props(4), props(6), props(8), "EnuG"); + M_M = M_cubic(props(5), props(7), props(9), "EnuG"); offset = 10; } @@ -186,7 +176,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D mat DM = M_M - M_A; mat L; mat M_eff = xi*M_M + (1. - xi)*M_A; - L = inv(M_eff); + L = inv_cubic(M_eff); //definition of the CTE tensor vec alpha = (alphaM_iso*xi + alphaA_iso*(1.-xi))*Ith(); @@ -373,10 +363,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D double dPhiFdxiF = 0.; double dPhiFdxiR = 0.; - //Relative to reverse transformation. See the matching block in the mechanical - //unified_T.cpp for why the \sigma /\xi -divergent dPhihatRd{xi,ET} and dYtRd{xi,ET} - //legacy variables are not assembled — the K(1,\cdot ) chain rule below uses - //ETMean as the natural intermediate and stays finite at \xi ->0. + //Relative to reverse transformation: only the finite sigma-gradients (K(1,.) goes through ETMean) vec dPhihatRdsigma = zeros(6); vec dA_xiRdsigma = zeros(6); double dA_xiRdxiF = 0.; @@ -400,7 +387,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D // Reuss mixing on compliance tensor M_eff = xi*M_M + (1. - xi)*M_A; - L = inv(M_eff); + L = inv_cubic(M_eff); DM_sig = DM*sigma; Dalpha_T = Dalpha*(T+DT-T_init); @@ -440,7 +427,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D HfR = 0.5*a2*(1. + pow(xi, n3)) - a3; } else - HfR = 0.5*a2* -a3; + HfR = - a3; // Find Hcur explicit if (Mises_stress(sigma) > sigmacrit) @@ -539,9 +526,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D dYtRdsigma = D*ETMean; dPhiRdsigma = -1.*dPhihatRdsigma + dA_xiRdsigma + dlambda0dsigma - dYtRdsigma; - // K(1,\cdot ) via \Lambda_ETMean^j chain rule — avoids the floating-point cancellation - // between dPhiRdxiF and sum(dPhiRdET\cdot lambdaTF) at small \xi . See the matching - // comment block in the mechanical unified_T.cpp for the full derivation. + // K(1,.) through ETMean: avoids a ~12-digit cancellation at small xi (Mechanical/SMA/unified_T.hpp) const vec dPhiRdETMean = -(1. + D) * sigma; vec Lambda_ETMean_F = zeros(6); if (Mises_strain(lambdaTF - ETMean) > simcoon::iota) { @@ -561,9 +546,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D B(1,0) = -1.*sum(dPhiRdsigma%kappa_j[0]) + K(1,0); B(1,1) = -1.*sum(dPhiRdsigma%kappa_j[1]) + K(1,1); - // Magnitude scales for the convergence measure |FB|/Y_crit, not the signed YtF/YtR - // (they cross zero when D < 0): see the convergence-measure note in - // Mechanical/SMA/unified_T.hpp. + // magnitude scales, not the signed YtF/YtR (they cross zero when D < 0) Y_crit(0) = std::max(fabs(Y0t) + fabs(DHM), simcoon::iota); Y_crit(1) = std::max(fabs(Y0t) + fabs(D*PhihatR), simcoon::iota); @@ -607,8 +590,6 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D double dPhiRdtheta = dA_xiRdtheta; - //Computation of the tangent modulus — continuum 2-mechanism SMA operator - //assembled via the shared leading-mechanism helper (doc §7.4). mat Bhat = zeros(2, 2); Bhat(0,0) = sum(dPhiFdsigma%kappa_j[0]) - K(0,0); Bhat(0,1) = sum(dPhiFdsigma%kappa_j[1]) - K(0,1); @@ -616,17 +597,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D Bhat(1,1) = sum(dPhiRdsigma%kappa_j[1]) - K(1,1); const std::vector dPhidsigma_l = { dPhiFdsigma, dPhiRdsigma }; - // tangent_none must NOT zero P_epsilon/invBhat here: these sensitivities - // feed the PHYSICAL heat source r and its linearization (drdE/drdT), not - // just the Newton operator. Explicit-integration callers get the continuum - // operator in the thermomechanical kernels (the mechanical-only kernels - // honor tangent_none). - // tangent_algorithmic is ALSO clamped to continuum for this kernel: the - // finite-difference flow Hessian makes the local transformation system - // near-singular on the plateau (rcond ~ 1e-17 — approx-solved state then - // NaN-poisons the mixture stiffness and aborts the solve; LAPACK-backend - // dependent). tangent_closest_point too, until this kernel gets the closest-point - // branch of its mechanical twin (unified_T.cpp). + // none/algorithmic/closest-point clamped to continuum: P_epsilon, invBhat feed r (see header) const int tangent_mode_eff = (tangent_mode == tangent_none || tangent_mode == tangent_algorithmic || tangent_mode == tangent_closest_point) @@ -634,12 +605,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D const ContinuumTangent ct = compute_tangent_operator( tangent_mode_eff, Bhat, kappa_j, dPhidsigma_l, Ds_j, L, [&]() -> std::vector { // lazy: evaluated only in algorithmic mode - // Simo-Hughes algorithmic tangent (closest-point). dLambda^F/dsigma by central finite - // difference of the transformation flow Hcur(sigma)*dDrucker(sigma) + analytic linear DM; - // ETMean held fixed (transformation-state coupling deferred to CPP, future release). - // NOTE: only the mechanical block dSdE is algorithmically corrected. The thermal - // cross-tangents below (dSdT/drdE/drdT) keep the continuum form (raw kappa_j, L); - // their consistent kappa-tilde/L-tilde version is part of the CPP rework. + // Simo-Hughes: dLambda^F/dsigma by central FD + DM, ETMean held fixed; dSdE only auto lambdaTF_at = [&](const vec &s) -> vec { return sma_transformation_flow(s, sigmacrit, Hmin, Hmax, k1, aniso_criteria, DFA_params, @@ -661,8 +627,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D const std::vector& P_epsilon = ct.P_epsilon; const mat& invBhat = ct.invBhat; - // Multiplier elimination Delta s = invBhat * rhs with Bhat(l, j) = criterion l, mechanism j - // (row = unknown, as in assemble_continuum_tangent). + // Delta s = invBhat * rhs, Bhat(l, j) = criterion l, mechanism j std::vector P_theta(2); const double rhsF = dPhiFdtheta - sum(dPhiFdsigma%(L*alpha)); const double rhsR = dPhiRdtheta - sum(dPhiRdsigma%(L*alpha)); @@ -686,11 +651,9 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D double eta = eta_r + eta_ir; double eta_start = eta_r_start + eta_ir_start; -// double A_theta = eta; vec dA_thetadsigma = alpha; double dA_thetadxiF = Dc_0*log((T+DT)/T_init) + sum(Dalpha%sigma) + rhoDs0; double dA_thetadxiR = -1.*Dc_0*log((T+DT)/T_init) - sum(Dalpha%sigma) - rhoDs0; -// double dA_thetadtheta = (c_0+Dc_0*xi)/(T+DT); double Deta = eta - eta_start; double Deta_r = eta_r - eta_r_start; @@ -698,8 +661,7 @@ void umat_sma_unified_T_T(const string &umat_name, const vec &Etot, const vec &D double Dgamma_loc = 0.5*sum((sigma_start+sigma)%(DET)) + 0.5*(A_xiF_start + A_xiF)*DxiF + 0.5*(A_xiR_start + A_xiR)*DxiR; - // Derivatives of the dissipation through dDxi = P and dDET = lambdaTF P_F + lambdaTR P_R - // (transformation directions held fixed over the increment) and of the end forces A_xi + // dissipation derivatives, transformation directions held fixed over the increment const double f_F = 0.5*(sum((sigma_start+sigma)%lambdaTF) + A_xiF_start + A_xiF); const double f_R = 0.5*(sum((sigma_start+sigma)%lambdaTR) + A_xiR_start + A_xiR); const vec dDgamma_dE = 0.5*(dSdE.t()*DET diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Prony_Nfast.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Prony_Nfast.cpp index 08ab8238e..244c3d494 100644 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Prony_Nfast.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Prony_Nfast.cpp @@ -1,4 +1,21 @@ -///@file Prony.hpp +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file Prony_Nfast.cpp ///@brief User subroutine for Prony series viscoelastic model in 3D case ///@author Chemisky, Chatzigeorgiou ///@version 1.0 @@ -15,9 +32,6 @@ using namespace std; using namespace arma; -// Model, props and statev layout: see the Doxygen block in -// simcoon/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Prony_Nfast.hpp - namespace simcoon { void umat_prony_Nfast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) @@ -125,8 +139,6 @@ void umat_prony_Nfast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r A_v_start[i] = L_i[i]*(eps_e_start - EV_i[i]); } - // Implicit (backward-Euler) step of the Maxwell branches in closed form: the exact solution of - // the discrete equations and its consistent tangent (linear_viscoelastic.hpp) const vec eps_e = Etot + DEtot - alpha*(T + DT - T_init); const LinearViscoStep st = maxwell_parallel_step(L0, L_i, H_i, EV_i_start, eps_e, alpha, DTime); EV_tilde = zeros(6); @@ -190,7 +202,6 @@ void umat_prony_Nfast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r Wt_r += (T+DT)*Deta_r; Wt_ir += (T+DT)*Deta_ir; - //Return the statev; statev(0) = T_init; //From the statev to the internal variables statev(1) = EV_tilde(0); diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_Nfast.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_Nfast.cpp index 5076bfc56..d6279c5ab 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_Nfast.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_Nfast.cpp @@ -1,4 +1,21 @@ -///@file Zener_fast.hpp +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file Zener_Nfast.cpp ///@brief User subroutine for Zener viscoelastic model with N viscoelastic Kelvin branches in series in 3D case, with thermoelastic effect ///@brief This implementation uses a single scalar internal variable for the evaluation of the viscoelastic strain increment ///@author Chemisky, Chatzigeorgiou @@ -16,9 +33,6 @@ using namespace std; using namespace arma; -// Model, props and statev layout: see the Doxygen block in -// simcoon/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_Nfast.hpp - namespace simcoon { void umat_zener_Nfast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) @@ -115,8 +129,6 @@ void umat_zener_Nfast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r //Variables at the start of the increment const std::vector EV_i_start = EV_i; - // Implicit (backward-Euler) step of the Kelvin branches in closed form: the exact solution of - // the discrete equations and its consistent tangent (linear_viscoelastic.hpp) const vec eps_e = Etot + DEtot - alpha*(T + DT - T_init); const LinearViscoStep st = kelvin_series_step(L0, L_i, H_i, EV_i_start, eps_e, alpha, DTime); EV = zeros(6); @@ -178,7 +190,6 @@ void umat_zener_Nfast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r Wt_r += (T+DT)*Deta_r; Wt_ir += (T+DT)*Deta_ir; - //Return the statev; statev(0) = T_init; //From the statev to the internal variables statev(1) = EV(0); diff --git a/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.cpp b/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.cpp index a99b9c826..0e31cafbb 100755 --- a/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.cpp +++ b/src/Continuum_mechanics/Umat/Thermomechanical/Viscoelasticity/Zener_fast.cpp @@ -1,3 +1,20 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + ///@file Zener_fast.cpp ///@brief User subroutine for Zener viscoelastic model in 3D case, with thermoelastic effect(Poynting-Thomson model) ///@brief This implementation uses a single scalar internal variable for the evaluation of the viscoelastic strain increment @@ -16,20 +33,6 @@ using namespace std; using namespace arma; -///@brief The viscoelastic Zener model requires 10 constants: -// ------------------- -///@brief props(0) = rho - density -///@brief props(1) = c_p - specific heat capacity -///@brief props(2) = E0 - Thermoelastic Young's modulus -///@brief props(3) = nu0 - Thermoelastic Poisson's ratio -///@brief props(4) = alpha_iso - Thermoelastic CTE -///@brief props(5) = E1 - Viscoelastic Young modulus of Zener branch -///@brief props(6) = nu0 - Viscoelastic Poisson ratio of Zener branch -///@brief props(7) = etaB1 - Viscoelastic Bulk viscosity of Zener branch -///@brief props(8) = etaS1 - Viscoelastic shear viscosity of Zener branch - -///@brief Number of statev required for thermoelastic constitutive law - namespace simcoon { void umat_zener_fast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, mat &dSdE, mat &dSdT, mat &drdE, mat &drdT, const mat &DR, const int &nprops, const vec &props, const int &nstatev, vec &statev, const double &T, const double &DT,const double &Time,const double &DTime, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, double &Wt, double &Wt_r, double &Wt_ir, const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tangent_mode) @@ -100,8 +103,6 @@ void umat_zener_fast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, //Variables at the start of the increment const vec EV1_start = EV1; - // Implicit (backward-Euler) step of the Kelvin branch in closed form: the exact solution of - // the discrete equations and its consistent tangent (linear_viscoelastic.hpp) const vec eps_e = Etot + DEtot - alpha*(T + DT - T_init); const LinearViscoStep st = kelvin_series_step(L0, {L1}, {H1}, {EV1_start}, eps_e, alpha, DTime); EV1 = st.EV_i[0]; @@ -154,7 +155,6 @@ void umat_zener_fast_T(const vec &Etot, const vec &DEtot, vec &sigma, double &r, Wt_r += (T+DT)*Deta_r; Wt_ir += (T+DT)*Deta_ir; - //Return the statev; statev(0) = T_init; statev(1) = v; statev(2) = EV1(0); diff --git a/src/Continuum_mechanics/Umat/fea_transfer.cpp b/src/Continuum_mechanics/Umat/fea_transfer.cpp index 40d0e5254..a9400da62 100644 --- a/src/Continuum_mechanics/Umat/fea_transfer.cpp +++ b/src/Continuum_mechanics/Umat/fea_transfer.cpp @@ -41,30 +41,8 @@ using namespace arma; namespace simcoon { -//============================================================================= -// ABAQUS UMAT TRANSFER FUNCTIONS -//============================================================================= +// ==== FE component maps (fea_transfer.hpp) ==== -namespace { - -// FE arrays hold the element's ntens components: entry i is the simcoon component K(i). The -// others have zero strain (ndi = 3) or are stress-free, solved by the material point (ndi < 3, -// condensed_state.hpp) -struct fea_components { - int ntens; - int K[6]; - int operator()(const int &i) const { return K[i]; } -}; - -// Abaqus: ndi direct, then the nshr shears 12, 13, 23, the kept set of the condensed state -fea_components abaqus_components(const condensed_state &cs) { - fea_components c{static_cast(cs.K.n_elem), {0, 0, 0, 0, 0, 0}}; - for (int i = 0; i < c.ntens; i++) c.K[i] = static_cast(cs.K(i)); - return c; -} - -// Ansys: 11, 22, 33, 12, 23, 13 in 3D (13 and 23 swapped), 11, 22, 33, 12 in plane strain and -// axisymmetry, 11, 22, 12 in plane stress, 11 in 1D fea_components ansys_components(const int &nDirect, const int &nShear) { if (nDirect == 3 && nShear == 3) return {6, {0, 1, 2, 3, 5, 4}}; if (nDirect == 3 && nShear == 1) return {4, {0, 1, 2, 3, 0, 0}}; @@ -75,17 +53,6 @@ fea_components ansys_components(const int &nDirect, const int &nShear) { "axisymmetric, plane stress and 1D are)"); } -// FE vector -> simcoon 6 entries (vec, 6x1 or 1x6 mat), zero outside K -template void fe2vec(const double *a, const fea_components &K, V &v) { - v.zeros(); - for (int i = 0; i < K.ntens; i++) v(K(i)) = a[i]; -} - -template void vec2fe(const V &v, const fea_components &K, double *a) { - for (int i = 0; i < K.ntens; i++) a[i] = v(K(i)); -} - -// column-major ntens x ntens <-> the K x K block of a 6x6 void fe2mat(const double *a, const fea_components &K, mat &M) { M.zeros(6, 6); for (int j = 0; j < K.ntens; j++) @@ -97,10 +64,19 @@ void mat2fe(const mat &M, const fea_components &K, double *a) { for (int i = 0; i < K.ntens; i++) a[j*K.ntens + i] = M(K(i), K(j)); } -// STATEV after the bridge's nheader entries: the stress-free strain (ndi < 3, condensed_state.hpp) -// in simcoon order, then the law. The free strain is read at the start of the increment, rotated -// with it (DR) as the FE code rotates the strain: the material point then solves its change, not -// the whole value +// ==== ABAQUS UMAT TRANSFER FUNCTIONS ==== + +namespace { + +// Abaqus: ndi direct, then the nshr shears 12, 13, 23, the kept set of the condensed state +fea_components abaqus_components(const condensed_state &cs) { + fea_components c{static_cast(cs.K.n_elem), {0, 0, 0, 0, 0, 0}}; + for (int i = 0; i < c.ntens; i++) c.K[i] = static_cast(cs.K(i)); + return c; +} + +// STATEV after nheader: the stress-free strain (simcoon order), then the law; it is rotated with DR +// like the FE strain, so the material point solves its change, not its whole value void unpack_statev(const double *statev, const int &nstatev, const int &nheader, const uvec &C, const mat &DR, vec &Etot, vec &statev_smart) { if (!C.is_empty()) { vec E_C = zeros(6); @@ -225,9 +201,7 @@ mat abaqus_jacobian(const mat &Lt, const vec &sigma) { } int abaqus_kinematic_form(const char *cmname) { - // CMNAME is a space-padded CHARACTER*80 (no terminator); a C caller's string stops at '\0' - // and is read up to it only. `_R` must be the whole suffix: any other tail (EPICP_REF, ...) - // is ignored, as before the suffix existed + // CMNAME: space-padded CHARACTER*80 (a C string stops at '\0'); '_R' must be the whole suffix if (std::find(cmname, cmname + 7, '\0') != cmname + 7) return corate_type_total; return (cmname[5] == '_' && (cmname[6] == 'R' || cmname[6] == 'r') && (cmname[7] == ' ' || cmname[7] == '\0')) @@ -261,7 +235,7 @@ bool abaqus_finite_route(const std::string &umat_name, const int &ndi, const int return corate_type != corate_type_total || total_formulation_serves_T(umat_name); const auto &names = finite_umat_names(); auto it = names.find(umat_name); - if (it == names.end() || it->second <= 1) // UMEXT, UMABA: foreign plugins keep their own convention + if (it == names.end()) // small-strain only, the external plugins included return false; return corate_type != corate_type_total || total_formulation_serves(umat_name); } @@ -403,9 +377,7 @@ void smart2abaqus_T(double *stress, double *ddsdde, double *ddsddt, double *drpl pack_statev(Etot_end, statev_smart, 7, cs.C, statev); } -//============================================================================= -// ANSYS USERMAT TRANSFER FUNCTIONS -//============================================================================= +// ==== ANSYS USERMAT TRANSFER FUNCTIONS ==== void ansys2smart_M(const double *stress, const double *dsdePl, const double *strain, const double *dstrain, const double &Time, const double &Temp, const double &dTemp, diff --git a/src/Continuum_mechanics/Umat/return_mapping.cpp b/src/Continuum_mechanics/Umat/return_mapping.cpp index 26833b670..2119d599e 100644 --- a/src/Continuum_mechanics/Umat/return_mapping.cpp +++ b/src/Continuum_mechanics/Umat/return_mapping.cpp @@ -23,6 +23,7 @@ #include #include #include +#include #include #include @@ -34,6 +35,17 @@ namespace simcoon { namespace { int resolved_maxiter(const ReturnMappingControl &c) { return (c.maxiter > 0) ? c.maxiter : simcoon::maxiter_umat; } double resolved_precision(const ReturnMappingControl &c) { return (c.precision > 0.) ? c.precision : simcoon::precision_umat; } + +// M += a A B for 6x6 operands, in place: no 6x6 temporary on the heap +void add_scaled_product(mat::fixed<6,6> &M, const double a, const mat &A, const mat &B) { + for (int k = 0; k < 6; k++) { + for (int m = 0; m < 6; m++) { + const double b = a*B(m, k); + if (b == 0.) continue; + for (int i = 0; i < 6; i++) M(i, k) += A(i, m)*b; + } + } +} } ReturnMappingResult closest_point_return_mapping( @@ -68,10 +80,7 @@ ReturnMappingResult closest_point_return_mapping( // Elastic block: Lc is its tangent at the iterate (the given L for a linear block). mat Lc = L; const bool nonlinear_block = static_cast(hooks.elastic_response); - // Constraints, flows and the stress residual (every trial point); the criterion gradients - // are evaluated at the Newton top only (eval_gradients) — with finite-difference callbacks - // each gradient is a full set of state re-solves, which the elastic guard and the rejected - // line-search trials do not need. + // criterion gradients only at the Newton top (eval_gradients): with FD callbacks each is a full state re-solve auto eval_basic = [&](const vec &sig, const vec &Dl) { for (int j = 0; j < N; j++) { Phi(j) = mechanisms[j].Phi(sig); @@ -115,9 +124,7 @@ ReturnMappingResult closest_point_return_mapping( } }; - // Elastic guard: admissible trial state -> return it (bit-identical elasticity across - // modes). With Dlambda = 0 the tangent assembly masks every mechanism, so the state - // callbacks are not needed. + // admissible trial: return it; Dlambda = 0 masks every mechanism, so no state callbacks if (Phi.max() <= 0.) { n_l.assign(N, zeros(6)); // masked by Dlambda = 0 in the assembly: the gradients are not evaluated fill_tangent_pieces(zeros(N, N), std::vector(N, zeros(6, 6)), std::vector(N, zeros(6))); @@ -125,32 +132,42 @@ ReturnMappingResult closest_point_return_mapping( return r; } - const mat I6 = eye(6, 6); const int snap_niter = 20; // plain Newton gets this many iterations before the snap may act + // buffers sized once; M factorized on the stack (small_lu) + std::vector c(N, zeros(6)); + std::vector Minv_kc(N, zeros(6)); // M^-1 (kappa_j + c_j), for B_red and the stress step + mat::fixed<6,6> M; + small_lu<6> lu_M; + for (r.niter = 0; r.niter < maxiter; r.niter++) { // Newton ingredients at the current (state-consistent) iterate. eval_gradients(r.sigma); mat K = hooks.K ? hooks.K(r.sigma, r.Dlambda) : zeros(N, N); - mat M = I6; + M.eye(); for (int j = 0; j < N; j++) { - D_j[j] = mechanisms[j].dLambda_dsigma ? mechanisms[j].dLambda_dsigma(r.sigma) - : zeros(6, 6); - M += r.Dlambda(j) * (Lc * D_j[j]); + if (mechanisms[j].dLambda_dsigma) { + D_j[j] = mechanisms[j].dLambda_dsigma(r.sigma); + } + else { + D_j[j].zeros(6, 6); + } + add_scaled_product(M, r.Dlambda(j), Lc, D_j[j]); } - mat Minv; - if (!inv(Minv, M)) return r; // condensation breakdown: caller step-cuts + if (!lu_M.factor(M)) return r; // condensation breakdown: caller step-cuts - std::vector c(N, zeros(6)); + for (int j = 0; j < N; j++) c[j].zeros(); if (hooks.flow_state_coupling) { const std::vector dLambda = hooks.flow_state_coupling(r.sigma, r.Dlambda); for (int j = 0; j < N && j < int(dLambda.size()); j++) c[j] = Lc * dLambda[j]; } - const vec MinvR = Minv * R_sigma; - std::vector Minv_kc(N); // M^-1 (kappa_j + c_j), for B_red and the stress step - for (int j = 0; j < N; j++) Minv_kc[j] = Minv * (kappa[j] + c[j]); + const vec MinvR = lu_M.solve(R_sigma); + for (int j = 0; j < N; j++) { + Minv_kc[j] = kappa[j] + c[j]; + lu_M.solve_in_place(Minv_kc[j].memptr()); + } vec Phi_red(N); mat B_red(N, N); for (int l = 0; l < N; l++) { @@ -171,10 +188,7 @@ ReturnMappingResult closest_point_return_mapping( return r; } - // Semi-smooth Newton step on the reduced multiplier system, over the active set: a row - // with Phi < 0, no multiplier and a vanishing diagonal (a reorientation surface at zero - // effective stress) has an identically zero FB row and would make the solve singular; - // its step is zero anyway. + // active set excludes rows with Phi < 0, no multiplier and a vanishing diagonal (zero FB row, singular solve) const uvec active = find((Phi >= 0.) + (r.Dlambda > 0.) + (abs(B_red.diag()) >= simcoon::iota)); vec dDl = zeros(N); if (active.n_elem > 0) { @@ -214,9 +228,7 @@ ReturnMappingResult closest_point_return_mapping( } } } - // A trial point whose inner state solve fails, or that is not finite, is a rejected - // step like a merit blow-up: halve it (the inner Newton of a backstress row has no - // solution past a dp where the relaxed backstress overtakes the stress). + // failed inner state solve or non-finite trial = rejected step: halve it bool trial_ok = refresh(r.sigma, r.Dlambda); if (trial_ok) { eval_basic(r.sigma, r.Dlambda); @@ -287,9 +299,10 @@ bool cutting_plane_return_mapping( vec ds = zeros(N); double error = 1.0; int iter = iter0; + ncp_step local_step(control.ncp); while (iter < maxiter && error > precision) { hooks.jacobian(B); - Fischer_Burmeister_m(Phi, Y_crit, B, Ds_total, ds, error); + local_step(Phi, Y_crit, B, Ds_total, ds, error); hooks.update(ds); hooks.refresh(); if (hooks.consistency) error += hooks.consistency(); diff --git a/src/Continuum_mechanics/Umat/tangent_assembly.cpp b/src/Continuum_mechanics/Umat/tangent_assembly.cpp index 9a93040b1..b81c91099 100644 --- a/src/Continuum_mechanics/Umat/tangent_assembly.cpp +++ b/src/Continuum_mechanics/Umat/tangent_assembly.cpp @@ -30,12 +30,8 @@ namespace simcoon { namespace { -// Shared N-mechanism assembly. Lt_dPhi is the operator that takes dPhi to the strain -// sensitivity, L^T: the multipliers follow Delta s = invBhat (dPhi . L Delta eps) = -// invBhat ((L^T dPhi) . Delta eps). The continuum form passes its symmetric L itself; the -// algorithmic form passes the transpose of L~ = M^-1 L, which is not symmetric when -// dLambda/dsigma is not (a flow whose magnitude depends on the stress along another direction -// than its own, e.g. the SMA transformation flow Hcur(sigma_eq) dDrucker(sigma)). +// Lt_dPhi = L^T maps dPhi to the strain sensitivity: Delta s = invBhat ((L^T dPhi) . Delta eps); +// the algorithmic form passes (M^-1 L)^T, not symmetric when dLambda/dsigma is not ContinuumTangent assemble_tangent( const arma::mat& Bhat, const std::vector& kappa_j, @@ -55,17 +51,12 @@ ContinuumTangent assemble_tangent( if (Ds_j(i) > simcoon::iota) op(i) = 1.; } - // Bbar = active block of Bhat with identity on the inactive diagonal, - // so the matrix is invertible even when some mechanisms are inactive. - // mask(i,j) = op(i)*op(j); Bbar = mask \circ Bhat + diag(1-op). + // Bbar = mask % Bhat + diag(1 - op): identity on the inactive rows keeps it invertible const arma::mat mask = op * op.t(); const arma::mat Bbar = mask % Bhat + arma::diagmat(1.0 - op); arma::mat invBbar; if (!arma::inv(invBbar, Bbar) || !invBbar.is_finite()) { - // Degenerate hardening system (e.g. SMA transformation plateau, - // Bhat -> 0): fall back to the elastic operator for this increment - // instead of poisoning Lt/P with inf (the solver's own K inversion - // would then throw and abort the whole solve). + // degenerate Bhat (e.g. SMA plateau): elastic operator rather than inf in Lt/P, which would abort the solve ContinuumTangent out; out.Lt = L; out.P_epsilon.assign(Nmech, arma::zeros(6)); @@ -74,10 +65,7 @@ ContinuumTangent assemble_tangent( } arma::mat invBhat = mask % invBbar; // zero out inactive rows/cols - // Stack \partial \Phi^m gradients into a 6\times Nmech matrix so the double sum over - // (l, m) becomes one GEMM. Bhat(l, j) has the criterion on the row and the mechanism - // on the column, so \Delta s = invBhat \cdot (\partial \Phi : L : \Delta \varepsilon ) and - // P^l = \sum_m invBhat(l, m) L \partial \Phi^m, i.e. P = (L \cdot \partial \Phi ) \cdot invBhat^T. + // P = (L dPhi) invBhat^T: Bhat rows = criteria, columns = mechanisms arma::mat DP(6, Nmech); for (arma::uword m = 0; m < Nmech; ++m) DP.col(m) = dPhidsigma_l[m]; const arma::mat P = (Lt_dPhi * DP) * invBhat.t(); // 6 \times Nmech @@ -120,9 +108,7 @@ ContinuumTangent assemble_continuum_tangent( if (Ds <= simcoon::iota || std::abs(Bhat_scalar) < simcoon::iota || !std::isfinite(Bhat_scalar)) { - // Mechanism inactive, or degenerate hardening (Bhat -> 0, e.g. SMA - // transformation plateau): invBhat masked to 0, P_eps = 0, Lt = L — - // 1/Bhat would poison Lt with inf and abort the solve downstream. + // inactive or degenerate (Bhat -> 0): Lt = L, 1/Bhat would put inf in Lt out.Lt = L; return out; } @@ -157,19 +143,12 @@ ContinuumTangent assemble_algorithmic_tangent( } } - // Fast path: when no mechanism contributes a \partial \Lambda /\partial \sigma correction (typical - // for every UMAT that has not yet been extended), M = I and the - // algorithmic tangent reduces exactly to the continuum tangent — return - // it without paying the 6\times 6 inverse + GEMM cost. + // no dLambda/dsigma correction: M = I, the algorithmic tangent is the continuum one if (arma::approx_equal(sumDsLambdaSigma, arma::zeros(6, 6), "absdiff", simcoon::iota)) { return assemble_continuum_tangent(Bhat_continuum, kappa_j, dPhidsigma_l, Ds_j, L); } - // The \partial \Lambda /\partial \sigma blocks may come from finite differences (SMA - // transformation flow) and can be ill-conditioned near activation - // boundaries; whether inv() flags such a matrix as singular is - // LAPACK-backend dependent. Fall back to the (always well-posed) - // continuum operator rather than aborting the whole solve. + // singular M (FD dLambda/dsigma near activation, LAPACK-dependent): continuum fallback arma::mat Minv; if (!arma::inv(Minv, I6 + L * sumDsLambdaSigma)) { return assemble_continuum_tangent(Bhat_continuum, kappa_j, dPhidsigma_l, Ds_j, L); @@ -190,10 +169,7 @@ ContinuumTangent assemble_algorithmic_tangent( } } - // With the algorithmic operators in hand, the assembly is structurally - // identical to the continuum case — one shared assembly keeps the active-set - // masking, Bbar regularisation, P_eps accumulation, and Lt update in - // exactly one place. L~ is not symmetric in general: its transpose acts on dPhi. + // L~ = M^-1 L is not symmetric: its transpose acts on dPhi return assemble_tangent(Bhat_tilde, kappa_tilde, dPhidsigma_l, Ds_j, L_tilde, L_tilde.t()); } @@ -259,9 +235,7 @@ ContinuumTangent compute_tangent_operator( return assemble_continuum_tangent(Bhat, kappa_j, dPhidsigma_l, Ds_j, L); } if (tangent_mode == simcoon::tangent_algorithmic || tangent_mode == simcoon::tangent_closest_point) { - // Closest-point: a kernel that integrates with closest_point_return_mapping() takes its - // exact operator from cpp_consistent_tangent() and does not come here; for the others - // mode 3 IS the algorithmic operator (documented degradation). + // closest-point kernels use cpp_consistent_tangent() and never come here; for the others mode 3 = algorithmic if (!dLambda_dsigma) { // Flow independent of stress: the algorithmic operator IS the // continuum one. diff --git a/src/Continuum_mechanics/Umat/umat_L_elastic.cpp b/src/Continuum_mechanics/Umat/umat_L_elastic.cpp index b38626156..b5b7641c5 100755 --- a/src/Continuum_mechanics/Umat/umat_L_elastic.cpp +++ b/src/Continuum_mechanics/Umat/umat_L_elastic.cpp @@ -20,6 +20,7 @@ #include #include +#include #include #include #include @@ -46,21 +47,12 @@ void get_L_elastic(phase_characteristics &rve) const auto it_umat = list_umat.find(rve.sptr_matprops->umat_name); const int method = (it_umat != list_umat.end()) ? it_umat->second : 0; - //Mean-field models: the caller gave the sub-phases; the ellipsoidal schemes need the - //quadrature points of the Eshelby integrals + //Mean-field models: the caller gave the sub-phases (the ellipsoidal schemes read the + //quadrature of their Eshelby integrals, [mp, np], from the props at each pass) const int shape = sub_phase_shape(rve.sptr_matprops->umat_name); if (shape > 0) { check_sub_phases(rve); } - if (shape == 2) { - ellipsoid_multi::mp = rve.sptr_matprops->props(0); - ellipsoid_multi::np = rve.sptr_matprops->props(1); - ellipsoid_multi::x.set_size(ellipsoid_multi::mp); - ellipsoid_multi::wx.set_size(ellipsoid_multi::mp); - ellipsoid_multi::y.set_size(ellipsoid_multi::np); - ellipsoid_multi::wy.set_size(ellipsoid_multi::np); - points(ellipsoid_multi::x, ellipsoid_multi::wx, ellipsoid_multi::y, ellipsoid_multi::wy,ellipsoid_multi::mp, ellipsoid_multi::np); - } rve.global2local(); auto umat_M = std::dynamic_pointer_cast(rve.sptr_sv_local); @@ -125,8 +117,14 @@ void get_L_elastic(phase_characteristics &rve) mat Lt_n = zeros(6,6); int nbiter=0; double error = 1.; + //Options (props of MISCN, see mean_field_options): stopping criterion, absolute by + //default, and depth of the Anderson acceleration of the fixed point Lt <- g(Lt) + const mean_field_options opt = mean_field_options_of(rve); + double tol = simcoon::precision_micro; + std::vector dG, dF; //differences of the last iterates g and residuals f = g - Lt + vec g_prev, f_prev; - while ((error > simcoon::precision_micro)&&(nbiter <= simcoon::maxiter_micro)) { + while ((error > tol)&&(nbiter <= simcoon::maxiter_micro)) { Lt_n = umat_M->Lt; for (unsigned int i=0; iLt - Lt_n, "fro"); + if (opt.rtol > 0.) + tol = opt.rtol*norm(umat_M->Lt, "fro"); nbiter++; + + // Anderson acceleration (type II, depth m); kept as is when the least squares fails + if (opt.anderson > 0 && error > tol) { + const vec g = vectorise(umat_M->Lt); + const vec f = g - vectorise(Lt_n); + if (!g_prev.is_empty()) { + dG.push_back(g - g_prev); + dF.push_back(f - f_prev); + if (static_cast(dF.size()) > opt.anderson) { + dG.erase(dG.begin()); + dF.erase(dF.begin()); + } + } + g_prev = g; + f_prev = f; + if (!dF.empty()) { + const uword k = dF.size(); + mat DF(36, k), DG(36, k); + for (uword q=0; qLt = reshape(g - DG*gamma, 6, 6); + } + } } break; } diff --git a/src/Continuum_mechanics/Umat/umat_smart.cpp b/src/Continuum_mechanics/Umat/umat_smart.cpp index dbdb63b44..942bbaece 100644 --- a/src/Continuum_mechanics/Umat/umat_smart.cpp +++ b/src/Continuum_mechanics/Umat/umat_smart.cpp @@ -14,7 +14,6 @@ along with simcoon. If not, see . */ -///@file umat_smart.cpp ///@file umat_smart.cpp ///@brief Selection of constitutive laws and transfer between Abaqus and simcoon formats, /// implemented in 1D-2D-3D @@ -39,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -107,27 +107,6 @@ void size_statev(phase_characteristics &rve, unsigned int &size) { void statev_2_phases(phase_characteristics &rve, unsigned int &pos, const vec &statev) { for(auto r : rve.sub_phases) { - //The number of statev is here determined for each phase, then a sub_vector of the statev vector is taken from this - //vec Etot -> 6X - //vec DEtot -> 6X 12 - //vec sigma -> 6X 18 - //vec sigma_start -> 6 - //mat F0 -> 9 - //mat F1 -> 9 - //double T -> 1X 19 - //double DT -> 1X 20 - //vec sigma_in -> 6 - //vec sigma_in_start -> 6 - - //vec Wm -> 3X 23 - //vec Wm_start -> 3X 26 - //mat L -> 36X 62 - //mat Lt -> 36 - - //int nstatev - //vec statev nstatevX 62+nstatev - //vec statev_start - shared_ptr umat_phase_M = std::dynamic_pointer_cast(r.sptr_sv_global); unsigned int nstatev = umat_phase_M->statev.n_elem; @@ -152,27 +131,6 @@ void statev_2_phases(phase_characteristics &rve, unsigned int &pos, const vec &s void phases_2_statev(vec &statev, unsigned int &pos, const phase_characteristics &rve) { for(auto r : rve.sub_phases) { - //The number of statev is here determined for each phase, then a sub_vector of the statev vector is taken from this - //vec Etot -> 6X - //vec DEtot -> 6X 12 - //vec sigma -> 6X 18 - //vec sigma_start -> 6 - //mat F0 -> 9 - //mat F1 -> 9 - //double T -> 1X 19 - //double DT -> 1X 20 - //vec sigma_in -> 6 - //vec sigma_in_start -> 6 - - //vec Wm -> 3X 23 - //vec Wm_start -> 3X 26 - //mat L -> 36X 62 - //mat Lt -> 36 - - //int nstatev - //vec statev nstatevX 62+nstatev - //vec statev_start - shared_ptr umat_phase_M = std::dynamic_pointer_cast(r.sptr_sv_global); unsigned int nstatev = umat_phase_M->statev.n_elem; @@ -197,21 +155,31 @@ const std::map &finite_umat_names() { // The finite dispatch's name -> id map. A file-scope accessor rather than a // function-local static inside select_umat_M_finite so the convention test can iterate it. - static const std::map list_umat = {{"UMEXT",0},{"UMABA",1},{"ELISO",201},{"ELIST",201},{"ELORT",201},{"HYPOO",5},{"EPICP",6},{"EPCHA",7},{"EPJCK",17},{"EPKCP",201},{"SNTVE",8},{"NEOHI",9},{"NEOHC",10},{"MOORI",11},{"YEOHH",12},{"ISHAH",13},{"GETHH",14},{"SWANH",15},{"HOLZA",16},{"EPHIL",201},{"EPTRI",201},{"EPHAC",201},{"EPANI",201},{"EPDFA",201},{"EPCHG",201},{"EPHIN",201},{"MODUL",200},{"OGDEN",22},{"PYEXT",300}}; + static const std::map list_umat = {{"ELISO",201},{"ELIST",201},{"ELORT",201},{"HYPOO",5},{"EPICP",6},{"EPCHA",7},{"EPJCK",17},{"EPKCP",201},{"SNTVE",8},{"NEOHI",9},{"NEOHC",10},{"MOORI",11},{"YEOHH",12},{"ISHAH",13},{"GETHH",14},{"SWANH",15},{"HOLZA",16},{"EPHIL",201},{"EPTRI",201},{"EPHAC",201},{"EPANI",201},{"EPDFA",201},{"EPCHG",201},{"EPHIN",201},{"MODUL",200},{"OGDEN",22},{"PYEXT",300}}; + return list_umat; +} + +// The small-strain mechanical dispatch's name -> id map (select_umat_M_3D); ids 0-2 are the plugins +static const std::map &small_strain_umat_names() +{ + static const std::map list_umat = {{"UMEXT",0},{"UMABA",1},{"UMANS",2},{"ELISO",201},{"ELIST",201},{"ELORT",201},{"EPICP",5},{"EPKCP",201},{"EPCHA",7},{"SMADI",8},{"SMADC",8},{"SMAAI",8},{"SMAAC",8},{"SMRDI",9},{"SMRDC",9},{"SMRAI",9},{"SMRAC",9},{"LLDM0",10},{"ZENER",11},{"ZENNK",12},{"PRONK",13},{"EPHIL",201},{"EPTRI",201},{"EPHAC",201},{"EPANI",201},{"EPDFA",201},{"EPCHG",201},{"EPHIN",201},{"SMAMO",23},{"SMAMC",24},{"EPJCK",25},{"MIHEN",100},{"MIMTN",101},{"MISCN",103},{"MIPLN",104},{"MODUL",200},{"PYEXT",300}}; return list_umat; } +bool is_external_plugin(const std::string &umat_name) +{ + const auto it = small_strain_umat_names().find(umat_name); + return it != small_strain_umat_names().end() && it->second <= 2; +} + namespace { -// The single convention table, plain types only: a function-local static of an armadillo type -// registers a destructor that runs at DLL unload, and on Windows that order is undefined. +// plain types only: no arma static (destructor at DLL unload, Windows) const std::map &umat_conventions() { using SM = StressMeasure; - // Every NATIVE kernel is Kirchhoff. The groups below differ in how the tangent reaches the - // solver's corate (fed the corotated strain: in-rate for free; built from F: converted by - // Dtau_LieDD_2_DtauDe_corate) and in the frame they run in (material or lab). + // groups differ in how the tangent reaches the solver's corate and in the frame the kernel runs in static const std::map conventions = { // --- log-strain boxes: fed the corotated strain, run in the material frame --- // {measure, material_frame, layout_declared, tensorial statev {offset, Tensor2Type}} @@ -242,24 +210,19 @@ const std::map &umat_conventions() // HYPOO: the rate counterpart of ELORT, tau_{n+1} = tau_n + L : DEel {"HYPOO", {SM::kirchhoff, true, true, {}}}, - // --- finite kernels: build the tangent from F, then convert it to corate_type --- - {"SNTVE", {SM::kirchhoff}}, - {"NEOHI", {SM::kirchhoff}}, - {"NEOHC", {SM::kirchhoff}}, - {"MOORI", {SM::kirchhoff}}, - {"YEOHH", {SM::kirchhoff}}, - {"ISHAH", {SM::kirchhoff}}, - {"GETHH", {SM::kirchhoff}}, - {"SWANH", {SM::kirchhoff}}, - {"OGDEN", {SM::kirchhoff}}, - {"HOLZA", {SM::kirchhoff}}, - - // --- foreign conventions simcoon does not own --- - // Plugin adapters: the contract is the host code's (Abaqus DDSDDE is Cauchy-based). - // UMABA is in fact unreachable today -- id 1 has no case in the finite switch, so it - // throws -- and UMEXT's body is fully commented out. Declared for completeness. - {"UMEXT", {SM::cauchy}}, - {"UMABA", {SM::cauchy}}, + // --- finite kernels: tangent built from F (native dS/dE), converted to corate_type --- + {"SNTVE", {SM::kirchhoff, false, false, {}, true}}, + {"NEOHI", {SM::kirchhoff, false, false, {}, true}}, + {"NEOHC", {SM::kirchhoff, false, false, {}, true}}, + {"MOORI", {SM::kirchhoff, false, false, {}, true}}, + {"YEOHH", {SM::kirchhoff, false, false, {}, true}}, + {"ISHAH", {SM::kirchhoff, false, false, {}, true}}, + {"GETHH", {SM::kirchhoff, false, false, {}, true}}, + {"SWANH", {SM::kirchhoff, false, false, {}, true}}, + {"OGDEN", {SM::kirchhoff, false, false, {}, true}}, + {"HOLZA", {SM::kirchhoff, false, false, {}, true}}, + + // --- foreign conventions (plugin adapters, host contract: Abaqus DDSDDE is Cauchy) --- }; return conventions; } @@ -282,13 +245,7 @@ umat_convention output_convention_of(const std::string &umat_name) bool stress_output_is_kirchhoff(const std::string &umat_name) { - // Total where output_convention_of throws, and it shares that function's table rather than - // calling it: the python wrapper asks this for EVERY name it serves, including - // small-strain-only ones (SMADI, ZENER, the micro family) that the finite dispatch never - // sees and that therefore declare no finite convention. For those the answer is "leave the - // stress alone", not "refuse the call" -- and it must not cost a thrown exception per call. - // Inside the finite dispatch, where a missing declaration IS a bug, use - // output_convention_of and let it throw. + // shares the table instead of calling output_convention_of: no thrown exception per call const auto &conventions = umat_conventions(); auto it = conventions.find(umat_name); return it != conventions.end() && it->second.stress == StressMeasure::kirchhoff; @@ -299,6 +256,75 @@ bool total_formulation_serves(const std::string &umat_name) return umat_name != "HYPOO" && stress_output_is_kirchhoff(umat_name); } +bool material_tangent_serves(const std::string &umat_name) +{ + const auto &conventions = umat_conventions(); + auto it = conventions.find(umat_name); + return it != conventions.end() && it->second.material_tangent; +} + +void check_ncp_serves(const std::string &umat_name, const int &ncp, const bool &thermomechanical, + const int &tangent_mode) +{ + if (ncp_method_of(ncp, "ncp") == NCPMethod::fischer_burmeister) return; + if (thermomechanical || (umat_name != "EPICP" && umat_name != "EPKCP" && umat_name != "MODUL")) { + throw std::invalid_argument("ncp='min' serves the mechanical cutting planes of EPICP, EPKCP and MODUL only; '" + + umat_name + "'" + (thermomechanical ? " (thermomechanical)" : "") + " does not read it"); + } + if (tangent_mode == tangent_closest_point) { + throw std::invalid_argument("ncp='min' serves the cutting-plane integrator only; the closest-point " + "integrator keeps Fischer-Burmeister"); + } +} + +void check_integrator_serves(const std::string &umat_name, const int &tangent_mode, const bool &thermomechanical) +{ + if (tangent_mode != tangent_closest_point) return; + // the laws with a closest-point update, or whose cutting plane is one by construction (EPJCK) + static const std::set mechanical = {"EPICP", "EPCHA", "EPJCK", "MODUL", + "SMADI", "SMADC", "SMAAI", "SMAAC", + "SMRDI", "SMRDC", "SMRAI", "SMRAC"}; + static const std::set thermo = {"EPICP", "EPKCP"}; + bool serves; + if (thermomechanical) { + serves = thermo.count(umat_name) > 0; + } else { + const auto &names = finite_umat_names(); + const auto it = names.find(umat_name); + serves = mechanical.count(umat_name) > 0 || (it != names.end() && it->second == 201); // modular adapters + } + if (!serves) { + throw std::invalid_argument("integrator='closest_point' serves the laws with a closest-point update only " + "(mechanical: EPICP, EPCHA, EPJCK, MODUL and its adapters, the SMA kernels; " + "thermomechanical: EPICP, EPKCP); '" + umat_name + "'" + + (thermomechanical ? " (thermomechanical)" : "") + " has none"); + } +} + +int kernel_tangent_mode(const int &integrator, const int &tangent_mode) +{ + // Migration message of 2.x, removed in 3.0 (CHANGELOG.md, "Planned for 3.0"): 3 then falls in the range check + if (tangent_mode == tangent_closest_point) { + throw std::invalid_argument("tangent_mode 3 (closest point) is no longer a tangent choice: give " + "integrator='closest_point' with tangent_mode='algorithmic'"); + } + if (tangent_mode < tangent_none || tangent_mode > tangent_algorithmic) { + throw std::invalid_argument("tangent_mode must be 0 (none), 1 (continuum) or 2 (algorithmic); got " + + std::to_string(tangent_mode)); + } + switch (integrator) { + case integrator_cutting_plane: + return tangent_mode; + case integrator_closest_point: + if (tangent_mode == tangent_algorithmic) return tangent_closest_point; + throw std::invalid_argument("integrator='closest_point' returns its exact (algorithmic) operator only: " + "give tangent_mode='algorithmic'"); + default: + throw std::invalid_argument("integrator must be 'cutting_plane' or 'closest_point'; got code " + + std::to_string(integrator)); + } +} + bool total_formulation_serves_T(const std::string &umat_name) { static const std::set served = {"ELISO", "ELIST", "ELORT", "EPICP", "EPKCP", "ZENER", "ZENNK", @@ -332,10 +358,6 @@ int thermo_umat_id(const std::string &umat_name) void call_umat_T(const int &id_umat, phase_characteristics &rve, state_variables_T &sv, const vec &strain, const vec &Dstrain, const mat &DR, const double &Time, const double &DTime, const int &ndi, const int &nshr, bool &start, double &tnew_dt) { switch (id_umat) { - case 0: { -// umat_external_T(strain, Dstrain, sv.sigma, sv.r, sv.dSdE, sv.dSdT, sv.drdE, sv.drdT, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, sv.nstatev, sv.statev, sv.T, sv.DT, Time, DTime, sv.Wm(0), sv.Wm(1), sv.Wm(2), sv.Wm(3), sv.Wt(0), sv.Wt(1), sv.Wt(2), ndi, nshr, start, tnew_dt); - break; - } case 1: { umat_elasticity_iso_T(strain, Dstrain, sv.sigma, sv.r, sv.dSdE, sv.dSdT, sv.drdE, sv.drdT, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, sv.nstatev, sv.statev, sv.T, sv.DT, Time, DTime, sv.Wm(0), sv.Wm(1), sv.Wm(2), sv.Wm(3), sv.Wt(0), sv.Wt(1), sv.Wt(2), ndi, nshr, start, tnew_dt, sv.tangent_mode); break; @@ -388,17 +410,12 @@ static void select_umat_T_3D(phase_characteristics &rve, const mat &DR_global,co { UNUSED(solver_type); - // Same frame handling as select_umat_M_finite: the caller's DR is global; - // rotate it with the other state variables so the local UMAT receives the - // material-frame increment. + // caller's DR is global: rotated with the state so the local UMAT gets the material-frame increment rve.sptr_sv_global->DR = DR_global; rve.global2local(); auto umat_T = std::dynamic_pointer_cast(rve.sptr_sv_local); call_umat_T(thermo_umat_id(rve.sptr_matprops->umat_name), rve, *umat_T, umat_T->Etot, umat_T->DEtot, umat_T->DR, Time, DTime, ndi, nshr, start, tnew_dt); - // Small strain (F = I, J = 1): the UMATs return Cauchy sigma; - // mirror the mechanical stress-route convention (see select_umat_M/_finite) so the - // Kirchhoff-route consumers (output tau/J, set_start, sinks) stay correct: tau = J sigma = sigma, - // PKII = sigma. Without this the thermomechanical outputs read an empty tau (zero stress). + // small strain (F = I): tau = PKII = sigma, else the Kirchhoff-route consumers read an empty tau umat_T->tau = umat_T->sigma; umat_T->PKII = umat_T->sigma; rve.local2global(); @@ -407,10 +424,8 @@ static void select_umat_T_3D(phase_characteristics &rve, const mat &DR_global,co namespace { -// One tensorial internal variable carried by the rotation-free increment M of corates 4 and 5, -// with its variance: Truesdell (4) is tensor2's own push-forward (stress F X F^T, strain -// F^-T X F^-1, no Piola factor); log_F (5) the similarity M X M^-1 for both, symmetrised. Same -// rules as the solver's transport of tau and etot. +// transport of a statev tensor by the rotation-free M: Truesdell (4) = tensor2 push-forward (no Piola +// factor), log_F (5) = symmetrised M X M^-1; same rules as the transport of tau and etot vec transport_convected(const vec &v, const mat &M, const int &corate_type, const Tensor2Type &type) { const tensor2 t = tensor2::from_voigt(v, type); @@ -424,11 +439,7 @@ vec transport_convected(const vec &v, const mat &M, const int &corate_type, cons } -// The work around a box kernel on the finite route, shared by select_umat_M_finite and -// select_umat_T_finite. Before the call: the start state transported with THIS increment's DR, -// the total form's dual stress T, the material frame R_hat and, under corates 4/5, the declared -// statev convected. After it: the stress back to the lab, the Kirchhoff route stress, the -// true work under the log corates, the stored strains restored. +// the work around a box kernel on the finite route (select_umat_M_finite, select_umat_T_finite) struct finite_box { vec etot_stored; // set_start transports it at commit, with the increment it belongs to vec Detot_stored; @@ -447,9 +458,7 @@ finite_box enter_finite_box(state_variables &sv, const umat_convention &conv, co box.total = (corate_type == corate_type_total); const mat &DR = sv.DR; - // Transport the start state with THIS increment's DR before the kernel sees it, so the - // total strain, the start stress and the internal variables (rotated by DR inside the - // kernels) all live in the same configuration. + // transport the start state with THIS increment's DR: strain, start stress and statev share one configuration box.etot_stored = sv.etot; box.tau_start_lab = sv.sigma; if (corate_type != 4 && corate_type != 5) { @@ -475,10 +484,7 @@ finite_box enter_finite_box(state_variables &sv, const umat_convention &conv, co if (box.total) sv.sigma = t2v_stress(Kirchoff2LogStressU(v2t_stress(sv.sigma), sv.F0)); - // A box kernel runs in the frame that follows the material, R_hat (see umat_convention), so - // its anisotropy axes rotate with the body and its statev lives in that frame. For corates - // 0-3, R_hat_{n+1} = DR R_hat_n and the frame-relative increment is I; for 4 and 5 it is the - // rotation-free stretch M, applied here to the declared statev tensors. + // box kernel frame R_hat: increment I for corates 0-3, stretch M (applied to the declared statev) for 4/5 box.Detot_stored = sv.Detot; if (corate_type == 4) { // Truesdell: the kernel's increment is the closed-form Almansi increment of DR = DF, @@ -531,10 +537,7 @@ void leave_finite_box(state_variables &sv, const finite_box &box, const int &cor if (box.conv.material_frame) sv.sigma = rotate_stress(sv.sigma, box.R_hat); - // tau is the canonical Kirchhoff route stress (Wm stays on the Kirchhoff route). Every - // NATIVE kernel returns it directly, so this is a pass-through for all but the - // foreign-convention ones (the UMEXT/UMABA plugin adapters, whose contract belongs to - // the host code). Cauchy is a derived OUTPUT (tau/J), never on the route. + // tau is the route stress: pass-through, converted for a kernel declaring cauchy if (box.total) { double dW_T; sv.tau = total_formulation_tau(sv.sigma, sv.Detot, sv.F1, box.conv.material_frame, dW_T); @@ -560,7 +563,7 @@ void leave_finite_box(state_variables &sv, const finite_box &box, const int &cor } // namespace -void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const double &Time,const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, const int &corate_type, double &tnew_dt) +void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const double &Time,const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, const int &corate_type, double &tnew_dt, const bool &material_tangent) { // The finite route runs 3D (the solver imposes stress-free components through mixed boundary // conditions); condensed states go through select_umat_M or sim.umat. @@ -569,10 +572,19 @@ void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const } const std::map &list_umat = finite_umat_names(); - // guarded lookup: operator[] would default-insert 0 (=UMEXT, a no-op) for an - // unknown name and silently return zero stress; -1 falls to the default case - auto it_umat = list_umat.find(rve.sptr_matprops->umat_name); - const int id_umat = (it_umat != list_umat.end()) ? it_umat->second : -1; + const std::string &law = rve.sptr_matprops->umat_name; + auto it_umat = list_umat.find(law); + if (it_umat == list_umat.end()) { + if (is_external_plugin(law)) { + throw std::invalid_argument("'" + law + "' is an external plugin: the plugin API has no " + "finite-strain entry, it serves the small-strain route only"); + } + if (small_strain_umat_names().count(law)) { + throw std::invalid_argument("'" + law + "' serves the small-strain route only (no finite-strain kernel)"); + } + throw std::invalid_argument("Unknown umat name in the finite-strain dispatch: " + law); + } + const int id_umat = it_umat->second; // The caller provides DR in global coordinates; rotate it with the other // state variables so that the local UMAT receives a consistent increment. @@ -580,12 +592,11 @@ void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const rve.global2local(); auto umat_M = std::dynamic_pointer_cast(rve.sptr_sv_local); - // ONE declaration per kernel (output_convention_of) answers the questions below. It THROWS - // for a kernel that has not declared, so a new one is caught instead of inheriting a - // default; unknown names fall to the switch's own error. - umat_convention conv{StressMeasure::kirchhoff}; - if (id_umat >= 0) - conv = output_convention_of(rve.sptr_matprops->umat_name); + // one declaration per kernel: output_convention_of throws for an undeclared one + const umat_convention conv = output_convention_of(law); + // The corate the kernel's tangent is expressed in: the solver's, or the material tangent + // dS/dE when asked for and declared (a kernel built on F: the corate shapes nothing else) + const int corate_Lt = (material_tangent && conv.material_tangent) ? corate_type_material_tangent : corate_type; if (id_umat == 200) { // MODUL // tau = d psi / d eps_el is a stored-energy law only when the accumulated strain is ln V: @@ -619,17 +630,12 @@ void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const switch (id_umat) { - case 0: { -/* umat_external_M(umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, umat_M->sigma_in, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, solver_type, tnew_dt); -*/ - break; - } case 5: { umat_hypoelasticity_ortho(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_type, umat_M->tangent_mode); break; } case 6: { - umat_plasticity_iso(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode); + umat_plasticity_iso(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode, umat_M->ncp); break; } case 17: { @@ -643,30 +649,30 @@ void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const break; } case 8: { - umat_saint_venant(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_type, umat_M->tangent_mode); + umat_saint_venant(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_Lt, umat_M->tangent_mode); break; } case 201: { // Legacy names served by the modular engine on the log-strain box, like EPICP: // run in the material frame, so their anisotropy axes follow the body. - umat_legacy_modular(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode); + umat_legacy_modular(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode, umat_M->ncp); break; } case 200: { // MODUL under NLGEOM is a Hencky hyperelastic composition: - umat_modular(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode, total ? KinematicForm::total : KinematicForm::rate); + umat_modular(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode, total ? KinematicForm::total : KinematicForm::rate, umat_M->ncp); break; } case 9: { - umat_neo_hookean_incomp(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_type, umat_M->tangent_mode); + umat_neo_hookean_incomp(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_Lt, umat_M->tangent_mode); break; } case 10: case 11: case 12: case 13: case 14: case 15: case 16: { - umat_generic_hyper_invariants(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_type, umat_M->tangent_mode); + umat_generic_hyper_invariants(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_Lt, umat_M->tangent_mode); break; } case 22: { - umat_generic_hyper_pstretch(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_type, umat_M->tangent_mode); + umat_generic_hyper_pstretch(rve.sptr_matprops->umat_name, umat_M->etot, umat_M->Detot, umat_M->F0, umat_M->F1, umat_M->sigma, umat_M->Lt, umat_M->L, DR_kernel, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, corate_Lt, umat_M->tangent_mode); break; } case 300: { @@ -676,7 +682,8 @@ void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const break; } default: { - throw std::invalid_argument("Unknown umat name in the finite-strain dispatch: " + rve.sptr_matprops->umat_name); + throw std::logic_error("select_umat_M_finite: no kernel case for the id " + std::to_string(id_umat) + + " of '" + law + "' (finite_umat_names and the switch disagree)"); } } @@ -686,7 +693,8 @@ void select_umat_M_finite(phase_characteristics &rve, const mat &DR_global,const umat_M->L = rotate_stiffness(umat_M->L, box.R_hat); } leave_finite_box(*umat_M, box, corate_type, umat_M->Wm); - // No tangent conversion: every kernel emits Lt in corate_type already. + // No tangent conversion: every kernel emits Lt in corate_type already (in dS/dE when the + // material tangent was asked for and served). rve.local2global(); } @@ -696,9 +704,8 @@ static void select_umat_T_finite(phase_characteristics &rve, const mat &DR_globa if (ndi < 3) { throw std::invalid_argument("select_umat_T_finite: condensed stress states (ndi < 3) are not supported on the finite route"); } - // log_R (3) accumulates ln V and the total form reads ln U: the box strain the kernels' - // entropy and energies were written for. Under 4/5 the stretched start stress would shift - // eta_start and produce a heat that does not exist; 0-2 are not the production rate. + // log_R (3) and total only: under 4/5 the stretched start stress shifts eta_start (spurious heat), + // 0-2 are not the production rate if (corate_type != 3 && corate_type != corate_type_total) { throw simcoon::exception_solver( "thermomechanical finite strain serves corate_type 3 (log_R) or the total " @@ -718,9 +725,7 @@ static void select_umat_T_finite(phase_characteristics &rve, const mat &DR_globa rve.global2local(); auto umat_T = std::dynamic_pointer_cast(rve.sptr_sv_local); - // Every thermomechanical kernel is secant in the strain it is fed, so it reads as a law in - // the log strain: a Kirchhoff box run in the material frame. No statev layout is declared, - // which corates 4/5 would need (refused above). + // thermo kernels: Kirchhoff boxes in the material frame, no statev layout (corates 4/5 refused above) const umat_convention conv{StressMeasure::kirchhoff, true, false, {}}; const finite_box box = enter_finite_box(*umat_T, conv, rve.sptr_matprops->umat_name, corate_type); call_umat_T(id_umat, rve, *umat_T, umat_T->etot, umat_T->Detot, box.DR_kernel, Time, DTime, ndi, nshr, start, tnew_dt); @@ -738,14 +743,24 @@ static void select_umat_T_finite(phase_characteristics &rve, const mat &DR_globa rve.local2global(); } +// The instance of an external plugin API (one library per API type), loaded on first use and kept +// until exit: the library before the object, so the object is destroyed while its code is loaded +template +static Api &plugin(const char *path) +{ + static dylib::library lib(path, dylib::decorations::os_default()); + using destroy_fn = void(Api*); + static std::unique_ptr api(lib.get_function("create_api")(), + lib.get_function("destroy_api")); + return *api; +} + static void select_umat_M_3D(phase_characteristics &rve, const mat &DR_global,const double &Time,const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, double &tnew_dt) { - static const std::map list_umat = {{"UMEXT",0},{"UMABA",1},{"ELISO",201},{"ELIST",201},{"ELORT",201},{"EPICP",5},{"EPKCP",201},{"EPCHA",7},{"SMADI",8},{"SMADC",8},{"SMAAI",8},{"SMAAC",8},{"SMRDI",9},{"SMRDC",9},{"SMRAI",9},{"SMRAC",9},{"LLDM0",10},{"ZENER",11},{"ZENNK",12},{"PRONK",13},{"EPHIL",201},{"EPTRI",201},{"EPHAC",201},{"EPANI",201},{"EPDFA",201},{"EPCHG",201},{"EPHIN",201},{"SMAMO",23},{"SMAMC",24},{"EPJCK",25},{"MIHEN",100},{"MIMTN",101},{"MISCN",103},{"MIPLN",104},{"MODUL",200},{"PYEXT",300}}; + const std::map &list_umat = small_strain_umat_names(); - // Same frame handling as select_umat_M_finite: the caller's DR is global; - // rotate it with the other state variables so the local UMAT receives the - // material-frame increment. + // caller's DR is global: rotated with the state so the local UMAT gets the material-frame increment rve.sptr_sv_global->DR = DR_global; rve.global2local(); auto umat_M = std::dynamic_pointer_cast(rve.sptr_sv_local); @@ -755,52 +770,20 @@ static void select_umat_M_3D(phase_characteristics &rve, const mat &DR_global,co switch (it_umat != list_umat.end() ? it_umat->second : -1) { case 0: { - //umat_external(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode); - - static dylib::library ext_lib("external/umat_plugin_ext", dylib::decorations::os_default()); - - using create_fn = umat_plugin_ext_api*(); - using destroy_fn = void(umat_plugin_ext_api*); - - static create_fn* ext_create = ext_lib.get_function("create_api"); - static destroy_fn* ext_destroy = ext_lib.get_function("destroy_api"); - - static std::unique_ptr external_umat( - ext_create(), - ext_destroy - ); - - // ABI-preserving dispatch: external user plugins (umat_plugin_ext_api - // in umat_plugin_api.hpp) are loaded as dylibs at runtime and were - // compiled against the pre-tangent_mode signature. Do NOT forward - // umat_M->tangent_mode here — it would mismatch the plugin's - // umat_external_M symbol and crash on dispatch. To enable - // tangent_mode for external plugins, bump umat_plugin_ext_api's - // pure-virtual signature and recompile every plugin DSO. - external_umat->umat_external_M(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt); - + // plugin ABI predates tangent_mode: never forward it (symbol mismatch); bump umat_plugin_ext_api to add it + plugin("external/umat_plugin_ext").umat_external_M(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt); break; } case 1: { - // - static dylib::library aba_lib("external/umat_plugin_aba", dylib::decorations::os_default()); - - using create_fn = umat_plugin_aba_api*(); - using destroy_fn = void(umat_plugin_aba_api*); - - static create_fn* aba_create = aba_lib.get_function("create_api"); - static destroy_fn* aba_destroy = aba_lib.get_function("destroy_api"); - - static std::unique_ptr abaqus_umat( - aba_create(), - aba_destroy - ); - - abaqus_umat->umat_abaqus(rve, DR, Time, DTime, ndi, nshr, start, solver_type, tnew_dt); + plugin("external/umat_plugin_aba").umat_abaqus(rve, DR, Time, DTime, ndi, nshr, start, solver_type, tnew_dt); + break; + } + case 2: { + plugin("external/umat_plugin_ans").umat_ansys(rve, DR, Time, DTime, ndi, nshr, start, solver_type, tnew_dt); break; } case 5: { - umat_plasticity_iso(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode); + umat_plasticity_iso(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode, umat_M->ncp); break; } case 25: { @@ -842,14 +825,12 @@ static void select_umat_M_3D(phase_characteristics &rve, const mat &DR_global,co } case 200: { // Modular UMAT - composable constitutive model - umat_modular(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode); + umat_modular(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode, KinematicForm::rate, umat_M->ncp); break; } case 201: { - // Legacy names served by the modular engine: props are translated - // by the per-name adapter (legacy_adapters.cpp), state/outputs - // flow through umat_modular untouched. - umat_legacy_modular(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode); + // legacy names on the modular engine: props translated per name (legacy_adapters.cpp) + umat_legacy_modular(rve.sptr_matprops->umat_name, umat_M->Etot, umat_M->DEtot, umat_M->sigma, umat_M->Lt, umat_M->L, DR, rve.sptr_matprops->nprops, rve.sptr_matprops->props, umat_M->nstatev, umat_M->statev, umat_M->T, umat_M->DT, Time, DTime, umat_M->Wm(0), umat_M->Wm(1), umat_M->Wm(2), umat_M->Wm(3), ndi, nshr, start, tnew_dt, umat_M->tangent_mode, umat_M->ncp); break; } case 100: case 101: case 103: case 104: { @@ -866,12 +847,15 @@ static void select_umat_M_3D(phase_characteristics &rve, const mat &DR_global,co throw std::invalid_argument("Unknown umat name in the small-strain dispatch: " + rve.sptr_matprops->umat_name); } } - // Small-strain control (control_type 1): F = I, J = 1, so the box stress is - // simultaneously the Cauchy and the Kirchhoff stress. Mirror it onto tau/PKII - // so the route (the Newton iterates on tau) and the outputs stay consistent - // with the finite dispatcher. + // small strain: F = I, J = 1, so the box stress is both Cauchy and Kirchhoff umat_M->tau = umat_M->sigma; - umat_M->PKII = t2v_stress(Kirchoff2PKII(v2t_stress(umat_M->tau), umat_M->F1)); + if (umat_M->F1.is_diagmat() && arma::all(umat_M->F1.diag() == 1.)) { + // F1 = I exactly (the small-strain route): F^-1 tau F^-T is tau itself + umat_M->PKII = umat_M->tau; + } + else { + umat_M->PKII = t2v_stress(Kirchoff2PKII(v2t_stress(umat_M->tau), umat_M->F1)); + } rve.local2global(); } @@ -986,7 +970,7 @@ void run_umat_T(phase_characteristics &rve, const mat &DR,const double &Time,con } } -void run_umat_M(phase_characteristics &rve, const mat &DR, const double &Time, const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, const unsigned int &control_type, const int &corate_type, double &tnew_dt) +void run_umat_M(phase_characteristics &rve, const mat &DR, const double &Time, const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, const unsigned int &control_type, const int &corate_type, double &tnew_dt, const bool &material_tangent) { tnew_dt = 1.; @@ -999,7 +983,7 @@ void run_umat_M(phase_characteristics &rve, const mat &DR, const double &Time, c break; } case 2: case 3: case 4: case 5: case 6: { - select_umat_M_finite(rve, DR, Time, DTime, ndi, nshr, start, solver_type, corate_type, tnew_dt); + select_umat_M_finite(rve, DR, Time, DTime, ndi, nshr, start, solver_type, corate_type, tnew_dt, material_tangent); break; } default: { diff --git a/src/Simulation/Geometry/cylinder.cpp b/src/Simulation/Geometry/cylinder.cpp index d348a13fe..2562b576d 100755 --- a/src/Simulation/Geometry/cylinder.cpp +++ b/src/Simulation/Geometry/cylinder.cpp @@ -15,7 +15,7 @@ */ -///@file ellipsoid_characteristics.cpp +///@file cylinder.cpp ///@brief Characteristics of an cylinder thermomechanical phase, which hereditates from: ///- phase_characteristics ///@version 1.0 @@ -37,10 +37,6 @@ namespace simcoon{ //=====Public methods for cylinder============================================ -/*! - \brief default constructor -*/ - //------------------------------------------------------------- cylinder::cylinder() : geometry() //------------------------------------------------------------- @@ -56,10 +52,6 @@ cylinder::cylinder() : geometry() phi_geom = 0.; } -/*! - \brief Constructor with parameters -*/ - //------------------------------------------------------------- cylinder::cylinder(const double &mconcentration, const int &mcoatingof, const int &mcoatedby, const double &mL, const double &mR, const double &mpsi_geom, const double &mtheta_geom, const double &mphi_geom) : geometry(mconcentration) //------------------------------------------------------------- @@ -75,11 +67,6 @@ cylinder::cylinder() : geometry() phi_geom = mphi_geom; } -/*! - \brief Copy constructor - \param s cylinder_characteristics object to duplicate -*/ - //------------------------------------------------------ cylinder::cylinder(const cylinder& sv) : geometry(sv) //------------------------------------------------------ @@ -95,21 +82,10 @@ cylinder::cylinder(const cylinder& sv) : geometry(sv) phi_geom = sv.phi_geom; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ - //------------------------------------- cylinder::~cylinder() {} //------------------------------------- -/*! - \brief Standard operator = for cylinder_characteristics -*/ - - //---------------------------------------------------------------------- cylinder& cylinder::operator = (const cylinder& sv) //---------------------------------------------------------------------- diff --git a/src/Simulation/Geometry/ellipsoid.cpp b/src/Simulation/Geometry/ellipsoid.cpp index d963e2919..fde0d45ca 100755 --- a/src/Simulation/Geometry/ellipsoid.cpp +++ b/src/Simulation/Geometry/ellipsoid.cpp @@ -15,7 +15,7 @@ */ -///@file ellipsoid_characteristics.cpp +///@file ellipsoid.cpp ///@brief Characteristics of an ellipsoidal thermomechanical phase, which hereditates from: ///- phase_characteristics ///@version 1.0 @@ -37,9 +37,6 @@ namespace simcoon{ //=====Public methods for ellipsoid============================================ -/*! - \brief default constructor -*/ //------------------------------------------------------------- ellipsoid::ellipsoid() : geometry() @@ -57,10 +54,6 @@ ellipsoid::ellipsoid() : geometry() phi_geom = 0.; } -/*! - \brief Constructor with parameters -*/ - //------------------------------------------------------------- ellipsoid::ellipsoid(const double &mconcentration, const int &mcoatingof, const int &mcoatedby, const double &ma1, const double &ma2, const double &ma3, const double &mpsi_geom, const double &mtheta_geom, const double &mphi_geom) : geometry(mconcentration) //------------------------------------------------------------- @@ -77,10 +70,6 @@ ellipsoid::ellipsoid() : geometry() phi_geom = mphi_geom; } -/*! - \brief Copy constructor - \param s ellipsoid_characteristics object to duplicate -*/ //------------------------------------------------------ ellipsoid::ellipsoid(const ellipsoid& sv) : geometry(sv) @@ -98,21 +87,11 @@ ellipsoid::ellipsoid(const ellipsoid& sv) : geometry(sv) phi_geom = sv.phi_geom; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ //------------------------------------- ellipsoid::~ellipsoid() {} //------------------------------------- -/*! - \brief Standard operator = for ellipsoid_characteristics -*/ - - //---------------------------------------------------------------------- ellipsoid& ellipsoid::operator = (const ellipsoid& sv) //---------------------------------------------------------------------- diff --git a/src/Simulation/Geometry/geometry.cpp b/src/Simulation/Geometry/geometry.cpp index 13d4c8ef2..23f79391e 100755 --- a/src/Simulation/Geometry/geometry.cpp +++ b/src/Simulation/Geometry/geometry.cpp @@ -35,9 +35,6 @@ namespace simcoon{ //=====Public methods for geometry============================================ -/*! - \brief default constructor -*/ //------------------------------------------------------------- geometry::geometry() @@ -46,9 +43,6 @@ geometry::geometry() concentration = 0.; } -/*! - \brief Constructor with parameters -*/ //------------------------------------------------------------- geometry::geometry(const double &mconcentration) @@ -57,10 +51,6 @@ geometry::geometry(const double &mconcentration) concentration = mconcentration; } -/*! - \brief Copy constructor - \param s geometry characteristics object to duplicate -*/ //------------------------------------------------------ geometry::geometry(const geometry& sv) @@ -69,21 +59,11 @@ geometry::geometry(const geometry& sv) concentration = sv.concentration; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ //------------------------------------- geometry::~geometry() {} //------------------------------------- -/*! - \brief Standard operator = for geometry -*/ - - //---------------------------------------------------------------------- geometry& geometry::operator = (const geometry& sv) //---------------------------------------------------------------------- diff --git a/src/Simulation/Geometry/layer.cpp b/src/Simulation/Geometry/layer.cpp index 5d4c6ab3c..9661d4133 100755 --- a/src/Simulation/Geometry/layer.cpp +++ b/src/Simulation/Geometry/layer.cpp @@ -37,9 +37,6 @@ namespace simcoon{ //=====Public methods for layer============================================ -/*! - \brief default constructor -*/ //------------------------------------------------------------- layer::layer() : geometry() @@ -53,9 +50,6 @@ layer::layer() : geometry() phi_geom = 0.; } -/*! - \brief Constructor with parameters -*/ //------------------------------------------------------------- layer::layer(const double &mconcentration, const int &mlayerup, const int &mlayerdown, const double &mpsi_geom, const double &mtheta_geom, const double &mphi_geom) : geometry(mconcentration) @@ -69,10 +63,6 @@ layer::layer(const double &mconcentration, const int &mlayerup, const int &mlaye phi_geom = mphi_geom; } -/*! - \brief Copy constructor - \param s layer characteristics object to duplicate -*/ //------------------------------------------------------ layer::layer(const layer& sv) : geometry(sv) @@ -86,21 +76,11 @@ layer::layer(const layer& sv) : geometry(sv) phi_geom = sv.phi_geom; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ //------------------------------------- layer::~layer() {} //------------------------------------- -/*! - \brief Standard operator = for layer -*/ - - //---------------------------------------------------------------------- layer& layer::operator = (const layer& sv) //---------------------------------------------------------------------- diff --git a/src/Simulation/Maths/eig_sym3.cpp b/src/Simulation/Maths/eig_sym3.cpp new file mode 100644 index 000000000..cf509a192 --- /dev/null +++ b/src/Simulation/Maths/eig_sym3.cpp @@ -0,0 +1,148 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file eig_sym3.cpp +///@brief Fixed-size spectral decomposition of symmetric 3x3 matrices (cyclic Jacobi, Kopp 2008) +///@version 1.0 + +#include +#include +#include +#include + +using namespace std; +using namespace arma; + +namespace simcoon{ + +bool eig_sym_3x3(vec::fixed<3> &lam, mat::fixed<3,3> &Q, const mat::fixed<3,3> &A_in) { + + // Working copy, symmetrised from the upper triangle (the lower one is never read) + double A[3][3]; + for (int i = 0; i < 3; i++) { + for (int j = i; j < 3; j++) { + A[i][j] = A_in(i, j); + A[j][i] = A_in(i, j); + if (!std::isfinite(A[i][j])) return false; + } + } + double w[3] = {A[0][0], A[1][1], A[2][2]}; + double V[3][3] = {{1., 0., 0.}, {0., 1., 0.}, {0., 0., 1.}}; + + bool converged = false; + for (int sweep = 0; sweep < 50; sweep++) { + const double so = std::fabs(A[0][1]) + std::fabs(A[0][2]) + std::fabs(A[1][2]); + if (so == 0.) { + converged = true; + break; + } + // threshold pivoting in the first sweeps, then every non-negligible pair + const double thresh = (sweep < 4) ? 0.2*so/9. : 0.; + for (int p = 0; p < 2; p++) { + for (int q = p + 1; q < 3; q++) { + const double apq = A[p][q]; + const double g = 100.*std::fabs(apq); + if (sweep > 4 && std::fabs(w[p]) + g == std::fabs(w[p]) && std::fabs(w[q]) + g == std::fabs(w[q])) { + A[p][q] = A[q][p] = 0.; // below the precision of both diagonal terms + } + else if (std::fabs(apq) > thresh) { + const double h = w[q] - w[p]; + double t; + if (std::fabs(h) + g == std::fabs(h)) { + t = apq/h; + } + else { + const double theta = 0.5*h/apq; + t = 1./(std::fabs(theta) + std::sqrt(1. + theta*theta)); + if (theta < 0.) t = -t; + } + const double c = 1./std::sqrt(1. + t*t); + const double s = t*c; + const double z = t*apq; + A[p][q] = A[q][p] = 0.; + w[p] -= z; + w[q] += z; + for (int r = 0; r < 3; r++) { + if (r == p || r == q) continue; + const double arp = A[r][p], arq = A[r][q]; + A[r][p] = A[p][r] = c*arp - s*arq; + A[r][q] = A[q][r] = s*arp + c*arq; + } + for (int r = 0; r < 3; r++) { + const double vrp = V[r][p], vrq = V[r][q]; + V[r][p] = c*vrp - s*vrq; + V[r][q] = s*vrp + c*vrq; + } + } + } + } + } + if (!converged) return false; + + // ascending order, as arma::eig_sym + int idx[3] = {0, 1, 2}; + if (w[idx[0]] > w[idx[1]]) std::swap(idx[0], idx[1]); + if (w[idx[1]] > w[idx[2]]) std::swap(idx[1], idx[2]); + if (w[idx[0]] > w[idx[1]]) std::swap(idx[0], idx[1]); + for (int k = 0; k < 3; k++) { + lam(k) = w[idx[k]]; + for (int r = 0; r < 3; r++) Q(r, k) = V[r][idx[k]]; + } + return true; +} + +mat::fixed<3,3> spectral_compose_3x3(const mat::fixed<3,3> &Q, const vec::fixed<3> &f) { + mat::fixed<3,3> out; + for (int i = 0; i < 3; i++) { + for (int j = i; j < 3; j++) { + double x = 0.; + for (int k = 0; k < 3; k++) x += Q(i, k)*f(k)*Q(j, k); + out(i, j) = x; + out(j, i) = x; + } + } + return out; +} + +bool sqrtmat_sympd_3x3(mat::fixed<3,3> &out, const mat::fixed<3,3> &A) { + vec::fixed<3> lam; + mat::fixed<3,3> Q; + if (!eig_sym_3x3(lam, Q, A) || lam(0) < 0.) return false; + for (int k = 0; k < 3; k++) lam(k) = std::sqrt(lam(k)); + out = spectral_compose_3x3(Q, lam); + return true; +} + +bool logmat_sympd_3x3(mat::fixed<3,3> &out, const mat::fixed<3,3> &A) { + vec::fixed<3> lam; + mat::fixed<3,3> Q; + if (!eig_sym_3x3(lam, Q, A) || !(lam(0) > 0.)) return false; + for (int k = 0; k < 3; k++) lam(k) = std::log(lam(k)); + out = spectral_compose_3x3(Q, lam); + return true; +} + +bool expmat_sym_3x3(mat::fixed<3,3> &out, const mat::fixed<3,3> &A) { + vec::fixed<3> lam; + mat::fixed<3,3> Q; + if (!eig_sym_3x3(lam, Q, A)) return false; + for (int k = 0; k < 3; k++) lam(k) = std::exp(lam(k)); + out = spectral_compose_3x3(Q, lam); + return true; +} + +} //namespace simcoon diff --git a/src/Simulation/Maths/lagrange.cpp b/src/Simulation/Maths/lagrange.cpp index bf7a8db6d..82054e98d 100755 --- a/src/Simulation/Maths/lagrange.cpp +++ b/src/Simulation/Maths/lagrange.cpp @@ -27,7 +27,6 @@ using namespace arma; namespace simcoon{ -//This function is used to determine an exponential Lagrange Multiplier (like contact in Abaqus) double lagrange_exp(const double &h, const double &c, const double &p0) { if(h <= -c) { @@ -39,7 +38,6 @@ double lagrange_exp(const double &h, const double &c, const double &p0) { } } -//This function is used to determine the first derivative of an exponential Lagrange Multiplier double dlagrange_exp(const double &h, const double &c, const double &p0) { if(h <= -c) { @@ -52,7 +50,6 @@ double dlagrange_exp(const double &h, const double &c, const double &p0) { } -//This function is used to determine a power-law Lagrange Multiplier for problem such x >= 0 double lagrange_pow_0(const double &x, const double &c, const double &p0, const double &n, const double &alpha) { double h = c; @@ -67,7 +64,6 @@ double lagrange_pow_0(const double &x, const double &c, const double &p0, const } } -//This function is used to determine the first derivative of a power-law Lagrange Multiplier for problem such x >= 0 double dlagrange_pow_0(const double &x, const double &c, const double &p0, const double &n, const double &alpha) { @@ -83,7 +79,6 @@ double dlagrange_pow_0(const double &x, const double &c, const double &p0, const } } -//This function is used to determine a power-law Lagrange Multiplier for problem such x <= 1 double lagrange_pow_1(const double &x, const double &c, const double &p0, const double &n, const double &alpha) { double h = 1.-c; @@ -98,7 +93,6 @@ double lagrange_pow_1(const double &x, const double &c, const double &p0, const } } -//This function is used to determine the first derivative of a power-law Lagrange Multiplier for problem such x <= 1 double dlagrange_pow_1(const double &x, const double &c, const double &p0, const double &n, const double &alpha) { double h = 1.-c; @@ -113,14 +107,13 @@ double dlagrange_pow_1(const double &x, const double &c, const double &p0, const } } -//This function is used to determine the SECOND derivative of a power-law Lagrange Multiplier for problem such x <= 1 double d2lagrange_pow_1(const double &x, const double &c, const double &p0, const double &n, const double &alpha) { double h = 1.-c; if(x <= h) { - return (p0 + 1.) * n * pow(1. - x, -n - 1.) + n * (n + 1.) * x * pow(1. - x, -n - 2.); + return p0*(2.*n*pow(1. - x, -n - 1.) + n*(n + 1.)*x*pow(1. - x, -n - 2.)); } else { diff --git a/src/Simulation/Maths/num_solve.cpp b/src/Simulation/Maths/num_solve.cpp index 0d8434c8e..134d6eac3 100755 --- a/src/Simulation/Maths/num_solve.cpp +++ b/src/Simulation/Maths/num_solve.cpp @@ -10,24 +10,23 @@ */ ///@file num_solve.cpp -///@brief random number generators +///@brief Newton and Fischer-Burmeister solvers of the local return-mapping systems ///@author Chemisky & Anagnostou ///@version 1.0 ///@date 10/23/2014 #include #include +#include +#include #include #include #include +#include using namespace std; using namespace arma; -/* Notice*/ -// Recall that these methods are intended to solve an problem by means of the Kuhn-Tucker conditions -//This means : Phi <= 0 ; Dp >= 0; Phi*Dp = 0, with Dp = pdot * Dt, a non-zero time state - namespace simcoon{ void Newton_Raphon(const vec &Phi, const vec &Y_crit, const mat &denom, vec &Dp, vec &dp, double &error) @@ -66,7 +65,7 @@ void Fischer_Burmeister(const vec &Phi, const vec &Y_crit, const mat &denom, vec int n=Phi.n_elem; for (int i=0; i 0); + assert(fabs(Y_crit(i)) > 0); } vec FB = zeros(n); @@ -132,7 +131,7 @@ void Fischer_Burmeister_limits(const vec &Phi_p, const vec &Phi_l, const vec &Y_ int n=Phi_p.n_elem; for (int i=0; i 0); + assert(fabs(Y_crit(i)) > 0); } vec Phi_1 = zeros(n); @@ -286,7 +285,7 @@ void Fischer_Burmeister_m(const vec &Phi, const vec &Y_crit, const mat &denom, v mat denomFB = zeros(n,n); mat delta = eye(n,n); - //Determine the eigenvalues of denom + // row scaling: |diagonal of denom| vec factor_denom = zeros(n); for (int i=0; i simcoon::limit) { - dp = -1.*solve(denomFB, FB); + // One or two mechanisms (the common cases): closed forms, no LAPACK call. + if (n == 1) { + const double a = denomFB(0,0); + if (fabs(a) > simcoon::limit) { + dp.set_size(1); + dp(0) = -FB(0)/a; } else { dp = zeros(n); } - } catch (const std::runtime_error &e) { - cerr << "Error in det or inv: throw inv exception " << e.what() << endl; - throw simcoon::exception_det("Error in det function inside Fischer_Burmeister_m."); - } + } + else if (n == 2) { + const double det_FB = denomFB(0,0)*denomFB(1,1) - denomFB(0,1)*denomFB(1,0); + if (fabs(det_FB) > simcoon::limit) { + dp.set_size(2); + dp(0) = -(denomFB(1,1)*FB(0) - denomFB(0,1)*FB(1))/det_FB; // Cramer + dp(1) = -(denomFB(0,0)*FB(1) - denomFB(1,0)*FB(0))/det_FB; + } + else { + dp = zeros(n); + } + } + else { + try { + if (fabs(det(denomFB)) > simcoon::limit) { + dp = -1.*solve(denomFB, FB); + } + else { + dp = zeros(n); + } + } catch (const std::runtime_error &e) { + cerr << "Error in det or inv: throw inv exception " << e.what() << endl; + throw simcoon::exception_det("Error in det function inside Fischer_Burmeister_m."); + } + } //New update of the transformation/orientation multipliers Dp = Dp + dp; @@ -354,6 +377,55 @@ void Fischer_Burmeister_m(const vec &Phi, const vec &Y_crit, const mat &denom, v } +NCPMethod ncp_method_of(const int &code, const string &where) +{ + if (code == 0) return NCPMethod::fischer_burmeister; + if (code == 1) return NCPMethod::min_fb_fallback; + throw std::invalid_argument(where + ": ncp must be 0 (Fischer-Burmeister) or 1 " + "(min-NCP with a Fischer-Burmeister fallback); got " + std::to_string(code)); +} + +void ncp_step::operator()(const vec &Phi, const vec &Y_crit, const mat &denom, vec &Dp, vec &dp, double &error) +{ + const uword n = Phi.n_elem; + // coupled rows: Fischer-Burmeister (a plain semi-smooth Newton cycles); uncoupled rows: scalar min steps + bool uncoupled = (method_ != NCPMethod::fischer_burmeister) && !fallback_; + for (uword i = 0; uncoupled && i < n; i++) + for (uword j = 0; j < n; j++) + if (i != j && denom(i, j) != 0.) { uncoupled = false; break; } + if (!uncoupled) { + Fischer_Burmeister_m(Phi, Y_crit, denom, Dp, dp, error); + return; + } + // min(Dp*, -Phi) = 0 row by row, Dp* = Dp |B|: an active row solves Phi + B dp = 0 (classic + // cutting plane), an inactive one resets its multiplier + vec r(n), J(n); + double err = 0.; + bool singular = false; + for (uword i = 0; i < n; i++) { + const double B = denom(i, i); + const double f = fabs(B); + const bool active = (Phi(i) > 0.) || (-Phi(i) < Dp(i)*f); + r(i) = active ? -Phi(i) : Dp(i)*f; + J(i) = active ? -B : ((f > 0.) ? f : 1.); + err += 2.*fabs(r(i))/fabs(Y_crit(i)); + singular = singular || !(fabs(J(i)) > simcoon::iota); + } + // a growing error: the semi-smooth Newton overshoots or cycles, Fischer-Burmeister from here + if ((prev_error_ >= 0. && err > prev_error_) || singular) { + fallback_ = true; + Fischer_Burmeister_m(Phi, Y_crit, denom, Dp, dp, error); + return; + } + prev_error_ = err; + dp.set_size(n); + for (uword i = 0; i < n; i++) { + dp(i) = -r(i)/J(i); + Dp(i) += dp(i); + } + error = err; +} + void Fischer_Burmeister_m_limits(const vec &Phi_p, const vec &Phi_l, const vec &Y_crit, const mat &denom, const mat &denom_l, vec &Dp, vec &dp, double &error) { int n=Phi_p.n_elem; @@ -368,7 +440,7 @@ void Fischer_Burmeister_m_limits(const vec &Phi_p, const vec &Phi_l, const vec & mat denomFB = zeros(n,n); mat delta = eye(n,n); - //Determine the eigenvalues of denom + // row scaling: |diagonal of denom| vec factor_denom = zeros(n); for (int i=0; i fill_voigt_stress(const double &alpha, const int &axis, const bool &active) { double act = active ? 1. : -1.; - mat voigt_stress = zeros(6,6); + mat::fixed<6,6> voigt_stress(fill::zeros); double c = cos(alpha); double s = act*sin(alpha); @@ -235,7 +235,7 @@ mat fill_voigt_stress(const double &alpha, const int &axis, const bool &active) } // Internal: Generate 6x6 stress Voigt rotation matrix from rotation matrix -mat fill_voigt_stress(const mat &DR, const bool &active) { +mat::fixed<6,6> fill_voigt_stress(const mat &DR, const bool &active) { double a, b, c, d, e, f, g, h, i; if(active) { @@ -248,7 +248,7 @@ mat fill_voigt_stress(const mat &DR, const bool &active) { c = DR(2,0); f = DR(2,1); i = DR(2,2); } - mat voigt_stress = zeros(6,6); + mat::fixed<6,6> voigt_stress(fill::zeros); voigt_stress(0,0) = a*a; voigt_stress(0,1) = b*b; voigt_stress(0,2) = c*c; voigt_stress(0,3) = 2.*a*b; voigt_stress(0,4) = 2.*a*c; voigt_stress(0,5) = 2.*b*c; voigt_stress(1,0) = d*d; voigt_stress(1,1) = e*e; voigt_stress(1,2) = f*f; @@ -266,10 +266,10 @@ mat fill_voigt_stress(const mat &DR, const bool &active) { } // Internal: Generate 6x6 strain Voigt rotation matrix from angle/axis -mat fill_voigt_strain(const double &alpha, const int &axis, const bool &active) { +mat::fixed<6,6> fill_voigt_strain(const double &alpha, const int &axis, const bool &active) { double act = active ? 1. : -1.; - mat voigt_strain = zeros(6,6); + mat::fixed<6,6> voigt_strain(fill::zeros); double c = cos(alpha); double s = act*sin(alpha); @@ -333,7 +333,7 @@ mat fill_voigt_strain(const double &alpha, const int &axis, const bool &active) } // Internal: Generate 6x6 strain Voigt rotation matrix from rotation matrix -mat fill_voigt_strain(const mat &DR, const bool &active) { +mat::fixed<6,6> fill_voigt_strain(const mat &DR, const bool &active) { double a, b, c, d, e, f, g, h, i; if(active) { @@ -346,7 +346,7 @@ mat fill_voigt_strain(const mat &DR, const bool &active) { c = DR(2,0); f = DR(2,1); i = DR(2,2); } - mat voigt_strain = zeros(6,6); + mat::fixed<6,6> voigt_strain(fill::zeros); voigt_strain(0,0) = a*a; voigt_strain(0,1) = b*b; voigt_strain(0,2) = c*c; voigt_strain(0,3) = a*b; voigt_strain(0,4) = a*c; voigt_strain(0,5) = b*c; voigt_strain(1,0) = d*d; voigt_strain(1,1) = e*e; voigt_strain(1,2) = f*f; @@ -663,19 +663,11 @@ vec::fixed<3> Rotation::as_rotvec(bool degrees) const { } mat::fixed<6,6> Rotation::as_voigt_stress_rotation(bool active) const { - mat R = as_matrix(); - mat vs_dyn = fill_voigt_stress(R, active); - mat::fixed<6,6> vs; - vs = vs_dyn; - return vs; + return fill_voigt_stress(as_matrix(), active); } mat::fixed<6,6> Rotation::as_voigt_strain_rotation(bool active) const { - mat R = as_matrix(); - mat ve_dyn = fill_voigt_strain(R, active); - mat::fixed<6,6> ve; - ve = ve_dyn; - return ve; + return fill_voigt_strain(as_matrix(), active); } // ============================================================================= @@ -933,6 +925,8 @@ Rotation Rotation::slerp(const Rotation& other, double t) const { // ============================================================================= // Convenience Free Functions // ============================================================================= +// Fixed-size Voigt operators and intermediate products: the right-associated product of the +// dynamic matrices allocated two 6x6 temporaries on the heap per call. vec rotate_vec(const vec &v, const mat &DR) { return DR*v; @@ -954,67 +948,75 @@ mat rotate_mat(const mat &m, const double &alpha, const int &axis) { } vec rotate_stress(const vec &V, const double &alpha, const int &axis, const bool &active) { - mat vs = fill_voigt_stress(alpha, axis, active); + const mat::fixed<6,6> vs = fill_voigt_stress(alpha, axis, active); return vs*V; } vec rotate_stress(const vec &V, const mat &DR, const bool &active) { - mat vs = fill_voigt_stress(DR, active); + const mat::fixed<6,6> vs = fill_voigt_stress(DR, active); return vs*V; } vec rotate_strain(const vec &V, const double &alpha, const int &axis, const bool &active) { - mat ve = fill_voigt_strain(alpha, axis, active); + const mat::fixed<6,6> ve = fill_voigt_strain(alpha, axis, active); return ve*V; } vec rotate_strain(const vec &V, const mat &DR, const bool &active) { - mat ve = fill_voigt_strain(DR, active); + const mat::fixed<6,6> ve = fill_voigt_strain(DR, active); return ve*V; } mat rotate_stiffness(const mat &L, const double &alpha, const int &axis, const bool &active) { - mat vs = fill_voigt_stress(alpha, axis, active); - return vs*(L*trans(vs)); + const mat::fixed<6,6> vs = fill_voigt_stress(alpha, axis, active); + const mat::fixed<6,6> tmp = L*trans(vs); + return vs*tmp; } mat rotate_stiffness(const mat &L, const mat &DR, const bool &active) { - mat vs = fill_voigt_stress(DR, active); - return vs*(L*trans(vs)); + const mat::fixed<6,6> vs = fill_voigt_stress(DR, active); + const mat::fixed<6,6> tmp = L*trans(vs); + return vs*tmp; } mat rotate_compliance(const mat &M, const double &alpha, const int &axis, const bool &active) { - mat ve = fill_voigt_strain(alpha, axis, active); - return ve*(M*trans(ve)); + const mat::fixed<6,6> ve = fill_voigt_strain(alpha, axis, active); + const mat::fixed<6,6> tmp = M*trans(ve); + return ve*tmp; } mat rotate_compliance(const mat &M, const mat &DR, const bool &active) { - mat ve = fill_voigt_strain(DR, active); - return ve*(M*trans(ve)); + const mat::fixed<6,6> ve = fill_voigt_strain(DR, active); + const mat::fixed<6,6> tmp = M*trans(ve); + return ve*tmp; } mat rotate_strain_concentration(const mat &A, const double &alpha, const int &axis, const bool &active) { - mat ve = fill_voigt_strain(alpha, axis, active); - mat vs = fill_voigt_stress(alpha, axis, active); - return ve*(A*trans(vs)); + const mat::fixed<6,6> ve = fill_voigt_strain(alpha, axis, active); + const mat::fixed<6,6> vs = fill_voigt_stress(alpha, axis, active); + const mat::fixed<6,6> tmp = A*trans(vs); + return ve*tmp; } mat rotate_strain_concentration(const mat &A, const mat &DR, const bool &active) { - mat ve = fill_voigt_strain(DR, active); - mat vs = fill_voigt_stress(DR, active); - return ve*(A*trans(vs)); + const mat::fixed<6,6> ve = fill_voigt_strain(DR, active); + const mat::fixed<6,6> vs = fill_voigt_stress(DR, active); + const mat::fixed<6,6> tmp = A*trans(vs); + return ve*tmp; } mat rotate_stress_concentration(const mat &B, const double &alpha, const int &axis, const bool &active) { - mat ve = fill_voigt_strain(alpha, axis, active); - mat vs = fill_voigt_stress(alpha, axis, active); - return vs*(B*trans(ve)); + const mat::fixed<6,6> ve = fill_voigt_strain(alpha, axis, active); + const mat::fixed<6,6> vs = fill_voigt_stress(alpha, axis, active); + const mat::fixed<6,6> tmp = B*trans(ve); + return vs*tmp; } mat rotate_stress_concentration(const mat &B, const mat &DR, const bool &active) { - mat ve = fill_voigt_strain(DR, active); - mat vs = fill_voigt_stress(DR, active); - return vs*(B*trans(ve)); + const mat::fixed<6,6> ve = fill_voigt_strain(DR, active); + const mat::fixed<6,6> vs = fill_voigt_stress(DR, active); + const mat::fixed<6,6> tmp = B*trans(ve); + return vs*tmp; } // ============================================================================= diff --git a/src/Simulation/Maths/solve.cpp b/src/Simulation/Maths/solve.cpp index 0f058bcae..5c760fcee 100755 --- a/src/Simulation/Maths/solve.cpp +++ b/src/Simulation/Maths/solve.cpp @@ -14,8 +14,8 @@ along with simcoon. If not, see . */ -///@file solve.hpp -///@brief random number generators +///@file solve.cpp +///@brief Roots of quadratic equations (real and complex) ///@version 1.0 #include diff --git a/src/Simulation/Maths/stats.cpp b/src/Simulation/Maths/stats.cpp index 32b8c49a9..49171e932 100755 --- a/src/Simulation/Maths/stats.cpp +++ b/src/Simulation/Maths/stats.cpp @@ -46,10 +46,6 @@ double normal_distrib(const double &x, const double &mean, const double &dev){ } double proba_distrib_weibull(const double &x, const double &alpha, const double &beta) { - //This is the implementation of a Wibull cumulative distribution function, where - // alpha > 0 is the shape parameter and λ > 0 is the scale parameter of the distribution. - // Its complementary cumulative distribution function is a stretched exponential function. The Weibull distribution is related to a number of other probability distributions; in particular, it interpolates between the exponential distribution (alpha = 1) and the Rayleigh distribution (alpha = 2 and \beta = \sqrt{2}\sigma) - assert(alpha > 0.); assert(beta > 0.); @@ -57,10 +53,6 @@ double proba_distrib_weibull(const double &x, const double &alpha, const double } double cumul_distrib_weibull(const double &x, const double &alpha, const double &beta) { - //This is the implementation of a Wibull cumulative distribution function, where - // alpha > 0 is the shape parameter and λ > 0 is the scale parameter of the distribution. - // Its complementary cumulative distribution function is a stretched exponential function. The Weibull distribution is related to a number of other probability distributions; in particular, it interpolates between the exponential distribution (alpha = 1) and the Rayleigh distribution (alpha = 2 and \beta = \sqrt{2}\sigma) - assert(alpha > 0.); assert(beta > 0.); diff --git a/src/Simulation/Phase/material_characteristics.cpp b/src/Simulation/Phase/material_characteristics.cpp index 89aaa9b57..e5fa99224 100755 --- a/src/Simulation/Phase/material_characteristics.cpp +++ b/src/Simulation/Phase/material_characteristics.cpp @@ -37,9 +37,6 @@ namespace simcoon{ //=====Public methods for material_characteristics============================================ -/*! - \brief default constructor -*/ //------------------------------------------------------------- material_characteristics::material_characteristics() @@ -55,14 +52,6 @@ material_characteristics::material_characteristics() nprops=0; } -/*! - \brief Constructor - \param nprops : size of the table statev - \param nstatev : size of the table statev - \param init boolean that indicates if the constructor has to initialize the state variables vectors and statev table (default value is true) \n - \n\n - \f$ \textbf{Examples :} \f$ \n -*/ //------------------------------------------------------------- material_characteristics::material_characteristics(const int &n, const bool &init, const double &value) @@ -87,13 +76,6 @@ material_characteristics::material_characteristics(const int &n, const bool &ini } } -/*! - \brief Constructor with parameters - \param nstatev : size of the table statev - \param init boolean that indicates if the constructor has to initialize the state variables vectors and statev table (default value is true) \n - \n\n - \f$ \textbf{Examples :} \f$ \n -*/ //------------------------------------------------------------- material_characteristics::material_characteristics(const int &mnumber, const string &mumat_name, const int &msave, const double &mpsi_mat, const double &mtheta_mat, const double &mphi_mat, const int &mnprops, const vec &mprops) @@ -114,10 +96,6 @@ material_characteristics::material_characteristics(const int &mnumber, const str props = mprops; } -/*! - \brief Copy constructor - \param s phase_characteristics object to duplicate -*/ //------------------------------------------------------ material_characteristics::material_characteristics(const material_characteristics& sv) @@ -136,11 +114,6 @@ material_characteristics::material_characteristics(const material_characteristic props = sv.props; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ //------------------------------------- material_characteristics::~material_characteristics() {} @@ -169,9 +142,6 @@ void material_characteristics::resize(const int &n, const bool &init, const doub } } -/*! - \brief Standard operator = for phase_characteristics -*/ //------------------------------------------------------------- void material_characteristics::update(const int &mnumber, const string &mumat_name, const int &msave, const double &mpsi_mat, const double &mtheta_mat, const double &mphi_mat, const int &mnprops, const vec &mprops) diff --git a/src/Simulation/Phase/phase_characteristics.cpp b/src/Simulation/Phase/phase_characteristics.cpp index 7698ab61b..3c27476d7 100755 --- a/src/Simulation/Phase/phase_characteristics.cpp +++ b/src/Simulation/Phase/phase_characteristics.cpp @@ -54,9 +54,6 @@ namespace simcoon{ //=====Public methods for phase_characteristics============================================ -/*! - \brief default constructor -*/ //------------------------------------------------------------- phase_characteristics::phase_characteristics() //------------------------------------------------------------- @@ -69,10 +66,6 @@ phase_characteristics::phase_characteristics() } -/*! - \brief Constructor with parameters - \f$ \textbf{Examples :} \f$ \n -*/ //------------------------------------------------------------- phase_characteristics::phase_characteristics(const int &mshape_type, const int &msv_type, const std::shared_ptr &msptr_shape, const std::shared_ptr &msptr_multi, const std::shared_ptr &msptr_matprops, const std::shared_ptr &msptr_sv_global, const std::shared_ptr &msptr_sv_local, const std::shared_ptr &msptr_out_global, const std::shared_ptr &msptr_out_local, const std::string &msub_phases_file) @@ -90,10 +83,6 @@ phase_characteristics::phase_characteristics(const int &mshape_type, const int & sub_phases_file = msub_phases_file; } -/*! - \brief Copy constructor - \param s phase_characteristics object to duplicate -*/ //------------------------------------------------------ phase_characteristics::phase_characteristics(const phase_characteristics& pc) @@ -114,18 +103,10 @@ phase_characteristics::phase_characteristics(const phase_characteristics& pc) } -/*! - \brief Destructor - - Deletes phase_characteristics, the shared ptr related object will be detroyed automatically if they is no pointer to it. -*/ //------------------------------------- phase_characteristics::~phase_characteristics() {} //------------------------------------- -/*! - \brief Standard operator = for phase_characteristics -*/ //------------------------------------------------------------- @@ -231,7 +212,7 @@ void phase_characteristics::to_start() } } sptr_multi->to_start(); - for(auto r : sub_phases) { + for(auto &r : sub_phases) { r.to_start(); } @@ -262,7 +243,7 @@ void phase_characteristics::set_start(const int &corate_type) } } sptr_multi->set_start(); - for(auto r : sub_phases) { + for(auto &r : sub_phases) { r.set_start(corate_type); } } @@ -345,13 +326,9 @@ void phase_characteristics::define_output(const std::string &path, const std::st std::string ext_filename = outputfile.substr(outputfile.length()-4,outputfile.length()); std::string filename = outputfile.substr(0,outputfile.length()-4); //to remove the extension -// if(sptr_matprops->number > 0) filename = filename + '-' + std::to_string(sptr_matprops->number) + ext_filename; std::string path_filename = path + "/" + filename; -// else -// filename = filename + ext_filename; -// std::ofstream of_file(filename); if(coordsys == "global") { sptr_out_global = make_shared(path_filename); } diff --git a/src/Simulation/Phase/state_variables.cpp b/src/Simulation/Phase/state_variables.cpp index 8edef37f8..1ea756222 100755 --- a/src/Simulation/Phase/state_variables.cpp +++ b/src/Simulation/Phase/state_variables.cpp @@ -40,9 +40,6 @@ namespace simcoon{ //=====Public methods for state_variables============================================ -/*! - \brief default constructor -*/ //------------------------------------------------------------- state_variables::state_variables() : Etot(6), DEtot(6), etot(6), Detot(6), PKII(6), PKII_start(6), tau(6), tau_start(6), sigma(6), sigma_start(6), F0(3,3), F1(3,3), U0(3,3), U1(3,3), R(3,3), DR(3,3) @@ -69,11 +66,6 @@ state_variables::state_variables() : Etot(6), DEtot(6), etot(6), Detot(6), PKII( nstatev=0; } -/*! - \brief Constructor with parameters - \n\n - \f$ \textbf{Examples :} \f$ \n -*/ //------------------------------------------------------------- state_variables::state_variables(const int &m, const bool &init, const double &value) : Etot(6), DEtot(6), etot(6), Detot(6), PKII(6), PKII_start(6), tau(6), tau_start(6), sigma(6), sigma_start(6), F0(3,3), F1(3,3), U0(3,3), U1(3,3), R(3,3), DR(3,3) @@ -158,10 +150,6 @@ state_variables::state_variables(const vec &mEtot, const vec &mDEtot, const vec } -/*! - \brief Copy constructor - \param s state_variables object to duplicate -*/ //------------------------------------------------------ state_variables::state_variables(const state_variables& sv) : Etot(6), DEtot(6), etot(6), Detot(6), PKII(6), PKII_start(6), tau(6), tau_start(6), sigma(6), sigma_start(6), F0(3,3), F1(3,3), U0(3,3), U1(3,3), R(3,3), DR(3,3) @@ -192,11 +180,6 @@ state_variables::state_variables(const state_variables& sv) : Etot(6), DEtot(6), } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ //------------------------------------- state_variables::~state_variables() @@ -205,15 +188,12 @@ state_variables::~state_variables() } -/*! - \brief Standard operator = for state_variables -*/ - //---------------------------------------------------------------------- state_variables& state_variables::operator = (const state_variables& sv) //---------------------------------------------------------------------- { tangent_mode = sv.tangent_mode; + ncp = sv.ncp; Etot = sv.Etot; DEtot = sv.DEtot; etot = sv.etot; @@ -371,6 +351,10 @@ void state_variables::set_start(const int &corate_type) // total formulation: the strain is a state function of F, re-read rather than summed etot = t2v_strain(0.5*H); } + else if (DR.is_diagmat() && arma::all(DR.diag() == 1.)) { + // DR = I exactly (small strain): the Voigt rotation is the identity + etot += Detot; + } else { etot = rotate_strain(etot,DR) + Detot; } @@ -478,11 +462,9 @@ state_variables& state_variables::rotate_l2g(const state_variables& sv, const fr state_variables& state_variables::rotate_g2l(const state_variables& sv, const frame_rotation &frame) //---------------------------------------------------------------------- { - // Configuration travels with the g->l transfer: the solver sets - // tangent_mode on the GLOBAL sv, the kernels read the LOCAL one — without - // this copy the solver's tangent_mode parameter is dead (kernels always - // run the compile-time default). + // The solver sets tangent_mode and ncp on the global sv, the kernels read the local one. tangent_mode = sv.tangent_mode; + ncp = sv.ncp; Etot = sv.Etot; DEtot = sv.DEtot; etot = sv.etot; diff --git a/src/Simulation/Phase/state_variables_M.cpp b/src/Simulation/Phase/state_variables_M.cpp index 891590db7..265b4cf88 100755 --- a/src/Simulation/Phase/state_variables_M.cpp +++ b/src/Simulation/Phase/state_variables_M.cpp @@ -37,10 +37,6 @@ namespace simcoon{ //=====Public methods for state_variables============================================ -/*! - \brief default constructor -*/ - //------------------------------------------------------------- state_variables_M::state_variables_M() : state_variables(), sigma_in(6), sigma_in_start(6), Wm(4), Wm_start(4), L(6,6), Lt(6,6) //------------------------------------------------------------- @@ -55,12 +51,6 @@ state_variables_M::state_variables_M() : state_variables(), sigma_in(6), sigma_i Lt = zeros(6,6); } -/*! - \brief Constructor with parameters - \n\n - \f$ \textbf{Examples :} \f$ \n -*/ - //------------------------------------------------------------- state_variables_M::state_variables_M(const vec &mEtot, const vec &mDEtot, const vec &metot, const vec &mDetot, const vec &mPKII, const vec &mPKII_start, const vec &mtau, const vec &mtau_start, const vec &msigma, const vec &msigma_start, const mat &mF0, const mat &mF1, const mat &mU0, const mat &mU1, const mat &mR, const mat &mDR, const vec &msigma_in, const vec &msigma_in_start, const double &mT, const double &mDT, const int &mnstatev, const vec &mstatev, const vec &mstatev_start, const vec &mWm, const vec& mWm_start, const mat &mL, const mat &mLt) : state_variables(mEtot, mDEtot, metot, mDetot, mPKII, mPKII_start, mtau, mtau_start, msigma, msigma_start, mF0, mF1, mU0, mU1, mR, mDR, mT, mDT, mnstatev, mstatev, mstatev_start), sigma_in(6), sigma_in_start(6), Wm(4), Wm_start(4), L(6,6), Lt(6,6) //------------------------------------------------------------- @@ -87,11 +77,6 @@ state_variables_M::state_variables_M(const vec &mEtot, const vec &mDEtot, const Lt = mLt; } -/*! - \brief Copy constructor - \param s state_variables object to duplicate -*/ - //------------------------------------------------------ state_variables_M::state_variables_M(const state_variables_M& sv) : state_variables(sv), sigma_in(6), sigma_in_start(6), Wm(4), Wm_start(4), L(6,6), Lt(6,6) //------------------------------------------------------ @@ -106,12 +91,6 @@ state_variables_M::state_variables_M(const state_variables_M& sv) : state_variab Lt = sv.Lt; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ - //------------------------------------- state_variables_M::~state_variables_M() //------------------------------------- @@ -119,10 +98,6 @@ state_variables_M::~state_variables_M() } -/*! - \brief Standard operator = for state_variables -*/ - //---------------------------------------------------------------------- state_variables_M& state_variables_M::operator = (const state_variables_M& sv) //---------------------------------------------------------------------- diff --git a/src/Simulation/Phase/state_variables_T.cpp b/src/Simulation/Phase/state_variables_T.cpp index 02b832086..5a9c0a417 100755 --- a/src/Simulation/Phase/state_variables_T.cpp +++ b/src/Simulation/Phase/state_variables_T.cpp @@ -37,10 +37,6 @@ namespace simcoon{ //=====Public methods for state_variables_T============================================ -/*! - \brief default constructor -*/ - //------------------------------------------------------------- state_variables_T::state_variables_T() : state_variables(), sigma_in(6), sigma_in_start(6), Wm(4), Wt(3), Wm_start(4), Wt_start(3), dSdE(6,6), dSdEt(6,6), dSdT(1,6), drdE(1,6), drdT(1,1) //------------------------------------------------------------- @@ -50,6 +46,8 @@ state_variables_T::state_variables_T() : state_variables(), sigma_in(6), sigma_i r_in = 0.; sigma_in = zeros(6); sigma_in_start = zeros(6); + Wm = zeros(4); + Wm_start = zeros(4); Wt = zeros(3); Wt_start = zeros(3); dSdE = zeros(6,6); @@ -59,12 +57,6 @@ state_variables_T::state_variables_T() : state_variables(), sigma_in(6), sigma_i drdT = zeros(1,1); } -/*! - \brief Constructor with parameters - \n\n - \f$ \textbf{Examples :} \f$ \n -*/ - //------------------------------------------------------------- state_variables_T::state_variables_T(const vec &mEtot, const vec &mDEtot, const vec &metot, const vec &mDetot, const vec &mPKII, const vec &mPKII_start, const vec &mtau, const vec &mtau_start, const vec &msigma, const vec &msigma_start, const mat &mF0, const mat &mF1, const mat &mU0, const mat &mU1, const mat &mR, const mat &mDR, const vec &msigma_in, const vec &msigma_in_start, const double &mT, const double &mDT, const int &mnstatev, const vec &mstatev, const vec &mstatev_start, const double &mQ, const double &mr, const double &mr_in, const vec &mWm, const vec &mWt, const vec &mWm_start, const vec &mWt_start, const mat &mdSdE, const mat &mdSdEt, const mat &mdSdT, const mat &mdrdE, const mat &mdrdT) : state_variables(mEtot, mDEtot, metot, mDetot, mPKII, mPKII_start, mtau, mtau_start, msigma, msigma_start, mF0, mF1, mU0, mU1, mR, mDR, mT, mDT, mnstatev, mstatev, mstatev_start), sigma_in(6), sigma_in_start(6), Wm(4), Wt(3), Wm_start(4), Wt_start(3), dSdE(6,6), dSdEt(6,6), dSdT(1,6), drdE(1,6), drdT(1,1) //------------------------------------------------------------- @@ -110,11 +102,6 @@ state_variables_T::state_variables_T(const vec &mEtot, const vec &mDEtot, const drdT = mdrdT; } -/*! - \brief Copy constructor - \param s state_variables_T object to duplicate -*/ - //------------------------------------------------------ state_variables_T::state_variables_T(const state_variables_T& sv) : state_variables(sv), sigma_in(6), sigma_in_start(6), Wm(4), Wt(3), Wm_start(4), Wt_start(3), dSdE(6,6), dSdEt(6,6), dSdT(1,6), drdE(1,6), drdT(1,1) //------------------------------------------------------ @@ -135,12 +122,6 @@ state_variables_T::state_variables_T(const state_variables_T& sv) : state_variab drdT = sv.drdT; } -/*! - \brief Destructor - - Deletes statev_variables, the vectors and matrix, and table statev if nstatev is not null. -*/ - //------------------------------------- state_variables_T::~state_variables_T() //------------------------------------- @@ -148,10 +129,6 @@ state_variables_T::~state_variables_T() } -/*! - \brief Standard operator = for state_variables -*/ - //---------------------------------------------------------------------- state_variables_T& state_variables_T::operator = (const state_variables_T& sv) //---------------------------------------------------------------------- @@ -313,9 +290,7 @@ state_variables_T& state_variables_T::rotate_l2g(const state_variables_T& sv, co Wt = sv.Wt; frame.rotate_stiffness(dSdE); - // dSdT and drdE are Voigt vectors stored as a mat (1x6 by the constructors, 6x1 by the - // solver): rotated as vectors, returned in the shape they came with. Both take the - // stress operator: drdE is a gradient w.r.t. the engineering strain (see the header). + // dSdT, drdE: Voigt vectors stored as mat, rotated with the stress operator, shape kept (see the header). vec dSdT_v = vectorise(dSdT); frame.rotate_stress(dSdT_v); dSdT = reshape(dSdT_v, size(dSdT)); diff --git a/src/Simulation/Solver/block.cpp b/src/Simulation/Solver/block.cpp index e005990d4..b73203ffd 100755 --- a/src/Simulation/Solver/block.cpp +++ b/src/Simulation/Solver/block.cpp @@ -35,12 +35,11 @@ using namespace arma; namespace simcoon{ -//=====Private methods for ellipsoid_characteristics=================================== +//=====Private methods for block=================================== -//=====Public methods for ellipsoid_characteristics============================================ +//=====Public methods for block============================================ -//@brief default constructor //------------------------------------------------------------- block::block() //------------------------------------------------------------- @@ -52,13 +51,6 @@ block::block() control_type=0; } -/*! - \brief Constructor - \param n : number of the block - \param m : number of steps - \param k : number of cycles - \param t : : loading type of steps inside the block -*/ //------------------------------------------------------------- block::block(const unsigned int &n, const unsigned int &m, const unsigned int &k, const unsigned int &t, const unsigned int &c_t) @@ -75,14 +67,6 @@ block::block(const unsigned int &n, const unsigned int &m, const unsigned int &k control_type=c_t; } -/*! - \brief Constructor with parameters - \param mnumber : number of the block - \param mnstep : number of steps - \param mncycle : number of cycles - \param mtype : loading type of steps inside the block - msteps : vector of steps (already defined) - */ //------------------------------------------------------------- block::block(const unsigned int &mnumber, const unsigned int &mnstep, const unsigned int &mncycle, const unsigned int &mtype, const unsigned int &mcontrol_type, const vector > &msteps) @@ -100,10 +84,6 @@ block::block(const unsigned int &mnumber, const unsigned int &mnstep, const unsi steps = msteps; } -/*! - \brief Copy constructor - \param bl block object to duplicate -*/ //------------------------------------------------------ block::block(const block &bl) @@ -114,14 +94,10 @@ block::block(const block &bl) ncycle = bl.ncycle; type = bl.type; -// generate(); steps = bl.steps; } -/*! - \brief destructor -*/ block::~block() {} @@ -176,10 +152,6 @@ void block::initialize() sptr_meca->generate(sptr_meca); break; } - // case 2: { - // - //break; - //} default: { cout << "Please provide a consistent loading type for the block " << number << "\n"; break; @@ -188,10 +160,6 @@ void block::initialize() }*/ -/*! - \brief Standard operator = for block -*/ - //---------------------------------------------------------------------- block& block::operator = (const block &bl) //---------------------------------------------------------------------- @@ -205,7 +173,6 @@ block& block::operator = (const block &bl) type = bl.type; control_type = bl.control_type; -// generate(); steps = bl.steps; diff --git a/src/Simulation/Solver/output.cpp b/src/Simulation/Solver/output.cpp index 24b9fb901..12fce5174 100755 --- a/src/Simulation/Solver/output.cpp +++ b/src/Simulation/Solver/output.cpp @@ -32,11 +32,10 @@ using namespace arma; namespace simcoon{ -//=====Private methods for ellipsoid_characteristics=================================== +//=====Private methods for solver_output=================================== -//=====Public methods for ellipsoid_characteristics============================================ +//=====Public methods for solver_output============================================ -//@brief default constructor //------------------------------------------------------------- solver_output::solver_output() //------------------------------------------------------------- @@ -52,11 +51,6 @@ solver_output::solver_output() o_tangent_modulus = 0; } -/*! - \brief Constructor with parameters - \param nblock : number of blocks - */ - //------------------------------------------------------------- solver_output::solver_output(const int &nblock) //------------------------------------------------------------- @@ -76,11 +70,6 @@ solver_output::solver_output(const int &nblock) o_tfreq.zeros(nblock); } -/*! - \brief Copy constructor - \param so solver_output object to duplicate - */ - //------------------------------------------------------ solver_output::solver_output(const solver_output& so) //------------------------------------------------------ @@ -102,16 +91,8 @@ solver_output::solver_output(const solver_output& so) o_tfreq = so.o_tfreq; } -/*! - \brief destructor - */ - solver_output::~solver_output() {} -/*! - \brief Standard operator = for solver_output objects - */ - //---------------------------------------------------------------------- solver_output& solver_output::operator = (const solver_output& so) //---------------------------------------------------------------------- diff --git a/src/Simulation/Solver/solver.cpp b/src/Simulation/Solver/solver.cpp index cd8071c3c..b410dc128 100755 --- a/src/Simulation/Solver/solver.cpp +++ b/src/Simulation/Solver/solver.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include #include @@ -37,6 +38,7 @@ #include #include #include +#include #include #include #include @@ -54,16 +56,10 @@ using namespace arma; namespace { -// Single definition of the step-cut decision: bisect the increment unless it -// is already at its minimal fraction. Returns whether the cut was applied — -// call sites decide what a refusal means (throw a typed error, rethrow, ...). -// How many increments in a row the inforce path may close before the prescribed state is -// declared unreachable. A legitimate correction is isolated - one increment that did not quite -// converge, whose residual the next one absorbs. A response saturated below the target stalls on -// every increment instead. Deliberately generous, and it only bites when the error is *not* -// decreasing, so a hard but converging problem is never interrupted. +// Consecutive inforced increments with a non-decreasing error before the target is declared unreachable. constexpr int inforce_stall_max = 5; +// Bisect unless already at Dn_mini; returns whether the cut was applied (callers decide what a refusal means). inline bool try_step_cut(const double &Dtinc_cur, const double &Dn_mini, const double &div_tnew_dt, double &tnew_dt) { if (fabs(Dtinc_cur - Dn_mini) > simcoon::iota) { @@ -73,9 +69,7 @@ inline bool try_step_cut(const double &Dtinc_cur, const double &Dn_mini, return false; } -// A step cut requested by the material (tnew_dt < 1) cannot be honoured at the minimal -// increment: compute_inc clamps back to Dn_mini and the same trial would be replayed forever. -// Inforce the increment instead, or report the failure when inforce is off. +// A material cut at Dn_mini would replay the same trial forever: inforce it, or report failure when inforce is off. inline bool refuse_cut_at_Dn_mini(const double &Dtinc_cur, const double &Dn_mini, const int &inforce_solver, const int &step_number, const int &inc, const double &tinc, double &tnew_dt) { @@ -90,8 +84,7 @@ inline bool refuse_cut_at_Dn_mini(const double &Dtinc_cur, const double &Dn_mini return true; } -// Step-cut-or-rethrow policy shared by every recoverable-failure catch in the -// solver Newton loops. Must only be called from inside a catch block. +// Must only be called from inside a catch block (bare rethrow). inline void step_cut_or_rethrow(const double &Dtinc_cur, const double &Dn_mini, const double &div_tnew_dt, double &tnew_dt, int &compteur) { @@ -102,9 +95,8 @@ inline void step_cut_or_rethrow(const double &Dtinc_cur, const double &Dn_mini, } } -// Control type 3 controls ln V and rebuilds F = exp(ln V) R. The stored etot is ln V for every -// corate but 4 (Truesdell), whose lower-convected strain is exactly the Almansi strain -// e_A = 1/2 (I - b^-1), hence ln V = -1/2 ln(I - 2 e_A). +// ln V of control type 3: etot itself, except corate 4 (Truesdell) where etot is the Almansi strain, +// ln V = -1/2 ln(I - 2 e_A). inline mat ct3_lnV(const vec &etot, const int &corate_type) { if (corate_type != 4) return simcoon::v2t_strain(etot); @@ -114,6 +106,74 @@ inline mat ct3_lnV(const vec &etot, const int &corate_type) { return -0.5*log_b_inv; } +// capacity hint: records of the o_nfreq blocks; time-period blocks grow the sink +std::size_t expected_records(const std::vector &blocks, const simcoon::solver_output &so) { + std::size_t n_records = 0; + for (std::size_t i = 0; i < blocks.size(); ++i) { + if (i >= so.o_type.n_elem || i >= so.o_nfreq.n_elem || so.o_type(i) != 1 || so.o_nfreq(i) <= 0) { + continue; + } + std::size_t n_inc = 0; + for (const auto &sptr : blocks[i].steps) { + if (sptr->mode == 3) { + n_inc += sptr->tab_data.n_rows; + } + else if (sptr->Dn_inc > 0.) { + n_inc += static_cast(std::round(1./sptr->Dn_inc)); + } + } + n_records += n_inc*blocks[i].ncycle/static_cast(so.o_nfreq(i)) + 1; + } + return n_records; +} + +// solver_memory_sink keeps every record in std::vectors: reserve them once at init(). +void reserve_memory_sink(simcoon::solver_results_sink &sink, const std::vector &blocks, + const simcoon::solver_output &so) { + if (auto *memory_sink = dynamic_cast(&sink)) { + memory_sink->reserve(expected_records(blocks, so)); + } +} + +// Delta = -K^-1 r by stack LU; false on a singular or non-finite K +bool newton_correction(const mat &K, const vec &r, vec &Delta) { + simcoon::small_lu<7> lu; + if (!lu.factor(K) || r.n_elem != K.n_rows) return false; + Delta = -lu.solve(r); + return Delta.is_finite(); +} + +// tolerance: see solver_params::precision_rel +double residual_tolerance(const simcoon::solver_params &ctrl, const vec &tau) { + if (ctrl.precision_rel > 0.) + return std::max(ctrl.precision, ctrl.precision_rel*norm(tau, 2)); + return ctrl.precision; +} + +// Newton on dS/dE of a law built on F; sv_M->Lt holds it until the loop ends, then the box +struct material_tangent_state { + bool held = false; + mat F1; + vec tau; +}; + +// dS/dE for the Jacobian: the held native tangent at its own state, else the box pulled back as +// without it (a restarted attempt whose state moved: the box of that evaluation, then the usual map) +mat jacobian_DSDE(const material_tangent_state &mt, const mat &Lt, const simcoon::state_variables &sv, const int &corate_type) { + if (!mt.held) + return simcoon::DtauDe_corate_2_DSDE(Lt, corate_type, sv.F1, simcoon::v2t_stress(sv.tau)); + if (approx_equal(mt.F1, sv.F1, "absdiff", 0.) && approx_equal(mt.tau, sv.tau, "absdiff", 0.)) + return Lt; + const mat Lt_box = simcoon::DSDE_2_DtauDe_corate(Lt, corate_type, mt.F1, simcoon::v2t_stress(mt.tau)); + return simcoon::DtauDe_corate_2_DSDE(Lt_box, corate_type, sv.F1, simcoon::v2t_stress(sv.tau)); +} + +void restore_box_tangent(material_tangent_state &mt, simcoon::state_variables_M &sv, const int &corate_type) { + if (!mt.held) return; + sv.Lt = simcoon::DSDE_2_DtauDe_corate(sv.Lt, corate_type, mt.F1, simcoon::v2t_stress(mt.tau)); + mt.held = false; +} + } // namespace namespace simcoon{ @@ -128,10 +188,8 @@ mat hughes_winget(const mat &BC_w, const double &DTime) { return HW_inv*(eye(3,3) + 0.5*BC_w*DTime); } -// Kinematics of a trial increment under the finite control types, once the driven strain is set -// (DEtot for 2, Detot for 3, U1 for 4, F1 for 5/6): DR the rotation increment of the step spin -// (2-4), F0 and F1, the corate's increment sv.DR and the strains the control does not drive. -// Shared by the mechanical and thermomechanical blocks. +// Once the driven strain is set (DEtot 2, Detot 3, U1 4, F1 5/6): DR of the step spin (2-4), F0, F1, +// sv.DR and the strains the control does not drive. void finite_kinematics(state_variables &sv, const unsigned int &control_type, const mat &BC_w, const mat &BC_R, const double &DTime, const int &corate_type, mat &DR) { mat D = zeros(3,3); @@ -141,8 +199,9 @@ void finite_kinematics(state_variables &sv, const unsigned int &control_type, co DR = hughes_winget(BC_w, DTime); sv.F0 = ER_to_F(v2t_strain(sv.Etot), BC_R); sv.F1 = ER_to_F(v2t_strain(sv.Etot + sv.DEtot), BC_R*DR); - corate_kinematics(corate_type, sv.DR, D, Omega, sv.F0, sv.F1, DTime); - sv.Detot = t2v_strain(Delta_log_strain_corate(sv.F0, sv.F1, sv.DR, D, Omega, DTime, corate_type)); + mat De; + corate_kinematics(corate_type, sv.DR, D, Omega, De, sv.F0, sv.F1, DTime); // + the strain increment, one spectral pass + sv.Detot = t2v_strain(De); break; } case 3: { @@ -159,22 +218,22 @@ void finite_kinematics(state_variables &sv, const unsigned int &control_type, co sv.F0 = BC_R*sv.U0; sv.F1 = (BC_R*DR)*sv.U1; // BC_R*DR (right): consistent with the accumulator BC_R = BC_R*DR (step_meca) sv.DEtot = t2v_strain(Green_Lagrange(sv.F1)) - sv.Etot; - corate_kinematics(corate_type, sv.DR, D, Omega, sv.F0, sv.F1, DTime); - sv.Detot = t2v_strain(Delta_log_strain_corate(sv.F0, sv.F1, sv.DR, D, Omega, DTime, corate_type)); + mat De; + corate_kinematics(corate_type, sv.DR, D, Omega, De, sv.F0, sv.F1, DTime); // + the strain increment, one spectral pass + sv.Detot = t2v_strain(De); break; } default: { // 5, 6: F1 prescribed - corate_kinematics(corate_type, sv.DR, D, Omega, sv.F0, sv.F1, DTime); - sv.Detot = t2v_strain(Delta_log_strain_corate(sv.F0, sv.F1, sv.DR, D, Omega, DTime, corate_type)); + mat De; + corate_kinematics(corate_type, sv.DR, D, Omega, De, sv.F0, sv.F1, DTime); // + the strain increment, one spectral pass + sv.Detot = t2v_strain(De); sv.DEtot = t2v_strain(Green_Lagrange(sv.F1)) - sv.Etot; } } } -// The strain a finite control type drives, from the increment's targets: DEtot (2), Detot (3), -// U1 (4), F1 (5/6). Under 5/6 Dtarget is the fraction of the increment's generator (Dtinc mecas, -// geodesic_F_increments): a cut increment advances F1 = exp(Dtinc dL) F0 along the increment's -// geodesic, and the sub-increment that closes it lands on the tabulated F exactly +// Under 5/6 a cut increment advances F1 = exp(Dtinc dL) F0 along the increment's geodesic; the +// closing sub-increment lands on the tabulated F exactly. void set_driven_strain(state_variables &sv, const unsigned int &control_type, const rowvec &Dtarget, const rowvec &BC_target, const bool &increment_end) { if (control_type == 2) @@ -199,9 +258,8 @@ void add_driven_strain(state_variables &sv, const unsigned int &control_type, co sv.U1 += v2t_strain(Delta); } -// Mechanical rows of the residual: on the static components, the stress measure of the control -// type against its target Dtinc mecas(inc) (1 Kirchhoff, 2 PKII, 3 Kirchhoff from the start stress -// in the polar frame F = V R rebuilds, 4 Biot); on the kinematic ones, its strain +// Static rows: the control type's stress measure (1 Kirchhoff, 2 PKII, 3 Kirchhoff in the rebuilt +// polar frame, 4 Biot) against Dtinc mecas(inc); kinematic rows: its strain. void mechanical_residual(state_variables &sv, const unsigned int &control_type, const Col &cBC_meca, const mat &mecas, const int &inc, const double &Dtinc, const mat &BC_R, const mat &DR, const double &lambda, vec &residual) { const vec *stress = &sv.tau, *stress_start = &sv.tau_start, *strain = &sv.DEtot; @@ -235,16 +293,16 @@ void mechanical_residual(state_variables &sv, const unsigned int &control_type, } } -// Jacobian of the control type's stress measure with respect to its strain, from the box tangent -// Lt = dtau/de: Lt itself (1, 3), dS/dE (2, corate-matched pull-back, no J), dBiot/dU (4) -// (written to C for 2 and 4, Lt returned as is otherwise) -const mat &control_tangent(state_variables &sv, const mat &Lt, const unsigned int &control_type, const int &corate_type, mat &C) { +// From Lt = dtau/de: Lt (1, 3), dS/dE in C (2, corate-matched pull-back, no J), dBiot/dU in C (4); +// with mt, a native dS/dE of a law built on F is used as is. +const mat &control_tangent(state_variables &sv, const mat &Lt, const unsigned int &control_type, const int &corate_type, mat &C, + const material_tangent_state *mt = nullptr) { if (control_type == 2) { - C = DtauDe_corate_2_DSDE(Lt, corate_type, sv.F1, v2t_stress(sv.tau)); + C = mt ? jacobian_DSDE(*mt, Lt, sv, corate_type) : DtauDe_corate_2_DSDE(Lt, corate_type, sv.F1, v2t_stress(sv.tau)); return C; } if (control_type == 4) { - const mat DSDE = DtauDe_corate_2_DSDE(Lt, corate_type, sv.F1, v2t_stress(sv.tau)); + const mat DSDE = mt ? jacobian_DSDE(*mt, Lt, sv, corate_type) : DtauDe_corate_2_DSDE(Lt, corate_type, sv.F1, v2t_stress(sv.tau)); mat R = zeros(3,3); mat U = zeros(3,3); RU_decomposition(R, U, sv.F1); @@ -254,11 +312,8 @@ const mat &control_tangent(state_variables &sv, const mat &Lt, const unsigned in return Lt; } -// Thermal columns of the thermomechanical Jacobian in the control type's variables, from the box -// derivatives dtau/dtheta and dr/de (at fixed F): dstress/dtheta at fixed control strain and -// dr/d(control strain). 2: dS/dtheta = F^-1 dtau/dtheta F^-T, dr/dE = P^T dr/de with P = de/dE -// (dH/dE, rotated by R under the rate form); 4: dBiot/dtheta = sym(U dS/dtheta), dr/dU through -// dE = 1/2 (dU U + U dU). +// 2: dS/dtheta = F^-1 dtau/dtheta F^-T, dr/dE = P^T dr/de, P = de/dE (rotated by R under the rate form); +// 4: dBiot/dtheta = sym(U dS/dtheta), dr/dU through dE = 1/2 (dU U + U dU). void control_thermal(const state_variables &sv, const vec &dSdT_box, const vec &drdE_box, const unsigned int &control_type, const int &corate_type, vec &dSdT, vec &drdE) { if (control_type != 2 && control_type != 4) { @@ -298,13 +353,16 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou throw std::invalid_argument("solver: tangent_mode must be 0 (none), 1 (continuum), 2 (algorithmic) or 3 (closest-point); got " + std::to_string(ctrl.tangent_mode)); } + const bool thermomechanical = std::any_of(blocks.begin(), blocks.end(), [](const block &b) { return b.type == 2; }); + check_ncp_serves(umat_name, ctrl.ncp, thermomechanical, ctrl.tangent_mode); + // the thermomechanical laws accepted are mechanical ones too, so one check covers a mixed run + check_integrator_serves(umat_name, ctrl.tangent_mode, thermomechanical); if (corate_type < 0 || corate_type > corate_type_total) { throw std::invalid_argument("solver: corate_type must be an objective rate (0..5) or the total-formulation code (" + std::to_string(corate_type_total) + "); got " + std::to_string(corate_type)); } - // Total formulation: the kernel strain is the material ln U, re-read from F at every commit, - // and the box is dT/dH. Logarithmic control reads the strain as ln V and takes the box as - // its Jacobian; a small-strain block accumulates a strain that F does not carry. + // Total formulation: kernel strain = material ln U re-read from F, box = dT/dH; incompatible with + // logarithmic control (reads ln V) and with small-strain blocks (strain not carried by F). if (corate_type == corate_type_total) { for (const auto &bl : blocks) { if (bl.control_type == 1 || bl.control_type == 3) { @@ -332,7 +390,9 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou const int &miniter_solver = ctrl.miniter; const int &maxiter_solver = ctrl.maxiter; const int &inforce_solver = ctrl.inforce; - const double &precision_solver = ctrl.precision; + // Convergence tolerance of the global residual: ctrl.precision, or max(precision, + // precision_rel |tau|) when a relative tolerance is set (re-evaluated with each residual) + double precision_solver = ctrl.precision; const double &lambda_solver = ctrl.lambda; const int &tangent_mode = ctrl.tangent_mode; @@ -352,8 +412,6 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou mat DR = eye(3,3); mat R = eye(3,3); -// mat dSdE = zeros(6,6); -// mat dSdT = zeros(1,6); mat dQdE = zeros(6,1); mat dQdT = zeros(1,1); @@ -376,6 +434,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou int nK = 0; // The size of the problem to solve mat K; mat invK; + material_tangent_state native_Lt; // see material_tangent_state int compteur = 0.; int inc = 0.; @@ -402,10 +461,6 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou else { size_meca = 9; } -// if((blocks[i].control_type == 1)||(blocks[i].control_type == 2)) -// size_meca = 6; -// else if(blocks[i].control_type == 3) -// size_meca = 9; shared_ptr sv_M; @@ -419,6 +474,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou sv_M = std::dynamic_pointer_cast(rve.sptr_sv_global); } sv_M->tangent_mode = tangent_mode; + sv_M->ncp = ctrl.ncp; sv_M->L = zeros(6,6); sv_M->Lt = zeros(6,6); @@ -432,9 +488,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou //Run the umat for the first time in the block. So that we get the proper tangent properties run_umat_M(rve, DR, Time, DTime, ndi, nshr, start, solver_type, blocks[i].control_type, corate_type, tnew_dt); if (tnew_dt < 1.) { - // Nothing to subdivide here (the increment is zero): the law refused the - // priming call and Lt is still the zeros set a few lines above, which would - // surface later as a singular Jacobian. Report the real cause. + // Zero increment, nothing to subdivide: report now rather than as a later singular Jacobian. throw simcoon::exception_solver( "block " + std::to_string(i + 1) + ": the constitutive law requested a step " "cut on the zero-increment call that primes the tangent operator; no " @@ -463,6 +517,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou if(start) { sink.init(rve); + reserve_memory_sink(sink, blocks, so); } //Set the start values of sigma_start=sigma and statev_start=statev for all phases rve.set_start((blocks[i].control_type == 1) ? 0 : corate_type); //DEtot = 0 and DT = 0 and DR = 0 so we can use it safely here @@ -551,6 +606,10 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou ///Saving stress and stress set point at the beginning of the loop error = 1.; + precision_solver = ctrl.precision; + // dS/dE straight from a law built on F for the PKII / Biot residuals + const bool ask_material_tangent = (blocks[i].control_type == 2 || blocks[i].control_type == 4) + && material_tangent_serves(umat_name); if (blocks[i].control_type == 1 || blocks[i].control_type == 2) sv_M->DEtot = zeros(6); @@ -560,28 +619,18 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou while((error > precision_solver)&&(compteur < maxiter_solver)) { if(solver_type != 1){ - // classic - ///Prediction of the strain increment using the tangent modulus given from the umat_ function - //we use the ddsdde (Lt) from the previous increment - Lt_2_K(control_tangent(*sv_M, sv_M->Lt, blocks[i].control_type, corate_type, C), K, sptr_meca->cBC_meca, lambda_solver); + // classic: Newton prediction with the tangent of the previous increment + Lt_2_K(control_tangent(*sv_M, sv_M->Lt, blocks[i].control_type, corate_type, C, &native_Lt), K, sptr_meca->cBC_meca, lambda_solver); - ///jacobian inversion - bool inv_success = inv(invK, K); + ///Newton correction (prediction of the component of the strain tensor) + bool inv_success = newton_correction(K, residual, Delta); if (!inv_success) { - // Degenerate tangent at the trial state (e.g. a - // branch-flip excursion under stress control): bisect - // the increment; at the minimal fraction fall through - // to the existing inforce path rather than throwing - // (a hard throw here overrides the inforce contract and - // is platform-fragile — LAPACK-backend-dependent - // singularity detection). + // Degenerate tangent: bisect; at Dn_mini fall through to the inforce + // path (singularity detection is LAPACK-backend dependent). try_step_cut(Dtinc_cur, sptr_meca->Dn_mini, div_tnew_dt_solver, tnew_dt); compteur = maxiter_solver; break; } - - /// Prediction of the component of the strain tensor - Delta = -invK * residual; } else if(solver_type == 1) { //RNL @@ -607,12 +656,18 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou finite_kinematics(*sv_M, blocks[i].control_type, sptr_meca->BC_w, sptr_meca->BC_R, DTime, corate_type, DR); } rve.to_start(); - run_umat_M(rve, sv_M->DR, Time, DTime, ndi, nshr, start, solver_type, blocks[i].control_type, corate_type, tnew_dt); + run_umat_M(rve, sv_M->DR, Time, DTime, ndi, nshr, start, solver_type, blocks[i].control_type, corate_type, tnew_dt, ask_material_tangent); + if (ask_material_tangent) { + native_Lt.held = true; + native_Lt.F1 = sv_M->F1; + native_Lt.tau = sv_M->tau; + } mechanical_residual(*sv_M, blocks[i].control_type, sptr_meca->cBC_meca, sptr_meca->mecas, inc, Dtinc, sptr_meca->BC_R, DR, lambda_solver, residual); compteur++; error = norm(residual, 2.); if (!std::isfinite(error)) error = 1.e30; // NaN would pass every "error > tol" test as converged + precision_solver = residual_tolerance(ctrl, sv_M->tau); if(tnew_dt < 1.) { @@ -622,6 +677,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou } } + restore_box_tangent(native_Lt, *sv_M, corate_type); } @@ -633,9 +689,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou } if(inforce_solver == 1) { - // Give up when the carried residual is not being absorbed: - // the prescribed state is out of reach, not merely hard. - // Status protocol, never a throw (see solver_sink.hpp). + // Give up (status, never a throw) when the carried residual is not absorbed. if (n_inforced == 0) { error_inforced = error; } @@ -646,8 +700,6 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou } cout << "The solver has been inforced to proceed (Solver issue) at step:" << sptr_meca->number << " inc: " << inc << " and fraction:" << tinc << ", with the error: " << error << "\n"; -// cout << "The next increment has integrated the error to avoid propagation\n"; - //The solver has been inforced! tnew_dt = 1.; if (inc+1ninc) { @@ -691,10 +743,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou } compteur = 0; } - // Recoverable failures of the trial state — F-reconstruction - // (sqrtmat/expmat), singular inverse in the kinematics or the - // tangent assembly, singular return-map Jacobian (FB det) — - // are bisected away; anything else propagates. + // Recoverable trial-state failures are bisected away; anything else propagates. catch (const simcoon::exception_sqrtmat_sympd &) { step_cut_or_rethrow(Dtinc_cur, sptr_meca->Dn_mini, div_tnew_dt_solver, tnew_dt, compteur); } @@ -708,10 +757,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou step_cut_or_rethrow(Dtinc_cur, sptr_meca->Dn_mini, div_tnew_dt_solver, tnew_dt, compteur); } - // Every branch above assigns this same expression, but only when it - // runs: the inforce path skips them while Dtinc has shrunk to Dn_mini, - // and assess_inc would then add a stale full-increment DTime once per - // forced sub-iteration. Recompute from the fraction actually accepted. + // The inforce path skips the branches above: recompute DTime from the accepted fraction. DTime = Dtinc*sptr_meca->times(inc); if (blocks[i].control_type == 1) { // small strain: the logarithmic strain is the infinitesimal one @@ -826,6 +872,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou if(start) { sink.init(rve); + reserve_memory_sink(sink, blocks, so); } //Set the start values of sigma_start=sigma and statev_start=statev for all phases rve.set_start((blocks[i].control_type == 1) ? 0 : corate_type); //DEtot = 0 and DT = 0 and DR = 0 so we can use it safely here @@ -952,6 +999,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou ///Saving stress and stress set point at the beginning of the loop error = 1.; + precision_solver = ctrl.precision; if (blocks[i].control_type == 1 || blocks[i].control_type == 2) sv_T->DEtot = zeros(6); @@ -988,16 +1036,14 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou while((error > precision_solver)&&(compteur < maxiter_solver)) { if(solver_type != 1){ - // classic - ///Prediction of the strain increment using the tangent modulus given from the umat_ function - //we use the ddsdde (Lt) from the previous increment + // classic: Newton prediction with the tangent of the previous increment if (finite_ct) Lth_2_K(control_tangent(*sv_T, sv_T->dSdE, blocks[i].control_type, corate_type, C), dSdT_c, dQdE, dQdT, K, sptr_thermomeca->cBC_meca, sptr_thermomeca->cBC_T, lambda_solver); else Lth_2_K(sv_T->dSdE, sv_T->dSdT, dQdE, dQdT, K, sptr_thermomeca->cBC_meca, sptr_thermomeca->cBC_T, lambda_solver); - ///jacobian inversion - bool inv_success = inv(invK, K); + ///Newton correction (prediction of the component of the strain tensor) + bool inv_success = newton_correction(K, residual, Delta); if (!inv_success) { // Same cut-then-inforce policy as the mechanical loop // (no throw at Dn_mini — respects the inforce contract). @@ -1005,9 +1051,6 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou compteur = maxiter_solver; break; } - - /// Prediction of the component of the strain tensor - Delta = -invK * residual; } else if(solver_type == 1) { //RNL @@ -1068,6 +1111,7 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou compteur++; error = norm(residual, 2.); if (!std::isfinite(error)) error = 1.e30; // NaN would pass every "error > tol" test as converged + precision_solver = residual_tolerance(ctrl, sv_T->tau); if(tnew_dt < 1.) { @@ -1082,13 +1126,11 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou /* if((fabs(Dtinc_cur - sptr_thermomeca->Dn_mini) < simcoon::iota)&&(tnew_dt < 1.)) { cout << "The subroutine has required a step reduction lower than the minimal indicated at" << sptr_thermomeca->number << " inc: " << inc << " and fraction:" << tinc << "\n"; - //The solver has been inforced! return; } if((error > 1000.*precision_solver)&&(Dtinc_cur == sptr_thermomeca->Dn_mini)) { cout << "The error has exceeded 1000 times the precision, the simulation has stopped at " << sptr_thermomeca->number << " inc: " << inc << " and fraction:" << tinc << "\n"; - //The solver has been inforced! return; } */ @@ -1103,7 +1145,6 @@ int solver_run(std::vector &blocks, const double &T_init, const solver_ou cout << "The solver has been inforced to proceed (Solver issue) at step:" << sptr_thermomeca->number << " inc: " << inc << " and fraction:" << tinc << ", with the error: " << error << "\n"; cout << "The next increment has integrated the error to avoid propagation\n"; - //The solver has been inforced! tnew_dt = 1.; if (inc+1ninc) { diff --git a/src/Simulation/Solver/solver_sink.cpp b/src/Simulation/Solver/solver_sink.cpp index ab92f0a45..ed7f20987 100644 --- a/src/Simulation/Solver/solver_sink.cpp +++ b/src/Simulation/Solver/solver_sink.cpp @@ -50,6 +50,22 @@ void solver_memory_sink::init(phase_characteristics &rve) { sv_type = 1; } +void solver_memory_sink::reserve(std::size_t n_records) { + auto grow = [n_records](auto &v) { v.reserve(v.size() + n_records); }; + grow(blocks_i); grow(cycles_i); grow(steps_i); grow(incs_i); grow(time); + grow(Etot); grow(etot); grow(PKII); grow(tau); grow(sigma); + grow(F1); grow(R); grow(DR); grow(T); grow(statev); grow(Wm); + if (sv_type == 2) { + grow(Wt); grow(Q); grow(r); + if (record_tangent) { + grow(dSdE); grow(dSdT); grow(drdE); grow(drdT); + } + } + else if (sv_type == 1 && record_tangent) { + grow(Lt); + } +} + void solver_memory_sink::record(phase_characteristics &rve, const solver_output &so, const int &kblock, const int &kcycle, const int &kstep, const int &kinc, const double &Time) { (void)so; auto sv = rve.sptr_sv_global; diff --git a/src/Simulation/Solver/step.cpp b/src/Simulation/Solver/step.cpp index ebf1d86ef..05c0dcb83 100755 --- a/src/Simulation/Solver/step.cpp +++ b/src/Simulation/Solver/step.cpp @@ -36,11 +36,10 @@ using namespace arma; namespace simcoon{ -//=====Private methods for ellipsoid_characteristics=================================== +//=====Private methods for step=================================== -//=====Public methods for ellipsoid_characteristics============================================ +//=====Public methods for step============================================ -//@brief default constructor //------------------------------------------------------------- step::step() //------------------------------------------------------------- @@ -57,12 +56,6 @@ step::step() } -/*! - \brief Constructor with parameters - \param mnumber : number of the step - \param mninc : number of increments - \param mmode : mode : type of loading - */ //------------------------------------------------------------- step::step(const int &mnumber, const double &mDn_init, const double &mDn_mini, const double &mDn_inc, const int &mmode, const unsigned int &mcontrol_type) @@ -88,35 +81,6 @@ step::step(const int &mnumber, const double &mDn_init, const double &mDn_mini, c } -/*! - \brief Copy constructor - \param st step object to duplicate - */ - -//------------------------------------------------------ -step::step(const step& st) -//------------------------------------------------------ -{ - number = st.number; - Dn_init = st.Dn_init; - Dn_mini = st.Dn_mini; - Dn_inc = st.Dn_inc; - ninc = st.ninc; - mode = st.mode; - control_type = st.control_type; - - times = st.times; - BC_Time = st.BC_Time; - - tab_data = st.tab_data; -} - -/*! - \brief destructor - */ - -step::~step() {} - //------------------------------------------------------------- void step::generate() //------------------------------------------------------------- @@ -171,14 +135,10 @@ bool step::compute_inc(double &tnew_dt, const int &inc, double &tinc, double &Dt if (Dtinc_cur < Dn_mini) { if (inforce_solver == 1) { -// cout << "Warning : The solver has been forced to continue with the minial increment at step:" << number << " inc: " << inc << " and fraction:" << tinc << "\n"; - //tnew_dt = 1.; Dtinc_cur = Dn_mini; } else { - // inforce_solver == 0: the caller asked NOT to force the minimal increment. - // Report it (not exit(0), which used to kill the host process silently): the - // solver aborts with status 1, like a non-converged Newton loop. + // inforce off: the solver aborts with status 1, as for a non-converged Newton loop. return false; } @@ -195,33 +155,6 @@ bool step::compute_inc(double &tnew_dt, const int &inc, double &tinc, double &Dt } return true; } - -/*! - \brief Standard operator = for block - */ - -//---------------------------------------------------------------------- -step& step::operator = (const step& st) -//---------------------------------------------------------------------- -{ -// assert(st.ninc>0); -// assert(st.mode>0); - - number = st.number; - Dn_init = st.Dn_init; - Dn_mini = st.Dn_mini; - Dn_inc = st.Dn_inc; - ninc = st.ninc; - mode = st.mode; - control_type = st.control_type; - - times = st.times; - BC_Time = st.BC_Time; - - tab_data = st.tab_data; - - return *this; -} //-------------------------------------------------------------------------- ostream& operator << (ostream& s, const step& st) diff --git a/src/Simulation/Solver/step_meca.cpp b/src/Simulation/Solver/step_meca.cpp index 7c0922ef5..e4dc18cc2 100755 --- a/src/Simulation/Solver/step_meca.cpp +++ b/src/Simulation/Solver/step_meca.cpp @@ -37,11 +37,10 @@ using namespace arma; namespace simcoon{ -//=====Private methods for ellipsoid_characteristics=================================== +//=====Private methods for step_meca=================================== -//=====Public methods for ellipsoid_characteristics============================================ +//=====Public methods for step_meca============================================ -//@brief default constructor //------------------------------------------------------------- step_meca::step_meca() : step() //------------------------------------------------------------- @@ -52,7 +51,7 @@ step_meca::step_meca() : step() step_meca::step_meca(const unsigned int &control_type) : step() { - if (control_type == 5 || control_type == 6) { //Control with F (4) or gradU (5) + if (control_type == 5 || control_type == 6) { // control by F (5) or grad u (6): 9 components cBC_meca = zeros >(9); BC_meca = zeros(9); } @@ -67,16 +66,6 @@ step_meca::step_meca(const unsigned int &control_type) : step() } -/*! - \brief Constructor with parameters - cBC_meca = mcBC_meca; Type of boundary conditions - BC_meca = mBC_meca; Target values of the BC - BC_T = mBC_T; target temperature - Etot = mEtot; current strain - sigma = msigma; current stress - T = mT; current temperature - */ - //------------------------------------------------------------- step_meca::step_meca(const int &mnumber, const double &mDn_init, const double &mDn_mini, const double &mDn_inc, const int &mmode, const unsigned int &mcontrol_type, const Col &mcBC_meca, const vec &mBC_meca, const mat &mmecas, const mat &mBC_mecas, const double &mBC_T, const int &mcBC_T, const vec &mTs, const vec &mBC_Ts, const mat &mBC_w, const mat &mBC_R) : step(mnumber, mDn_init, mDn_mini, mDn_inc, mmode, mcontrol_type) //------------------------------------------------------------- @@ -93,33 +82,6 @@ step_meca::step_meca(const int &mnumber, const double &mDn_init, const double &m BC_R = mBC_R; } -/*! - \brief Copy constructor - \param bl block object to duplicate - */ - -//------------------------------------------------------ -step_meca::step_meca(const step_meca& stm) : step(stm) -//------------------------------------------------------ -{ - cBC_meca = stm.cBC_meca; - BC_meca = stm.BC_meca; - mecas = stm.mecas; - BC_mecas = stm.mecas; - BC_T = stm.BC_T; - cBC_T = stm.cBC_T; - Ts = stm.Ts; - BC_Ts = stm.BC_Ts; - BC_w = stm.BC_w; - BC_R = stm.BC_R; -} - -/*! - \brief destructor - */ - -step_meca::~step_meca() {} - //------------------------------------------------------------- void step_meca::generate(const double &mTime, const vec &mEtot, const vec &msigma, const double &mT) //------------------------------------------------------------- @@ -203,10 +165,7 @@ void step_meca::generate(const double &mTime, const vec &mEtot, const vec &msigm //Read all the informations and fill the meca accordingly const mat tab_rows = mode3_rows(size_BC); if (tab_data.n_rows == 0 && tab_rows.n_rows > 0 && tab_rows(0,0) < BC_file_n(0)) { - // Legacy mode-3 FILE convention: the time axis restarts at each step - // (first row = anchor at the current state). Shift the axis so it - // continues from the current time. In-memory tab_data keeps the strict - // absolute-time contract (see solver/blocks.py). + // Legacy mode-3 file: the time axis restarts at each step, shift it; in-memory tab_data stays absolute. BC_file_n(0) = tab_rows(0,0); } @@ -259,22 +218,13 @@ void step_meca::generate(const double &mTime, const vec &mEtot, const vec &msigm namespace { -// Real 3x3 matrix logarithm via inverse scaling-and-squaring with -// Denman-Beavers square roots — real arithmetic + inv() only. Avoids -// arma::logmat's complex Schur (zgees) LAPACK path, which crashes (access -// violation) inside netlib reference LAPACK on Windows. Valid for matrices -// with no eigenvalue on the closed negative real axis — true for the relative -// deformation gradient of a physically meaningful step (det F > 0, rotation -// per step < 180 deg). Returns false when the iteration does not converge; -// the caller falls back to arma::logmat. +// Real log (inverse scaling-and-squaring, Denman-Beavers): avoids arma::logmat's zgees, which crashes in +// netlib LAPACK on Windows. Needs no eigenvalue on the closed negative real axis; false if not converged. bool real_logmat_3x3(const arma::mat &A_in, arma::mat &logA) { const arma::mat I = arma::eye(3, 3); arma::mat A = A_in; int k = 0; - // Square-root until A is close to I (log converges fast there). - // Frobenius, not the spectral norm: norm(X,2) is a full SVD (LAPACK - // dgesdd) used here only as a proximity test, and ||.||_2 <= ||.||_F - // always, so the 0.25 cutoff stays at least as conservative. + // Square-root until A is near I; Frobenius norm avoids an SVD and ||.||_2 <= ||.||_F keeps the cutoff conservative. while (arma::norm(A - I, "fro") > 0.25) { if (++k > 40) return false; arma::mat Y = A, Z = I; // Denman-Beavers: Y -> sqrt(A) @@ -327,9 +277,7 @@ void geodesic_F_increments(const arma::mat &F_prev, const arma::vec &BC_meca, co } arma::mat F_tilde = F_target * F_prev_inv; - // Logarithm of the total transformation: real-arithmetic path first - // (portable — see real_logmat_3x3), arma::logmat (complex Schur) only - // as a fallback for pathological steps. + // arma::logmat only as a fallback of real_logmat_3x3. arma::mat logF; if (!real_logmat_3x3(F_tilde, logF)) { logF = arma::real(arma::logmat(F_tilde)); @@ -472,10 +420,7 @@ void step_meca::generate_kin(const double &mTime, const mat &mF, const double &m //Read all the informations and fill the meca accordingly const mat tab_rows = mode3_rows(size_BC); if (tab_data.n_rows == 0 && tab_rows.n_rows > 0 && tab_rows(0,0) < BC_file_n(0)) { - // Legacy mode-3 FILE convention: the time axis restarts at each step - // (first row = anchor at the current state). Shift the axis so it - // continues from the current time. In-memory tab_data keeps the strict - // absolute-time contract (see solver/blocks.py). + // Legacy mode-3 file: the time axis restarts at each step, shift it; in-memory tab_data stays absolute. BC_file_n(0) = tab_rows(0,0); } @@ -521,10 +466,6 @@ void step_meca::generate_kin(const double &mTime, const mat &mF, const double &m } - /*! - \brief Standard operator = for block - */ - //---------------------------------------------------------------------- void step_meca::assess_inc(const double &tnew_dt, double &tinc, const double &Dtinc, phase_characteristics &rve, double &Time, const double &DTime, const mat &DR, const int &corate_type) { @@ -538,35 +479,7 @@ void step_meca::assess_inc(const double &tnew_dt, double &tinc, const double &Dt rve.set_start(corate_type); } } - -//---------------------------------------------------------------------- -step_meca& step_meca::operator = (const step_meca& stm) -//---------------------------------------------------------------------- -{ -// assert(stm.ninc>0); -// assert(stm.mode>0); - - number = stm.number; - Dn_init = stm.Dn_init; - Dn_mini = stm.Dn_mini; - Dn_inc = stm.Dn_inc; - ninc = stm.ninc; - mode = stm.mode; - - BC_Time = stm.BC_Time; - times = stm.times; - - cBC_meca = stm.cBC_meca; - BC_meca = stm.BC_meca; - mecas = stm.mecas; - BC_T = stm.BC_T; - cBC_T = stm.cBC_T; - Ts = stm.Ts; - BC_w = stm.BC_w; - BC_R = stm.BC_R; - - return *this; -} + //-------------------------------------------------------------------------- ostream& operator << (ostream& s, const step_meca& stm) diff --git a/src/Simulation/Solver/step_thermomeca.cpp b/src/Simulation/Solver/step_thermomeca.cpp index ebd46bb78..170305808 100755 --- a/src/Simulation/Solver/step_thermomeca.cpp +++ b/src/Simulation/Solver/step_thermomeca.cpp @@ -37,11 +37,10 @@ using namespace arma; namespace simcoon{ -//=====Private methods for ellipsoid_characteristics=================================== +//=====Private methods for step_thermomeca=================================== -//=====Public methods for ellipsoid_characteristics============================================ +//=====Public methods for step_thermomeca============================================ -//@brief default constructor //------------------------------------------------------------- step_thermomeca::step_thermomeca() : step() //------------------------------------------------------------- @@ -66,14 +65,6 @@ step_thermomeca::step_thermomeca(const unsigned int &control_type) : step() BC_R = eye(3,3); } -/*! - \brief Constructor with parameters - cBC_meca = mcBC_meca; Type of mechanical boundary conditions - BC_meca = mBC_meca; Target values of the mechanical BC - cBC_meca = mcBC_meca; Type of thermal boundary conditions - BC_T = mBC_T; target values of the thermal BC - */ - //------------------------------------------------------------- step_thermomeca::step_thermomeca(const int &mnumber, const double &mDn_init, const double &mDn_mini, const double &mDn_inc, const int &mmode, const unsigned int &mcontrol_type, const Col &mcBC_meca, const vec &mBC_meca, const mat &mmecas, const mat &mBC_mecas, const double &mBC_T, const int &mcBC_T, const vec &mTs, const vec &mBC_Ts, const mat &mBC_w, const mat &mBC_R) : step(mnumber, mDn_init, mDn_mini, mDn_inc, mmode, mcontrol_type) //------------------------------------------------------------- @@ -90,33 +81,6 @@ step_thermomeca::step_thermomeca(const int &mnumber, const double &mDn_init, con BC_R = mBC_R; } -/*! - \brief Copy constructor - \param bl block object to duplicate - */ - -//------------------------------------------------------ -step_thermomeca::step_thermomeca(const step_thermomeca& stm) : step(stm) -//------------------------------------------------------ -{ - cBC_meca = stm.cBC_meca; - BC_meca = stm.BC_meca; - mecas = stm.mecas; - BC_mecas = stm.mecas; - BC_T = stm.BC_T; - cBC_T = stm.cBC_T; - Ts = stm.Ts; - BC_Ts = stm.BC_Ts; - BC_w = stm.BC_w; - BC_R = stm.BC_R; -} - -/*! - \brief destructor - */ - -step_thermomeca::~step_thermomeca() {} - //------------------------------------------------------------- void step_thermomeca::generate(const double &mTime, const vec &mEtot, const vec &msigma, const double &mT) //------------------------------------------------------------- @@ -214,10 +178,7 @@ void step_thermomeca::generate(const double &mTime, const vec &mEtot, const vec //Read all the informations and fill the meca accordingly const mat tab_rows = mode3_rows(size_BC); if (tab_data.n_rows == 0 && tab_rows.n_rows > 0 && tab_rows(0,0) < BC_file_n(0)) { - // Legacy mode-3 FILE convention: the time axis restarts at each step - // (first row = anchor at the current state). Shift the axis so it - // continues from the current time. In-memory tab_data keeps the strict - // absolute-time contract (see solver/blocks.py). + // Legacy mode-3 file: the time axis restarts at each step, shift it; in-memory tab_data stays absolute. BC_file_n(0) = tab_rows(0,0); } @@ -276,9 +237,6 @@ void step_thermomeca::generate(const double &mTime, const vec &mEtot, const vec } -/*! - \brief Standard operator = for block - */ //------------------------------------------------------------- void step_thermomeca::generate_kin(const double &mTime, const mat &mF, const double &mT) @@ -370,10 +328,7 @@ void step_thermomeca::generate_kin(const double &mTime, const mat &mF, const dou //Read all the informations and fill the meca accordingly const mat tab_rows = mode3_rows(size_BC); if (tab_data.n_rows == 0 && tab_rows.n_rows > 0 && tab_rows(0,0) < BC_file_n(0)) { - // Legacy mode-3 FILE convention: the time axis restarts at each step - // (first row = anchor at the current state). Shift the axis so it - // continues from the current time. In-memory tab_data keeps the strict - // absolute-time contract (see solver/blocks.py). + // Legacy mode-3 file: the time axis restarts at each step, shift it; in-memory tab_data stays absolute. BC_file_n(0) = tab_rows(0,0); } @@ -436,35 +391,7 @@ void step_thermomeca::assess_inc(const double &tnew_dt, double &tinc, const doub rve.set_start(corate_type); } } - -//---------------------------------------------------------------------- -step_thermomeca& step_thermomeca::operator = (const step_thermomeca& stm) -//---------------------------------------------------------------------- -{ -// assert(stm.ninc>0); -// assert(stm.mode>0); - - number = stm.number; - Dn_init = stm.Dn_init; - Dn_mini = stm.Dn_mini; - Dn_inc = stm.Dn_inc; - ninc = stm.ninc; - mode = stm.mode; - - BC_Time = stm.BC_Time; - times = stm.times; - - cBC_meca = stm.cBC_meca; - BC_meca = stm.BC_meca; - mecas = stm.mecas; - BC_T = stm.BC_T; - cBC_T = stm.cBC_T; - Ts = stm.Ts; - BC_w = stm.BC_w; - BC_R = stm.BC_R; - return *this; -} //-------------------------------------------------------------------------- ostream& operator << (ostream& s, const step_thermomeca& stm) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index ed797da1f..95043ad37 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,9 +23,13 @@ set(TEST_SRCS # Libraries - Phase test/Libraries/Phase/Tmaterial_characteristics.cpp + # Libraries - Solver + test/Libraries/Solver/Tstep.cpp + # Libraries - Maths test/Libraries/Maths/Tlagrange.cpp test/Libraries/Maths/Tnum_solve.cpp + test/Libraries/Maths/Tparallel.cpp test/Libraries/Maths/Trotation.cpp test/Libraries/Maths/Trotation_class.cpp test/Libraries/Maths/Tstats.cpp @@ -49,6 +53,7 @@ set(TEST_SRCS test/Libraries/Umat/Tdamage_LLD_0.cpp test/Libraries/Umat/Tunified_TR.cpp test/Libraries/Umat/Modular/Tmodular_umat.cpp + test/Libraries/Umat/Modular/Tmodular_umat_cache.cpp test/Libraries/Umat/Treference_umats.cpp test/Libraries/Umat/Ttutorial_umat.cpp @@ -59,6 +64,7 @@ set(TEST_SRCS # External test files (non-MSVC only) set(TEST_EXTERN test_extern/Umats/UMABA/TUMABA.cpp + test_extern/Umats/UMANS/TUMANS.cpp ) # Add test executables @@ -92,4 +98,6 @@ if(NOT MSVC) foreach(testSrc ${TEST_EXTERN}) add_simcoon_test(${testSrc}) endforeach() + # TUMANS loads the plugin itself: libsimcoon links dylib PRIVATE, so its headers are not inherited + target_link_libraries(TUMANS PRIVATE dylib) endif() diff --git a/test/Libraries/Continuum_mechanics/Tconstitutive.cpp b/test/Libraries/Continuum_mechanics/Tconstitutive.cpp index d0bfcb140..c62e46082 100755 --- a/test/Libraries/Continuum_mechanics/Tconstitutive.cpp +++ b/test/Libraries/Continuum_mechanics/Tconstitutive.cpp @@ -25,7 +25,9 @@ #include #include +#include #include +#include #include using namespace std; @@ -369,3 +371,83 @@ TEST(Tconstitutive, Isotropize) EXPECT_GT(props(1), -1.); EXPECT_LT(props(1), 0.5); } + +TEST(Tconstitutive, isotropic_tensors_are_the_expressions_they_replace) +{ + // L_iso, M_iso, Idev, Idev2 and H_iso are built entry by entry: the same values, to the bit, + // as the operator expressions on Ivol / Ireal they used to evaluate + const double E = 70000., nu = 0.3; + const double K = E/(3.*(1.-2*nu)), mu = E/(2.*(1.+nu)); + const mat Idev_ref = Ireal() - Ivol(); + const mat Idev2_ref = Ireal2() - Ivol(); + EXPECT_TRUE(approx_equal(Idev(), Idev_ref, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(Idev2(), Idev2_ref, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(L_iso(E, nu, "Enu"), mat(3.*K*Ivol() + 2.*mu*Idev_ref), "absdiff", 0.)); + EXPECT_TRUE(approx_equal(M_iso(E, nu, "Enu"), mat(1/(3.*K)*Ivol() + 1/(2.*mu)*Idev2_ref), "absdiff", 0.)); + EXPECT_TRUE(approx_equal(H_iso(3., 5.), mat(3.*3.*Ivol() + 2.*5.*Idev_ref), "absdiff", 0.)); +} + +TEST(Tconstitutive, inv_cubic) +{ + // closed-form inverse of isotropic and cubic tensors and of their Reuss mixtures + const mat L_A = L_iso(70000., 0.33, "Enu"), L_M = L_cubic(30000., 0.3, 9000., "EnuG"); + const std::vector cases = {L_A, M_iso(70000., 0.33, "Enu"), L_M, M_cubic(30000., 0.3, 9000., "EnuG"), + 0.3*M_cubic(30000., 0.3, 9000., "EnuG") + 0.7*M_cubic(70000., 0.33, 26000., "EnuG")}; + for (const mat &A : cases) { + const mat Ai = inv_cubic(A); + EXPECT_LT(abs(Ai*A - eye(6, 6)).max(), 1.E-13); + EXPECT_LT(abs(Ai - inv(A)).max(), 1.E-13*abs(Ai).max()); + } + // not cubic: the general inverse + mat B = L_A; + B(0, 5) = B(5, 0) = 1000.; + EXPECT_LT(abs(inv_cubic(B) - inv(B)).max(), 1.E-13*abs(inv(B)).max()); + // singular + mat S = L_A; + S(3, 3) = S(4, 4) = S(5, 5) = 0.; + EXPECT_THROW(inv_cubic(S), simcoon::exception_inv); +} + +TEST(Tconstitutive, cubic_coefficients_and_is_isotropic) +{ + const mat L_A = L_iso(70000., 0.33, "Enu"), L_C = L_cubic(30000., 0.3, 9000., "EnuG"); + double a, b, c; + EXPECT_TRUE(cubic_coefficients(L_C, a, b, c)); + EXPECT_EQ(a, L_C(0,0)); + EXPECT_EQ(b, L_C(0,1)); + EXPECT_EQ(c, L_C(3,3)); + // the isotropy relation depends on the Voigt convention of the tensor + const mat M_A = M_iso(70000., 0.33, "Enu"); + EXPECT_TRUE(is_isotropic(L_A, Tensor4Type::stiffness)); + EXPECT_TRUE(is_isotropic(M_A, Tensor4Type::compliance)); + EXPECT_TRUE(is_isotropic(mat(0.3*eye(6, 6)), Tensor4Type::strain_concentration)); + EXPECT_TRUE(is_isotropic(mat(0.3*eye(6, 6)), Tensor4Type::stress_concentration)); + EXPECT_FALSE(is_isotropic(M_A, Tensor4Type::stiffness)); + EXPECT_FALSE(is_isotropic(L_A, Tensor4Type::strain_concentration)); + EXPECT_FALSE(is_isotropic(L_C, Tensor4Type::stiffness)); // cubic, not isotropic + // round-off: exact structure refused, 1e-14 accepted; a real perturbation refused + mat P = L_A; + P(1, 1) *= 1. + 1.E-15; + EXPECT_FALSE(cubic_coefficients(P, a, b, c)); + EXPECT_TRUE(is_isotropic(P, Tensor4Type::stiffness)); + P(0, 4) = 1.E-6*L_A(0, 0); + EXPECT_FALSE(is_isotropic(P, Tensor4Type::stiffness)); + // never cubic with a NaN, nor for a non-6x6 matrix + mat N = L_A; + N(2, 5) = datum::nan; + EXPECT_FALSE(cubic_coefficients(N, a, b, c, 1.E-14)); + EXPECT_FALSE(is_isotropic(N, Tensor4Type::stiffness)); + EXPECT_FALSE(cubic_coefficients(mat(3, 3, fill::eye), a, b, c)); +} + +TEST(Tconstitutive, invalid_conventions_throw) +{ + EXPECT_THROW(L_iso(70000., 0.3, "Emu"), std::invalid_argument); + EXPECT_THROW(M_iso(70000., 0.3, "Emu"), std::invalid_argument); + EXPECT_THROW(L_cubic(30000., 0.3, 9000., "Enu"), std::invalid_argument); + EXPECT_THROW(M_cubic(30000., 0.3, 9000., "Enu"), std::invalid_argument); + EXPECT_THROW(L_ortho(1., 1., 1., 0.3, 0.3, 0.3, 1., 1., 1., "Enu"), std::invalid_argument); + EXPECT_THROW(M_ortho(1., 1., 1., 0.3, 0.3, 0.3, 1., 1., 1., "Enu"), std::invalid_argument); + EXPECT_THROW(L_isotrans(100., 10., 0.3, 0.4, 5., 4), std::invalid_argument); + EXPECT_THROW(M_isotrans(100., 10., 0.3, 0.4, 5., 0), std::invalid_argument); +} diff --git a/test/Libraries/Continuum_mechanics/Thyperelastic.cpp b/test/Libraries/Continuum_mechanics/Thyperelastic.cpp index ec042bb31..d6b94c3d0 100755 --- a/test/Libraries/Continuum_mechanics/Thyperelastic.cpp +++ b/test/Libraries/Continuum_mechanics/Thyperelastic.cpp @@ -24,7 +24,13 @@ #include #include #include +#include +#include +#include +#include #include +#include +#include using namespace std; using namespace arma; @@ -304,4 +310,98 @@ TEST(Thyperelastic, L_iso_hyper_pstretch_symmetry) // Tangent modulus should have major symmetry EXPECT_LT(norm(L_iso - L_iso.t(), 2), 1.E-6); -} \ No newline at end of file +} +namespace { + +// L_iso_hyper_invariants as the full expression of all five gamma terms (before the zero terms +// were skipped) +mat L_iso_hyper_invariants_full(const double &d1, const double &d2, const double &d11, const double &d12, const double &d22, const mat &b, const double &J) { + const mat b_bar = pow(J,-2./3.)*b; + const mat b_bar2 = powmat(b_bar,2); + const mat dev_b_bar = dev(b_bar); + const mat dev_b_bar2 = dev(b_bar2); + const vec I_bar = isochoric_invariants(b,J); + const mat Id = eye(3,3); + const mat H_bar = auto_sym_dyadic_operator(b_bar); + const mat devdevbb2 = I_bar(0)*dev(b_bar) - dev(b_bar2); + const mat gamma_1 = (4./3.)*(I_bar(0)*Idev() - (sym_dyadic(dev_b_bar,Id)+sym_dyadic(Id,dev_b_bar))); + const mat gamma_2 = (8./3.)*(I_bar(1)*(Ireal() - 2.*Ivol()) - I_bar(0)*(sym_dyadic(dev_b_bar,Id)+sym_dyadic(Id,dev_b_bar)) + + (sym_dyadic(dev_b_bar2,Id)+sym_dyadic(Id,dev_b_bar2))) + 4*(auto_sym_dyadic(b_bar)-H_bar); + const mat gamma_11 = 4.*auto_sym_dyadic(dev_b_bar); + const mat gamma_22 = 4.*auto_sym_dyadic(devdevbb2); + const mat gamma_12 = 4.*(sym_dyadic(dev_b_bar,devdevbb2)+sym_dyadic(devdevbb2,dev_b_bar)); + return (1./J)*(gamma_1*d1+gamma_2*d2+gamma_11*d11+gamma_12*d12+gamma_22*d22); +} + +} // namespace + +TEST(Thyperelastic, L_iso_hyper_invariants_skips_only_zero_terms) +{ + arma_rng::set_seed(23); + const mat F = eye(3,3) + 0.2*randn(3,3); + const mat b = F*F.t(); + const double J = det(F); + const std::vector> coefs = { + {0.5, 0., 0., 0., 0.}, // Neo-Hookean: gamma_1 alone + {0.26, -0.045, 0., 0., 0.}, // Mooney-Rivlin + {0.4, 0., 0.03, 0., 0.}, // Yeoh-like: a second derivative in I_1 + {0.4, 0.1, 0.03, 0.02, 0.01}}; // every term + for (const auto &c : coefs) { + const mat L = L_iso_hyper_invariants(c[0], c[1], c[2], c[3], c[4], b, J); + const mat L_ref = L_iso_hyper_invariants_full(c[0], c[1], c[2], c[3], c[4], b, J); + EXPECT_LT(abs(L - L_ref).max(), 1.E-15*abs(L_ref).max()); + } +} + +TEST(Thyperelastic, pure_stretch_box_tangent_closed_form) +{ + // Dtau_LieDD_2_DtauDe_stretch (Hencky block of the modular engine) against the pull-back + + // exact spectral map of corate 3 at F = V, on an isotropic potential + arma_rng::set_seed(29); + const vec props = {0.5, 0.02, 0.003, 500.}; // YEOHH + for (int t = 0; t < 10; t++) { + mat e = 0.3*randn(3,3); + e = 0.5*(e + e.t()); + if (t == 0) e = diagmat(vec({0.1, 0.1, -0.05})); // repeated eigenvalue + vec::fixed<3> ev; + mat::fixed<3,3> N; + ASSERT_TRUE(eig_sym_3x3(ev, N, e)); + const mat V = spectral_compose_3x3(N, exp(vec(ev))); + const mat b = V*V; + const double J = det(V); + const hyper_invariants_dW dW = hyper_potential_derivatives(HyperPotential::YEOHH, props, isochoric_invariants(b, J), J); + vec tau_v; + mat Lt_ref, Lt_Lie; + hyper_invariants_response(dW, b, J, V, 3, tau_v, Lt_ref, {}); + hyper_invariants_response(dW, b, J, V, 4, tau_v, Lt_Lie, {}); + const mat Lt = Dtau_LieDD_2_DtauDe_stretch(Lt_Lie, ev, N, v2t_stress(tau_v)); + EXPECT_LT(abs(Lt - Lt_ref).max(), 1.E-12*abs(Lt_ref).max()); + } +} + +TEST(Thyperelastic, material_tangent_form) +{ + // corate_type_material_tangent: a kernel built on F returns dS/dE, the pull-back of its box + // tangent in any corate, with the same stress and work + arma_rng::set_seed(31); + const mat F0 = eye(3,3); + const mat F1 = eye(3,3) + 0.15*randn(3,3); + const vec props = {0.26, -0.045, 10000.}; // MOORI + const vec etot = zeros(6), Detot = t2v_strain(Log_strain(F1)); + for (int corate : {0, 1, 2, 3, 4, 5, corate_type_total}) { + vec sig_box = zeros(6), sig_mat = zeros(6), sv_box = zeros(1), sv_mat = zeros(1); + mat Lt_box(6,6), Lt_mat(6,6), L(6,6); + double Wm_b = 0., Wr_b = 0., Wir_b = 0., Wd_b = 0., Wm_m = 0., Wr_m = 0., Wir_m = 0., Wd_m = 0., tnew = 1.; + umat_generic_hyper_invariants("MOORI", etot, Detot, F0, F1, sig_box, Lt_box, L, eye(3,3), 3, props, 1, sv_box, 293., 0., 0., 1., + Wm_b, Wr_b, Wir_b, Wd_b, 3, 3, false, tnew, corate, tangent_default); + umat_generic_hyper_invariants("MOORI", etot, Detot, F0, F1, sig_mat, Lt_mat, L, eye(3,3), 3, props, 1, sv_mat, 293., 0., 0., 1., + Wm_m, Wr_m, Wir_m, Wd_m, 3, 3, false, tnew, corate_type_material_tangent, tangent_default); + EXPECT_TRUE(approx_equal(sig_box, sig_mat, "absdiff", 0.)); + EXPECT_EQ(Wm_b, Wm_m); + const mat DSDE_ref = DtauDe_corate_2_DSDE(Lt_box, corate, F1, v2t_stress(sig_box)); + EXPECT_LT(abs(Lt_mat - DSDE_ref).max(), 1.E-12*abs(DSDE_ref).max()); + // and the dispatchers are the identity on that form + EXPECT_TRUE(approx_equal(DtauDe_corate_2_DSDE(Lt_mat, corate_type_material_tangent, F1, v2t_stress(sig_mat)), Lt_mat, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(DSDE_2_DtauDe_corate(Lt_mat, corate_type_material_tangent, F1, v2t_stress(sig_mat)), Lt_mat, "absdiff", 0.)); + } +} diff --git a/test/Libraries/Continuum_mechanics/Tkinematics.cpp b/test/Libraries/Continuum_mechanics/Tkinematics.cpp index 36e74eaf2..e7b8473d1 100644 --- a/test/Libraries/Continuum_mechanics/Tkinematics.cpp +++ b/test/Libraries/Continuum_mechanics/Tkinematics.cpp @@ -23,6 +23,8 @@ #include #include +#include +#include #include using namespace std; @@ -285,3 +287,80 @@ TEST(Tkinematics, finite_DQ) mat DQ_zero = finite_DQ(zeros(3, 3), zeros(3, 3), DTime); EXPECT_LT(norm(DQ_zero - eye(3, 3), 2), 1.E-9); } + +TEST(Tkinematics, eig_sym_3x3_matches_eig_sym) +{ + // the fixed-size Jacobi solver against LAPACK: same ascending spectrum, orthonormal basis, + // reconstruction to roundoff, on random, near-identity, repeated and stiff spectra + arma_rng::set_seed(1); + for (int t = 0; t < 400; t++) { + mat F; + mat Q0, R0; + qr(Q0, R0, randn(3, 3)); + switch (t % 4) { + case 0: F = eye(3, 3) + 0.3*randn(3, 3); break; + case 1: F = eye(3, 3) + 1.e-7*randn(3, 3); break; + case 2: F = Q0*diagmat(vec({1.2, 1.2, 0.9}))*Q0.t(); break; + default: F = Q0*diagmat(vec({30., 1., 0.1}))*Q0.t(); break; + } + const mat A = F.t()*F; + vec l_ref; + mat Q_ref; + ASSERT_TRUE(eig_sym(l_ref, Q_ref, A)); + vec::fixed<3> l; + mat::fixed<3,3> Q; + ASSERT_TRUE(eig_sym_3x3(l, Q, A)); + EXPECT_LT(norm(vec(l) - l_ref, "inf"), 1.E-12*norm(l_ref, "inf")); + EXPECT_TRUE(l(0) <= l(1) && l(1) <= l(2)); + EXPECT_LT(abs(mat(Q.t()*Q) - eye(3, 3)).max(), 1.E-14); + EXPECT_LT(abs(mat(spectral_compose_3x3(Q, l)) - A).max(), 1.E-13*abs(A).max()); + } + // exact on a diagonal matrix + vec::fixed<3> l; + mat::fixed<3,3> Q; + ASSERT_TRUE(eig_sym_3x3(l, Q, diagmat(vec({2., 0.5, 1.})))); + EXPECT_EQ(l(0), 0.5); + EXPECT_EQ(l(1), 1.); + EXPECT_EQ(l(2), 2.); + // failure on a non-finite input, as arma::eig_sym + mat A = eye(3, 3); + A(0, 1) = datum::nan; + EXPECT_FALSE(eig_sym_3x3(l, Q, A)); +} + +TEST(Tkinematics, spectral_functions_3x3) +{ + arma_rng::set_seed(2); + const mat F = eye(3, 3) + 0.2*randn(3, 3); + const mat B = F*F.t(); + mat::fixed<3,3> X; + ASSERT_TRUE(sqrtmat_sympd_3x3(X, B)); + EXPECT_LT(abs(mat(X) - sqrtmat_sympd(B)).max(), 1.E-13); + ASSERT_TRUE(logmat_sympd_3x3(X, B)); + EXPECT_LT(abs(mat(X) - logmat_sympd(B)).max(), 1.E-13); + ASSERT_TRUE(expmat_sym_3x3(X, 0.5*(F + F.t()))); + EXPECT_LT(abs(mat(X) - expmat_sym(0.5*(F + F.t()))).max(), 1.E-13); + // not SPD: sqrt and log refuse, as their arma counterparts + const mat N = diagmat(vec({1., -1.e-3, 2.})); + EXPECT_FALSE(sqrtmat_sympd_3x3(X, N)); + EXPECT_FALSE(logmat_sympd_3x3(X, N)); +} + +TEST(Tkinematics, RU_spectral_basis) +{ + // one eigen-solve gives R, the eigenbasis of C and, through R N, the one of B = F F^T + arma_rng::set_seed(3); + const mat F = eye(3, 3) + 0.25*randn(3, 3); + mat::fixed<3,3> R, N; + vec::fixed<3> c; + RU_spectral(R, c, N, F); + mat R_ref, U_ref; + RU_decomposition(R_ref, U_ref, F); + EXPECT_LT(abs(mat(R) - R_ref).max(), 1.E-14); + const mat Nb = R*N; + EXPECT_LT(abs(mat(Nb*diagmat(vec(c))*Nb.t()) - F*F.t()).max(), 1.E-13*abs(F*F.t()).max()); + // a singular F is reported as RU_decomposition did + mat F_sing = F; + F_sing.col(2) = F_sing.col(0); + EXPECT_THROW(RU_spectral(R, c, N, F_sing), simcoon::simcoon_error); +} diff --git a/test/Libraries/Continuum_mechanics/Tobjective_rates.cpp b/test/Libraries/Continuum_mechanics/Tobjective_rates.cpp index f2f3a4efd..2957270ae 100755 --- a/test/Libraries/Continuum_mechanics/Tobjective_rates.cpp +++ b/test/Libraries/Continuum_mechanics/Tobjective_rates.cpp @@ -784,6 +784,47 @@ TEST(Tobjective_rates, total_formulation_increment_is_a_state_function) EXPECT_FALSE(work_correction_applies(6)); } +TEST(Tobjective_rates, corate_kinematics_with_increment_matches_the_two_calls) +{ + // the one-pass overload returns the same DR, D, Omega as corate_kinematics and the increment + // of Delta_log_strain_corate, for every rate and the total form + arma_rng::set_seed(17); + for (int t = 0; t < 20; t++) { + const mat F0 = eye(3, 3) + 0.12*randn(3, 3); + const mat F1 = (eye(3, 3) + 0.02*randn(3, 3))*F0; + const double dt = 0.1; + for (int corate : {0, 1, 2, 3, 4, 5, corate_type_total}) { + mat DR, D, Omega, DR2, D2, Omega2, De; + corate_kinematics(corate, DR, D, Omega, F0, F1, dt); + const mat De_ref = Delta_log_strain_corate(F0, F1, DR, D, Omega, dt, corate); + corate_kinematics(corate, DR2, D2, Omega2, De, F0, F1, dt); + EXPECT_LT(abs(DR2 - DR).max(), 1.E-15); + EXPECT_LT(abs(D2 - D).max(), 1.E-15*(1. + abs(D).max())); + EXPECT_LT(abs(Omega2 - Omega).max(), 1.E-15*(1. + abs(Omega).max())); + EXPECT_LT(abs(De - De_ref).max(), 1.E-15*(1. + abs(De_ref).max())); + } + } +} + +TEST(Tobjective_rates, log_R_increment_is_A_R_D) +{ + // De = A^R:D by congruence on the eigenbasis of B1, transported by DR, equals the 6x6 A_R route + arma_rng::set_seed(19); + for (int t = 0; t < 20; t++) { + const mat F0 = eye(3, 3) + 0.15*randn(3, 3); + const mat F1 = (eye(3, 3) + 0.05*randn(3, 3))*F0; + const double dt = 0.5; + mat DR, D, Omega; + corate_kinematics(3, DR, D, Omega, F0, F1, dt); + const mat De = Delta_log_strain_corate(F0, F1, DR, D, Omega, dt, 3); + const mat Dh = v2t_strain(A_R(F1)*t2v_strain(D)); + const mat De_ref = 0.5*(Dh + DR*Dh*DR.t())*dt; + EXPECT_LT(abs(De - De_ref).max(), 1.E-13*abs(De_ref).max()); + // DR is the inverse Cayley image of Omega + EXPECT_LT(abs(Hughes_Winget(Omega, dt) - DR).max(), 1.E-12); + } +} + TEST(Tobjective_rates, dH_dE_close_eigenvalues) { // dH/dE is continuous in C: two eigenvalues a relative 3e-12 apart, just past the coincident diff --git a/test/Libraries/Continuum_mechanics/Ttensor.cpp b/test/Libraries/Continuum_mechanics/Ttensor.cpp index 031267404..69852f0b0 100644 --- a/test/Libraries/Continuum_mechanics/Ttensor.cpp +++ b/test/Libraries/Continuum_mechanics/Ttensor.cpp @@ -1006,3 +1006,27 @@ TEST(Ttensor_batch, SizeMismatchThrows) EXPECT_THROW(batch_contract(t4_slice_eye, Tensor4Type::stiffness, t2_mismatch, Tensor2Type::strain), std::invalid_argument); } + +// batch_rotate computes every point on its own: the thread count must not change a bit, and each +// column/slice must be the single-point rotation (past the serial cutoff of the parallel loop). +TEST(Ttensor_batch, RotateThreadCountAndPointwise) +{ + const int N = 500; + mat v(6, N, fill::randn); + cube t4(6, 6, N, fill::randn); + cube R(3, 3, N); + for (int i = 0; i < N; i++) + R.slice(i) = Rotation::from_rotvec(vec::fixed<3>{0.01 * i, -0.3, 0.002 * i}).as_matrix(); + + const mat v_serial = batch_rotate(v, Tensor2Type::strain, R, true, 1); + const cube t4_serial = batch_rotate(t4, Tensor4Type::stiffness, R, false, 1); + for (unsigned int n_threads : {0u, 2u, 4u}) { + EXPECT_TRUE(approx_equal(batch_rotate(v, Tensor2Type::strain, R, true, n_threads), v_serial, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(batch_rotate(t4, Tensor4Type::stiffness, R, false, n_threads), t4_serial, "absdiff", 0.)); + } + for (int i : {0, 137, N - 1}) { + const Rotation Ri = Rotation::from_matrix(mat::fixed<3,3>(R.slice(i))); + EXPECT_LT(norm(v_serial.col(i) - vec(Ri.apply_strain(vec(v.col(i)))), "inf"), 1e-14); + EXPECT_LT(norm(t4_serial.slice(i) - mat(Ri.apply_stiffness(mat(t4.slice(i)), false)), "inf"), 1e-13); + } +} diff --git a/test/Libraries/Homogenization/Teshelby.cpp b/test/Libraries/Homogenization/Teshelby.cpp index 1b63a4d85..5666f63fe 100755 --- a/test/Libraries/Homogenization/Teshelby.cpp +++ b/test/Libraries/Homogenization/Teshelby.cpp @@ -111,3 +111,141 @@ TEST(Teshelby, memo_keys_on_exact_medium_and_geometry) memo.clear(); EXPECT_EQ(memo.size(), 0u); } + +#include +#include + +// eshelby_quadrature_points: the tables of points(), one shared immutable instance per (mp, np), +// whatever the thread that asks for it. +TEST(Teshelby, quadrature_tables_are_shared_and_exact) +{ + const eshelby_quadrature &q = eshelby_quadrature_points(20, 30); + vec x(20), wx(20), y(30), wy(30); + points(x, wx, y, wy, 20, 30); + EXPECT_EQ(q.mp, 20); + EXPECT_EQ(q.np, 30); + EXPECT_TRUE(approx_equal(q.x, x, "absdiff", 0.) && approx_equal(q.wx, wx, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(q.y, y, "absdiff", 0.) && approx_equal(q.wy, wy, "absdiff", 0.)); + EXPECT_EQ(&eshelby_quadrature_points(20, 30), &q); + EXPECT_NE(&eshelby_quadrature_points(30, 20), &q); + EXPECT_EQ(&eshelby_quadrature_points(20, 30), &q); + EXPECT_THROW(eshelby_quadrature_points(0, 10), std::invalid_argument); + + // threads alternating (mp, np) all get the tables of the size they asked for + std::vector wrong(8, 0); + std::vector threads; + for (int t = 0; t < 8; t++) { + threads.emplace_back([t, &wrong]() { + for (int it = 0; it < 200; it++) { + const int m = 5 + (t + it) % 7; + const eshelby_quadrature &qt = eshelby_quadrature_points(m, m + 1); + if (qt.mp != m || qt.np != m + 1 || qt.x.n_elem != (uword)m || qt.wy.n_elem != (uword)(m + 1)) + wrong[t]++; + } + }); + } + for (auto &th : threads) th.join(); + for (int t = 0; t < 8; t++) EXPECT_EQ(wrong[t], 0); +} + +// Gauss is the weighted sum of calG over the quadrature points (it inlines calG and hoists the +// azimuthal trigonometry out of the loops): same sums in the same order. +TEST(Teshelby, Gauss_is_the_weighted_sum_of_calG) +{ + Mat Id = {{0, 3, 4}, {3, 1, 5}, {4, 5, 2}}; + mat L_ortho = zeros(6, 6); + L_ortho.submat(0, 0, 2, 2) = mat({{120000., 30000., 20000.}, {30000., 80000., 25000.}, {20000., 25000., 40000.}}); + L_ortho(3, 3) = 30000.; L_ortho(4, 4) = 20000.; L_ortho(5, 5) = 15000.; + const std::vector media = {L_iso(70000., 0.3, "Enu"), L_ortho}; + const std::vector shapes = {{1., 1., 1.}, {3., 1., 0.5}, {10., 1., 1.}}; + const int mp = 13, np = 7; + vec x(mp), wx(mp), y(np), wy(np); + points(x, wx, y, wy, mp, np); + for (const mat &L : media) { + for (const vec &a : shapes) { + mat G = zeros(6, 6); + Gauss(Id, L, G, a(0), a(1), a(2), x, wx, y, wy, mp, np); + mat G_ref = zeros(6, 6); + mat H = zeros(6, 6); + for (int l = 0; l < mp; l++) { + for (int i = 0; i < np; i++) { + calG(y(i), a(0), a(1), a(2), x(l), Id, L, H); + G_ref += wx(l) * (H * wy(i)); + } + } + EXPECT_LT(norm(G - G_ref, "fro"), 1.E-14 * norm(G_ref, "fro")); + } + } +} + +// Eshelby_closed_form: the sphere / spheroid formulas for an isotropic medium, and nothing else. +TEST(Teshelby, closed_form_applies_to_isotropic_spheroids_only) +{ + const double nu = 0.3; + mat L = L_iso(70000., nu, "Enu"); + mat S; + ASSERT_TRUE(Eshelby_closed_form(L, 1., 1., 1., S)); + EXPECT_LT(norm(S - Eshelby_sphere(nu), "fro"), 1.E-14); + ASSERT_TRUE(Eshelby_closed_form(L, 4., 2., 2., S)); + EXPECT_LT(norm(S - Eshelby_prolate(nu, 2.), "fro"), 1.E-14); + ASSERT_TRUE(Eshelby_closed_form(L, 0.5, 2., 2., S)); + EXPECT_LT(norm(S - Eshelby_oblate(nu, 0.25), "fro"), 1.E-14); + // and they are the limit of the integral + const eshelby_quadrature &q = eshelby_quadrature_points(200, 200); + for (double ar : {3., 0.3}) { + ASSERT_TRUE(Eshelby_closed_form(L, ar, 1., 1., S)); + mat S_num = Eshelby(L, ar, 1., 1., q.x, q.wx, q.y, q.wy, q.mp, q.np); + EXPECT_LT(norm(S - S_num, "fro"), 1.E-9 * norm(S, "fro")) << "aspect ratio " << ar; + } + mat S0 = eye(6, 6); + S = S0; + EXPECT_FALSE(Eshelby_closed_form(L, 1., 2., 3., S)); // general ellipsoid + EXPECT_FALSE(Eshelby_closed_form(L, 2., 2., 1., S)); // spheroid of axis 3 + EXPECT_FALSE(Eshelby_closed_form(L, 1.005, 1., 1., S)); // too close to a sphere + mat L_ti = L; + L_ti(0, 0) *= 1.01; + EXPECT_FALSE(Eshelby_closed_form(L_ti, 2., 1., 1., S)); // anisotropic medium + EXPECT_EQ(norm(S - S0, "fro"), 0.); +} + +// The penny-shaped crack (normal 1) is the ar -> 0 limit of the oblate tensor and of the integral. +TEST(Teshelby, penny_is_the_flat_oblate_limit) +{ + const eshelby_quadrature &q = eshelby_quadrature_points(200, 200); + for (double nu : {0., 0.25, 0.3, 0.45}) { + const mat S = Eshelby_penny(nu); + EXPECT_LT(norm(S - Eshelby_oblate(nu, 1.E-6), "fro"), 1.E-4) << "nu " << nu; + const mat L = L_iso(70000., nu, "Enu"); + const mat S_num = Eshelby(L, 1.E-5, 1., 1., q.x, q.wx, q.y, q.wy, q.mp, q.np); + EXPECT_LT(norm(S - S_num, "fro"), 1.E-3) << "nu " << nu; + } +} + +// Rule 1 (half sphere, trapezoidal rule in phi): its own tables, the same integral. +TEST(Teshelby, half_sphere_rule) +{ + const eshelby_quadrature &g = eshelby_quadrature_points(40, 40); + const eshelby_quadrature &b = eshelby_quadrature_points(40, 40, 1); + const eshelby_quadrature &c = eshelby_quadrature_points(40, 40, 1, true); + EXPECT_NE(&g, &b); + EXPECT_NE(&b, &c); + EXPECT_EQ(&eshelby_quadrature_points(40, 40, 1), &b); + EXPECT_EQ(b.mp, 20); + EXPECT_EQ(b.np, 40); + EXPECT_NEAR(accu(b.wx), 2., 1.E-11); // twice the measure of [0, 1] + EXPECT_NEAR(accu(b.wy), 2. * simcoon::pi, 1.E-13); + EXPECT_GE(b.x.min(), 0.); + EXPECT_FALSE(g.closed_form); + EXPECT_TRUE(c.closed_form); + EXPECT_THROW(eshelby_quadrature_points(40, 40, 2), std::invalid_argument); + + const double nu = 0.3; + mat L = L_iso(70000., nu, "Enu"); + mat S_b = Eshelby(L, 1., 1., 1., b.x, b.wx, b.y, b.wy, b.mp, b.np); + EXPECT_LT(norm(S_b - Eshelby_sphere(nu), "fro"), 1.E-12); + // a prolate spheroid: the half-sphere rule with half the points is as accurate + mat S_ref = Eshelby_prolate(nu, 10.); + mat S_g = Eshelby(L, 10., 1., 1., g.x, g.wx, g.y, g.wy, g.mp, g.np); + mat S_10 = Eshelby(L, 10., 1., 1., b.x, b.wx, b.y, b.wy, b.mp, b.np); + EXPECT_LT(norm(S_10 - S_ref, "fro"), 1.5 * norm(S_g - S_ref, "fro")); +} diff --git a/test/Libraries/Maths/Tlagrange.cpp b/test/Libraries/Maths/Tlagrange.cpp index d0cd4a4f7..a7ebb88c2 100644 --- a/test/Libraries/Maths/Tlagrange.cpp +++ b/test/Libraries/Maths/Tlagrange.cpp @@ -112,10 +112,13 @@ TEST(Tlagrange, lagrange_pow_1_second_derivative) double n = 2.; double alpha = 100.; - // Second derivative should be finite and computable - double x = 0.5; - double d2 = d2lagrange_pow_1(x, c, p0, n, alpha); - EXPECT_TRUE(std::isfinite(d2)); + // the derivative of dlagrange_pow_1, below and above the junction x = 1 - c (p0 != 1) + const double h = 1.E-6; + for (double x : {0.2, 0.5, 0.85, 0.95}) { + const double fd = (dlagrange_pow_1(x + h, c, p0, n, alpha) - dlagrange_pow_1(x - h, c, p0, n, alpha))/(2.*h); + const double d2 = d2lagrange_pow_1(x, c, p0, n, alpha); + EXPECT_NEAR(d2, fd, 1.E-6*std::max(1., fabs(fd))) << "x = " << x; + } } TEST(Tlagrange, lagrange_pow_1_boundary) diff --git a/test/Libraries/Maths/Tnum_solve.cpp b/test/Libraries/Maths/Tnum_solve.cpp index cfe6d277a..ae2f5e7fc 100644 --- a/test/Libraries/Maths/Tnum_solve.cpp +++ b/test/Libraries/Maths/Tnum_solve.cpp @@ -147,3 +147,93 @@ TEST(Tnum_solve, Fischer_Burmeister_zeros) EXPECT_LT(fabs(error), 1.E-9); } + +TEST(Tnum_solve, Fischer_Burmeister_m_small_systems) +{ + // the closed-form steps of one and two mechanisms against the LAPACK solve of the same + // Newton system (n = 3 still takes the general path: the third, decoupled, row checks it) + arma_rng::set_seed(5); + for (int t = 0; t < 50; t++) { + const vec Phi3 = randn(3), Dp3 = abs(randn(3)), Y3 = {1., 2., 0.5}; + mat B3 = -3.*eye(3, 3) - 0.2*abs(randn(3, 3)); + B3(0, 2) = B3(1, 2) = B3(2, 0) = B3(2, 1) = 0.; + for (int n : {1, 2}) { + const vec Phi = Phi3.head(n), Y = Y3.head(n); + const mat B = B3.submat(0, 0, n - 1, n - 1); + vec Dp = Dp3.head(n), dp; + double error; + Fischer_Burmeister_m(Phi, Y, B, Dp, dp, error); + vec Dp_g = Dp3, dp_g; + double error_g; + Fischer_Burmeister_m(Phi3, Y3, B3, Dp_g, dp_g, error_g); + const mat J = denom_FB_m(Phi, B, Dp3.head(n)); + vec FB(n); + for (int i = 0; i < n; i++) { + const double Dps = Dp3(i)*fabs(B(i, i)); + FB(i) = sqrt(Phi(i)*Phi(i) + Dps*Dps) + Phi(i) - Dps; + } + const vec dp_ref = -solve(J, FB); + EXPECT_LT(norm(dp - dp_ref, "inf"), 1.E-12*(1. + norm(dp_ref, "inf"))); + if (n == 2) { // the 2x2 block is the leading block of the block-diagonal 3x3 system + EXPECT_LT(norm(dp - dp_g.head(n), "inf"), 1.E-12*(1. + norm(dp_ref, "inf"))); + } + } + } +} + +TEST(Tnum_solve, min_ncp_step) +{ + // linear single surface Phi(Dp) = Phi0 + B Dp: the min-NCP step from Dp = 0 is the exact + // return, where Fischer-Burmeister takes two thirds of it + const vec Phi = {30.}, Y = {300.}; + mat B(1, 1); + B(0, 0) = -2000.; + vec Dp = zeros(1), dp; + double error; + ncp_step step(NCPMethod::min_fb_fallback); + step(Phi, Y, B, Dp, dp, error); + EXPECT_NEAR(Dp(0), 30./2000., 1.e-15); + EXPECT_FALSE(step.fell_back()); + vec Dp_fb = zeros(1); + Fischer_Burmeister_m(Phi, Y, B, Dp_fb, dp, error); + EXPECT_NEAR(Dp_fb(0), (2./3.)*30./2000., 1.e-15); + // default method: Fischer_Burmeister_m itself, bit for bit + vec Dp_d = zeros(1), dp_d; + double error_d; + ncp_step def; + def(Phi, Y, B, Dp_d, dp_d, error_d); + EXPECT_EQ(Dp_d(0), Dp_fb(0)); + // a growing error switches to Fischer-Burmeister for the rest of the solve + vec Dp2 = zeros(1); + ncp_step step2(NCPMethod::min_fb_fallback); + step2(Phi, Y, B, Dp2, dp, error); + const vec Phi_up = {60.}; + step2(Phi_up, Y, B, Dp2, dp, error); + EXPECT_TRUE(step2.fell_back()); + // uncoupled rows (diagonal B): a surface next to an always-satisfied row (Phi = -Y_crit, + // the viscoelastic / damage rows of MODUL) takes the full step, the inert row stays at zero + const vec Phi2 = {30., -1.}, Y2 = {300., 1.}; + mat B2 = zeros(2, 2); + B2(0, 0) = -2000.; + B2(1, 1) = -1.; + vec Dp3 = zeros(2), dp3; + double error3; + ncp_step step3(NCPMethod::min_fb_fallback); + step3(Phi2, Y2, B2, Dp3, dp3, error3); + EXPECT_EQ(Dp3(0), Dp(0)); + EXPECT_EQ(Dp3(1), 0.); + EXPECT_FALSE(step3.fell_back()); + // coupled rows keep Fischer-Burmeister, bit for bit + B2(0, 1) = B2(1, 0) = -10.; + vec Dp4 = zeros(2), dp4, Dp5 = zeros(2), dp5; + double error4, error5; + ncp_step step4(NCPMethod::min_fb_fallback); + step4(Phi2, Y2, B2, Dp4, dp4, error4); + Fischer_Burmeister_m(Phi2, Y2, B2, Dp5, dp5, error5); + EXPECT_EQ(Dp4(0), Dp5(0)); + EXPECT_EQ(Dp4(1), Dp5(1)); + // option values + EXPECT_EQ(ncp_method_of(0, "t"), NCPMethod::fischer_burmeister); + EXPECT_EQ(ncp_method_of(1, "t"), NCPMethod::min_fb_fallback); + EXPECT_THROW(ncp_method_of(2, "t"), std::invalid_argument); +} diff --git a/test/Libraries/Maths/Tparallel.cpp b/test/Libraries/Maths/Tparallel.cpp new file mode 100644 index 000000000..0b870d50f --- /dev/null +++ b/test/Libraries/Maths/Tparallel.cpp @@ -0,0 +1,70 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file Tparallel.cpp +///@brief Test of simcoon_parallel_for_safe: coverage, n_threads cap, exceptions +///@version 1.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +// Every item runs once, and no more than n_threads threads ever run the loop body. +TEST(Tparallel, n_threads_caps_the_threads) +{ + const int N = 4000; + for (unsigned int n_threads : {1u, 2u, 3u, 0u}) { + std::vector count(N, 0); + std::set ids; + std::mutex mtx; + std::atomic running{0}; + std::atomic peak{0}; + simcoon_parallel_for_safe(N, [&](int i) { + const int now = ++running; + int p = peak.load(); + while (now > p && !peak.compare_exchange_weak(p, now)) {} + count[i]++; + { + std::lock_guard lk(mtx); + ids.insert(std::this_thread::get_id()); + } + std::this_thread::sleep_for(std::chrono::microseconds(20)); + --running; + }, simcoon_parallel_cutoff, n_threads); + for (int i = 0; i < N; i++) ASSERT_EQ(count[i], 1) << "item " << i << ", n_threads " << n_threads; + if (n_threads > 0) { + EXPECT_LE(ids.size(), n_threads) << "n_threads " << n_threads; + EXPECT_LE(peak.load(), static_cast(n_threads)) << "n_threads " << n_threads; + } + } +} + +// The first exception of the loop body is rethrown once the loop is over, whatever n_threads. +TEST(Tparallel, exception_is_rethrown_after_the_loop) +{ + for (unsigned int n_threads : {1u, 2u, 0u}) { + EXPECT_THROW(simcoon_parallel_for_safe(1000, [](int i) { + if (i == 517) throw std::runtime_error("item 517"); + }, simcoon_parallel_cutoff, n_threads), std::runtime_error); + } +} diff --git a/test/Libraries/Solver/Tstep.cpp b/test/Libraries/Solver/Tstep.cpp new file mode 100644 index 000000000..30d7d1fea --- /dev/null +++ b/test/Libraries/Solver/Tstep.cpp @@ -0,0 +1,82 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + + +///@file Tstep.cpp +///@brief Copies of step_meca / step_thermomeca reproduce every field (memberwise, defaulted) +///@version 1.0 + +#include +#include +#include +#include + +using namespace arma; +using namespace simcoon; + +namespace { + +template +void fill_step(S &s) { + s.number = 3; s.Dn_init = 0.5; s.Dn_mini = 0.01; s.Dn_inc = 0.2; s.ninc = 4; s.mode = 3; + s.control_type = 3; s.times = linspace(0., 1., 4); s.BC_Time = 1.; s.tab_data = randu(4, 7); + s.cBC_meca = Col({0, 1, 1, 1, 1, 1}); s.BC_meca = randu(6); s.mecas = randu(4, 6); + s.BC_mecas = randu(4, 6); s.BC_w = randu(3, 3); s.BC_R = randu(3, 3); s.BC_T = 320.; + s.cBC_T = 1; s.Ts = randu(4); s.BC_Ts = randu(4); +} + +template +void expect_same(const S &a, const S &b) { + EXPECT_EQ(a.number, b.number); EXPECT_EQ(a.Dn_init, b.Dn_init); EXPECT_EQ(a.Dn_mini, b.Dn_mini); + EXPECT_EQ(a.Dn_inc, b.Dn_inc); EXPECT_EQ(a.ninc, b.ninc); EXPECT_EQ(a.mode, b.mode); + EXPECT_EQ(a.control_type, b.control_type); EXPECT_EQ(a.BC_Time, b.BC_Time); + EXPECT_TRUE(approx_equal(a.times, b.times, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(a.tab_data, b.tab_data, "absdiff", 0.)); + EXPECT_TRUE(all(a.cBC_meca == b.cBC_meca)); + EXPECT_TRUE(approx_equal(a.BC_meca, b.BC_meca, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(a.mecas, b.mecas, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(a.BC_mecas, b.BC_mecas, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(a.BC_w, b.BC_w, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(a.BC_R, b.BC_R, "absdiff", 0.)); + EXPECT_EQ(a.BC_T, b.BC_T); EXPECT_EQ(a.cBC_T, b.cBC_T); + EXPECT_TRUE(approx_equal(a.Ts, b.Ts, "absdiff", 0.)); + EXPECT_TRUE(approx_equal(a.BC_Ts, b.BC_Ts, "absdiff", 0.)); +} + +} // namespace + +TEST(Tstep, step_meca_copy_and_assignment) +{ + step_meca s; + fill_step(s); + const step_meca copied(s); + expect_same(s, copied); + step_meca assigned; + assigned = s; + expect_same(s, assigned); +} + +TEST(Tstep, step_thermomeca_copy_and_assignment) +{ + step_thermomeca s; + fill_step(s); + const step_thermomeca copied(s); + expect_same(s, copied); + step_thermomeca assigned; + assigned = s; + expect_same(s, assigned); +} diff --git a/test/Libraries/Umat/Modular/Tmodular_umat_cache.cpp b/test/Libraries/Umat/Modular/Tmodular_umat_cache.cpp new file mode 100644 index 000000000..4a2fcb1b0 --- /dev/null +++ b/test/Libraries/Umat/Modular/Tmodular_umat_cache.cpp @@ -0,0 +1,230 @@ +/* This file is part of simcoon. + +simcoon is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +simcoon is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with simcoon. If not, see . + +*/ + +///@file Tmodular_umat_cache.cpp +///@brief umat_modular keeps configured ModularUMAT instances per thread: every call must +/// return, bit for bit, what a fresh instance returns (the behaviour before the cache) + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include + +using namespace std; +using namespace arma; +using namespace simcoon; + +namespace { + +double code(ElasticityType t) { return static_cast(t); } +double code(MechanismType t) { return static_cast(t); } +double code(YieldType t) { return static_cast(t); } +double code(IsoHardType t) { return static_cast(t); } +double code(KinHardType t) { return static_cast(t); } +double code(DamageType t) { return static_cast(t); } + +struct material { + vec props; + int nstatev; + int tangent_mode; +}; + +// Elastoplastic (von Mises, Voce) + linear damage: the damage compliance cache M_cached_. +material plastic_damage(double sigma_Y) { + return {vec{code(ElasticityType::ISOTROPIC), 0.0, 210000.0, 0.3, 1e-5, + 2.0, + code(MechanismType::PLASTICITY), code(YieldType::VON_MISES), code(IsoHardType::VOCE), + code(KinHardType::NONE), 1.0, 0.0, sigma_Y, 200.0, 10.0, + code(MechanismType::DAMAGE), code(DamageType::LINEAR), 0.05, 5.0}, + 10, tangent_algorithmic}; +} + +// Linear isotropic + Armstrong-Frederick kinematic hardening, closest-point integrator. +material plastic_af() { + return {vec{code(ElasticityType::ISOTROPIC), 0.0, 70000.0, 0.33, 0.0, + 1.0, + code(MechanismType::PLASTICITY), code(YieldType::VON_MISES), code(IsoHardType::LINEAR), + code(KinHardType::ARMSTRONG_FREDERICK), 1.0, 1.0, 150.0, 500.0, 20000.0, 100.0}, + 14, tangent_closest_point}; +} + +// One Prony branch + power-law plasticity: the viscoelastic dEV/dE map of predict(). +material visco_plastic() { + return {vec{code(ElasticityType::ISOTROPIC), 0.0, 3000.0, 0.4, 0.0, + 2.0, + code(MechanismType::VISCOELASTICITY), 1.0, 2000.0, 0.4, 1.0e4, 1.0e4, + code(MechanismType::PLASTICITY), code(YieldType::VON_MISES), code(IsoHardType::POWER_LAW), + code(KinHardType::NONE), 1.0, 0.0, 40.0, 50.0, 0.4}, + 15, tangent_continuum}; +} + +struct point { + vec Etot = zeros(6), sigma = zeros(6), statev; + mat Lt = zeros(6, 6), L = zeros(6, 6); + double Wm = 0., Wm_r = 0., Wm_ir = 0., Wm_d = 0., tnew_dt = 1.; + explicit point(int nstatev) : statev(zeros(nstatev)) {} +}; + +// One increment through umat_modular (the cached instances) ... +void call_cached(const material& m, point& p, const vec& DEtot, double Time, bool start) { + p.tnew_dt = 1.; + umat_modular("MODUL", p.Etot, DEtot, p.sigma, p.Lt, p.L, eye(3, 3), static_cast(m.props.n_elem), + m.props, m.nstatev, p.statev, 293., 0., Time, 0.1, p.Wm, p.Wm_r, p.Wm_ir, p.Wm_d, + 3, 3, start, p.tnew_dt, m.tangent_mode); + p.Etot += DEtot; +} + +// ... and through a fresh instance, what umat_modular did before it kept them. +void call_fresh(const material& m, point& p, const vec& DEtot, double Time, bool start) { + p.tnew_dt = 1.; + ModularUMAT fresh; + fresh.set_kinematic_form(KinematicForm::rate); + fresh.run("MODUL", p.Etot, DEtot, p.sigma, p.Lt, p.L, eye(3, 3), static_cast(m.props.n_elem), + m.props, m.nstatev, p.statev, 293., 0., Time, 0.1, p.Wm, p.Wm_r, p.Wm_ir, p.Wm_d, + 3, 3, start, p.tnew_dt, m.tangent_mode); + p.Etot += DEtot; +} + +template +bool same_bits(const T& a, const T& b) { + return a.n_rows == b.n_rows && a.n_cols == b.n_cols + && std::memcmp(a.memptr(), b.memptr(), a.n_elem * sizeof(double)) == 0; +} + +bool same_bits(const point& a, const point& b) { + const double wa[5] = {a.Wm, a.Wm_r, a.Wm_ir, a.Wm_d, a.tnew_dt}; + const double wb[5] = {b.Wm, b.Wm_r, b.Wm_ir, b.Wm_d, b.tnew_dt}; + return same_bits(a.sigma, b.sigma) && same_bits(a.statev, b.statev) && same_bits(a.Lt, b.Lt) + && same_bits(a.L, b.L) && std::memcmp(wa, wb, sizeof(wa)) == 0; +} + +// Strain increment of a load / unload history, scaled per material. +vec increment(int inc, double scale) { + const vec dir = {1.0, -0.35, -0.25, 0.4, -0.2, 0.3}; + return (inc < 8 ? 1.0 : -1.0) * scale * dir; +} + +// Interleave the materials call by call (as the phases of a multiphase material do), each with +// two material points; the second point starts mid-history (start = true on an instance that +// already holds the first point's plastic state). Returns the number of mismatching calls. +int interleaved_mismatches(const std::vector& mats, const std::vector& scales) { + std::vector cached, fresh; + for (const auto& m : mats) { + for (int k = 0; k < 2; ++k) { + cached.emplace_back(m.nstatev); + fresh.emplace_back(m.nstatev); + } + } + int mismatches = 0; + for (int inc = 0; inc < 12; ++inc) { + for (size_t i = 0; i < mats.size(); ++i) { + for (int k = 0; k < 2; ++k) { + const int first = (k == 0) ? 0 : 5; + if (inc < first) continue; + const size_t ip = 2 * i + k; + const vec DEtot = increment(inc, scales[i]); + call_cached(mats[i], cached[ip], DEtot, 0.1 * inc, inc == first); + call_fresh(mats[i], fresh[ip], DEtot, 0.1 * inc, inc == first); + if (!same_bits(cached[ip], fresh[ip])) ++mismatches; + } + } + } + return mismatches; +} + +} // namespace + +// Three configurations alternating at every call, with damage, viscoelasticity, the cutting-plane +// and closest-point integrators: the cached instances return what fresh ones do, bit for bit. +TEST(ModularUMATCache, AlternatingPropsMatchFreshInstances) { + const std::vector mats = {plastic_damage(300.0), plastic_af(), visco_plastic()}; + const std::vector scales = {1.2e-3, 1.5e-3, 4.0e-3}; + + // the histories are not trivial: plasticity, damage and viscous flow all activate + std::vector pts; + for (size_t i = 0; i < mats.size(); ++i) { + pts.emplace_back(mats[i].nstatev); + for (int inc = 0; inc < 8; ++inc) { + call_fresh(mats[i], pts[i], increment(inc, scales[i]), 0.1 * inc, inc == 0); + } + } + EXPECT_GT(pts[0].statev(1), 0.0) << "plasticity (damage material) never activated"; + EXPECT_GT(pts[0].statev(8), 0.0) << "damage never activated"; + EXPECT_GT(pts[1].statev(1), 0.0) << "plasticity (AF material) never activated"; + EXPECT_GT(pts[2].statev(8), 0.0) << "plasticity (viscoplastic material) never activated"; + + EXPECT_EQ(interleaved_mismatches(mats, scales), 0); +} + +// More configurations than cache entries: every call evicts the least recently used one. +TEST(ModularUMATCache, MoreConfigurationsThanEntries) { + std::vector mats; + std::vector scales; + for (int i = 0; i < 11; ++i) { + mats.push_back(plastic_damage(250.0 + 15.0 * i)); + scales.push_back(1.2e-3); + } + EXPECT_EQ(interleaved_mismatches(mats, scales), 0); +} + +// The instances are per thread: concurrent interleaved histories stay exact. +TEST(ModularUMATCache, ConcurrentThreads) { + const std::vector mats = {plastic_damage(300.0), plastic_af(), visco_plastic()}; + const std::vector scales = {1.2e-3, 1.5e-3, 4.0e-3}; + std::atomic mismatches{0}; + std::vector threads; + for (int t = 0; t < 4; ++t) { + threads.emplace_back([&]() { mismatches += interleaved_mismatches(mats, scales); }); + } + for (auto& th : threads) th.join(); + EXPECT_EQ(mismatches.load(), 0); +} + +// A call that throws must not leave its instance in the cache: here initialize() fails half-way +// (variables registered, statev too short to unpack: a configuration error, std::logic_error), and +// reusing that instance would register the variables a second time. +TEST(ModularUMATCache, FailedCallIsNotReused) { + const material m = plastic_damage(333.0); // props used by no other test: a new entry + point bad(1); // nstatev says 10, statev holds 1 value + EXPECT_THROW(call_cached(m, bad, increment(0, 1.2e-3), 0.0, true), std::logic_error); + + point cached(m.nstatev), fresh(m.nstatev); + for (int inc = 0; inc < 10; ++inc) { + ASSERT_NO_THROW(call_cached(m, cached, increment(inc, 1.2e-3), 0.1 * inc, inc == 0)); + call_fresh(m, fresh, increment(inc, 1.2e-3), 0.1 * inc, inc == 0); + EXPECT_TRUE(same_bits(cached, fresh)) << "increment " << inc; + } + + // the same on an instance already in use: the failing call drops it, the next one rebuilds it + EXPECT_THROW(call_cached(m, bad, increment(3, 1.2e-3), 0.3, false), std::logic_error); + for (int inc = 10; inc < 12; ++inc) { + ASSERT_NO_THROW(call_cached(m, cached, increment(inc, 1.2e-3), 0.1 * inc, false)); + call_fresh(m, fresh, increment(inc, 1.2e-3), 0.1 * inc, false); + EXPECT_TRUE(same_bits(cached, fresh)) << "increment " << inc; + } +} diff --git a/test/Libraries/Umat/Treference_umats.cpp b/test/Libraries/Umat/Treference_umats.cpp index 49e7c28d0..4ce71317b 100644 --- a/test/Libraries/Umat/Treference_umats.cpp +++ b/test/Libraries/Umat/Treference_umats.cpp @@ -103,7 +103,8 @@ void expect_equiv(const UmatFn& reference, const std::string& name, double amp = 0.02, int n_quarter = 25) { const vec ref = drive(reference, name, props, nstatev, amp, n_quarter, /*pre-2.0 continuum literal*/ 0); - const vec mod = drive(umat_legacy_modular, name, props, nstatev, amp, + // the adapter's trailing ncp option keeps its default through the forwarding lambda + const vec mod = drive([](auto&&... a) { umat_legacy_modular(a...); }, name, props, nstatev, amp, n_quarter, tangent_continuum); const double peak = std::max(std::abs(ref.max()), std::abs(ref.min())); ASSERT_GT(peak, 0.); diff --git a/test/Libraries/Umat/Ttangent_EPICP.cpp b/test/Libraries/Umat/Ttangent_EPICP.cpp index 8d989b6f1..695858a6e 100644 --- a/test/Libraries/Umat/Ttangent_EPICP.cpp +++ b/test/Libraries/Umat/Ttangent_EPICP.cpp @@ -158,3 +158,36 @@ TEST(Ttangent_EPICP, convergence_rate_continuum_vs_algorithmic) << "algorithmic iters " << r_algo.size() << " not fewer than continuum " << r_cont.size(); } + +// ncp = 1 (min-NCP local step) gives the converged state of the default Fischer-Burmeister step +// to the local tolerance. The closed-form radial-return tangent of tangent_mode 2 is the exact +// operator of the closest-point update (tangent_mode 3, generic assembly) at that state. +TEST(Ttangent_EPICP, ncp_and_closed_form_tangent_match) +{ + const vec props = {70000., 0.3, 1.e-5, 200., 1000., 0.3}; + arma_rng::set_seed(7); + for (int t = 0; t < 20; t++) { + const vec dir = randn(6); + const vec DEtot = 6.e-3*dir/norm(dir); + mat Lt_ref; + vec sigma_ref, statev_ref; + for (int variant = 0; variant < 3; variant++) { + const int ncp = (variant == 1) ? 1 : 0; + const int mode = (variant == 2) ? tangent_closest_point : tangent_algorithmic; + vec Etot = zeros(6), statev = zeros(8), sigma = zeros(6); + mat Lt = zeros(6, 6), L = zeros(6, 6); + double Wm = 0., Wm_r = 0., Wm_ir = 0., Wm_d = 0., tnew_dt = 1.; + umat_plasticity_iso("EPICP", Etot, DEtot, sigma, Lt, L, eye(3, 3), props.n_elem, props, 8, statev, + 293.15, 0., 0., 1., Wm, Wm_r, Wm_ir, Wm_d, 3, 3, true, tnew_dt, mode, ncp); + if (variant == 0) { + Lt_ref = Lt; + sigma_ref = sigma; + statev_ref = statev; + continue; + } + EXPECT_LT(norm(sigma - sigma_ref, "inf"), 1.e-8*norm(sigma_ref, "inf")); + EXPECT_LT(fabs(statev(1) - statev_ref(1)), 1.e-8*fabs(statev_ref(1))); + EXPECT_LT(abs(Lt - Lt_ref).max(), 1.e-6*abs(Lt_ref).max()); + } + } +} diff --git a/test/Libraries/Umat/Ttangent_EPJCK.cpp b/test/Libraries/Umat/Ttangent_EPJCK.cpp index 4e39210da..886f61075 100644 --- a/test/Libraries/Umat/Ttangent_EPJCK.cpp +++ b/test/Libraries/Umat/Ttangent_EPJCK.cpp @@ -77,7 +77,14 @@ Out run_epjck(const vec &DEtot, double DTime, int tangent_mode, const vec &props Out run_epicp(const vec &DEtot, int tangent_mode) { // EPICP props = [E, nu, alpha, sigmaY, k, m] : the rate-independent, isothermal JC law const vec props = {JC_PROPS(0), JC_PROPS(1), JC_PROPS(2), JC_PROPS(3), JC_PROPS(4), JC_PROPS(5)}; - return run_mech(&umat_plasticity_iso, "EPICP", props, 8, DEtot, 1., tangent_mode); + // EPICP takes the ncp option past the shared kernel signature: wrapped at its default + const umat_M_fn epicp = [](const string &n, const vec &E, const vec &DE, vec &s, mat &Lt, mat &L, const mat &DR, + const int &np, const vec &p, const int &ns, vec &sv, const double &T, const double &DT, + const double &t, const double &dt, double &Wm, double &Wm_r, double &Wm_ir, double &Wm_d, + const int &ndi, const int &nshr, const bool &start, double &tnew_dt, const int &tm) { + umat_plasticity_iso(n, E, DE, s, Lt, L, DR, np, p, ns, sv, T, DT, t, dt, Wm, Wm_r, Wm_ir, Wm_d, ndi, nshr, start, tnew_dt, tm); + }; + return run_mech(epicp, "EPICP", props, 8, DEtot, 1., tangent_mode); } // Stress-target outer Newton using the kernel's Lt as the Jacobian, at fixed DTime; returns diff --git a/test/Libraries/Umat/Ttutorial_umat.cpp b/test/Libraries/Umat/Ttutorial_umat.cpp index 84e507842..fb1cdf6db 100644 --- a/test/Libraries/Umat/Ttutorial_umat.cpp +++ b/test/Libraries/Umat/Ttutorial_umat.cpp @@ -96,7 +96,8 @@ TEST(Ttutorial_umat, matches_EPICP_reference) { const auto path = cyclic_path(0.02, 25); const Out tut = run(umat_tutorial_J2, PROPS_TUTORIAL, 8, path, tangent_continuum); - const Out ref = run(umat_plasticity_iso, PROPS_EPICP, 8, path, + // EPICP's trailing ncp option keeps its default through the forwarding lambda + const Out ref = run([](auto&&... a) { umat_plasticity_iso(a...); }, PROPS_EPICP, 8, path, tangent_continuum); const double peak = std::max(std::abs(ref.sigma.max()), std::abs(ref.sigma.min())); diff --git a/test/Libraries/Umat/Tumat_conventions.cpp b/test/Libraries/Umat/Tumat_conventions.cpp index 94e5bf53c..5a421df08 100644 --- a/test/Libraries/Umat/Tumat_conventions.cpp +++ b/test/Libraries/Umat/Tumat_conventions.cpp @@ -68,22 +68,17 @@ TEST(UmatConventions, EveryNativeKernelDeclaresOnlyItsStressMeasure) { EXPECT_EQ(output_convention_of("HYPOO").stress, StressMeasure::kirchhoff); // Kirchhoff rate } -// Every NATIVE kernel is Kirchhoff, HYPOO included (it integrates the Kirchhoff rate). The only -// Cauchy declarations left are the plugin adapters, whose convention simcoon does not own, and -// they are declared EXPLICITLY rather than inherited by omission. -TEST(UmatConventions, OnlyForeignConventionsAreCauchyAndTheyAreDeclared) { +// Every kernel of the finite dispatch is Kirchhoff-native, HYPOO included (it integrates the +// Kirchhoff rate). The external plugins (UMEXT, UMABA, UMANS), whose convention simcoon does not +// own, serve the small-strain route only and are not in the finite dispatch. +TEST(UmatConventions, EveryFiniteKernelIsKirchhoffNative) { for (const auto &entry : finite_umat_names()) { - const umat_convention conv = output_convention_of(entry.first); - if (conv.stress == StressMeasure::cauchy) { - const bool foreign = (entry.first == "UMEXT" // external dylib plugin - || entry.first == "UMABA"); // Abaqus wrapper - EXPECT_TRUE(foreign) - << "'" << entry.first << "' declares Cauchy, but every NATIVE kernel should be " - << "Kirchhoff-native. If this is deliberate, add it to the foreign list here " - << "with the reason; if not, the kernel needs converting."; - } + EXPECT_EQ(output_convention_of(entry.first).stress, StressMeasure::kirchhoff) + << "'" << entry.first << "' does not declare a Kirchhoff stress: the kernel needs converting"; + } + for (const char *plugin : {"UMEXT", "UMABA", "UMANS"}) { + EXPECT_EQ(finite_umat_names().count(plugin), 0u) << plugin << " must stay off the finite route"; } - EXPECT_EQ(output_convention_of("UMEXT").stress, StressMeasure::cauchy); } // stress_output_is_kirchhoff is consumed by the python wrapper for EVERY name it serves, @@ -93,7 +88,7 @@ TEST(UmatConventions, TheKirchhoffPredicateIsTotal) { EXPECT_TRUE(stress_output_is_kirchhoff("MODUL")); EXPECT_TRUE(stress_output_is_kirchhoff("HOLZA")); EXPECT_TRUE(stress_output_is_kirchhoff("HYPOO")); - EXPECT_FALSE(stress_output_is_kirchhoff("UMEXT")); // plugin: the host's Cauchy convention + EXPECT_FALSE(stress_output_is_kirchhoff("UMEXT")); // plugin, small-strain only: the host's Cauchy convention EXPECT_FALSE(stress_output_is_kirchhoff("SMADI")); // small-strain only: no finite convention EXPECT_FALSE(stress_output_is_kirchhoff("NOPE1")); // unknown: leave the stress alone } diff --git a/testBin/Umats/UMABA/external/UMAT_ABAQUS_ELASTIC.f b/testBin/Umats/UMABA/external/UMAT_ABAQUS_ELASTIC.f deleted file mode 100755 index c8877ba24..000000000 --- a/testBin/Umats/UMABA/external/UMAT_ABAQUS_ELASTIC.f +++ /dev/null @@ -1,308 +0,0 @@ - SUBROUTINE UMAT(STRESS,STATEV,DDSDDE,SSE,SPD,SCD, - 1 RPL,DDSDDT,DRPLDE,DRPLDT, - 2 STRAN,DSTRAN,TIME,DTIME,TEMP,DTEMP,PREDEF,DPRED,CMNAME, - 3 NDI,NSHR,NTENS,NSTATV,PROPS,NPROPS,COORDS,DROT,PNEWDT, - 4 CELENT,DFGRD0,DFGRD1,NOEL,NPT,LAYER,KSPT,KSTEP,KINC) -! -! INCLUDE 'ABA_PARAM.INC' -! WARNING - the aba_param.inc file declares - Implicit real*8(a-h,o-z) - parameter (nprecd=2) -! This means that, by default, any variables with -! first letter between a-h or o-z are double precision. -! The rest are integers. -! Note that this also means that if you type a variable -! name incorrectly, the compiler won't catch your typo. -! - CHARACTER*80 CMNAME - DIMENSION STRESS(NTENS),STATEV(NSTATV), - 1 DDSDDE(NTENS,NTENS),DDSDDT(NTENS),DRPLDE(NTENS), - 2 STRAN(NTENS),DSTRAN(NTENS),TIME(2),PREDEF(1),DPRED(1), - 3 PROPS(NPROPS),COORDS(3),DROT(3,3),DFGRD0(3,3),DFGRD1(3,3) - - -! -! DDSDDE(NTENS,NTENS) -! Jacobian matrix of the constitutive model. -! DDSDDE(I,J) defines the change in the Ith stress component -! at the end of the time increment caused by an infinitesimal -! perturbation of the Jth component of the strain increment array. -! Unless you invoke the unsymmetric equation solution capability -! for the user-defined material, ABAQUS/Standard will use only -! the symmetric part of DDSDDE. The symmetric part of the matrix -! is calculated by taking one half the sum of the matrix and its transpose. - - -! STRESS(NTENS) -! This array is passed in as the stress tensor at the beginning -! of the increment and must be updated in this routine to be the -! stress tensor at the end of the increment. If you specified -! initial stresses (“Initial conditions,” Section 19.2.1), this -! array will contain the initial stresses at the start of the -! analysis. The size of this array depends on the value of NTENS -! as defined below. In finite-strain problems the stress tensor -! has already been rotated to account for rigid body motion in -! the increment before UMAT is called, so that only the corotational -! part of the stress integration should be done in UMAT. The -! measure of stress used is “true” (Cauchy) stress. -! -! STATEV(NSTATV) -! An array containing the solution-dependent state variables. -! These are passed in as the values at the beginning of the -! increment unless they are updated in user subroutines USDFLD -! (“USDFLD,” Section 25.2.39) or UEXPAN (“UEXPAN,” Section 25.2.20), -! in which case the updated values are passed in. In all cases -! STATEV must be returned as the values at the end of the increment. -! The size of the array is defined as described in -! “Allocating space” in “User subroutines: overview,” Section 25.1.1. -! -! In finite-strain problems any vector-valued or tensor-valued -! state variables must be rotated to account for rigid body -! motion of the material, in addition to any update in the -! values associated with constitutive behavior. The rotation -! increment matrix, DROT, is provided for this purpose. -! -! SSE, SPD, SCD -! Specific elastic strain energy, plastic dissipation, and -! “creep” dissipation, respectively. These are passed in as -! the values at the start of the increment and should be -! updated to the corresponding specific energy values at -! the end of the increment. They have no effect on the solution, -! except that they are used for energy output. -! -! Only in a fully coupled thermal-stress analysis -! RPL -! Volumetric heat generation per unit time at the end of the increment -! caused by mechanical working of the material. -! -! DDSDDT(NTENS) -! Variation of the stress increments with respect to the temperature. -! -! DRPLDE(NTENS) -! Variation of RPL with respect to the strain increments. -! -! DRPLDT -! Variation of RPL with respect to the temperature. -! -! Variables that can be updated -! -! PNEWDT -! Ratio of suggested new time increment to the time increment being -! used (DTIME, see discussion later in this section). This variable -! allows you to provide input to the automatic time incrementation -! algorithms in ABAQUS/Standard (if automatic time incrementation is chosen). -! For a quasi-static procedure the automatic time stepping that ABAQUS/Standard -! uses, which is based on techniques for integrating standard creep laws -! (see “Quasi-static analysis,” Section 6.2.5), cannot be controlled from within -! the UMAT subroutine. -! PNEWDT is set to a large value before each call to UMAT. -! If PNEWDT is redefined to be less than 1.0, ABAQUS/Standard must abandon the -! time increment and attempt it again with a smaller time increment. The -! suggested new time increment provided to the automatic time integration -! algorithms is PNEWDT × DTIME, where the PNEWDT used is the minimum value -! for all calls to user subroutines that allow redefinition of PNEWDT for this -! iteration. -! If PNEWDT is given a value that is greater than 1.0 for all calls to user -! subroutines for this iteration and the increment converges in this iteration, -! ABAQUS/Standard may increase the time increment. The suggested new time increment -! provided to the automatic time integration algorithms is PNEWDT × DTIME, where -! the PNEWDT used is the minimum value for all calls to user subroutines for -! this iteration. -! If automatic time incrementation is not selected in the analysis procedure, -! values of PNEWDT that are greater than 1.0 will be ignored and values of -! PNEWDT that are less than 1.0 will cause the job to terminate. -! -! Variables passed in for information -! -! STRAN(NTENS) -! An array containing the total strains at the beginning of the increment. -! If thermal expansion is included in the same material definition, the -! strains passed into UMAT are the mechanical strains only (that is, the -! thermal strains computed based upon the thermal expansion coefficient have -! been subtracted from the total strains). These strains are available for output -! as the “elastic” strains. -! -! In finite-strain problems the strain components have been rotated to account for -! rigid body motion in the increment before UMAT is called and are approximations -! to logarithmic strain. - -! DSTRAN(NTENS) -! Array of strain increments. If thermal expansion is included in the same -! material definition, these are the mechanical strain increments (the total -! strain increments minus the thermal strain increments). -! -! TIME(1) -! Value of step time at the beginning of the current increment. -! -! TIME(2) -! Value of total time at the beginning of the current increment. -! -! DTIME -! Time increment. -! -! TEMP -! Temperature at the start of the increment. -! -! DTEMP -! Increment of temperature. -! -! PREDEF -! Array of interpolated values of predefined field variables at this point -! at the start of the increment, based on the values read in at the nodes. -! -! DPRED -! Array of increments of predefined field variables. -! -! CMNAME -! User-defined material name, left justified. Some internal material models are given names starting with the “ABQ_” character string. To avoid conflict, you should not use “ABQ_” as the leading string for CMNAME. -! -! NDI -! Number of direct stress components at this point. -! -! NSHR -! Number of engineering shear stress components at this point. -! -! NTENS -! Size of the stress or strain component array (NDI + NSHR). -! -! NSTATV -! Number of solution-dependent state variables that are associated with -! this material type (defined as described in “Allocating space” in “User -! subroutines: overview,” Section 25.1.1). -! -! PROPS(NPROPS) -! User-specified array of material constants associated with this user material. -! -! NPROPS -! User-defined number of material constants associated with this user material. -! -! COORDS -! An array containing the coordinates of this point. These are the current -! coordinates if geometric nonlinearity is accounted for during the step -! (see “Procedures: overview,” Section 6.1.1); otherwise, the array contains -! the original coordinates of the point. -! -! DROT(3,3) -! Rotation increment matrix. This matrix represents the increment of rigid -! body rotation of the basis system in which the components of stress -! (STRESS) and strain (STRAN) are stored. It is provided so that vector- or -! tensor-valued state variables can be rotated appropriately in this subroutine: -! stress and strain components are already rotated by this amount before UMAT -! is called. This matrix is passed in as a unit matrix for small-displacement -! analysis and for large-displacement analysis if the basis system for the -! material point rotates with the material (as in a shell element or when a -! local orientation is used). -! -! CELENT -! Characteristic element length, which is a typical length of a line across -! an element for a first-order element; it is half of the same typical length -! for a second-order element. For beams and trusses it is a characteristic length -! along the element axis. For membranes and shells it is a characteristic length -! in the reference surface. For axisymmetric elements it is a characteristic length -! in the plane only. For cohesive elements it is equal to the constitutive -! thickness. -! -! DFGRD0(3,3) -! Array containing the deformation gradient at the beginning of the increment. -! See the discussion regarding the availability of the deformation gradient for -! various element types. -! -! DFGRD1(3,3) -! Array containing the deformation gradient at the end of the increment. -! The components of this array are set to zero if nonlinear geometric effects -! are not included in the step definition associated with this increment. See -! the discussion regarding the availability of the deformation gradient for -! various element types. -! -! NOEL -! Element number. -! -! NPT -! Integration point number. -! -! LAYER -! Layer number (for composite shells and layered solids). -! -! KSPT -! Section point number within the current layer. -! -! KSTEP -! Step number. -! -! KINC -! Increment number. - -! user coding to define DDSDDE, STRESS, STATEV, SSE, SPD, SCD -! and, if necessary, RPL, DDSDDT, DRPLDE, DRPLDT, PNEWDT -! -! Local variables - - double precision E,xnu - integer i,j - - ddsdde = 0.d0 - - E = props(1) - xnu = props(2) - -! for debugging, you can use -! write(6,*) ' Hello ' -! Output is then written to the .dat file - - If (ndi==3 .and. nshr==1) then ! Plane strain or axisymmetry - ddsdde(1,1) = 1.d0-xnu - ddsdde(1,2) = xnu - ddsdde(1,3) = xnu - ddsdde(2,1) = xnu - ddsdde(2,2) = 1.d0-xnu - ddsdde(2,3) = xnu - ddsdde(3,1) = xnu - ddsdde(3,2) = xnu - ddsdde(3,3) = 1.d0-xnu - ddsdde(4,4) = 0.5d0*(1.d0-2.d0*xnu) - ddsdde = ddsdde*E/( (1.d0+xnu)*(1.d0-2.d0*xnu) ) - else if (ndi==2 .and. nshr==1) then ! Plane stress - ddsdde(1,1) = 1.d0 - ddsdde(1,2) = xnu - ddsdde(2,1) = xnu - ddsdde(2,2) = 1.d0 - ddsdde(3,3) = 0.5d0*(1.d0-xnu) - ddsdde = ddsdde*E/( (1.d0+xnu*xnu) ) - else ! 3D - ddsdde(1,1) = 1.d0-xnu - ddsdde(1,2) = xnu - ddsdde(1,3) = xnu - ddsdde(2,1) = xnu - ddsdde(2,2) = 1.d0-xnu - ddsdde(2,3) = xnu - ddsdde(3,1) = xnu - ddsdde(3,2) = xnu - ddsdde(3,3) = 1.d0-xnu - ddsdde(4,4) = 0.5d0*(1.d0-2.d0*xnu) - ddsdde(5,5) = ddsdde(4,4) - ddsdde(6,6) = ddsdde(4,4) - ddsdde = ddsdde*E/( (1.d0+xnu)*(1.d0-2.d0*xnu) ) - endif - - do i = 1,ntens - statev(1) = statev(1) + 0.5*stress(i)*dstran(i) !Wm - statev(2) = statev(2) + 0.5*stress(i)*dstran(i) !Wm_r - end do -! -! NOTE: ABAQUS uses engineering shear strains, -! i.e. stran(ndi+1) = 2*e_12, etc... - do i = 1,ntens - do j = 1,ntens - stress(i) = stress(i) + ddsdde(i,j)*dstran(j) - end do - end do - - do i = 1,ntens - statev(1) = statev(1) + 0.5*stress(i)*dstran(i) !Wm - statev(2) = statev(2) + 0.5*stress(i)*dstran(i) !Wm_r - end do - statev(3) = 0.d0 !Wm_ir - statev(4) = 0.d0 !Wm_d - - RETURN - END \ No newline at end of file diff --git a/testBin/Umats/UMABA/external/libumat_plugin_aba.dylib b/testBin/Umats/UMABA/external/libumat_plugin_aba.dylib index 25fc90f71..82237ea9d 100755 Binary files a/testBin/Umats/UMABA/external/libumat_plugin_aba.dylib and b/testBin/Umats/UMABA/external/libumat_plugin_aba.dylib differ diff --git a/testBin/Umats/UMABA/external/umat_plugin_aba.cpp b/testBin/Umats/UMABA/external/umat_plugin_aba.cpp deleted file mode 100755 index 44244fca5..000000000 --- a/testBin/Umats/UMABA/external/umat_plugin_aba.cpp +++ /dev/null @@ -1,111 +0,0 @@ -/* This file is part of simcoon. - - simcoon is free software: you can redistribute it and/or modify - it under the terms of the GNU General Public License as published by - the Free Software Foundation, either version 3 of the License, or - (at your option) any later version. - - simcoon is distributed in the hope that it will be useful, - but WITHOUT ANY WARRANTY; without even the implied warranty of - MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - GNU General Public License for more details. - - You should have received a copy of the GNU General Public License - along with simcoon. If not, see . - - */ - -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include - -using namespace std; -using namespace arma; -using namespace simcoon; - -// LIB_EXPORT comes from umat_plugin_api.hpp -//declaration of the extern function to use -extern "C"{ - void umat_(double *stress, double *statev, double *ddsdde, double &sse, double &spd, double &scd, double &rpl, double *ddsddt, double *drplde, double &drpldt, const double *stran, const double *dstran, const double *time, const double &dtime, const double &temperature, const double &Dtemperature, const double &predef, const double &dpred, char *cmname, const int &ndi, const int &nshr, const int &ntens, const int &nstatev, const double *props, const int &nprops, const double &coords, const double *drot, double &pnewdt, const double &celent, const double *dfgrd0, const double *dfgrd1, const int &noel, const int &npt, const double &layer, const int &kspt, const int &kstep, const int &kinc); - } - -class LIB_EXPORT umat_plugin_aba : public umat_plugin_aba_api { -public: - - std::string name() const { - return "umaba"; - } - - void umat_abaqus(simcoon::phase_characteristics &rve, const arma::mat &DR, const double &Time, const double &DTime, const int &ndi, const int &nshr, bool &start, const int &solver_type, double &tnew_dt) { - - ///@brief Macroscopic state variables and control increments - double stress[6]; - double stran[6]; - double dstran[6]; - double temperature; - double Dtemperature; - - ///@brief Umat variable list unused here - double sse = 0.; - double spd = 0.; - double scd = 0.; - double rpl = 0.; - double drpldt = 0.; - double predef = 0.; - double dpred = 0.; - int layer = 0; - int kspt = 0; - double celent = 0.; - double dfgrd0[9]; - double dfgrd1[3]; - double drplde[6]; - double coords = 0; - double drot[9]; - - ///@brief Usefull UMAT variables - int ntens = 6; - int noel = 1; - int npt = 1; - int kstep = 0; - int kinc = 0; - double ddsdde[36]; - double ddsddt[6]; - char cmname[6]; - strcpy(cmname, rve.sptr_matprops->umat_name.c_str()); - int nprops = rve.sptr_matprops->nprops; - double props[nprops]; - int nstatev = rve.sptr_sv_global->nstatev; - double statev[nstatev+4]; //+4 for a mechanical response to store Wm components - double pnewdt = tnew_dt; - double time[2]; - double dtime; - - auto rve_sv_M = std::dynamic_pointer_cast(rve.sptr_sv_local); - - smart2abaqus_M_full(stress, ddsdde, stran, dstran, time, dtime, temperature, Dtemperature, nprops, props, nstatev, statev, ndi, nshr, drot, rve_sv_M->sigma, rve_sv_M->Lt, rve_sv_M->Etot, rve_sv_M->DEtot, rve_sv_M->T, rve_sv_M->DT, Time, DTime, rve.sptr_matprops->props, rve_sv_M->Wm, rve_sv_M->statev, DR, start); -// smart2abaqus_M(stress, ddsdde, statev, ndi, nshr, rve_sv_M->sigma, rve_sv_M->statev, rve_sv_M->Wm, rve_sv_M->Lt); - - umat_(stress, statev, ddsdde, sse, spd, scd, rpl, ddsddt, drplde, drpldt, stran, dstran, time, dtime, temperature, Dtemperature, predef, dpred, cmname, ndi, nshr, ntens, nstatev, props, nprops, coords, drot, pnewdt, celent, dfgrd0, dfgrd1, noel, npt, layer, kspt, kstep, kinc); - - abaqus2smart_M_light(stress, ddsdde, nstatev, statev, ndi, nshr, rve_sv_M->sigma, rve_sv_M->Lt, rve_sv_M->Wm, rve_sv_M->statev); - } - - ~umat_plugin_aba() {} -}; - -// Export create/destroy functions using dylib macros -extern "C" LIB_EXPORT umat_plugin_aba_api* create_api() { - return new umat_plugin_aba(); -} - -extern "C" LIB_EXPORT void destroy_api(umat_plugin_aba_api* p) { - delete p; -} \ No newline at end of file diff --git a/testBin/Umats/UMEXT/external/libumat_plugin_ext.dylib b/testBin/Umats/UMEXT/external/libumat_plugin_ext.dylib index 48c2d824c..26bd386ea 100755 Binary files a/testBin/Umats/UMEXT/external/libumat_plugin_ext.dylib and b/testBin/Umats/UMEXT/external/libumat_plugin_ext.dylib differ diff --git a/test_extern/Umats/UMABA/TUMABA.cpp b/test_extern/Umats/UMABA/TUMABA.cpp index d5008f125..73a35c657 100755 --- a/test_extern/Umats/UMABA/TUMABA.cpp +++ b/test_extern/Umats/UMABA/TUMABA.cpp @@ -16,7 +16,7 @@ */ ///@file TUMABA.cpp -///@brief The UMABA plugin driven by the solver: the historical reference case, its +///@brief The Abaqus (UMABA) and Ansys (UMANS) plugins driven by the solver on the historical reference case, its ///loading programme built in code (nothing is read from a file since the 2.0 JSON-only ///migration; the plugin itself is what the test exercises). ///@version 1.0 @@ -38,7 +38,9 @@ using namespace std; using namespace arma; using namespace simcoon; -TEST(TUMABA, TUMABA_solver) +class TUMABA : public ::testing::TestWithParam {}; + +TEST_P(TUMABA, solver) { //The material and the programme of the reference case: an elastic plugin (E = 70000 MPa, //nu = 0.3), loaded in uniaxial tension to 2 % strain and unloaded, 100 increments each @@ -79,7 +81,7 @@ TEST(TUMABA, TUMABA_solver) solver_memory_sink sink; sink.record_tangent = false; - const int status = solver_run(blocks, T_init, so, "UMABA", props, 1, 0., 0., 0., 0, 2, ctrl, sink); + const int status = solver_run(blocks, T_init, so, GetParam(), props, 1, 0., 0., 0., 0, 2, ctrl, sink); ASSERT_EQ(status, 0); //The committed reference: strain in columns 8:14, stress in 14:20 @@ -93,3 +95,6 @@ TEST(TUMABA, TUMABA_solver) } } } + +// the same elastic law (E = 70000 MPa, nu = 0.3) through the two FE-compatible plugin interfaces +INSTANTIATE_TEST_SUITE_P(Plugins, TUMABA, ::testing::Values("UMABA", "UMANS")); diff --git a/test_extern/Umats/UMANS/TUMANS.cpp b/test_extern/Umats/UMANS/TUMANS.cpp new file mode 100644 index 000000000..c9a5d8055 --- /dev/null +++ b/test_extern/Umats/UMANS/TUMANS.cpp @@ -0,0 +1,91 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file TUMANS.cpp +///@brief The UMANS plugin's data layout against a USERMAT that exposes what it receives +/// (usermat_layout.cpp): component order, column-major tangent and rotation, the law's +/// statev without header, energies, condensed (plane stress) components +///@version 1.0 + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace arma; +using namespace simcoon; + +namespace { + +struct ans_plugin { + dylib::library lib{"external/umat_plugin_ans", dylib::decorations::os_default()}; + std::unique_ptr api{ + lib.get_function("create_api")(), + lib.get_function("destroy_api")}; +}; + +// one call of the plugin on a fresh point with nstatev law variables; returns the local state +std::shared_ptr call(ans_plugin &p, const mat &DR, const int &ndi, const int &nshr) { + const int nstatev = 10; + phase_characteristics rve; + rve.construct(0, 1); + rve.sptr_matprops->update(0, "UMANS", 1, 0., 0., 0., 3, vec{70000., 0.3, 0.}); + rve.sptr_sv_global->update(zeros(6), zeros(6), zeros(6), zeros(6), zeros(6), zeros(6), zeros(6), zeros(6), + zeros(6), zeros(6), eye(3, 3), eye(3, 3), eye(3, 3), eye(3, 3), eye(3, 3), eye(3, 3), 290., 0., + nstatev, zeros(nstatev), zeros(nstatev)); + rve.global2local(); + auto sv = std::dynamic_pointer_cast(rve.sptr_sv_local); + sv->DEtot = vec{1e-3, 0., 0., 0., 0., 0.}; + bool start = false; + double tnew_dt = 1.; + p.api->umat_ansys(rve, DR, 0., 1., ndi, nshr, start, 0, tnew_dt); + return sv; +} + +} // namespace + +TEST(TUMANS, data_layout_3D) +{ + ans_plugin p; + const mat DR = Rotation::from_euler(0.3, 0.5, 0.7, "zxz").as_matrix(); + auto sv = call(p, DR, 3, 3); + const int K[6] = {0, 1, 2, 3, 5, 4}; // Ansys 11, 22, 33, 12, 23, 13 + for (int i = 0; i < 6; i++) { + EXPECT_EQ(sv->sigma(K[i]), 10.*(i + 1)); + for (int j = 0; j < 6; j++) EXPECT_EQ(sv->Lt(K[i], K[j]), 100.*(i + 1) + (j + 1)) << i << "," << j; + } + for (int k = 0; k < 9; k++) EXPECT_EQ(sv->statev(k), DR(k % 3, k / 3)); // rotateM(r,c) = DR(r,c) + EXPECT_EQ(sv->statev(9), 10.); // the law's own count, no simcoon header + EXPECT_EQ(sv->Wm(1), 1.5); + EXPECT_EQ(sv->Wm(3), 2.5); +} + +TEST(TUMANS, data_layout_plane_stress) +{ + ans_plugin p; + auto sv = call(p, eye(3, 3), 2, 1); + const int K[3] = {0, 1, 3}; // Ansys plane stress 11, 22, 12 + for (int i = 0; i < 3; i++) + for (int j = 0; j < 3; j++) EXPECT_EQ(sv->Lt(K[i], K[j]), 100.*(i + 1) + (j + 1)); + EXPECT_EQ(sv->Lt(2, 2), 0.); // absent components stay zero +} diff --git a/test_extern/Umats/UMANS/usermat_layout.cpp b/test_extern/Umats/UMANS/usermat_layout.cpp new file mode 100644 index 000000000..16cf4d7f1 --- /dev/null +++ b/test_extern/Umats/UMANS/usermat_layout.cpp @@ -0,0 +1,42 @@ +/* This file is part of simcoon. + + simcoon is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + simcoon is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with simcoon. If not, see . + + */ + +///@file usermat_layout.cpp +///@brief A test USERMAT (Ansys interface) that exposes what it receives: a non-symmetric tangent +/// dsdde(i,j) = 100 i + j, rotateM and nStatev copied into ustatev, fixed energies +///@version 1.0 + +extern "C" void usermat_(int *, int *, int *, int *, int *, int *, int *, int *keycut, + int *, int *, int *ncomp, int *nStatev, int *, + double *, double *, double *, double *, + double *stress, double *ustatev, double *dsdde, + double *sedEl, double *sedPl, double *, + double *, double *, double *, + double *, double *, double *rotateM, + double *, double *, double *, double *, + int *, int *, int *, int *, int *) +{ + const int n = *ncomp; + for (int j = 0; j < n; j++) + for (int i = 0; i < n; i++) dsdde[i + n*j] = 100.*(i + 1) + (j + 1); // Fortran dsdde(i,j) + for (int i = 0; i < n; i++) stress[i] = 10.*(i + 1); + for (int k = 0; k < 9 && k < *nStatev; k++) ustatev[k] = rotateM[k]; + if (*nStatev > 9) ustatev[9] = *nStatev; + *sedEl = 1.5; + *sedPl = 2.5; + *keycut = 0; +}