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
31 changes: 30 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,33 @@
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [2.0.0] - 2026-04-02

### Breaking

- `PeriodicArray` gained a fifth type parameter `G` for the inverse map (`imap`).
The type signature changed from `PeriodicArray{T,N,A,F}` to
`PeriodicArray{T,N,A,F,G}`. Any code with explicit type annotations,
dispatch rules, or type introspection on the four-parameter form will need
to be updated.
- The internal field previously named `.map` is now named `.fmap`; a new field
`.imap` holds the inverse map. Direct field access must be updated accordingly.
- The module was reorganised into separate source files
(`types.jl`, `indexing.jl`, `broadcast.jl`, `vector_interface.jl`,
`repeat.jl`, `circshift.jl`, `reverse.jl`, `mapped_ref.jl`).

### Added

- `PeriodicArray` now accepts an explicit `imap` argument (the inverse map used by
`setindex!`). When omitted, `imap` defaults to `NegatedShiftMap(fmap)`, i.e.
`(x, shifts...) -> fmap(x, -shifts...)`, preserving the previous behaviour.
Supplying a custom `imap` is useful when `fmap` is not self-inverse under shift
negation, or when mutation should be explicitly forbidden (pass an `imap` that throws).
- New `MappedRef` type and `mapped_ref(arr, I...)` function. `mapped_ref` returns a
lazy, mutable wrapper for an out-of-bounds element that applies the forward map on
reads and the inverse map on writes, solving the long-standing limitation where
iterated indexing for mutation silently did nothing (e.g. `x[i][j] = v`).
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "PeriodicArrays"
uuid = "343d6138-6384-4525-8bee-38906309ab36"
authors = ["Andreas Feuerpfeil <development@manybodylab.com>"]
version = "1.1.1"
version = "2.0.0"

[compat]
julia = "1.10"
53 changes: 32 additions & 21 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,33 @@
[codestyle-img]: https://img.shields.io/badge/code_style-%E1%9A%B1%E1%9A%A2%E1%9A%BE%E1%9B%81%E1%9A%B2-black
[codestyle-url]: https://github.com/fredrikekre/Runic.jl

