MayaFlux 0.3.0
Digital-First Multimedia Processing Framework
Loading...
Searching...
No Matches
EnergyAnalyzer.hpp
Go to the documentation of this file.
1#pragma once
2
5
7#include <Eigen/Dense>
8
10
12
13/**
14 * @file EnergyAnalyzer.hpp
15 * @brief Span-based energy analysis for digital signals in Maya Flux
16 *
17 * Defines the EnergyAnalyzer using the new UniversalAnalyzer framework with
18 * zero-copy span processing and automatic structure handling via OperationHelper.
19 * This analyzer extracts energy-related features from digital signals with multiple
20 * computation methods and flexible output configurations.
21 *
22 * Key Features:
23 * - **Zero-copy processing:** Uses spans for maximum efficiency
24 * - **Template-flexible I/O:** Instance defines input/output types at construction
25 * - **Multiple energy methods:** RMS, peak, spectral, zero-crossing, harmonic, power, dynamic range
26 * - **Parallel processing:** Utilizes std::execution for performance
27 * - **Energy classification:** Maps values to qualitative levels (silent, quiet, etc.)
28 * - **Window-based analysis:** Configurable window size and hop size
29 * - **Automatic data handling:** OperationHelper manages all extraction/conversion
30 */
31
32namespace MayaFlux::Yantra {
33
34/**
35 * @enum EnergyMethod
36 * @brief Supported energy computation methods
37 */
38enum class EnergyMethod : uint8_t {
39 RMS, ///< Root Mean Square energy
40 PEAK, ///< Peak amplitude
41 SPECTRAL, ///< Spectral energy (FFT-based)
42 ZERO_CROSSING, ///< Zero-crossing rate
43 HARMONIC, ///< Harmonic energy (low-frequency content)
44 POWER, ///< Power (sum of squares)
45 DYNAMIC_RANGE ///< Dynamic range (dB)
46};
47
48/**
49 * @enum EnergyLevel
50 * @brief Qualitative classification of energy values
51 */
52enum class EnergyLevel : uint8_t {
53 SILENT,
54 QUIET,
56 LOUD,
57 PEAK
58};
59
60struct MAYAFLUX_API ChannelEnergy {
61 std::vector<double> energy_values;
62 double mean_energy {};
63 double max_energy {};
64 double min_energy {};
65 double variance {};
66
67 std::vector<EnergyLevel> classifications;
68 std::array<int, 5> level_counts {}; // [SILENT, QUIET, MODERATE, LOUD, PEAK]
69 std::vector<std::pair<size_t, size_t>> window_positions;
70
71 // Positions of detected energy events (e.g., peaks, zero crossings, flux)
72 std::vector<size_t> event_positions;
73};
74
75/**
76 * @struct EnergyAnalysis
77 * @brief Analysis result structure for energy analysis
78 */
79struct MAYAFLUX_API EnergyAnalysis {
80 std::vector<ChannelEnergy> channels;
81
82 EnergyMethod method_used {};
83 uint32_t window_size {};
84 uint32_t hop_size {};
85};
86
87/**
88 * @class EnergyAnalyzer
89 * @brief High-performance energy analyzer with zero-copy processing
90 *
91 * The EnergyAnalyzer provides comprehensive energy analysis capabilities for
92 * digital signals using span-based processing for maximum efficiency.
93 * All data extraction and conversion is handled automatically by OperationHelper.
94 *
95 * Example usage:
96 * ```cpp
97 * // DataVariant -> VectorXd analyzer
98 * auto energy_analyzer = std::make_shared<EnergyAnalyzer<Kakshya::DataVariant, Eigen::VectorXd>>();
99 *
100 * // User-facing analysis
101 * auto analysis = energy_analyzer->analyze_data(audio_data);
102 * auto energy_result = safe_any_cast<EnergyAnalysis>(analysis);
103 *
104 * // Pipeline usage
105 * auto pipeline_output = energy_analyzer->apply_operation(IO{audio_data});
106 * ```
107 */
108template <ComputeData InputType = std::vector<Kakshya::DataVariant>, ComputeData OutputType = Eigen::VectorXd>
109class MAYAFLUX_API EnergyAnalyzer : public UniversalAnalyzer<InputType, OutputType> {
110public:
114
115 /**
116 * @brief Construct EnergyAnalyzer with configurable window parameters
117 * @param window_size Size of analysis window in samples (default: 512)
118 * @param hop_size Step size between windows in samples (default: 256)
119 */
120 explicit EnergyAnalyzer(uint32_t window_size = 256, uint32_t hop_size = 128)
121 : m_window_size(window_size)
122 , m_hop_size(hop_size)
123 {
124 validate_window_parameters();
125 }
126
127 /**
128 * @brief Type-safe energy analysis method
129 * @param data Input data
130 * @return EnergyAnalysis directly
131 */
132 EnergyAnalysis analyze_energy(const InputType& data)
133 {
134 auto result = this->analyze_data(data);
135 return safe_any_cast_or_throw<EnergyAnalysis>(result);
136 }
137
138 /**
139 * @brief Get last energy analysis result (type-safe)
140 * @return EnergyAnalysis from last operation
141 */
143 {
144 return safe_any_cast_or_throw<EnergyAnalysis>(this->get_current_analysis());
145 }
146
147 /**
148 * @brief Get analysis type category
149 * @return AnalysisType::FEATURE (energy is a feature extraction operation)
150 */
151 [[nodiscard]] AnalysisType get_analysis_type() const override
152 {
153 return AnalysisType::FEATURE;
154 }
155
156 /**
157 * @brief Get available analysis methods
158 * @return Vector of supported energy method names
159 */
160 [[nodiscard]] std::vector<std::string> get_available_methods() const override
161 {
162 return Reflect::get_enum_names_lowercase<EnergyMethod>();
163 }
164
165 /**
166 * @brief Check if a specific method is supported
167 * @param method Method name to check
168 * @return True if method is supported
169 */
170 [[nodiscard]] bool supports_method(const std::string& method) const override
171 {
172 return std::find_if(get_available_methods().begin(), get_available_methods().end(),
173 [&method](const std::string& m) {
174 return std::equal(method.begin(), method.end(), m.begin(), m.end(),
175 [](char a, char b) { return std::tolower(a) == std::tolower(b); });
176 })
177 != get_available_methods().end();
178 }
179
180 /**
181 * @brief Set energy computation method
182 */
183 void set_energy_method(EnergyMethod method) { m_method = method; }
184
185 /**
186 * @brief Get current energy computation method
187 */
188 [[nodiscard]] EnergyMethod get_energy_method() const { return m_method; }
189
190 /**
191 * @brief Set window parameters for analysis
192 * @param window_size Window size in samples
193 * @param hop_size Hop size in samples
194 */
195 void set_window_parameters(uint32_t window_size, uint32_t hop_size)
196 {
197 m_window_size = window_size;
198 m_hop_size = hop_size;
199 validate_window_parameters();
200 }
201
202 /**
203 * @brief Set energy level classification thresholds
204 * @param silent Threshold for silent level
205 * @param quiet Threshold for quiet level
206 * @param moderate Threshold for moderate level
207 * @param loud Threshold for loud level
208 */
209 void set_energy_thresholds(double silent, double quiet, double moderate, double loud)
210 {
211 if (silent >= quiet || quiet >= moderate || moderate >= loud) {
212 error<std::invalid_argument>(Journal::Component::Yantra, Journal::Context::ComputeMatrix, std::source_location::current(), "Energy thresholds must be in ascending order");
213 }
214 m_silent_threshold = silent;
215 m_quiet_threshold = quiet;
216 m_moderate_threshold = moderate;
217 m_loud_threshold = loud;
218 }
219
220 /**
221 * @brief Enable or disable energy level classification
222 * @param enabled True to enable classification
223 */
224 void enable_classification(bool enabled) { m_classification_enabled = enabled; }
225
226 /**
227 * @brief Classify energy value into qualitative level
228 * @param energy Energy value to classify
229 * @return EnergyLevel classification
230 */
231 [[nodiscard]] EnergyLevel classify_energy_level(double energy) const
232 {
233 if (energy <= m_silent_threshold)
234 return EnergyLevel::SILENT;
235 if (energy <= m_quiet_threshold)
236 return EnergyLevel::QUIET;
237 if (energy <= m_moderate_threshold)
238 return EnergyLevel::MODERATE;
239 if (energy <= m_loud_threshold)
240 return EnergyLevel::LOUD;
241 return EnergyLevel::PEAK;
242 }
243
244 /**
245 * @brief Get count of windows in a specific energy level for a channel
246 * @param channel ChannelEnergy result
247 * @param level EnergyLevel to query
248 * @return Count of windows in that level
249 */
250 [[nodiscard]]
251 int get_level_count(const ChannelEnergy& channel, EnergyLevel level)
252 {
253 return channel.level_counts[static_cast<size_t>(level)];
254 }
255
256 /**
257 * @brief Convert energy method enum to string
258 * @param method EnergyMethod value
259 * @return String representation
260 */
261 static std::string method_to_string(EnergyMethod method)
262 {
263 return Reflect::enum_to_lowercase_string(method);
264 }
265
266 /**
267 * @brief Convert string to energy method enum
268 * @param str String representation
269 * @return EnergyMethod value
270 */
271 static EnergyMethod string_to_method(const std::string& str)
272 {
273 if (str == "default")
274 return EnergyMethod::RMS;
275 return Reflect::string_to_enum_or_throw_case_insensitive<EnergyMethod>(str, "EnergyMethod");
276 }
277
278 /**
279 * @brief Convert energy level enum to string
280 * @param level EnergyLevel value
281 * @return String representation
282 */
283 static std::string energy_level_to_string(EnergyLevel level)
284 {
285 return Reflect::enum_to_lowercase_string(level);
286 }
287
288protected:
289 /**
290 * @brief Get analyzer name
291 * @return "EnergyAnalyzer"
292 */
293 [[nodiscard]] std::string get_analyzer_name() const override
294 {
295 return "EnergyAnalyzer";
296 }
297
298 /**
299 * @brief Core analysis implementation - creates analysis result AND pipeline output
300 * @param input Input data wrapped in Datum container
301 * @return Pipeline output (data flow for chaining operations)
302 */
304 {
305 try {
306 auto [data_span, structure_info] = OperationHelper::extract_structured_double(
307 const_cast<input_type&>(input));
308
309 std::vector<std::span<const double>> channel_spans;
310 for (auto& span : data_span)
311 channel_spans.emplace_back(span.data(), span.size());
312
313 for (const auto& channel_span : channel_spans) {
314 if (channel_span.size() < m_window_size) {
315 error<std::runtime_error>(Journal::Component::Yantra, Journal::Context::ComputeMatrix, std::source_location::current(), "One or more channels in input data are smaller than window size ({} samples)", m_window_size);
316 }
317 }
318
319 std::vector<std::vector<double>> energy_values;
320 energy_values.reserve(channel_spans.size());
321 for (const auto& channel_span : channel_spans) {
322 energy_values.push_back(compute_energy_values(channel_span, m_method));
323 }
324
325 EnergyAnalysis analysis_result = create_analysis_result(
326 energy_values, channel_spans, structure_info);
327
328 this->store_current_analysis(analysis_result);
329
330 return create_pipeline_output(input, analysis_result, structure_info);
331 } catch (const std::exception& e) {
332 MF_ERROR(Journal::Component::Yantra, Journal::Context::ComputeMatrix, "Energy analysis failed: {}", e.what());
333 output_type error_result;
334 error_result.metadata = input.metadata;
335 error_result.metadata["error"] = std::string("Analysis failed: ") + e.what();
336 return error_result;
337 }
338 }
339
340 /**
341 * @brief Handle analysis-specific parameters
342 */
343 void set_analysis_parameter(const std::string& name, std::any value) override
344 {
345 if (name == "method") {
346 try {
347 auto method_str = safe_any_cast_or_throw<std::string>(value);
348 m_method = string_to_method(method_str);
349 return;
350 } catch (const std::runtime_error&) {
351 error_rethrow(Journal::Component::Yantra, Journal::Context::ComputeMatrix, std::source_location::current(), "Invalid method parameter - expected string or EnergyMethod enum");
352 }
353 if (auto* method_enum = std::any_cast<EnergyMethod>(&value)) {
354 m_method = *method_enum;
355 return;
356 }
357 } else if (name == "window_size") {
358 if (auto* size = std::any_cast<uint32_t>(&value)) {
359 m_window_size = *size;
360 validate_window_parameters();
361 return;
362 }
363 } else if (name == "hop_size") {
364 if (auto* size = std::any_cast<uint32_t>(&value)) {
365 m_hop_size = *size;
366 validate_window_parameters();
367 return;
368 }
369 } else if (name == "classification_enabled") {
370 if (auto* enabled = std::any_cast<bool>(&value)) {
371 m_classification_enabled = *enabled;
372 return;
373 }
374 }
375
376 base_type::set_analysis_parameter(name, std::move(value));
377 }
378
379 /**
380 * @brief Get analysis-specific parameter
381 */
382 [[nodiscard]] std::any get_analysis_parameter(const std::string& name) const override
383 {
384 if (name == "method")
385 return std::any(method_to_string(m_method));
386 if (name == "window_size")
387 return std::any(m_window_size);
388 if (name == "hop_size")
389 return std::any(m_hop_size);
390 if (name == "classification_enabled")
391 return std::any(m_classification_enabled);
392
393 return base_type::get_analysis_parameter(name);
394 }
395
396private:
397 uint32_t m_window_size { 512 };
398 uint32_t m_hop_size { 256 };
399 EnergyMethod m_method { EnergyMethod::RMS };
400 bool m_classification_enabled { false };
401
402 double m_silent_threshold { 0.01 };
403 double m_quiet_threshold = { 0.1 };
404 double m_moderate_threshold { 0.5 };
405 double m_loud_threshold { 0.8 };
406
408 {
409 if (m_window_size == 0) {
410 error<std::invalid_argument>(Journal::Component::Yantra, Journal::Context::ComputeMatrix, std::source_location::current(), "Window size must be greater than 0");
411 }
412 if (m_hop_size == 0) {
413 error<std::invalid_argument>(Journal::Component::Yantra, Journal::Context::ComputeMatrix, std::source_location::current(), "Hop size must be greater than 0");
414 }
415 if (m_hop_size > m_window_size) {
416 error<std::invalid_argument>(Journal::Component::Yantra, Journal::Context::ComputeMatrix, std::source_location::current(), "Hop size should not exceed window size");
417 }
418 }
419
420 /**
421 * @brief Create comprehensive analysis result from energy computation
422 */
424 const std::vector<std::vector<double>>& energy_values,
425 std::vector<std::span<const double>> original_data,
426 const DataStructureInfo& /*structure_info*/) const
427 {
428 EnergyAnalysis result;
429 result.method_used = m_method;
430 result.window_size = m_window_size;
431 result.hop_size = m_hop_size;
432
433 if (energy_values.empty()) {
434 return result;
435 }
436
437 result.channels.resize(energy_values.size());
438
439 for (size_t ch = 0; ch < energy_values.size(); ch++) {
440
441 auto& channel_result = result.channels[ch];
442 const auto& ch_energy = energy_values[ch];
443
444 channel_result.energy_values = ch_energy;
445
446 if (!ch_energy.empty()) {
447 auto [min_it, max_it] = std::ranges::minmax_element(ch_energy);
448 channel_result.min_energy = *min_it;
449 channel_result.max_energy = *max_it;
450
451 const double sum = std::accumulate(ch_energy.begin(), ch_energy.end(), 0.0);
452 channel_result.mean_energy = sum / static_cast<double>(ch_energy.size());
453
454 const double mean = channel_result.mean_energy;
455 const double var_sum = std::transform_reduce(
456 ch_energy.begin(), ch_energy.end(), 0.0, std::plus {},
457 [mean](double val) { return (val - mean) * (val - mean); });
458 channel_result.variance = var_sum / static_cast<double>(ch_energy.size());
459 }
460
461 const size_t data_size = (ch < original_data.size()) ? original_data[ch].size() : 0;
462 channel_result.window_positions.reserve(ch_energy.size());
463
464 for (size_t i = 0; i < ch_energy.size(); ++i) {
465 const size_t start = i * m_hop_size;
466 const size_t end = std::min(start + m_window_size, data_size);
467 channel_result.window_positions.emplace_back(start, end);
468 }
469
470 if (ch < original_data.size()) {
471 switch (m_method) {
472 case EnergyMethod::ZERO_CROSSING:
473 channel_result.event_positions = Kinesis::Discrete::zero_crossing_positions(
474 original_data[ch], 0.0);
475 break;
476
477 case EnergyMethod::PEAK: {
478 double peak_threshold = m_classification_enabled ? m_quiet_threshold : 0.01;
479 channel_result.event_positions = Kinesis::Discrete::peak_positions(
480 original_data[ch], peak_threshold, m_hop_size / 4);
481 break;
482 }
483
484 case EnergyMethod::RMS:
485 case EnergyMethod::POWER:
486 case EnergyMethod::SPECTRAL:
487 case EnergyMethod::HARMONIC:
488 case EnergyMethod::DYNAMIC_RANGE:
489 default:
490 // event_positions remains empty
491 break;
492 }
493 }
494
495 if (m_classification_enabled) {
496 channel_result.classifications.reserve(ch_energy.size());
497 channel_result.level_counts.fill(0);
498
499 for (double energy : ch_energy) {
500 EnergyLevel level = classify_energy_level(energy);
501 channel_result.classifications.push_back(level);
502 channel_result.level_counts[static_cast<size_t>(level)]++;
503 }
504 }
505 }
506
507 return result;
508 }
509
510 /**
511 * @brief Create pipeline output from input and energy values
512 */
514 {
515 std::vector<std::vector<double>> channel_energies;
516 channel_energies.reserve(analysis_result.channels.size());
517
518 for (const auto& ch : analysis_result.channels) {
519 channel_energies.push_back(ch.energy_values);
520 }
521
522 output_type output = this->convert_result(channel_energies, info);
523
524 output.metadata = input.metadata;
525
526 output.metadata["source_analyzer"] = "EnergyAnalyzer";
527 output.metadata["energy_method"] = method_to_string(analysis_result.method_used);
528 output.metadata["window_size"] = analysis_result.window_size;
529 output.metadata["hop_size"] = analysis_result.hop_size;
530 output.metadata["num_channels"] = analysis_result.channels.size();
531
532 if (!analysis_result.channels.empty()) {
533 std::vector<double> channel_means, channel_maxs, channel_mins, channel_variances;
534 std::vector<size_t> channel_window_counts;
535
536 for (const auto& ch : analysis_result.channels) {
537 channel_means.push_back(ch.mean_energy);
538 channel_maxs.push_back(ch.max_energy);
539 channel_mins.push_back(ch.min_energy);
540 channel_variances.push_back(ch.variance);
541 channel_window_counts.push_back(ch.energy_values.size());
542 }
543
544 output.metadata["mean_energy_per_channel"] = channel_means;
545 output.metadata["max_energy_per_channel"] = channel_maxs;
546 output.metadata["min_energy_per_channel"] = channel_mins;
547 output.metadata["variance_per_channel"] = channel_variances;
548 output.metadata["window_count_per_channel"] = channel_window_counts;
549 }
550
551 return output;
552 }
553
554 /**
555 * @brief Compute energy values using span (zero-copy processing)
556 */
557 [[nodiscard]] std::vector<double> compute_energy_values(std::span<const double> data, EnergyMethod method) const
558 {
559 namespace D = MayaFlux::Kinesis::Discrete;
560 const size_t num_windows = calculate_num_windows(data.size());
561
562 switch (method) {
563 case EnergyMethod::PEAK:
564 return D::peak(data, num_windows, m_hop_size, m_window_size);
565 case EnergyMethod::ZERO_CROSSING:
566 return D::zero_crossing_rate(data, num_windows, m_hop_size, m_window_size);
567 case EnergyMethod::POWER:
568 return D::power(data, num_windows, m_hop_size, m_window_size);
569 case EnergyMethod::DYNAMIC_RANGE:
570 return D::dynamic_range(data, num_windows, m_hop_size, m_window_size);
571 case EnergyMethod::SPECTRAL:
572 return D::spectral_energy(data, num_windows, m_hop_size, m_window_size);
573 case EnergyMethod::HARMONIC:
574 return D::low_frequency_energy(data, num_windows, m_hop_size, m_window_size);
575 case EnergyMethod::RMS:
576 default:
577 return D::rms(data, num_windows, m_hop_size, m_window_size);
578 }
579 }
580
581 /**
582 * @brief Calculate number of windows for given data size
583 */
584 [[nodiscard]] size_t calculate_num_windows(size_t data_size) const
585 {
586 if (data_size < m_window_size)
587 return 0;
588 return (data_size - m_window_size) / m_hop_size + 1;
589 }
590};
591
592/// Standard energy analyzer: DataVariant -> MatrixXd
594
595/// Container energy analyzer: SignalContainer -> MatrixXd
597
598/// Region energy analyzer: Region -> MatrixXd
600
601/// Raw energy analyzer: produces double vectors
602template <ComputeData InputType = std::vector<Kakshya::DataVariant>>
604
605/// Variant energy analyzer: produces DataVariant output
606template <ComputeData InputType = std::vector<Kakshya::DataVariant>>
608
609} // namespace MayaFlux::Yantra
Discrete sequence analysis primitives for MayaFlux::Kinesis.
#define MF_ERROR(comp, ctx,...)
size_t a
size_t b
Modern, digital-first universal analyzer framework for Maya Flux.
EnergyMethod get_energy_method() const
Get current energy computation method.
size_t calculate_num_windows(size_t data_size) const
Calculate number of windows for given data size.
bool supports_method(const std::string &method) const override
Check if a specific method is supported.
output_type create_pipeline_output(const input_type &input, const EnergyAnalysis &analysis_result, DataStructureInfo &info)
Create pipeline output from input and energy values.
static EnergyMethod string_to_method(const std::string &str)
Convert string to energy method enum.
void enable_classification(bool enabled)
Enable or disable energy level classification.
AnalysisType get_analysis_type() const override
Get analysis type category.
EnergyAnalyzer(uint32_t window_size=256, uint32_t hop_size=128)
Construct EnergyAnalyzer with configurable window parameters.
EnergyAnalysis create_analysis_result(const std::vector< std::vector< double > > &energy_values, std::vector< std::span< const double > > original_data, const DataStructureInfo &) const
Create comprehensive analysis result from energy computation.
void set_energy_method(EnergyMethod method)
Set energy computation method.
std::any get_analysis_parameter(const std::string &name) const override
Get analysis-specific parameter.
int get_level_count(const ChannelEnergy &channel, EnergyLevel level)
Get count of windows in a specific energy level for a channel.
static std::string method_to_string(EnergyMethod method)
Convert energy method enum to string.
output_type analyze_implementation(const input_type &input) override
Core analysis implementation - creates analysis result AND pipeline output.
void set_energy_thresholds(double silent, double quiet, double moderate, double loud)
Set energy level classification thresholds.
std::vector< double > compute_energy_values(std::span< const double > data, EnergyMethod method) const
Compute energy values using span (zero-copy processing)
std::vector< std::string > get_available_methods() const override
Get available analysis methods.
void set_window_parameters(uint32_t window_size, uint32_t hop_size)
Set window parameters for analysis.
static std::string energy_level_to_string(EnergyLevel level)
Convert energy level enum to string.
std::string get_analyzer_name() const override
Get analyzer name.
EnergyAnalysis analyze_energy(const InputType &data)
Type-safe energy analysis method.
void set_analysis_parameter(const std::string &name, std::any value) override
Handle analysis-specific parameters.
EnergyLevel classify_energy_level(double energy) const
Classify energy value into qualitative level.
EnergyAnalysis get_energy_analysis() const
Get last energy analysis result (type-safe)
High-performance energy analyzer with zero-copy processing.
Template-flexible analyzer base with instance-defined I/O types.
AnalysisType
Categories of analysis operations for discovery and organization.
EnergyMethod
Supported energy computation methods.
@ SPECTRAL
Spectral energy (FFT-based)
@ DYNAMIC_RANGE
Dynamic range (dB)
@ RMS
Root Mean Square energy.
@ ZERO_CROSSING
Zero-crossing rate.
@ HARMONIC
Harmonic energy (low-frequency content)
@ POWER
Power (sum of squares)
EnergyLevel
Qualitative classification of energy values.
double mean(const std::vector< double > &data)
Calculate mean of single-channel data.
Definition Yantra.cpp:41
std::vector< size_t > event_positions
std::vector< EnergyLevel > classifications
std::vector< std::pair< size_t, size_t > > window_positions
std::array< int, 5 > level_counts
std::vector< double > energy_values
Metadata about data structure for reconstruction.
std::unordered_map< std::string, std::any > metadata
Associated metadata.
Definition DataIO.hpp:28
Input/Output container for computation pipeline data flow with structure preservation.
Definition DataIO.hpp:24
std::vector< ChannelEnergy > channels
Analysis result structure for energy analysis.