mat-data-handler 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mat_data_handler-0.1.0/.github/CODEOWNERS +5 -0
- mat_data_handler-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +13 -0
- mat_data_handler-0.1.0/.github/workflows/ci.yml +53 -0
- mat_data_handler-0.1.0/.github/workflows/release.yml +24 -0
- mat_data_handler-0.1.0/.gitignore +12 -0
- mat_data_handler-0.1.0/CHANGELOG.md +5 -0
- mat_data_handler-0.1.0/CONTRIBUTING.md +33 -0
- mat_data_handler-0.1.0/LICENSE +21 -0
- mat_data_handler-0.1.0/PKG-INFO +91 -0
- mat_data_handler-0.1.0/README.md +71 -0
- mat_data_handler-0.1.0/conda-recipe/meta.yaml +49 -0
- mat_data_handler-0.1.0/environment.yml +13 -0
- mat_data_handler-0.1.0/pyproject.toml +43 -0
- mat_data_handler-0.1.0/src/mat_data_handler/__init__.py +18 -0
- mat_data_handler-0.1.0/src/mat_data_handler/data_source.py +89 -0
- mat_data_handler-0.1.0/src/mat_data_handler/database.py +211 -0
- mat_data_handler-0.1.0/src/mat_data_handler/extract.py +315 -0
- mat_data_handler-0.1.0/src/mat_data_handler/py.typed +0 -0
- mat_data_handler-0.1.0/tests/conftest.py +22 -0
- mat_data_handler-0.1.0/tests/test_database.py +7 -0
- mat_data_handler-0.1.0/tests/test_extract.py +10 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
# Route data PRs to whoever reviews material parameter values;
|
|
2
|
+
# route code PRs to whoever maintains the validation/export API.
|
|
3
|
+
# Replace placeholders with actual GitHub usernames/teams before use.
|
|
4
|
+
/packages/mat-data-handler-data/ @<materials-reviewer>
|
|
5
|
+
/packages/mat-data-handler/ @<core-maintainer>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
## What does this PR change?
|
|
2
|
+
|
|
3
|
+
- [ ] New/updated material entry (data)
|
|
4
|
+
- [ ] Schema change
|
|
5
|
+
- [ ] Code change (validation, export, CLI)
|
|
6
|
+
- [ ] Docs/CI only
|
|
7
|
+
|
|
8
|
+
## Checklist
|
|
9
|
+
|
|
10
|
+
- [ ] `pytest` passes locally for affected package(s)
|
|
11
|
+
- [ ] For data changes: entry validates against `material.schema.json`
|
|
12
|
+
- [ ] For schema changes: existing entries still validate, or migration notes are included
|
|
13
|
+
- [ ] `CHANGELOG.md` updated for the affected package
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
test:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
strategy:
|
|
12
|
+
matrix:
|
|
13
|
+
python-version: ["3.9", "3.12"]
|
|
14
|
+
steps:
|
|
15
|
+
- name: Check out mat-data-handler
|
|
16
|
+
uses: actions/checkout@v4
|
|
17
|
+
|
|
18
|
+
- name: Check out mat-data (test fixture data)
|
|
19
|
+
uses: actions/checkout@v4
|
|
20
|
+
with:
|
|
21
|
+
repository: ICAMS/mat-data
|
|
22
|
+
# Pin to a released tag for reproducibility once available, e.g. ref: v2026.9.0
|
|
23
|
+
ref: main
|
|
24
|
+
path: mat-data-checkout
|
|
25
|
+
|
|
26
|
+
- uses: actions/setup-python@v5
|
|
27
|
+
with:
|
|
28
|
+
python-version: ${{ matrix.python-version }}
|
|
29
|
+
|
|
30
|
+
- name: Install package
|
|
31
|
+
run: pip install -e ".[test]"
|
|
32
|
+
|
|
33
|
+
- name: Validate mat-data entries
|
|
34
|
+
env:
|
|
35
|
+
MAT_DATA_LOCAL_PATH: ${{ github.workspace }}/mat-data-checkout
|
|
36
|
+
run: mat-build-database --output /tmp/materials.yaml --validate-only
|
|
37
|
+
|
|
38
|
+
- name: Rebuild database and check reproducibility
|
|
39
|
+
env:
|
|
40
|
+
MAT_DATA_LOCAL_PATH: ${{ github.workspace }}/mat-data-checkout
|
|
41
|
+
run: |
|
|
42
|
+
mat-build-database --output /tmp/materials.yaml
|
|
43
|
+
mat-build-database --output /tmp/materials.yaml --check
|
|
44
|
+
|
|
45
|
+
- name: Run tests
|
|
46
|
+
env:
|
|
47
|
+
MAT_DATA_LOCAL_PATH: ${{ github.workspace }}/mat-data-checkout
|
|
48
|
+
run: pytest -v
|
|
49
|
+
|
|
50
|
+
- name: Export a sample material
|
|
51
|
+
env:
|
|
52
|
+
MAT_DATA_LOCAL_PATH: ${{ github.workspace }}/mat-data-checkout
|
|
53
|
+
run: mat-extract-params copper_generic --outdir /tmp/includes
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
name: Release
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
tags:
|
|
6
|
+
- "v*"
|
|
7
|
+
|
|
8
|
+
permissions:
|
|
9
|
+
id-token: write # required for PyPI Trusted Publishing
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build-and-publish:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: actions/setup-python@v5
|
|
17
|
+
with:
|
|
18
|
+
python-version: "3.12"
|
|
19
|
+
- run: pip install build
|
|
20
|
+
- run: python -m build --outdir dist
|
|
21
|
+
- name: Publish to PyPI
|
|
22
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
23
|
+
with:
|
|
24
|
+
packages-dir: dist
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
This repository contains only code: validation, database-building, and
|
|
4
|
+
Abaqus `.inc` export logic for material parameter sets. It has no material
|
|
5
|
+
data of its own — that lives in the separate
|
|
6
|
+
[mat-data](https://github.com/ICAMS/mat-data) repository.
|
|
7
|
+
|
|
8
|
+
## Adding or updating a material (data change)
|
|
9
|
+
|
|
10
|
+
Open a pull request against [mat-data](https://github.com/ICAMS/mat-data),
|
|
11
|
+
not here. See its `CONTRIBUTING.md`.
|
|
12
|
+
|
|
13
|
+
## Changing the API or export logic (code change)
|
|
14
|
+
|
|
15
|
+
1. Make changes under `src/mat_data_handler/`.
|
|
16
|
+
2. Add/update tests under `tests/`.
|
|
17
|
+
3. Follow SemVer: patch for bug fixes, minor for backward-compatible
|
|
18
|
+
additions, major for breaking changes to the public API
|
|
19
|
+
(`build_database`, `export_entry`, CLI flags).
|
|
20
|
+
4. Update `CHANGELOG.md`.
|
|
21
|
+
|
|
22
|
+
## Running tests locally
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
pip install -e ".[test]"
|
|
26
|
+
export MAT_DATA_LOCAL_PATH=../mat-data # point at a sibling mat-data checkout
|
|
27
|
+
pytest
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
## Reporting issues
|
|
31
|
+
|
|
32
|
+
Please include the command run and the full error output. If the issue is
|
|
33
|
+
about specific material values, file it against `mat-data` instead.
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 ICAMS, Ruhr University Bochum
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: mat-data-handler
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Validate, combine, and export crystal-plasticity material parameter sets for ICAMS CP-UMAT.
|
|
5
|
+
Project-URL: Homepage, https://github.com/ICAMS/mat-data-handler
|
|
6
|
+
Project-URL: Repository, https://github.com/ICAMS/mat-data-handler
|
|
7
|
+
Author: ICAMS, Ruhr University Bochum
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
License-File: LICENSE
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Scientific/Engineering
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Requires-Dist: jsonschema>=4.18
|
|
15
|
+
Requires-Dist: pyyaml>=6.0
|
|
16
|
+
Requires-Dist: referencing>=0.30
|
|
17
|
+
Provides-Extra: test
|
|
18
|
+
Requires-Dist: pytest; extra == 'test'
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
|
|
21
|
+
# mat-data-handler – Organizing Material Data for Crystal Plasticity
|
|
22
|
+
|
|
23
|
+
Plain Python package to validate, combine, and export crystal-plasticity material parameter sets. The material data itself (YAML
|
|
24
|
+
entries, JSON schemas, PROPS mapping table) is retrieved from the
|
|
25
|
+
[mat-data](https://github.com/ICAMS/mat-data) repository and fetched over
|
|
26
|
+
the network (and cached locally) at runtime.
|
|
27
|
+
Currently only support for the crystal plasticity model [ICAMS CP-UMAT](https://github.com/ICAMS/Crystal_Plasticity_UMAT.git) is provided. Use the `mat_extract_params` command to extract the require .inc files that contains the values for PROPS[9:] read by the ICAMS CP-UMAT.
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
## Installation
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
pip install mat-data-handler
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
First use of the CLI/API fetches the pinned `mat-data` ref (default: `main`)
|
|
37
|
+
and caches it under `~/.cache/mat-data-handler`. For offline use or local
|
|
38
|
+
development against a sibling checkout, set:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export MAT_DATA_LOCAL_PATH=/path/to/mat-data # skips network access entirely
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Other environment variables: `MAT_DATA_REPO` (default `ICAMS/mat-data`),
|
|
45
|
+
`MAT_DATA_REF` (default `main`; pin to a release tag for reproducibility),
|
|
46
|
+
`MAT_DATA_FETCH_METHOD` (`http` default, stdlib-only tarball download; or
|
|
47
|
+
`git`, which shells out to the `git` binary), `MAT_DATA_CACHE_DIR`.
|
|
48
|
+
|
|
49
|
+
## API
|
|
50
|
+
|
|
51
|
+
- `mat_data_handler.data_source.{entries_dir, schemas_dir, mapping_path}()` —
|
|
52
|
+
resolve (fetching/caching as needed) local paths to the `mat-data` content.
|
|
53
|
+
- `mat_data_handler.database.build_database(entries, schemas, output_format="yaml")`
|
|
54
|
+
— validate all entries and return the combined database as text.
|
|
55
|
+
- `mat_data_handler.database.validate_only(entries, schemas)` — validate only,
|
|
56
|
+
returns the number of valid materials.
|
|
57
|
+
- `mat_data_handler.extract.export_entry(entry, mapping, schemas)` — export
|
|
58
|
+
one loaded material entry to Abaqus `PROPS` records.
|
|
59
|
+
- `mat_data_handler.extract.load_document(path)` / `parse_mapping(path)` —
|
|
60
|
+
helpers to load a YAML/JSON entry or the `mapping.csv` table.
|
|
61
|
+
|
|
62
|
+
## CLI
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
mat-build-database --output build/materials.yaml # generate combined database
|
|
66
|
+
mat-extract-params copper_generic --outdir build/includes
|
|
67
|
+
mat-extract-params all --outdir build/includes
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Both commands default to fetching data from `mat-data`; pass
|
|
71
|
+
`--entries`/`--schemas`/`--mapping-file` to point at a different data source
|
|
72
|
+
(e.g. a local checkout) directly.
|
|
73
|
+
|
|
74
|
+
## Contributing
|
|
75
|
+
|
|
76
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). For adding/updating material data,
|
|
77
|
+
see [mat-data](https://github.com/ICAMS/mat-data) instead — this repo has no
|
|
78
|
+
data to edit.
|
|
79
|
+
|
|
80
|
+
## Development
|
|
81
|
+
|
|
82
|
+
```bash
|
|
83
|
+
pip install -e ".[test]"
|
|
84
|
+
export MAT_DATA_LOCAL_PATH=../mat-data # or wherever your mat-data checkout lives
|
|
85
|
+
pytest
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
## License
|
|
89
|
+
|
|
90
|
+
MIT — see [LICENSE](LICENSE). (Material data in `mat-data` is licensed
|
|
91
|
+
separately, under CC-BY-4.0.)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# mat-data-handler – Organizing Material Data for Crystal Plasticity
|
|
2
|
+
|
|
3
|
+
Plain Python package to validate, combine, and export crystal-plasticity material parameter sets. The material data itself (YAML
|
|
4
|
+
entries, JSON schemas, PROPS mapping table) is retrieved from the
|
|
5
|
+
[mat-data](https://github.com/ICAMS/mat-data) repository and fetched over
|
|
6
|
+
the network (and cached locally) at runtime.
|
|
7
|
+
Currently only support for the crystal plasticity model [ICAMS CP-UMAT](https://github.com/ICAMS/Crystal_Plasticity_UMAT.git) is provided. Use the `mat_extract_params` command to extract the require .inc files that contains the values for PROPS[9:] read by the ICAMS CP-UMAT.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
## Installation
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install mat-data-handler
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
First use of the CLI/API fetches the pinned `mat-data` ref (default: `main`)
|
|
17
|
+
and caches it under `~/.cache/mat-data-handler`. For offline use or local
|
|
18
|
+
development against a sibling checkout, set:
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
export MAT_DATA_LOCAL_PATH=/path/to/mat-data # skips network access entirely
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
Other environment variables: `MAT_DATA_REPO` (default `ICAMS/mat-data`),
|
|
25
|
+
`MAT_DATA_REF` (default `main`; pin to a release tag for reproducibility),
|
|
26
|
+
`MAT_DATA_FETCH_METHOD` (`http` default, stdlib-only tarball download; or
|
|
27
|
+
`git`, which shells out to the `git` binary), `MAT_DATA_CACHE_DIR`.
|
|
28
|
+
|
|
29
|
+
## API
|
|
30
|
+
|
|
31
|
+
- `mat_data_handler.data_source.{entries_dir, schemas_dir, mapping_path}()` —
|
|
32
|
+
resolve (fetching/caching as needed) local paths to the `mat-data` content.
|
|
33
|
+
- `mat_data_handler.database.build_database(entries, schemas, output_format="yaml")`
|
|
34
|
+
— validate all entries and return the combined database as text.
|
|
35
|
+
- `mat_data_handler.database.validate_only(entries, schemas)` — validate only,
|
|
36
|
+
returns the number of valid materials.
|
|
37
|
+
- `mat_data_handler.extract.export_entry(entry, mapping, schemas)` — export
|
|
38
|
+
one loaded material entry to Abaqus `PROPS` records.
|
|
39
|
+
- `mat_data_handler.extract.load_document(path)` / `parse_mapping(path)` —
|
|
40
|
+
helpers to load a YAML/JSON entry or the `mapping.csv` table.
|
|
41
|
+
|
|
42
|
+
## CLI
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
mat-build-database --output build/materials.yaml # generate combined database
|
|
46
|
+
mat-extract-params copper_generic --outdir build/includes
|
|
47
|
+
mat-extract-params all --outdir build/includes
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
Both commands default to fetching data from `mat-data`; pass
|
|
51
|
+
`--entries`/`--schemas`/`--mapping-file` to point at a different data source
|
|
52
|
+
(e.g. a local checkout) directly.
|
|
53
|
+
|
|
54
|
+
## Contributing
|
|
55
|
+
|
|
56
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md). For adding/updating material data,
|
|
57
|
+
see [mat-data](https://github.com/ICAMS/mat-data) instead — this repo has no
|
|
58
|
+
data to edit.
|
|
59
|
+
|
|
60
|
+
## Development
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
pip install -e ".[test]"
|
|
64
|
+
export MAT_DATA_LOCAL_PATH=../mat-data # or wherever your mat-data checkout lives
|
|
65
|
+
pytest
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## License
|
|
69
|
+
|
|
70
|
+
MIT — see [LICENSE](LICENSE). (Material data in `mat-data` is licensed
|
|
71
|
+
separately, under CC-BY-4.0.)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
{% set name = "mat-data-handler" %}
|
|
2
|
+
{% set version = "0.1.0" %}
|
|
3
|
+
|
|
4
|
+
package:
|
|
5
|
+
name: {{ name|lower }}
|
|
6
|
+
version: {{ version }}
|
|
7
|
+
|
|
8
|
+
source:
|
|
9
|
+
url: https://pypi.io/packages/source/{{ name[0] }}/{{ name }}/{{ name }}-{{ version }}.tar.gz
|
|
10
|
+
# sha256: <fill in after the first PyPI release>
|
|
11
|
+
|
|
12
|
+
build:
|
|
13
|
+
number: 0
|
|
14
|
+
noarch: python
|
|
15
|
+
script: {{ PYTHON }} -m pip install . -vv
|
|
16
|
+
entry_points:
|
|
17
|
+
- mat-build-database = mat_data_handler.database:main
|
|
18
|
+
- mat-extract-params = mat_data_handler.extract:main
|
|
19
|
+
|
|
20
|
+
requirements:
|
|
21
|
+
host:
|
|
22
|
+
- python >=3.9
|
|
23
|
+
- pip
|
|
24
|
+
- hatchling
|
|
25
|
+
run:
|
|
26
|
+
- python >=3.9
|
|
27
|
+
- pyyaml >=6.0
|
|
28
|
+
- jsonschema >=4.18
|
|
29
|
+
- referencing >=0.30
|
|
30
|
+
# Note: no data dependency here. mat-data-handler fetches material data from
|
|
31
|
+
# https://github.com/ICAMS/mat-data over the network at runtime (or from
|
|
32
|
+
# MAT_DATA_LOCAL_PATH), rather than depending on a packaged data distribution.
|
|
33
|
+
|
|
34
|
+
test:
|
|
35
|
+
imports:
|
|
36
|
+
- mat_data_handler
|
|
37
|
+
commands:
|
|
38
|
+
- mat-build-database --help
|
|
39
|
+
- mat-extract-params --help
|
|
40
|
+
|
|
41
|
+
about:
|
|
42
|
+
home: https://github.com/ICAMS/mat-data-handler
|
|
43
|
+
license: MIT
|
|
44
|
+
license_file: LICENSE
|
|
45
|
+
summary: Validate, combine, and export crystal-plasticity material parameter sets for Abaqus UMATs.
|
|
46
|
+
|
|
47
|
+
extra:
|
|
48
|
+
recipe-maintainers:
|
|
49
|
+
- <github-username>
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "mat-data-handler"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Validate, combine, and export crystal-plasticity material parameter sets for ICAMS CP-UMAT."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "ICAMS, Ruhr University Bochum" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"PyYAML>=6.0",
|
|
15
|
+
"jsonschema>=4.18",
|
|
16
|
+
"referencing>=0.30",
|
|
17
|
+
]
|
|
18
|
+
# Note: material data is NOT a pip/conda dependency. It is fetched at runtime
|
|
19
|
+
# from the separate https://github.com/ICAMS/mat-data repository and cached
|
|
20
|
+
# locally; see mat_data_handler.data_source. Requires network access on first
|
|
21
|
+
# use (or MAT_DATA_LOCAL_PATH pointing at a local checkout for offline use).
|
|
22
|
+
classifiers = [
|
|
23
|
+
"License :: OSI Approved :: MIT License",
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"Topic :: Scientific/Engineering",
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
[project.optional-dependencies]
|
|
29
|
+
test = ["pytest"]
|
|
30
|
+
|
|
31
|
+
[project.scripts]
|
|
32
|
+
mat-build-database = "mat_data_handler.database:main"
|
|
33
|
+
mat-extract-params = "mat_data_handler.extract:main"
|
|
34
|
+
|
|
35
|
+
[project.urls]
|
|
36
|
+
Homepage = "https://github.com/ICAMS/mat-data-handler"
|
|
37
|
+
Repository = "https://github.com/ICAMS/mat-data-handler"
|
|
38
|
+
|
|
39
|
+
[tool.hatch.build.targets.wheel]
|
|
40
|
+
packages = ["src/mat_data_handler"]
|
|
41
|
+
|
|
42
|
+
[tool.pytest.ini_options]
|
|
43
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""Public API: validate/combine material YAML entries and export Abaqus .inc files.
|
|
2
|
+
|
|
3
|
+
See :mod:`mat_data_handler.database` for building the combined database and
|
|
4
|
+
:mod:`mat_data_handler.extract` for exporting selected materials to Abaqus
|
|
5
|
+
``PROPS`` include files.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib import metadata
|
|
9
|
+
|
|
10
|
+
try:
|
|
11
|
+
__version__ = metadata.version("mat-data-handler")
|
|
12
|
+
except metadata.PackageNotFoundError: # local checkout, not installed
|
|
13
|
+
__version__ = "0.0.0+dev"
|
|
14
|
+
|
|
15
|
+
from .database import build_database, collect, validate_only
|
|
16
|
+
from .extract import export_material
|
|
17
|
+
|
|
18
|
+
__all__ = ["build_database", "collect", "validate_only", "export_material", "__version__"]
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""Resolve the mat-data material dataset over the network (no pip/conda dependency).
|
|
2
|
+
|
|
3
|
+
The material entries, JSON schemas, and PROPS mapping table live in the
|
|
4
|
+
separate `mat-data` repository (https://github.com/ICAMS/mat-data), not as an
|
|
5
|
+
installable package. This module fetches (and caches) that repository's
|
|
6
|
+
content, or uses a local checkout directly during development.
|
|
7
|
+
|
|
8
|
+
Resolution order:
|
|
9
|
+
1. ``MAT_DATA_LOCAL_PATH`` env var, if set: use that directory as-is (no
|
|
10
|
+
network access). Intended for local development against a sibling
|
|
11
|
+
checkout of ``mat-data``, and for CI, which checks out ``mat-data``
|
|
12
|
+
itself and points this at it.
|
|
13
|
+
2. Cached download for the requested ref under ``MAT_DATA_CACHE_DIR``
|
|
14
|
+
(defaults to ``~/.cache/mat-data-handler``).
|
|
15
|
+
3. Fresh download via ``MAT_DATA_FETCH_METHOD`` (``http`` tarball download by
|
|
16
|
+
default, stdlib-only; or ``git clone`` if explicitly requested).
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import os
|
|
22
|
+
import subprocess
|
|
23
|
+
import tarfile
|
|
24
|
+
import urllib.request
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
|
|
27
|
+
DEFAULT_REPO = "ICAMS/mat-data"
|
|
28
|
+
DEFAULT_REF = "main"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def _cache_dir() -> Path:
|
|
32
|
+
return Path(os.environ.get("MAT_DATA_CACHE_DIR", Path.home() / ".cache" / "mat-data-handler"))
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _fetch_http(repo: str, ref: str, dest: Path) -> None:
|
|
36
|
+
"""Download and extract the repo's tarball for ``ref`` via GitHub's codeload API."""
|
|
37
|
+
url = f"https://codeload.github.com/{repo}/tar.gz/{ref}"
|
|
38
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
39
|
+
archive = dest.parent / f"{dest.name}.tar.gz"
|
|
40
|
+
with urllib.request.urlopen(url) as response, open(archive, "wb") as stream:
|
|
41
|
+
stream.write(response.read())
|
|
42
|
+
try:
|
|
43
|
+
with tarfile.open(archive) as tar:
|
|
44
|
+
# GitHub tarballs contain a single top-level "<repo>-<ref>/" directory.
|
|
45
|
+
members = tar.getmembers()
|
|
46
|
+
top = os.path.commonpath([m.name for m in members])
|
|
47
|
+
safe_members = [m for m in members if not (m.name.startswith("/") or ".." in Path(m.name).parts)]
|
|
48
|
+
tar.extractall(dest.parent, members=safe_members) # noqa: S202 -- paths sanitized above
|
|
49
|
+
(dest.parent / top).replace(dest)
|
|
50
|
+
finally:
|
|
51
|
+
archive.unlink(missing_ok=True)
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def _fetch_git(repo: str, ref: str, dest: Path) -> None:
|
|
55
|
+
"""Shallow-clone the repo at ``ref`` (branch or tag) using the system ``git`` binary."""
|
|
56
|
+
dest.parent.mkdir(parents=True, exist_ok=True)
|
|
57
|
+
subprocess.run(
|
|
58
|
+
["git", "clone", "--depth", "1", "--branch", ref, f"https://github.com/{repo}.git", str(dest)],
|
|
59
|
+
check=True,
|
|
60
|
+
)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def data_root(repo: str = None, ref: str = None) -> Path:
|
|
64
|
+
"""Return a local directory containing entries/, schemas/, mapping_icams_cp.csv."""
|
|
65
|
+
local = os.environ.get("MAT_DATA_LOCAL_PATH")
|
|
66
|
+
if local:
|
|
67
|
+
return Path(local)
|
|
68
|
+
|
|
69
|
+
repo = repo or os.environ.get("MAT_DATA_REPO", DEFAULT_REPO)
|
|
70
|
+
ref = ref or os.environ.get("MAT_DATA_REF", DEFAULT_REF)
|
|
71
|
+
method = os.environ.get("MAT_DATA_FETCH_METHOD", "http")
|
|
72
|
+
|
|
73
|
+
dest = _cache_dir() / repo.replace("/", "__") / ref
|
|
74
|
+
if not dest.is_dir():
|
|
75
|
+
fetch = _fetch_git if method == "git" else _fetch_http
|
|
76
|
+
fetch(repo, ref, dest)
|
|
77
|
+
return dest
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def entries_dir(repo: str = None, ref: str = None) -> Path:
|
|
81
|
+
return data_root(repo, ref) / "entries"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def schemas_dir(repo: str = None, ref: str = None) -> Path:
|
|
85
|
+
return data_root(repo, ref) / "schemas"
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def mapping_path(repo: str = None, ref: str = None) -> Path:
|
|
89
|
+
return data_root(repo, ref) / "mapping_icams_cp.csv"
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""Validate individual materials offline and atomically build a collection.
|
|
2
|
+
|
|
3
|
+
Ported from the ``cp-work`` proof of concept (materials/build_database.py),
|
|
4
|
+
adapted to load default data from the installable ``mat-data-handler-data``
|
|
5
|
+
package instead of a fixed repo-relative ``ROOT``.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import json
|
|
12
|
+
import math
|
|
13
|
+
import os
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
import re
|
|
16
|
+
import sys
|
|
17
|
+
import tempfile
|
|
18
|
+
|
|
19
|
+
import yaml
|
|
20
|
+
from jsonschema import Draft201909Validator
|
|
21
|
+
from referencing import Registry, Resource
|
|
22
|
+
|
|
23
|
+
PLASTIC_MODELS = ("ICAMS CP-UMAT", "DAMASK phenopowerlaw")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EntryLoader(yaml.SafeLoader):
|
|
27
|
+
def compose_node(self, parent, index):
|
|
28
|
+
if self.check_event(yaml.AliasEvent):
|
|
29
|
+
raise ValueError("YAML aliases are not allowed")
|
|
30
|
+
return super().compose_node(parent, index)
|
|
31
|
+
|
|
32
|
+
def construct_mapping(self, node, deep=False):
|
|
33
|
+
result = {}
|
|
34
|
+
for key_node, value_node in node.value:
|
|
35
|
+
key = self.construct_object(key_node, deep=deep)
|
|
36
|
+
if not isinstance(key, str):
|
|
37
|
+
raise ValueError(f"line {key_node.start_mark.line + 1}: mapping keys must be strings")
|
|
38
|
+
if key in result:
|
|
39
|
+
raise ValueError(f"line {key_node.start_mark.line + 1}: duplicate key {key!r}")
|
|
40
|
+
result[key] = self.construct_object(value_node, deep=deep)
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def json_errors(value, path="$"):
|
|
45
|
+
"""Reject YAML-only values and non-finite numbers before schema validation."""
|
|
46
|
+
if isinstance(value, dict):
|
|
47
|
+
for key, child in value.items():
|
|
48
|
+
yield from json_errors(child, f"{path}.{key}")
|
|
49
|
+
elif isinstance(value, list):
|
|
50
|
+
for index, child in enumerate(value):
|
|
51
|
+
yield from json_errors(child, f"{path}[{index}]")
|
|
52
|
+
elif isinstance(value, float) and not math.isfinite(value):
|
|
53
|
+
yield f"{path}: number must be finite"
|
|
54
|
+
elif value is not None and type(value) not in (str, int, float, bool):
|
|
55
|
+
yield f"{path}: unsupported YAML value ({type(value).__name__}); quote text identifiers"
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def validator(schema_dir: Path) -> Draft201909Validator:
|
|
59
|
+
resources = []
|
|
60
|
+
for path in sorted(Path(schema_dir).glob("*.json")):
|
|
61
|
+
schema = json.loads(path.read_text(encoding="utf-8"))
|
|
62
|
+
Draft201909Validator.check_schema(schema)
|
|
63
|
+
resources.append((path.resolve().as_uri(), Resource.from_contents(schema)))
|
|
64
|
+
# Registry has no retrieval callback: unresolved references cannot use the network.
|
|
65
|
+
registry = Registry().with_resources(resources)
|
|
66
|
+
uri = (Path(schema_dir) / "material_schema.json").resolve().as_uri()
|
|
67
|
+
return Draft201909Validator({"$ref": uri}, registry=registry)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def schema_messages(error):
|
|
71
|
+
if error.validator == "oneOf" and error.context:
|
|
72
|
+
# Select only diagnostics from the named model's branch. Validation
|
|
73
|
+
# still runs against the complete schema, including shared constraints.
|
|
74
|
+
if isinstance(error.instance, dict):
|
|
75
|
+
model = error.instance.get("plastic_model_name")
|
|
76
|
+
matching = [
|
|
77
|
+
index for index, branch in enumerate(error.validator_value)
|
|
78
|
+
if branch.get("properties", {}).get("plastic_model_name", {}).get("const") == model
|
|
79
|
+
and model in PLASTIC_MODELS
|
|
80
|
+
]
|
|
81
|
+
if len(matching) == 1:
|
|
82
|
+
for child in error.context:
|
|
83
|
+
if child.schema_path and child.schema_path[0] == matching[0]:
|
|
84
|
+
yield from schema_messages(child)
|
|
85
|
+
return
|
|
86
|
+
yield f"{error.json_path}: does not match an allowed schema alternative"
|
|
87
|
+
for child in error.context:
|
|
88
|
+
yield from schema_messages(child)
|
|
89
|
+
else:
|
|
90
|
+
yield f"{error.json_path}: {error.message}"
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def collect(entries: Path, schemas: Path):
|
|
94
|
+
"""Validate every YAML entry in ``entries`` against ``schemas``.
|
|
95
|
+
|
|
96
|
+
Returns ``(database, errors)``. ``database`` maps material key (filename
|
|
97
|
+
stem) to the parsed entry; ``errors`` lists human-readable problems.
|
|
98
|
+
"""
|
|
99
|
+
entries, schemas = Path(entries), Path(schemas)
|
|
100
|
+
check = validator(schemas)
|
|
101
|
+
files = sorted(p for p in entries.iterdir() if p.is_file() and p.suffix in (".yaml", ".yml"))
|
|
102
|
+
if not files:
|
|
103
|
+
raise ValueError(f"{entries}: no YAML entries found")
|
|
104
|
+
database, errors = {}, []
|
|
105
|
+
for path in files:
|
|
106
|
+
if not re.fullmatch(r"[a-z0-9_]+", path.stem):
|
|
107
|
+
errors.append(f"{path}: filename stem must contain lowercase letters, digits, or underscores")
|
|
108
|
+
continue
|
|
109
|
+
if path.stem in database:
|
|
110
|
+
errors.append(f"{path}: duplicate material key {path.stem!r}")
|
|
111
|
+
continue
|
|
112
|
+
try:
|
|
113
|
+
entry = yaml.load(path.read_text(encoding="utf-8"), Loader=EntryLoader)
|
|
114
|
+
problems = list(json_errors(entry))
|
|
115
|
+
if not problems and isinstance(entry, dict):
|
|
116
|
+
model_object = entry.get("constitutive_model", {})
|
|
117
|
+
if isinstance(model_object, dict):
|
|
118
|
+
model = model_object.get("plastic_model_name")
|
|
119
|
+
if model not in PLASTIC_MODELS:
|
|
120
|
+
problems = [
|
|
121
|
+
"$.constitutive_model.plastic_model_name: "
|
|
122
|
+
f"missing or unsupported plastic model name {model!r}; "
|
|
123
|
+
f"expected one of {PLASTIC_MODELS!r}"
|
|
124
|
+
]
|
|
125
|
+
if not problems:
|
|
126
|
+
problems = [
|
|
127
|
+
message
|
|
128
|
+
for error in check.iter_errors(entry)
|
|
129
|
+
for message in schema_messages(error)
|
|
130
|
+
]
|
|
131
|
+
errors.extend(f"{path}: {problem}" for problem in problems)
|
|
132
|
+
database[path.stem] = entry
|
|
133
|
+
except (ValueError, yaml.YAMLError) as error:
|
|
134
|
+
errors.append(f"{path}: {error}")
|
|
135
|
+
return database, errors
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def validate_only(entries: Path, schemas: Path) -> int:
|
|
139
|
+
"""Validate entries and return the count of valid materials, raising on error."""
|
|
140
|
+
database, errors = collect(entries, schemas)
|
|
141
|
+
if errors:
|
|
142
|
+
raise ValueError("\n".join(errors))
|
|
143
|
+
return len(database)
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def build_database(entries: Path, schemas: Path, output_format: str = "yaml") -> str:
|
|
147
|
+
"""Validate entries and return the serialized combined database as text."""
|
|
148
|
+
database, errors = collect(entries, schemas)
|
|
149
|
+
if errors:
|
|
150
|
+
raise ValueError("\n".join(errors))
|
|
151
|
+
if output_format == "json":
|
|
152
|
+
return json.dumps(database, indent=2, sort_keys=True, ensure_ascii=False, allow_nan=False) + "\n"
|
|
153
|
+
return yaml.safe_dump(database, sort_keys=True, allow_unicode=True)
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def main(argv=None):
|
|
157
|
+
from .data_source import entries_dir as default_entries_dir
|
|
158
|
+
from .data_source import schemas_dir as default_schemas_dir
|
|
159
|
+
|
|
160
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
161
|
+
parser.add_argument("--entries", type=Path, help="Defaults to the mat-data repo's entries/ (fetched/cached)")
|
|
162
|
+
parser.add_argument("--schemas", type=Path, help="Defaults to the mat-data repo's schemas/ (fetched/cached)")
|
|
163
|
+
parser.add_argument("--output", "-o", type=Path, required=True, help="Destination (.yaml/.yml or .json)")
|
|
164
|
+
parser.add_argument("--format", choices=("yaml", "json"), help="Defaults to output extension, or YAML")
|
|
165
|
+
mode = parser.add_mutually_exclusive_group()
|
|
166
|
+
mode.add_argument("--check", action="store_true", help="Fail if the output is missing or stale; write nothing")
|
|
167
|
+
mode.add_argument("--validate-only", action="store_true", help="Validate entries without building")
|
|
168
|
+
args = parser.parse_args(argv)
|
|
169
|
+
try:
|
|
170
|
+
entries = args.entries or default_entries_dir()
|
|
171
|
+
schemas = args.schemas or default_schemas_dir()
|
|
172
|
+
|
|
173
|
+
output = args.output
|
|
174
|
+
output_format = args.format or ("json" if output.suffix == ".json" else "yaml")
|
|
175
|
+
if output.resolve().parent in (Path(entries).resolve(), Path(schemas).resolve()):
|
|
176
|
+
raise ValueError("Output must be outside the entries and schemas directories")
|
|
177
|
+
|
|
178
|
+
if args.validate_only:
|
|
179
|
+
count = validate_only(entries, schemas)
|
|
180
|
+
print(f"Validated {count} materials")
|
|
181
|
+
return 0
|
|
182
|
+
|
|
183
|
+
content = build_database(entries, schemas, output_format)
|
|
184
|
+
data = content.encode("utf-8")
|
|
185
|
+
if args.check:
|
|
186
|
+
if not output.is_file() or output.read_bytes() != data:
|
|
187
|
+
print(f"{output}: missing or stale; rebuild without --check", file=sys.stderr)
|
|
188
|
+
return 1
|
|
189
|
+
print(f"{output}: up to date")
|
|
190
|
+
return 0
|
|
191
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
192
|
+
temporary = None
|
|
193
|
+
try:
|
|
194
|
+
with tempfile.NamedTemporaryFile(dir=output.parent, delete=False) as stream:
|
|
195
|
+
temporary = Path(stream.name)
|
|
196
|
+
stream.write(data)
|
|
197
|
+
stream.flush()
|
|
198
|
+
os.fsync(stream.fileno())
|
|
199
|
+
temporary.replace(output)
|
|
200
|
+
finally:
|
|
201
|
+
if temporary is not None:
|
|
202
|
+
temporary.unlink(missing_ok=True)
|
|
203
|
+
print(f"Wrote database to {output}")
|
|
204
|
+
return 0
|
|
205
|
+
except Exception as error:
|
|
206
|
+
print(f"Build failed: {error}", file=sys.stderr)
|
|
207
|
+
return 1
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
if __name__ == "__main__":
|
|
211
|
+
sys.exit(main())
|
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
"""Export schema-valid material entries to shared Abaqus PROPS(9:) include files.
|
|
2
|
+
|
|
3
|
+
Ported from ``cp-work``'s ``materials/extract_params.py``. Business logic for
|
|
4
|
+
which parameters are active and how they map to Abaqus PROPS positions is
|
|
5
|
+
unchanged; only default data locations moved to the ``mat-data-handler-data``
|
|
6
|
+
package.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import ast
|
|
13
|
+
import csv
|
|
14
|
+
import io
|
|
15
|
+
import json
|
|
16
|
+
import math
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
import re
|
|
19
|
+
import sys
|
|
20
|
+
|
|
21
|
+
import yaml
|
|
22
|
+
|
|
23
|
+
from .database import EntryLoader, json_errors, schema_messages, validator
|
|
24
|
+
|
|
25
|
+
FLAG_LAYOUT = dict(zip((
|
|
26
|
+
"flag_isotropic_hardening", "flag_back_stress", "flag_gradient_plasticity",
|
|
27
|
+
"flag_int", "flag_super_alloy", "flag_trip", "flag_thermal",
|
|
28
|
+
), range(7)))
|
|
29
|
+
BASE = ["C11", "C12", "C44", "number_slip_systems", "reference_shear_rate",
|
|
30
|
+
"stress_exponent", "initial_critical_resolved_shear_stress"]
|
|
31
|
+
ISO = ["saturated_slip_resistance", "reference_hardening_rate", "self_hardening",
|
|
32
|
+
"cross_hardening", "hardening_exponent"]
|
|
33
|
+
KIN = ["nslip_kinematic_hardening", "kinematic_hardening_a1", "kinematic_hardening_b1",
|
|
34
|
+
"ohno_wang_exponent", "chaboche_parameter_a2", "chaboche_parameter_b2",
|
|
35
|
+
"chaboche_parameter_a3", "chaboche_parameter_b3"]
|
|
36
|
+
GRAD = ["flag_octree", "C_taui", "L_size", "I_gnd_iso", "I_gnd_kin", "C_unit", "B_lattice"]
|
|
37
|
+
THERM = ["activation_energy", "c11_tcoeff", "c12_tcoeff", "c44_tcoeff",
|
|
38
|
+
"kinematic_hardening_a1_tcoeff", "crss0_coef_tcoeff"]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def load_document(path: Path):
|
|
42
|
+
def unique_object(pairs):
|
|
43
|
+
result = {}
|
|
44
|
+
for key, value in pairs:
|
|
45
|
+
if key in result:
|
|
46
|
+
raise ValueError(f"duplicate key: {key}")
|
|
47
|
+
result[key] = value
|
|
48
|
+
return result
|
|
49
|
+
|
|
50
|
+
path = Path(path)
|
|
51
|
+
text = path.read_text(encoding="utf-8")
|
|
52
|
+
# JSON's exponent syntax (e.g. 1e-06) differs from PyYAML's resolver.
|
|
53
|
+
value = (json.loads(text, object_pairs_hook=unique_object) if path.suffix.lower() == ".json"
|
|
54
|
+
else yaml.load(text, Loader=EntryLoader))
|
|
55
|
+
errors = list(json_errors(value))
|
|
56
|
+
if errors:
|
|
57
|
+
raise ValueError("; ".join(errors))
|
|
58
|
+
return value
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def parse_mapping(path: Path):
|
|
62
|
+
with Path(path).open(encoding="utf-8-sig", newline="") as stream:
|
|
63
|
+
rows = [{k.strip(): v.strip() for k, v in row.items()} for row in csv.DictReader(stream, delimiter=";")]
|
|
64
|
+
result = {}
|
|
65
|
+
for row in rows:
|
|
66
|
+
key = row[next(k for k in row if k.startswith("json_key"))]
|
|
67
|
+
if key in result:
|
|
68
|
+
raise ValueError(f"duplicate mapping key: {key}")
|
|
69
|
+
result[key] = row
|
|
70
|
+
return result
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def condition(expression, env, provided=False):
|
|
74
|
+
expression = expression.strip()
|
|
75
|
+
if expression in ("always", ""):
|
|
76
|
+
return True
|
|
77
|
+
if expression == "never":
|
|
78
|
+
return False
|
|
79
|
+
if expression == "provided":
|
|
80
|
+
return provided
|
|
81
|
+
node = ast.parse(expression, mode="eval")
|
|
82
|
+
allowed = (ast.Expression, ast.Compare, ast.Name, ast.Load, ast.Constant,
|
|
83
|
+
ast.Eq, ast.NotEq, ast.Gt, ast.GtE, ast.Lt, ast.LtE,
|
|
84
|
+
ast.BoolOp, ast.And, ast.Or, ast.UnaryOp, ast.Not)
|
|
85
|
+
if any(not isinstance(n, allowed) for n in ast.walk(node)):
|
|
86
|
+
raise ValueError(f"unsupported mapping condition: {expression}")
|
|
87
|
+
return bool(eval(compile(node, "<mapping condition>", "eval"), {"__builtins__": {}}, env))
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def scalar(value, key):
|
|
91
|
+
while isinstance(value, list):
|
|
92
|
+
if len(value) != 1:
|
|
93
|
+
raise ValueError(f"{key}: current Fortran reader supports only one family / coefficient")
|
|
94
|
+
value = value[0]
|
|
95
|
+
if isinstance(value, bool) or not isinstance(value, (float, int)) or not math.isfinite(value):
|
|
96
|
+
raise ValueError(f"{key}: expected a finite number")
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def export_entry(entry, mapping, schemas: Path):
|
|
101
|
+
model = entry.get("constitutive_model", {}) if isinstance(entry, dict) else {}
|
|
102
|
+
if not isinstance(model, dict) or model.get("plastic_model_name") != "ICAMS CP-UMAT":
|
|
103
|
+
raise ValueError("CP-UMAT export requires plastic_model_name: ICAMS CP-UMAT")
|
|
104
|
+
errors = [m for e in validator(schemas).iter_errors(entry) for m in schema_messages(e)]
|
|
105
|
+
if errors:
|
|
106
|
+
raise ValueError("\n".join(errors))
|
|
107
|
+
plastic = model["plastic_parameters"]
|
|
108
|
+
elastic = model["elastic_parameters"]
|
|
109
|
+
units = model["units"]
|
|
110
|
+
records, notes = [], []
|
|
111
|
+
|
|
112
|
+
def raw(key):
|
|
113
|
+
if key == "space_group_number":
|
|
114
|
+
if key in plastic and plastic[key] != entry[key]:
|
|
115
|
+
raise ValueError("conflicting space_group_number values")
|
|
116
|
+
return entry[key]
|
|
117
|
+
return elastic.get(key) if key in elastic else plastic.get(key)
|
|
118
|
+
|
|
119
|
+
flags = {}
|
|
120
|
+
packed = plastic.get("super_flag")
|
|
121
|
+
if packed is not None and not 0 <= packed <= 0x7FFFFFFF:
|
|
122
|
+
raise ValueError("super_flag exceeds signed 32-bit range")
|
|
123
|
+
for key, pos in FLAG_LAYOUT.items():
|
|
124
|
+
if key in plastic:
|
|
125
|
+
flags[key] = plastic[key]
|
|
126
|
+
if packed is not None and ((packed >> (4 * pos)) & 15) != flags[key]:
|
|
127
|
+
raise ValueError(f"super_flag mismatch for {key}")
|
|
128
|
+
elif packed is not None:
|
|
129
|
+
flags[key] = (packed >> (4 * pos)) & 15
|
|
130
|
+
notes.append(f"{key}: decoded from super_flag = {flags[key]}")
|
|
131
|
+
else:
|
|
132
|
+
flags[key] = int(mapping[key]["default_value"])
|
|
133
|
+
notes.append(f"{key}: mapping default = {flags[key]}")
|
|
134
|
+
maximum = 3 if key == "flag_back_stress" else 1
|
|
135
|
+
if not 0 <= flags[key] <= maximum:
|
|
136
|
+
raise ValueError(f"{key}: unsupported mode {flags[key]}")
|
|
137
|
+
super_flag = sum(value << (4 * FLAG_LAYOUT[key]) for key, value in flags.items())
|
|
138
|
+
if packed is not None and packed != super_flag:
|
|
139
|
+
raise ValueError("super_flag contains unsupported spare bits")
|
|
140
|
+
env = dict(zip(("iso_mode", "kin_mode", "grad_mode", "int_mode", "super_mode", "trip_mode", "thermal_mode"), flags.values()))
|
|
141
|
+
if env["super_mode"]:
|
|
142
|
+
raise ValueError("superalloy export unavailable: the schema/mapping do not represent all 12 values read by the UMAT")
|
|
143
|
+
if env["trip_mode"] or env["int_mode"]:
|
|
144
|
+
raise ValueError("TRIP/internal-stress export requires additional constitutive compatibility checks; currently unsupported")
|
|
145
|
+
if scalar(plastic.get("number_slip_families", 1), "number_slip_families") != 1:
|
|
146
|
+
raise ValueError("current Fortran reader supports one slip family only")
|
|
147
|
+
if any(key in elastic for key in ("C33", "C66", "C13")):
|
|
148
|
+
raise ValueError("current Fortran reader consumes only C11, C12, C44; additional elastic constants cannot be exported")
|
|
149
|
+
for key, value in plastic.items():
|
|
150
|
+
if isinstance(value, list):
|
|
151
|
+
scalar(value, key)
|
|
152
|
+
if env["thermal_mode"]:
|
|
153
|
+
if plastic.get("temperature_polynomial_degree", 1) != 1:
|
|
154
|
+
raise ValueError("current Fortran reader supports linear thermal slopes only")
|
|
155
|
+
extra = [k for k in plastic if k.endswith("_tcoeff") and k not in THERM]
|
|
156
|
+
if extra:
|
|
157
|
+
raise ValueError(f"thermal coefficients not read by CP-UMAT: {extra}")
|
|
158
|
+
|
|
159
|
+
def append(key, value, source, unit="-"):
|
|
160
|
+
records.append(dict(props_position=9 + len(records), key=key, value=value, source=source, unit=unit))
|
|
161
|
+
|
|
162
|
+
def mapped(key):
|
|
163
|
+
row = mapping[key]
|
|
164
|
+
value = raw(key)
|
|
165
|
+
provided = value is not None
|
|
166
|
+
if not condition(row["active_if"], env, provided):
|
|
167
|
+
raise ValueError(f"mapping disables {key}, but the Fortran reader requires its slot")
|
|
168
|
+
source = "entry"
|
|
169
|
+
if not provided:
|
|
170
|
+
if condition(row["required_if"], env, provided):
|
|
171
|
+
raise ValueError(f"missing required active parameter: {key}")
|
|
172
|
+
value = float(row["default_value"].replace(",", "."))
|
|
173
|
+
source = "mapping default"
|
|
174
|
+
value = scalar(value, key)
|
|
175
|
+
if row["fortran_type"].startswith("integer"):
|
|
176
|
+
if value != int(value):
|
|
177
|
+
raise ValueError(f"{key}: expected integer")
|
|
178
|
+
value = int(value)
|
|
179
|
+
elif not row["fortran_type"].startswith("real"):
|
|
180
|
+
raise ValueError(f"{key}: unsupported mapping type")
|
|
181
|
+
if key in ("number_slip_systems", "nslip_kinematic_hardening") and not 1 <= value <= 60:
|
|
182
|
+
raise ValueError(f"{key}: must be within current Nslp_mx=60")
|
|
183
|
+
if key in ("I_gnd_iso", "I_gnd_kin") and value not in (0, 1):
|
|
184
|
+
raise ValueError(f"{key}: expected 0 or 1")
|
|
185
|
+
unit = row["unit"]
|
|
186
|
+
if provided and unit.startswith("MPa"):
|
|
187
|
+
category = "Stiffness" if key in elastic or key in ("c11_tcoeff", "c12_tcoeff", "c44_tcoeff") else "Stress"
|
|
188
|
+
if units[category] == "Pa":
|
|
189
|
+
value *= 1e-6
|
|
190
|
+
source += " (Pa converted to MPa)"
|
|
191
|
+
if source == "mapping default":
|
|
192
|
+
notes.append(f"{key}: mapping default = {value} {unit}")
|
|
193
|
+
append(key, value, source, unit)
|
|
194
|
+
|
|
195
|
+
mapped("space_group_number")
|
|
196
|
+
append("super_flag", super_flag, "packed flags")
|
|
197
|
+
for key in BASE:
|
|
198
|
+
mapped(key)
|
|
199
|
+
if env["iso_mode"]:
|
|
200
|
+
for key in ISO:
|
|
201
|
+
mapped(key)
|
|
202
|
+
if env["kin_mode"]:
|
|
203
|
+
for key in KIN:
|
|
204
|
+
mapped(key)
|
|
205
|
+
if env["grad_mode"]:
|
|
206
|
+
for key in GRAD:
|
|
207
|
+
mapped(key)
|
|
208
|
+
# Not yet in mapping/schema; retain the explicit reader initialization.
|
|
209
|
+
append("CD_smooth", 3e-15, "src/mod_material.f default")
|
|
210
|
+
notes.append("CD_smooth: reader default = 3e-15 (not configurable in current schema/mapping)")
|
|
211
|
+
if env["thermal_mode"]:
|
|
212
|
+
for key in THERM:
|
|
213
|
+
mapped(key)
|
|
214
|
+
return records, notes
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def render_outputs(name, entry, records, notes, source):
|
|
218
|
+
values = [str(r["value"]) if isinstance(r["value"], int) else format(r["value"], ".17g") for r in records]
|
|
219
|
+
inc_name = f"{name}_inp_{len(values)}p.inc"
|
|
220
|
+
lines = [", ".join(values[i:i + 8]) + ("," if i + 8 < len(values) else "") for i in range(0, len(values), 8)]
|
|
221
|
+
stream = io.StringIO()
|
|
222
|
+
writer = csv.DictWriter(stream, fieldnames=list(records[0]))
|
|
223
|
+
writer.writeheader()
|
|
224
|
+
writer.writerows(records)
|
|
225
|
+
manifest = [f"Source: {source}", f"Include: {inc_name}", f"Include count: {len(values)}",
|
|
226
|
+
f"Total Abaqus constant count: {len(values) + 8}",
|
|
227
|
+
"PROPS(1:8) supplied by caller: material selector, three Euler angles (rad), four spare values.",
|
|
228
|
+
"", "Abaqus usage (replace Euler angles for each grain):",
|
|
229
|
+
f"*User Material, constants={len(values) + 8}",
|
|
230
|
+
f'{entry["material_id"]}, 0., 0., 0., 0., 0., 0., 0.,',
|
|
231
|
+
f"*Include, input={inc_name}", "", "Positions:"]
|
|
232
|
+
manifest += [f'PROPS({r["props_position"]}): {r["key"]} = {r["value"]} [{r["unit"]}] ({r["source"]})' for r in records]
|
|
233
|
+
manifest += ["", "Defaults and notes:", *notes]
|
|
234
|
+
return {inc_name: "\n".join(lines) + "\n", f"{name}_props_flat.csv": stream.getvalue(),
|
|
235
|
+
f"{name}_props_manifest.txt": "\n".join(manifest) + "\n"}
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def export_material(name: str, entry, mapping, schemas: Path):
|
|
239
|
+
"""Export one already-loaded material entry. Returns ``(records, notes)``."""
|
|
240
|
+
return export_entry(entry, mapping, schemas)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def _default_data_paths():
|
|
244
|
+
from .data_source import entries_dir, mapping_path, schemas_dir
|
|
245
|
+
|
|
246
|
+
return entries_dir(), schemas_dir(), mapping_path()
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def main(argv=None):
|
|
250
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
251
|
+
parser.add_argument("material", help="Material key or 'all'")
|
|
252
|
+
parser.add_argument("--input", "--yaml_file", "--json_file", dest="input", type=Path,
|
|
253
|
+
help="Entry directory, single entry, or combined YAML/JSON database; "
|
|
254
|
+
"defaults to the mat-data repo's entries/ (fetched/cached)")
|
|
255
|
+
parser.add_argument("--mapping-file", "--mapping_file", dest="mapping_file", type=Path,
|
|
256
|
+
help="Defaults to the mat-data repo's mapping.csv (fetched/cached)")
|
|
257
|
+
parser.add_argument("--schemas", type=Path, help="Defaults to the mat-data repo's schemas/ (fetched/cached)")
|
|
258
|
+
parser.add_argument("--outdir", type=Path, default=Path("."))
|
|
259
|
+
parser.add_argument("--values-per-line", type=int, choices=[8], default=8)
|
|
260
|
+
args = parser.parse_args(argv)
|
|
261
|
+
try:
|
|
262
|
+
if args.input is None or args.mapping_file is None or args.schemas is None:
|
|
263
|
+
default_entries, default_schemas, default_mapping = _default_data_paths()
|
|
264
|
+
input_path = args.input or default_entries
|
|
265
|
+
mapping_file = args.mapping_file or default_mapping
|
|
266
|
+
schemas = args.schemas or default_schemas
|
|
267
|
+
else:
|
|
268
|
+
input_path, mapping_file, schemas = args.input, args.mapping_file, args.schemas
|
|
269
|
+
|
|
270
|
+
mapping = parse_mapping(mapping_file)
|
|
271
|
+
input_path = Path(input_path)
|
|
272
|
+
if input_path.is_dir():
|
|
273
|
+
files = sorted(p for p in input_path.iterdir() if p.suffix in (".yaml", ".yml") and p.is_file())
|
|
274
|
+
selected = files if args.material == "all" else [p for p in files if p.stem == args.material]
|
|
275
|
+
entries = {}
|
|
276
|
+
for path in selected:
|
|
277
|
+
if path.stem in entries:
|
|
278
|
+
raise ValueError(f"duplicate material key: {path.stem}")
|
|
279
|
+
entries[path.stem] = load_document(path)
|
|
280
|
+
else:
|
|
281
|
+
data = load_document(input_path)
|
|
282
|
+
if not isinstance(data, dict):
|
|
283
|
+
raise ValueError("input must be a material object or keyed database")
|
|
284
|
+
entries = {input_path.stem: data} if "constitutive_model" in data else data
|
|
285
|
+
if args.material != "all":
|
|
286
|
+
entries = {k: v for k, v in entries.items() if k == args.material}
|
|
287
|
+
if not entries:
|
|
288
|
+
raise ValueError(f"no matching materials: {args.material}")
|
|
289
|
+
|
|
290
|
+
outputs, errors = {}, []
|
|
291
|
+
for name, entry in sorted(entries.items()):
|
|
292
|
+
try:
|
|
293
|
+
if not isinstance(name, str) or not re.fullmatch("[a-z0-9_]+", name):
|
|
294
|
+
raise ValueError("invalid material key")
|
|
295
|
+
records, notes = export_entry(entry, mapping, schemas)
|
|
296
|
+
outputs.update(render_outputs(name, entry, records, notes, input_path))
|
|
297
|
+
except (ValueError, KeyError) as error:
|
|
298
|
+
errors.append(f"{name}: {error}")
|
|
299
|
+
if errors:
|
|
300
|
+
raise ValueError("\n".join(errors))
|
|
301
|
+
|
|
302
|
+
# Validate the whole selection before touching any output files.
|
|
303
|
+
args.outdir.mkdir(parents=True, exist_ok=True)
|
|
304
|
+
for name, content in outputs.items():
|
|
305
|
+
path = args.outdir / name
|
|
306
|
+
path.write_text(content, encoding="utf-8")
|
|
307
|
+
print(f"Wrote {path}")
|
|
308
|
+
return 0
|
|
309
|
+
except Exception as error:
|
|
310
|
+
print(f"Export failed: {error}", file=sys.stderr)
|
|
311
|
+
return 1
|
|
312
|
+
|
|
313
|
+
|
|
314
|
+
if __name__ == "__main__":
|
|
315
|
+
raise SystemExit(main())
|
|
File without changes
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"""Point tests at a local mat-data checkout instead of fetching over the network.
|
|
2
|
+
|
|
3
|
+
Assumes the sibling layout used during development:
|
|
4
|
+
``~/Codes/mat-data-handler`` and ``~/Codes/mat-data``. CI instead sets
|
|
5
|
+
``MAT_DATA_LOCAL_PATH`` itself after checking out the ``mat-data`` repo.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import os
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
_SIBLING_MAT_DATA = Path(__file__).resolve().parents[3] / "mat-data"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@pytest.fixture(autouse=True, scope="session")
|
|
17
|
+
def _mat_data_local_path():
|
|
18
|
+
if "MAT_DATA_LOCAL_PATH" not in os.environ and _SIBLING_MAT_DATA.is_dir():
|
|
19
|
+
os.environ["MAT_DATA_LOCAL_PATH"] = str(_SIBLING_MAT_DATA)
|
|
20
|
+
if "MAT_DATA_LOCAL_PATH" not in os.environ:
|
|
21
|
+
pytest.skip("MAT_DATA_LOCAL_PATH not set and no sibling mat-data checkout found")
|
|
22
|
+
yield
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
from mat_data_handler.data_source import entries_dir, mapping_path, schemas_dir
|
|
2
|
+
from mat_data_handler.extract import export_entry, load_document, parse_mapping
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def test_export_copper():
|
|
6
|
+
entry = load_document(entries_dir() / "copper_generic.yaml")
|
|
7
|
+
mapping = parse_mapping(mapping_path())
|
|
8
|
+
records, notes = export_entry(entry, mapping, schemas_dir())
|
|
9
|
+
assert records[0]["props_position"] == 9
|
|
10
|
+
assert any(r["key"] == "C11" for r in records)
|