Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Simplify and standardize dimensions and coordinates #151

Merged
merged 6 commits into from
Dec 30, 2024
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
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "PALEOboxes"
uuid = "804b410e-d900-4b2a-9ecd-f5a06d4c1fd4"
authors = ["Stuart Daines <[email protected]>"]
version = "0.21.40"
version = "0.22.0"

[deps]
Atomix = "a9b6321e-bd34-4604-b9c9-b65b8de01458"
Expand Down
41 changes: 23 additions & 18 deletions docs/src/DomainsVariablesFields.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,15 +21,38 @@ CurrentModule = PALEOboxes
Domain
```

### Dimensions and Coordinates

PALEO approximately follows the [Common Data Model](https://docs.unidata.ucar.edu/netcdf-c/current/netcdf_data_model.html) used by NetCDF and other geoscience data formats.

Domains provide named dimensions, to which coordinate variables may be attached (these are just normal PALEO variables).
- Domain spatial dimensions are provided by the Domain grid (see `Grids`)
- Additional Domain data dimensions (eg a wavelength grid) may be set by Reactions (see [`set_data_dimension!`](@ref)).

```@meta
CurrentModule = PALEOboxes
```
```@docs
NamedDimension
get_dimensions
get_dimension
get_coordinates
set_coordinates!
```

### Grids
```@meta
CurrentModule = PALEOboxes
```
```@docs
AbstractMesh

Grids.available_spaces
has_internal_cartesian
internal_size
cartesian_size
Grids.cartesian_to_internal
Grids.internal_to_cartesian
```
```@meta
CurrentModule = PALEOboxes.Grids
Expand Down Expand Up @@ -61,23 +84,6 @@ subdomain_view
subdomain_indices
```

#### Regions, Dimensions and Coordinates
```@meta
CurrentModule = PALEOboxes.Grids
```
```@docs
get_region
```

Grids may define name dimensions and attach coordinates for the convenience of output visualisation. Any coordinate information required by Reactions should be supplied as Variables.
```@meta
CurrentModule = PALEOboxes
```
```@docs
NamedDimension
FixedCoord
```

## Variables

```@meta
Expand Down Expand Up @@ -148,7 +154,6 @@ Examples:
```@docs
Field
get_field
wrap_field
```

## Spaces
Expand Down
208 changes: 35 additions & 173 deletions src/CoordsDims.jl
Original file line number Diff line number Diff line change
@@ -1,199 +1,61 @@

################################
# Coordinates
#################################


#################################################
# Dimensions
#####################################################

"""
FixedCoord
NamedDimension(name, size)

A fixed (state independent) coordinate
A named dimension
"""
mutable struct FixedCoord
struct NamedDimension
name::String
values::Vector{Float64}
attributes::Dict{Symbol, Any}
size::Int64
end

"""
append_units(name::AbstractString, attributes) -> "name (units)"

Utility function to append variable units string to a variable name for display.
"""
function append_units(name::AbstractString, attributes::Dict{Symbol, Any})
units = get(attributes, :units, "")
if isempty(units)
return name
function Base.show(io::IO, nd::NamedDimension)
if get(io, :typeinfo, nothing) === NamedDimension
print(io, "(name=", nd.name, ", size=", nd.size, ")")
else
return name*" ($units)"
print(io, "NamedDimension(name=", nd.name, ", size=", nd.size, ")")
end
return nothing
end

append_units(name::AbstractString, attributes::Nothing) = name

"""
build_coords_edges(coords_vec::Vector{FixedCoord}) -> Vector{Float64}

Build a vector of coordinate edges (length `n+1``) from `coords_vec`, assuming the PALEO
convention that `coords_vec` contains three elements with
cell midpoints, lower edges, upper edges each of length `n`, in that order.
function get_dimensions(obj) -> Vector{NamedDimension}

Falls back to just returning the first entry in `coords_vec` for other cases.
Get all dimensions for PALEO object `obj`
"""
function build_coords_edges(coords_vec::Vector{FixedCoord})

if length(coords_vec) == 1 || length(coords_vec) > 3
# 1 coordinate or something we don't understand - take first
co = first(coords_vec)
co_values = co.values
co_label = append_units(co.name, co.attributes)
elseif length(coords_vec) in (2, 3)
# 2 coordinates assume lower, upper edges
# 3 coordinates assume mid, lower, upper
co_lower = coords_vec[end-1]
co_upper = coords_vec[end]
co_label = append_units(co_lower.name*", "*co_upper.name, co_lower.attributes)
first(co_lower.values) < first(co_upper.values) ||
@warn "build_coords_edges: $co_label co_lower is > co_upper - check model grid"
if co_lower.values[end] > co_lower.values[1] # ascending order
co_lower.values[2:end] == co_upper.values[1:end-1] ||
@warn "build_coords_edges: $co_label lower and upper edges don't match"
co_values = [co_lower.values; co_upper.values[end]]
else # descending order
co_lower.values[1:end-1] == co_upper.values[2:end] ||
@warn "build_coords_edges: $co_label lower and upper edges don't match"
co_values = [co_upper.values[1]; co_lower.values]
end

