Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 13 additions & 20 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,25 +1,19 @@
# This file was created automatically with `myst init --gh-pages` 🪄 💚

name: aiida-atomistic GitHub Pages Deploy
on:
push:
# Runs on pushes targeting the default branch
branches: [main, develop]
env:
# `BASE_URL` determines the website is served from, including CSS & JS assets
# You may need to change this to: `BASE_URL: ''`
BASE_URL: /${{ github.event.repository.name }}
branches: [main, develop, mkdocs]

# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.

# Allow only one concurrent deployment; do NOT cancel in-progress runs.
concurrency:
group: 'pages'
cancel-in-progress: false

jobs:
build:
environment:
Expand All @@ -28,25 +22,24 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # required by git-revision-date-localized
- name: Setup Pages
uses: actions/configure-pages@v4
- uses: actions/setup-python@v4
- uses: actions/setup-python@v5
with:
python-version: '3.10'
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install -r docs/requirements.txt
- name: Build Sphinx documentation
run: |
make html
working-directory: ./docs
pip install -e .[docs]
- name: Build MkDocs documentation
run: mkdocs build --site-dir ./build
- name: Upload artifact
uses: actions/upload-pages-artifact@v3
with:
path: './docs/build/html'
# Deployment job
path: './build'

deploy:
environment:
name: github-pages
Expand Down
20 changes: 0 additions & 20 deletions docs/Makefile

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@

This guide explains how to add new properties to `StructureData` and `StructureBuilder`.

:::{note}
If you want to add a temporary/experimental property, you can add it as [custom property](../how_to/define_custom.md).
If instead you want to contribute to this package, this is the right page!
:::
!!! note
If you want to add a temporary/experimental property, you can add it as [custom property](../how_to/define_custom.md).
If instead you want to contribute to this package, this is the right page!


This guide covers two types of properties:

1. **Site properties** - Properties that vary per atom (e.g., `charge`)
2. **Global properties** - Properties that apply to the entire structure (e.g., `tot_charge`)

