diff --git a/python/lsst/analysis/tools/actions/vector/__init__.py b/python/lsst/analysis/tools/actions/vector/__init__.py
index 701cb5b25..03962d18e 100644
--- a/python/lsst/analysis/tools/actions/vector/__init__.py
+++ b/python/lsst/analysis/tools/actions/vector/__init__.py
@@ -1,3 +1,4 @@
+from .calcFwhmZernikes import *
from .calcRhoStatistics import *
from .calcShapeSize import CalcShapeSize
from .ellipticity import *
diff --git a/python/lsst/analysis/tools/actions/vector/calcFwhmZernikes.py b/python/lsst/analysis/tools/actions/vector/calcFwhmZernikes.py
new file mode 100644
index 000000000..6d8f30700
--- /dev/null
+++ b/python/lsst/analysis/tools/actions/vector/calcFwhmZernikes.py
@@ -0,0 +1,97 @@
+# This file is part of analysis_tools.
+#
+# Developed for the LSST Data Management System.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from __future__ import annotations
+
+__all__ = ("CalcFwhmZernikesBase", "CalcFwhmZernikesLsst", "CalcFwhmZernikesLatiss")
+
+import numpy as np
+from lsst.pex.config import Field
+from ...interfaces import KeyedData, KeyedDataSchema, Vector, ScalarAction, Scalar
+
+
+class CalcFwhmZernikesBase(ScalarAction):
+
+ vectorKey = Field[str](doc="Vector on which to compute statistics")
+ # conversion_factors need to be assigned in daughter class
+ conversion_factors = None
+
+ def getInputSchema(self, **kwargs) -> KeyedDataSchema:
+ return ((self.vectorKey, Vector),)
+
+ def __call__(self, data: KeyedData, **kwargs) -> Scalar:
+
+ results = np.sqrt(np.sum((data[self.vectorKey] * self.conversion_factors) ** 2.0))
+
+ return results
+
+
+class CalcFwhmZernikesLsst(CalcFwhmZernikesBase):
+
+ conversion_factors = np.array(
+ [
+ 0.751,
+ 0.271,
+ 0.271,
+ 0.819,
+ 0.819,
+ 0.396,
+ 0.396,
+ 1.679,
+ 0.937,
+ 0.937,
+ 0.517,
+ 0.517,
+ 1.755,
+ 1.755,
+ 1.089,
+ 1.089,
+ 0.635,
+ 0.635,
+ 2.810,
+ ]
+ )
+
+
+class CalcFwhmZernikesLatiss(CalcFwhmZernikesBase):
+
+ conversion_factors = np.array(
+ [
+ 3.395,
+ 1.969,
+ 1.969,
+ 4.374,
+ 4.374,
+ 2.802,
+ 2.802,
+ 7.592,
+ 5.726,
+ 5.726,
+ 3.620,
+ 3.620,
+ 8.696,
+ 8.696,
+ 7.146,
+ 7.146,
+ 4.434,
+ 4.434,
+ 12.704,
+ ]
+ )
diff --git a/python/lsst/analysis/tools/analysisMetrics/__init__.py b/python/lsst/analysis/tools/analysisMetrics/__init__.py
index bf54b6b19..c39618d77 100644
--- a/python/lsst/analysis/tools/analysisMetrics/__init__.py
+++ b/python/lsst/analysis/tools/analysisMetrics/__init__.py
@@ -19,6 +19,7 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see .
from .analysisMetrics import *
+from .aosMetrics import *
from .apDiaSourceMetrics import *
from .apSsoMetrics import *
from .limitingMagnitudeMetric import *
diff --git a/python/lsst/analysis/tools/analysisMetrics/aosMetrics.py b/python/lsst/analysis/tools/analysisMetrics/aosMetrics.py
new file mode 100644
index 000000000..3bcfa5746
--- /dev/null
+++ b/python/lsst/analysis/tools/analysisMetrics/aosMetrics.py
@@ -0,0 +1,54 @@
+# This file is part of analysis_tools.
+#
+# Developed for the LSST Data Management System.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from __future__ import annotations
+
+__all__ = ("ZernikesFwhmMetricLsst", "ZernikesFwhmMetricLatiss")
+
+from ..actions.vector import CalcFwhmZernikesLsst, CalcFwhmZernikesLatiss
+from ..interfaces import AnalysisMetric
+
+
+class ZernikesFwhmMetricLsst(AnalysisMetric):
+ """Calculate FWHM from Zernikes for LsstCam."""
+
+ def setDefaults(self):
+ super().setDefaults()
+
+ # Calculate FWHM from Zernike Coefficients
+ self.process.calculateActions.ZernikesFwhmMetric = CalcFwhmZernikesLsst(
+ vectorKey="zernikeEstimateAvg"
+ )
+
+ self.produce.units = {"ZernikesFwhmMetric": "arcsec"}
+
+
+class ZernikesFwhmMetricLatiss(AnalysisMetric):
+ """Calculate FWHM from Zernikes for LAtiss."""
+
+ def setDefaults(self):
+ super().setDefaults()
+
+ # Calculate FWHM from Zernike Coefficients
+ self.process.calculateActions.ZernikesFwhmMetric = CalcFwhmZernikesLatiss(
+ vectorKey="zernikeEstimateAvg"
+ )
+
+ self.produce.units = {"ZernikesFwhmMetric": "arcsec"}
diff --git a/python/lsst/analysis/tools/tasks/__init__.py b/python/lsst/analysis/tools/tasks/__init__.py
index c9e85dbd2..b4c0600ef 100644
--- a/python/lsst/analysis/tools/tasks/__init__.py
+++ b/python/lsst/analysis/tools/tasks/__init__.py
@@ -1,3 +1,4 @@
+from .aosAnalysis import *
from .associatedSourcesTractAnalysis import *
from .catalogMatch import *
from .ccdVisitTableAnalysis import *
diff --git a/python/lsst/analysis/tools/tasks/aosAnalysis.py b/python/lsst/analysis/tools/tasks/aosAnalysis.py
new file mode 100644
index 000000000..174f434e3
--- /dev/null
+++ b/python/lsst/analysis/tools/tasks/aosAnalysis.py
@@ -0,0 +1,50 @@
+# This file is part of analysis_tools.
+#
+# Developed for the LSST Data Management System.
+# This product includes software developed by the LSST Project
+# (https://www.lsst.org).
+# See the COPYRIGHT file at the top-level directory of this distribution
+# for details of code ownership.
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU General Public License as published by
+# the Free Software Foundation, either version 3 of the License, or
+# (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+# GNU General Public License for more details.
+#
+# You should have received a copy of the GNU General Public License
+# along with this program. If not, see .
+from __future__ import annotations
+
+__all__ = ("aosAnalysisConnections", "aosAnalysisConfig", "aosAnalysisTask")
+
+from lsst.pipe.base import connectionTypes as ct
+
+from .base import AnalysisBaseConfig, AnalysisBaseConnections, AnalysisPipelineTask
+
+
+class aosAnalysisConnections(
+ AnalysisBaseConnections,
+ dimensions=("visit", "detector", "instrument"),
+):
+ data = ct.Input(
+ doc="Zernikes from detector.",
+ name="zernikeEstimateAvg",
+ storageClass="NumpyArray",
+ dimensions=("visit", "detector", "instrument"),
+ deferLoad=True,
+ )
+
+
+class aosAnalysisConfig(AnalysisBaseConfig, pipelineConnections=aosAnalysisConnections):
+ pass
+
+
+class aosAnalysisTask(AnalysisPipelineTask):
+
+ ConfigClass = aosAnalysisConfig
+ _DefaultName = "aosAnalysis"
diff --git a/python/lsst/analysis/tools/tasks/base.py b/python/lsst/analysis/tools/tasks/base.py
index ac308693d..3b84af5a3 100644
--- a/python/lsst/analysis/tools/tasks/base.py
+++ b/python/lsst/analysis/tools/tasks/base.py
@@ -371,7 +371,15 @@ def loadData(self, handle: DeferredDatasetHandle, names: Iterable[str] | None =
"""
if names is None:
names = self.collectInputNames()
- return cast(KeyedData, handle.get(parameters={"columns": names}))
+
+ # If input data is a numpy array instead of a catalog
+ # or dataframe, we need to create an object with keys.
+ if handle.ref.datasetType.storageClass_name == "NumpyArray":
+ dataLabel = list(names)[0]
+ dataDict = {dataLabel: handle.get()}
+ return cast(KeyedData, dataDict)
+ else:
+ return cast(KeyedData, handle.get(parameters={"columns": names}))
def collectInputNames(self) -> Iterable[str]:
"""Get the names of the inputs.