end

return co_values, co_label
end

"guess coordinate edges from midpoints, assuming uniform spacing"
function guess_coords_edges(x_midpoints)
first_x = x_midpoints[1] - 0.5*(x_midpoints[2] - x_midpoints[1])
last_x = x_midpoints[end] + 0.5*(x_midpoints[end] - x_midpoints[end-1])
return [first_x; 0.5.*(x_midpoints[1:end-1] .+ x_midpoints[2:end]); last_x]
end


function get_region(fc::FixedCoord, indices::AbstractVector)
return FixedCoord(fc.name, fc.values[indices], fc.attributes)
end

function get_region(fcv::Vector{FixedCoord}, indices::AbstractVector)
return [FixedCoord(fc.name, fc.values[indices], fc.attributes) for fc in fcv]
end


"find indices of coord from first before range[1] to first after range[2]"
function find_indices(coord::AbstractVector, range)
length(range) == 2 ||
throw(ArgumentError("find_indices: length(range) != 2 $range"))

idxstart = findlast(t -> t<=range[1], coord)
isnothing(idxstart) && (idxstart = 1)

idxend = findfirst(t -> t>=range[2], coord)
isnothing(idxend) && (idxend = length(coord))

return idxstart:idxend, (coord[idxstart], coord[idxend])
end

"find indices of coord nearest val"
function find_indices(coord::AbstractVector, val::Real)
idx = 1
for i in 1:length(coord)
if abs(coord[i] - val) < abs(coord[idx] - val)
idx = i
end
end

return [idx], coord[idx]
end

#################################################
# Dimensions
#####################################################
function get_dimensions end

"""
NamedDimension

A named dimension, with optional attached fixed coordinates `coords`
function get_dimension(obj, dimname) -> NamedDimension

PALEO convention is that where possible `coords` contains three elements, for cell
midpoints, lower edges, upper edges, in that order.
Get all dimension `dimname` for PALEO object `obj`
"""
mutable struct NamedDimension
name::String
size::Int64
coords::Vector{FixedCoord} # may be empty
end

"create from size only (no coords)"
function NamedDimension(name, size::Integer)
return NamedDimension(
name,
size,
FixedCoord[],
)
end

"create from coord mid-points"
function NamedDimension(name, coord::AbstractVector)
return NamedDimension(
name,
length(coord),
[
FixedCoord(name, coord, Dict{Symbol, Any}()),
]
)
end
function get_dimension end

"create from coord mid-points and edges"
function NamedDimension(name, coord::AbstractVector, coord_edges::AbstractVector)
if coord[end] > coord[1]
# ascending order
coord_lower = coord_edges[1:end-1]
coord_upper = coord_edges[2:end]
else
# descending order
coord_lower = coord_edges[2:end]
coord_upper = coord_edges[1:end-1]
end
return NamedDimension(
name,
length(coord),
[
FixedCoord(name, coord, Dict{Symbol, Any}()),
FixedCoord(name*"_lower", coord_lower, Dict{Symbol, Any}()),
FixedCoord(name*"_upper", coord_upper, Dict{Symbol, Any}()),
]
)
end
"""
function set_coordinates!(obj, dimname, coordinates::Vector{String})

function get_region(nd::NamedDimension, indices::AbstractVector)
return NamedDimension(nd.name, length(indices), get_region(nd.coords, indices))
end
Set coordinates (variable names) attached to `dimname` for PALEO object `obj`

PALEO convention is that where possible `coordinates` contains:
- three variable names, for cell midpoints, lower edges, upper edges, in that order.
- two variable names, for cell midpoints and a bounds variable (with a bounds dimension as the first dimensions)
"""
build_coords_edges(nd::NamedDimension) -> Vector{Float64}
function set_coordinates! end

Call [`build_coords_edges`](@ref)(nd.coords), or fallback to just returning indices
if no coords present.
"""
function build_coords_edges(nd::NamedDimension)
if !isempty(nd.coords)
return build_coords_edges(nd.coords)
else
@warn "no coords for NamedDimension $(nd.name), returning indices"
return collect(1:nd.size), nd.name*" (indices)"
end
end
function get_coordinates(obj, dimname) -> coordinates::Vector{String}

function Base.show(io::IO, nd::NamedDimension)
print(io, "NamedDimension(name=", nd.name, ", size=", nd.size, ", coords=(")
join(io, [c.name for c in nd.coords], ", ")
print(io, "))")
return nothing
end
Get coordinates (if any) attached to `dimname` for PALEO object `obj`

PALEO convention is that where possible `coordinates` contains:
- three variable names, for cell midpoints, lower edges, upper edges, in that order.
- two variable names, for cell midpoints and a bounds variable (with a bounds dimension as the first dimensions)
"""
function get_coordinates end
Loading
Loading