Expand All @@ -21,20 +22,20 @@ Go to the [aiida-atomistic GitHub repository](https://github.com/aiidateam/aiida

### 2. Clone Your Fork

```bash
```text
git clone https://github.com/YOUR-USERNAME/aiida-atomistic.git
cd aiida-atomistic
```

### 3. Create a Feature Branch

```bash
```text
git checkout -b add-new-property
```

### 4. Install in Editable Mode

```bash
```text
pip install -e .
```

Expand All @@ -43,16 +44,14 @@ Now you can make changes and they'll be immediately reflected in your Python env
### 5. Create a Pull Request

Go to your fork on GitHub and click "New Pull Request". Provide a clear description of:

- What property you're adding
- Why it's useful
- Any relevant scientific context
- Link to related issues (if any)

:::{tip}
Before creating a PR, it's a good idea to open an issue on the main repository to discuss whether the property should be added. The maintainers can provide guidance on the best implementation approach.
:::

---
!!! tip
Before creating a PR, it's a good idea to open an issue on the main repository to discuss whether the property should be added. The maintainers can provide guidance on the best implementation approach.

## Adding a Site Property

Expand All @@ -76,43 +75,44 @@ class Site(BaseModel):
)
```

:::{important}
**Why `default=None` in Field and `"default"` in `json_schema_extra`?**
!!! note "**Why `default=None` in Field and `"default"` in `json_schema_extra`?**"

We use a two-level default system:
We use a two-level default system:

1. **Field `default=None`**: This is the Pydantic field default. Setting it to `None` means:
- When you create a site without specifying `property_A`, the field is truly undefined (`None`)
- This lets us distinguish between "property not set" vs "property set to zero"
- Without this, if we used `default=0.0`, every site would appear to have `property_A` defined, even when unset
1. **Field `default=None`**: This is the Pydantic field default. Setting it to `None` means:

2. **`json_schema_extra["default"]`**: This is used when expanding arrays:
- When one site has `property_A=2.5` but another has `property_A=None`
- The array needs a concrete value for the undefined site, if the above condition is verified
- We use `Site.get_default_values()['property_A']` to get `0.0` as the fill value
- Result: `charges = [2.5, 0.0]` (not `[2.5, None]` which breaks numpy arrays)
- When you create a site without specifying `property_A`, the field is truly undefined (`None`)
- This lets us distinguish between "property not set" vs "property set to zero"
- Without this, if we used `default=0.0`, every site would appear to have `property_A` defined, even when unset

**Example:**
```python
# Without property_A set
site1 = Site(symbol="Fe", position=[0, 0, 0])
site1.property_A # None - we know it's undefined
2. **`json_schema_extra["default"]`**: This is used when expanding arrays:

# With property_A set to zero
site2 = Site(symbol="Fe", position=[0, 0, 0], property_A=0.0)
site2.property_A # 0.0 - explicitly set
- When one site has `property_A=2.5` but another has `property_A=None`
- The array needs a concrete value for the undefined site, if the above condition is verified
- We use `Site.get_default_values()['property_A']` to get `0.0` as the fill value
- Result: `charges = [2.5, 0.0]` (not `[2.5, None]` which breaks numpy arrays)

# Array expansion uses json_schema_extra["default"]
structure = StructureData(sites=[site1, site2])
structure.properties.property_A_array # [1.0, 0.0] - we populated property_A_array[0] with the json_schema_extra["default"] = 1
```
**Example:**
```python
# Without property_A set
site1 = Site(symbol="Fe", position=[0, 0, 0])
site1.property_A # None - we know it's undefined

# With property_A set to zero
site2 = Site(symbol="Fe", position=[0, 0, 0], property_A=0.0)
site2.property_A # 0.0 - explicitly set

# Array expansion uses json_schema_extra["default"]
structure = StructureData(sites=[site1, site2])
structure.properties.property_A_array # [1.0, 0.0] - we populated property_A_array[0] with the json_schema_extra["default"] = 1
```

This pattern allows:
This pattern allows:

- Detecting if a property is truly set or not
- Creating valid numpy arrays without `None` values
- Distinguishing "zero" from "undefined"

- Detecting if a property is truly set or not
- Creating valid numpy arrays without `None` values
- Distinguishing "zero" from "undefined"
:::

You can also a validation, if needed:

Expand Down Expand Up @@ -158,19 +158,18 @@ def property_A_array(self) -> t.Optional[np.ndarray]:
])
```

:::{important}
**Key Points:**
!!! note "**Key Points**"

1. **Check for all None first**: If all sites have `property_A=None`, return `None` for the entire array. This indicates the property is truly undefined for the structure.

1. **Check for all None first**: If all sites have `property_A=None`, return `None` for the entire array. This indicates the property is truly undefined for the structure.
2. **Use `Site.get_default_values()`**: Instead of hardcoding defaults, retrieve them from the Site field metadata:
```python
default_value = Site.get_default_values().get('property_A', 0.0)
```
This keeps the default value definition in one place (the Site field's `json_schema_extra`).

2. **Use `Site.get_default_values()`**: Instead of hardcoding defaults, retrieve them from the Site field metadata:
```python
default_value = Site.get_default_values().get('property_A', 0.0)
```
This keeps the default value definition in one place (the Site field's `json_schema_extra`).
3. **Always include `singular_form`**: This metadata is **required** for loading structures from the database. See the metadata explanation below.

3. **Always include `singular_form`**: This metadata is **required** for loading structures from the database. See the metadata explanation below.
:::

#### Step 3: storage backend decision and additional computed fields for efficient querying

Expand Down Expand Up @@ -308,6 +307,7 @@ Add the property to the documentation:
Global properties (like temperature, pressure) follow a simpler pattern than site properties since they don't need array expansion.

Key differences from site properties:

- No need for `"default"` in `json_schema_extra` (no array expansion needed)
- Only need `default=None` in Field definition
- No need for `Site.get_default_values()` in computed fields
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -93,57 +93,56 @@ class MyCalculation(CalcJob):
]
```

:::{important}
**Use Plural Forms for Site Properties**
!!! note "**Use Plural Forms for Site Properties**"

When defining `supported_properties`, you must use the **plural form** for site-based properties (properties that vary per atom).
For example:
When defining `supported_properties`, you must use the **plural form** for site-based properties (properties that vary per atom).
For example:

