Skip to content

Commit b927d63

Browse files
committed
Add vectorize enhancement
Addresses Unidata#527. Issue with values as seen through OPeNDAP fixed by Unidata/netcdf-java#1482
1 parent f4df071 commit b927d63

File tree

8 files changed

+346
-0
lines changed

8 files changed

+346
-0
lines changed
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
/*
2+
* Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata
3+
* See LICENSE for license information.
4+
*/
5+
6+
package thredds.exp.enhancement.vectorize;
7+
8+
import static com.google.common.truth.Truth.assertThat;
9+
10+
import java.io.IOException;
11+
import org.junit.BeforeClass;
12+
import org.junit.Test;
13+
import thredds.client.catalog.Access;
14+
import thredds.client.catalog.Catalog;
15+
import thredds.client.catalog.Dataset;
16+
import thredds.client.catalog.ServiceType;
17+
import thredds.client.catalog.tools.DataFactory;
18+
import thredds.server.catalog.TdsLocalCatalog;
19+
import ucar.ma2.Array;
20+
import ucar.ma2.MAMath;
21+
import ucar.ma2.MAMath.MinMax;
22+
import ucar.nc2.dataset.NetcdfDataset;
23+
24+
public class TestVectorize {
25+
private static Dataset dsNcmlFile;
26+
private static Dataset dsNcmlCat;
27+
private static DataFactory fac = new DataFactory();
28+
29+
@BeforeClass
30+
public static void setup() {
31+
Catalog cat = TdsLocalCatalog.open("/catalog/catalog.xml");
32+
dsNcmlFile = cat.findDatasetByID("vectorizeNcmlFile");
33+
dsNcmlCat = cat.findDatasetByID("vectorizeNcmlCat");
34+
assert dsNcmlFile != null;
35+
assert dsNcmlCat != null;
36+
}
37+
38+
@Test
39+
public void testTestVectorizeFileOpendap() throws IOException {
40+
Access access = dsNcmlFile.getAccess(ServiceType.OPENDAP);
41+
assertThat(access).isNotNull();
42+
testAccessMethod(access);
43+
}
44+
45+
@Test
46+
public void testTestVectorizeCatOpendap() throws IOException {
47+
Access access = dsNcmlCat.getAccess(ServiceType.OPENDAP);
48+
assertThat(access).isNotNull();
49+
testAccessMethod(access);
50+
}
51+
52+
@Test
53+
public void testTestVectorizeFileCdmremote() throws IOException {
54+
Access access = dsNcmlFile.getAccess(ServiceType.CdmRemote);
55+
assertThat(access).isNotNull();
56+
testAccessMethod(access);
57+
}
58+
59+
@Test
60+
public void testTestVectorizeCatCdmremote() throws IOException {
61+
Access access = dsNcmlCat.getAccess(ServiceType.CdmRemote);
62+
assertThat(access).isNotNull();
63+
testAccessMethod(access);
64+
}
65+
66+
private void testAccessMethod(Access access) throws IOException {
67+
NetcdfDataset ds = fac.openDataset(access, false, null, null);
68+
assertThat(ds).isNotNull();
69+
checkSpeed(ds);
70+
checkDir(ds);
71+
}
72+
73+
private void checkSpeed(NetcdfDataset ds) throws IOException {
74+
Array speed = ds.findVariable("cspd").read();
75+
MinMax minMaxSpeed = MAMath.getMinMax(speed);
76+
assertThat(minMaxSpeed.min).isWithin(0.01).of(0.05);
77+
assertThat(minMaxSpeed.max).isWithin(0.01).of(0.27);
78+
}
79+
80+
private void checkDir(NetcdfDataset ds) throws IOException {
81+
Array dir = ds.findVariable("cdir").read();
82+
MinMax minMaxDir = MAMath.getMinMax(dir);
83+
assertThat(minMaxDir.min).isWithin(0.1).of(121.1);
84+
assertThat(minMaxDir.max).isWithin(0.1).of(223.9);
85+
}
86+
}
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata
3+
* See LICENSE for license information.
4+
*/
5+
6+
package thredds.exp.enhancement.vectorize;
7+
8+
import java.util.Set;
9+
import java.util.concurrent.locks.ReentrantLock;
10+
import ucar.ma2.DataType;
11+
import ucar.nc2.Variable;
12+
import ucar.nc2.dataset.NetcdfDataset.Enhance;
13+
import ucar.nc2.dataset.VariableDS;
14+
import ucar.nc2.filter.Enhancement;
15+
import ucar.nc2.filter.EnhancementProvider;
16+
17+
public class VectorDirection extends Vectorize {
18+
19+
private static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(VectorDirection.class);
20+
21+
public static final String ATTRIBUTE_NAME = "vectorize_dir";
22+
23+
public VectorDirection(Variable var) {
24+
super(var);
25+
}
26+
27+
ReentrantLock lock = new ReentrantLock();
28+
29+
@Override
30+
public double convert(double num) {
31+
lock.lock();
32+
try {
33+
double u_val = uVar.read(indexToCoords((int) num), this.n_dimensional_array).getDouble(0);
34+
double v_val = vVar.read(indexToCoords((int) num), this.n_dimensional_array).getDouble(0);
35+
36+
// atan2(0, 0) is undefined, so just return 0
37+
if (Math.sqrt(u_val * u_val + v_val * v_val) == 0.0f) {
38+
return 0.0f;
39+
}
40+
41+
// return values in the [0, 360) range
42+
return ((Math.toDegrees(Math.atan2(u_val, v_val)) + 360.0f + this.convention_offset) % 360.0f);
43+
44+
} catch (Exception ex) {
45+
logger.error("error converting u and v to direction", ex);
46+
return Double.NaN;
47+
} finally {
48+
lock.unlock();
49+
}
50+
}
51+
52+
@Override
53+
protected String getAttributeName() {
54+
return ATTRIBUTE_NAME;
55+
}
56+
57+
public static class Provider implements EnhancementProvider {
58+
59+
@Override
60+
public String getAttributeName() {
61+
return ATTRIBUTE_NAME;
62+
}
63+
64+
@Override
65+
public boolean appliesTo(Set<Enhance> enhance, DataType dt) {
66+
return dt.isNumeric();
67+
}
68+
69+
@Override
70+
public Enhancement create(VariableDS var) {
71+
return new VectorDirection(var);
72+
}
73+
}
74+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
/*
2+
* Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata
3+
* See LICENSE for license information.
4+
*/
5+
6+
package thredds.exp.enhancement.vectorize;
7+
8+
import java.util.Set;
9+
import java.util.concurrent.locks.ReentrantLock;
10+
import ucar.ma2.DataType;
11+
import ucar.nc2.Variable;
12+
import ucar.nc2.dataset.NetcdfDataset.Enhance;
13+
import ucar.nc2.dataset.VariableDS;
14+
import ucar.nc2.filter.Enhancement;
15+
import ucar.nc2.filter.EnhancementProvider;
16+
17+
public class VectorMagnitude extends Vectorize {
18+
19+
private static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(VectorMagnitude.class);
20+
21+
public static final String ATTRIBUTE_NAME = "vectorize_mag";
22+
23+
public VectorMagnitude(Variable var) {
24+
super(var);
25+
}
26+
27+
ReentrantLock lock = new ReentrantLock();
28+
29+
@Override
30+
public double convert(double num) {
31+
lock.lock();
32+
try {
33+
double u_val = uVar.read(indexToCoords((int) num), this.n_dimensional_array).getDouble(0);
34+
double v_val = vVar.read(indexToCoords((int) num), this.n_dimensional_array).getDouble(0);
35+
return Math.sqrt(u_val * u_val + v_val * v_val);
36+
} catch (Exception ex) {
37+
logger.error("error converting u and v to magnitude", ex);
38+
return Double.NaN;
39+
} finally {
40+
lock.unlock();
41+
}
42+
}
43+
44+
@Override
45+
protected String getAttributeName() {
46+
return ATTRIBUTE_NAME;
47+
}
48+
49+
public static class Provider implements EnhancementProvider {
50+
51+
@Override
52+
public String getAttributeName() {
53+
return ATTRIBUTE_NAME;
54+
}
55+
56+
@Override
57+
public boolean appliesTo(Set<Enhance> enhance, DataType dt) {
58+
return dt.isNumeric();
59+
}
60+
61+
@Override
62+
public Enhancement create(VariableDS var) {
63+
return new VectorMagnitude(var);
64+
}
65+
}
66+
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
/*
2+
* Copyright (c) 2025 University Corporation for Atmospheric Research/Unidata
3+
* See LICENSE for license information.
4+
*/
5+
6+
package thredds.exp.enhancement.vectorize;
7+
8+
import ucar.nc2.Attribute;
9+
import ucar.nc2.Variable;
10+
import ucar.nc2.filter.Enhancement;
11+
12+
public abstract class Vectorize implements Enhancement {
13+
private static org.slf4j.Logger logger = org.slf4j.LoggerFactory.getLogger(Vectorize.class);
14+
15+
protected Variable uVar;
16+
protected Variable vVar;
17+
private String convention;
18+
protected float convention_offset;
19+
private int nDims;
20+
protected int[] shape;
21+
protected int[] n_dimensional_array;
22+
23+
public Vectorize(Variable var) {
24+
try {
25+
Attribute att = var.findAttribute(getAttributeName());
26+
String[] vars = att.getStringValue().split("/");
27+
this.uVar = var.getParentGroup().findVariableLocal(vars[0]);
28+
this.vVar = var.getParentGroup().findVariableLocal(vars[1]);
29+
30+
this.convention = vars[2];
31+
if ("to".equals(this.convention)) {
32+
this.convention_offset = 0.0f;
33+
} else if ("from".equals(this.convention)) {
34+
this.convention_offset = 180.0f;
35+
} else {
36+
throw new IllegalArgumentException("The convention must be either 'to' or 'from'.");
37+
}
38+
39+
this.shape = var.getShape();
40+
this.nDims = this.shape.length;
41+
if (!validateDims()) {
42+
return;
43+
}
44+
45+
this.n_dimensional_array = new int[this.nDims];
46+
for (int d = 0; d < this.nDims; d++) {
47+
this.n_dimensional_array[d] = 1;
48+
}
49+
} catch (NullPointerException ex) {
50+
logger.error("Could not parse attribute {}", getAttributeName());
51+
}
52+
}
53+
54+
protected int[] indexToCoords(int index) {
55+
int[] coords = new int[this.nDims];
56+
int innerDims = 1;
57+
for (int i = this.nDims - 1; i >= 0; i--) {
58+
coords[i] = (index / innerDims) % this.shape[i];
59+
innerDims *= this.shape[i];
60+
}
61+
return coords;
62+
}
63+
64+
private boolean validateDims() {
65+
return checkDims(this.uVar) && checkDims(this.vVar);
66+
}
67+
68+
private boolean checkDims(Variable var) {
69+
if (this.nDims != var.getShape().length) {
70+
return false;
71+
}
72+
for (int i = 0; i < this.nDims; i++) {
73+
if (this.shape[i] != var.getShape()[i]) {
74+
return false;
75+
}
76+
}
77+
return true;
78+
}
79+
80+
abstract protected String getAttributeName();
81+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
thredds.exp.enhancement.vectorize.VectorMagnitude$Provider
2+
thredds.exp.enhancement.vectorize.VectorDirection$Provider

tds/src/test/content/thredds/catalog.xml

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,26 @@
6969
</variable>
7070
</ncml:netcdf>
7171
</dataset>
72+
<dataset name="Vectorize NcML File" ID="vectorizeNcmlFile" urlPath="localContent/vectorize/test_dataset.ncml" dataType="Grid"/>
73+
<dataset name="Vectorize NcML Catalog" ID="vectorizeNcmlCat" urlPath="vectorizeNcmlCat.nc">
74+
<serviceName>all</serviceName>
75+
<ncml:netcdf xmlns="http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2"
76+
enhance="all"
77+
location="content/testdata/vectorize/vectorize_test.nc">
78+
<variable name="cspd" shape="time depth latitude longitude" type="float">
79+
<attribute name="vectorize_mag" value="uo/vo/to" />
80+
<attribute name="long_name" value="current speed" />
81+
<attribute name="units" value="m/s" />
82+
<values start="0" increment="1" />
83+
</variable>
84+
<variable name="cdir" shape="time depth latitude longitude" type="float">
85+
<attribute name="vectorize_dir" value="uo/vo/to" />
86+
<attribute name="long_name" value="current direction" />
87+
<attribute name="units" value="degrees" />
88+
<values start="0" increment="1" />
89+
</variable>
90+
</ncml:netcdf>
91+
</dataset>
7292
</dataset>
7393

7494
<dataset name="Test Dap4 Dataset" ID="testDap4Dataset" serviceName="dap4"
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<netcdf xmlns="http://www.unidata.ucar.edu/namespaces/netcdf/ncml-2.2"
3+
enhance="all"
4+
location="vectorize_test.nc">
5+
<variable name="cspd" shape="time depth latitude longitude" type="float">
6+
<attribute name="vectorize_mag" value="uo/vo/to" />
7+
<attribute name="long_name" value="current speed" />
8+
<attribute name="units" value="m/s" />
9+
<values start="0" increment="1" />
10+
</variable>
11+
<variable name="cdir" shape="time depth latitude longitude" type="float">
12+
<attribute name="vectorize_dir" value="uo/vo/to" />
13+
<attribute name="long_name" value="current direction" />
14+
<attribute name="units" value="degrees" />
15+
<values start="0" increment="1" />
16+
</variable>
17+
</netcdf>
Binary file not shown.

0 commit comments

Comments
 (0)