`PeriodicArrays.jl` adds the `PeriodicArray` type which can be backed by any `AbstractArray`. The idea of this package is based on [`CircularArrays.jl`](https://github.com/Vexatos/CircularArrays.jl) and extends its functionality to support user-defined translation rules for periodic indexing.
A `PeriodicArray{T,N,A,F}` is an `AbstractArray{T,N}` backed by a data array of type `A<:AbstractArray{T,N}` and a map `f` of type `F`.
The map defines how data in out-of-bounds indices is translated to valid indices in the data array.
`PeriodicArrays.jl` adds the `PeriodicArray` type which can be backed by any `AbstractArray`. The idea of this package is based on [`CircularArrays.jl`](https://github.com/Vexatos/CircularArrays.jl) and extends its functionality to support user-defined translation rules for periodic indexing.
A `PeriodicArray{T,N,A,F,G}` is an `AbstractArray{T,N}` backed by a data array of type `A<:AbstractArray{T,N}`, a forward map `fmap` of type `F`, and an inverse map `imap` of type `G`.
The maps define how data at out-of-bounds indices is translated to and from valid indices in the data array.

`f` can be any callable object (e.g. a function or a struct), which defines
```julia
f(x, shift::Vararg{Int,N})
`fmap` and `imap` can be any callable objects (e.g. functions or structs) that define
```julia
fmap(x, shift::Vararg{Int,N})
```
where `x` is an element of the array and shift encodes the unit cell, in which we index.
`f` has to satisfy the following properties, which are not checked at construction time:
- The output type of `f` has to be the same as the element type of the data array.
- `f` is invertible with inverse `f(x, -shift...)`, i.e. it satisfies `f(f(x, shift...), -shift...) == x`.

If `f` is not provided, the identity map is used and the `PeriodicArray` behaves like a `CircularArray`.
where `x` is an element of the array and `shift` encodes the unit cell in which we index.

`PeriodicArray` accepts the maps as `PeriodicArray(data, fmap)` or
`PeriodicArray(data, fmap, imap)`.
If neither map is provided, both default to the identity and the array behaves like a `CircularArray`.

**Constraints on `imap`** (the inverse map used by `setindex!`):
- `imap` must satisfy `imap(fmap(x, shift...), shift...) == x` for all valid `x` and
`shift`, so that round-tripping a value through `getindex`/`setindex!` is lossless.
- When `imap` is omitted, it defaults to `(x, shifts...) -> fmap(x, -shifts...)`.
This default is correct whenever `fmap` is self-inverse under shift negation, i.e.
`fmap(fmap(x, s...), -s...) == x`.
- If `fmap` does **not** satisfy the self-inverse property, supply a custom `imap`.
If mutation through out-of-bounds indices should be explicitly forbidden, pass an
`imap` that e.g. throws:
```julia
imap_error(x, shift...) = error("mutation through out-of-bounds indices is not supported")
a = PeriodicArray(data, fmap, imap_error)
```

This package is compatible with [`OffsetArrays.jl`](https://github.com/JuliaArrays/OffsetArrays.jl).

Expand Down Expand Up @@ -110,20 +123,18 @@ x[out_of_bounds_index][i, j] = value
silently does nothing to `x`. The reason is that `x[out_of_bounds_index]` applies the map and returns a *new, transformed copy* of the element; the subsequent assignment mutates only that temporary object, not the underlying data.

For in-bounds indices the element is returned by reference and mutation works as expected.
As a workaround, operate directly on the underlying data:

```julia
parent(x)[mod_index][i, j] = value # bypasses the map entirely
```

or set the whole element at once (which goes through `setindex!` on `x` and correctly applies the inverse map):
**Workaround — `mapped_ref`:**

```julia
tmp = copy(x[out_of_bounds_index])
tmp[i, j] = value
x[out_of_bounds_index] = tmp
ref = mapped_ref(x, out_of_bounds_index)
ref[i, j] = value # applies imap and writes back into parent(x)
```

`mapped_ref` returns a `MappedRef`: a lazy wrapper that applies the forward map on reads
and the inverse map on writes, so no temporary copy is created and the mutation propagates
correctly into the underlying data.

## License

PeriodicArrays.jl is licensed under the [MIT License](LICENSE). By using or interacting with this software in any way, you agree to the license of this software.
2 changes: 1 addition & 1 deletion docs/Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ PeriodicArrays = "343d6138-6384-4525-8bee-38906309ab36"
[compat]
Documenter = "1"
Literate = "2"
PeriodicArrays = "1.0"
PeriodicArrays = "1.0,2.0"
55 changes: 46 additions & 9 deletions docs/files/README.jl
Original file line number Diff line number Diff line change
@@ -1,19 +1,32 @@
# # PeriodicArrays.jl

# `PeriodicArrays.jl` adds the `PeriodicArray` type which can be backed by any `AbstractArray`. The idea of this package is based on [`CircularArrays.jl`](https://github.com/Vexatos/CircularArrays.jl) and extends its functionality to support user-defined translation rules for periodic indexing.
# A `PeriodicArray{T,N,A,F}` is an `AbstractArray{T,N}` backed by a data array of type `A<:AbstractArray{T,N}` and a map `f` of type `F`.
# The map defines how data in out-of-bounds indices is translated to valid indices in the data array.
# A `PeriodicArray{T,N,A,F,G}` is an `AbstractArray{T,N}` backed by a data array of type `A<:AbstractArray{T,N}`, a forward map `fmap` of type `F`, and an inverse map `imap` of type `G`.
# The maps define how data at out-of-bounds indices is translated to and from valid indices in the data array.

# `f` can be any callable object (e.g. a function or a struct), which defines
# `fmap` and `imap` can be any callable objects (e.g. functions or structs) that define
# ```julia
# f(x, shift::Vararg{Int,N})
# fmap(x, shift::Vararg{Int,N})
# ```
# where `x` is an element of the array and shift encodes the unit cell, in which we index.
# `f` has to satisfy the following properties, which are not checked at construction time:
# - The output type of `f` has to be the same as the element type of the data array.
# - `f` is invertible with inverse `f(x, -shift...)`, i.e. it satisfies `f(f(x, shift...), -shift...) == x`.
# where `x` is an element of the array and `shift` encodes the unit cell in which we index.

# If `f` is not provided, the identity map is used and the `PeriodicArray` behaves like a `CircularArray`.
# `PeriodicArray` accepts the maps as `PeriodicArray(data, fmap)` or
# `PeriodicArray(data, fmap, imap)`.
# If neither map is provided, both default to the identity and the array behaves like a `CircularArray`.

# **Constraints on `imap`** (the inverse map used by `setindex!`):
# - `imap` must satisfy `imap(fmap(x, shift...), shift...) == x` for all valid `x` and
# `shift`, so that round-tripping a value through `getindex`/`setindex!` is lossless.
# - When `imap` is omitted, it defaults to `(x, shifts...) -> fmap(x, -shifts...)`.
# This default is correct whenever `fmap` is self-inverse under shift negation, i.e.
# `fmap(fmap(x, s...), -s...) == x`.
# - If `fmap` does **not** satisfy the self-inverse property, supply a custom `imap`.
# If mutation through out-of-bounds indices should be explicitly forbidden, pass an
# `imap` that e.g. throws:
# ```julia
# imap_error(x, shift...) = error("mutation through out-of-bounds indices is not supported")
# a = PeriodicArray(data, fmap, imap_error)
# ```

# This package is compatible with [`OffsetArrays.jl`](https://github.com/JuliaArrays/OffsetArrays.jl).

Expand Down Expand Up @@ -69,6 +82,30 @@
# 12 15 18 22 25
# ```

# ## Known Limitations

# **Iterated indexing for mutation does not work** when the map is non-trivial.
# For a `PeriodicArray` whose elements are themselves mutable (e.g. an array of matrices), writing

# ```julia
# x[out_of_bounds_index][i, j] = value
# ```

# silently does nothing to `x`. The reason is that `x[out_of_bounds_index]` applies the map and returns a *new, transformed copy* of the element; the subsequent assignment mutates only that temporary object, not the underlying data.

# For in-bounds indices the element is returned by reference and mutation works as expected.

# **Workaround — `mapped_ref`:**

# ```julia
# ref = mapped_ref(x, out_of_bounds_index)
# ref[i, j] = value # applies imap and writes back into parent(x)
# ```

# `mapped_ref` returns a `MappedRef`: a lazy wrapper that applies the forward map on reads
# and the inverse map on writes, so no temporary copy is created and the mutation propagates
# correctly into the underlying data.

# ## License

# PeriodicArrays.jl is licensed under the MIT License. By using or interacting with this software in any way, you agree to the license of this software.
Loading
Loading