- `'charges'` (not `'charge'`)
- `'masses'` (not `'mass'`)
- `'magmoms'` (not `'magmom'`)
- `'positions'` (not `'position'`)
- `'charges'` (not `'charge'`)
- `'masses'` (not `'mass'`)
- `'magmoms'` (not `'magmom'`)
- `'positions'` (not `'position'`)

Global properties (that apply to the entire structure) use their standard name:
Global properties (that apply to the entire structure) use their standard name:

- `'cell'`
- `'pbc'`
- `'temperature'`
- `'cell'`
- `'pbc'`
- `'temperature'`

**How to Find Property Names:**
**How to Find Property Names:**

To see all available properties and their correct names:
To see all available properties and their correct names:

```python
from aiida_atomistic import StructureData
```python
from aiida_atomistic import StructureData

# Get all supported properties
props = StructureData.get_supported_properties()
# Get all supported properties
props = StructureData.get_supported_properties()

# Site properties (use plural forms in supported_properties)
print(props['site'])
# {'charge', 'mass', 'magmom', 'magnetization', 'weight', ...}
# Site properties (use plural forms in supported_properties)
print(props['site'])
# {'charge', 'mass', 'magmom', 'magnetization', 'weight', ...}

# Global properties (use as-is in supported_properties)
print(props['global'])
# {'cell', 'pbc', 'tot_charge', 'temperature', ...}
# Global properties (use as-is in supported_properties)
print(props['global'])
# {'cell', 'pbc', 'tot_charge', 'temperature', ...}

# Computed properties (arrays from site properties - use plural)
print(props['computed'])
# {'charges', 'masses', 'magmoms', 'positions', 'symbols', ...}
```
# Computed properties (arrays from site properties - use plural)
print(props['computed'])
# {'charges', 'masses', 'magmoms', 'positions', 'symbols', ...}
```

**Why Plural?**
**Why Plural?**

Site properties in the `Site` model are singular (`charge`, `mass`), but they're accessed as arrays through computed fields with plural names (`charges`, `masses`). The validation checks for the array forms since that's what your plugin actually uses:
Site properties in the `Site` model are singular (`charge`, `mass`), but they're accessed as arrays through computed fields with plural names (`charges`, `masses`). The validation checks for the array forms since that's what your plugin actually uses:

```python
# In your plugin, you access:
structure.properties.charges # Array of all charges, what you will use
# Not:
structure.properties.sites[0].charge # Individual site charge (you don't validate this)
```

```python
# In your plugin, you access:
structure.properties.charges # Array of all charges, what you will use
# Not:
structure.properties.sites[0].charge # Individual site charge (you don't validate this)
```
:::

#### Step 2: Add Validation Logic

Expand Down Expand Up @@ -201,13 +200,13 @@ class MyCalculation(CalcJob):
)
```

:::{note}
You can also put the validation in the WorkChains using the given CalcJob, to stop prematurely instead of performing some steps and then exit only when trying to submit with the wrong structure.
:::
!!! tip
You can also put the validation in the WorkChains using the given CalcJob, to stop prematurely instead of performing some steps and then exit only when trying to submit with the wrong structure.


!!! tip
Of course, you will also need to add the logic to write your input file starting from the structure properties, which before were defined in the `parameters` input `orm.Dict` (at least in the aiida-quantumespresso plugin).

:::{note}
Of course, you will also need to add the logic to write your input file starting from the structure properties, which before were defined in the `parameters` input `orm.Dict` (at least in the aiida-quantumespresso plugin).
:::

### Real-World Example: aiida-quantumespresso

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -88,10 +88,10 @@ the ZIP central directory. It does **not** decompress any other entry.
Accessing `positions` in a file that also contains `charges`, `magmoms`, and
`symbols` only decompresses `positions.npy`.

:::{note}
The full load (no `keys` argument) is cached on the node object after the
first call, so repeated access to `.properties` does not re-read the file.
:::
!!! note
The full load (no `keys` argument) is cached on the node object after the
first call, so repeated access to `.properties` does not re-read the file.


---

Expand Down
Loading
Loading