diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..36afbbb --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +.git +.gitignore +.vs/ +.vscode/ +.idea/ +.dockerignore + +docs/ + +**/*.user +**/*.md +**/bin/ +**/obj/ +**/out/ + +Dockerfile +README.md diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..3895306 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,189 @@ +# editorconfig.org + +# top-most EditorConfig file +root = true + +# Default settings: +# A newline ending every file +# Use 4 spaces as indentation +[*] +charset = utf-8 +insert_final_newline = true +indent_style = space +indent_size = 4 +trim_trailing_whitespace = true + +[project.json] +indent_size = 2 + +# C# files +[*.cs] +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true + +# Indentation preferences +csharp_indent_block_contents = true +csharp_indent_braces = false +csharp_indent_case_contents = true +csharp_indent_case_contents_when_block = true +csharp_indent_switch_labels = true +csharp_indent_labels = one_less_than_current + +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion + +# avoid this. unless absolutely necessary +dotnet_style_qualification_for_field = false:suggestion +dotnet_style_qualification_for_property = false:suggestion +dotnet_style_qualification_for_method = false:suggestion +dotnet_style_qualification_for_event = false:suggestion + +# Types: use keywords instead of BCL types, and permit var only when the type is clear +#csharp_style_var_for_built_in_types = false:suggestion +#csharp_style_var_when_type_is_apparent = false:none +#csharp_style_var_elsewhere = false:suggestion +dotnet_style_predefined_type_for_locals_parameters_members = true:suggestion +dotnet_style_predefined_type_for_member_access = true:suggestion + +# name all constant fields using PascalCase +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = error +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style + +# static fields should have s_ prefix +dotnet_naming_symbols.static_fields.applicable_kinds = field +dotnet_naming_symbols.static_fields.required_modifiers = static +dotnet_naming_symbols.static_fields.applicable_accessibilities = private, internal, private_protected +dotnet_naming_style.static_prefix_style.required_prefix = s_ +dotnet_naming_style.static_prefix_style.capitalization = camel_case +dotnet_naming_rule.static_fields_should_have_prefix.severity = suggestion +dotnet_naming_rule.static_fields_should_have_prefix.symbols = static_fields +dotnet_naming_rule.static_fields_should_have_prefix.style = static_prefix_style + +# internal and private fields should be _camelCase +dotnet_naming_symbols.private_internal_fields.applicable_kinds = field +dotnet_naming_symbols.private_internal_fields.applicable_accessibilities = private, internal +dotnet_naming_style.camel_case_underscore_style.required_prefix = _ +dotnet_naming_style.camel_case_underscore_style.capitalization = camel_case +dotnet_naming_rule.camel_case_for_private_internal_fields.severity = error +dotnet_naming_rule.camel_case_for_private_internal_fields.symbols = private_internal_fields +dotnet_naming_rule.camel_case_for_private_internal_fields.style = camel_case_underscore_style + +# Code style defaults +csharp_using_directive_placement = outside_namespace:suggestion +dotnet_sort_system_directives_first = true +csharp_prefer_braces = true +csharp_preserve_single_line_blocks = true +csharp_preserve_single_line_statements = false +csharp_prefer_static_local_function = true:suggestion +csharp_prefer_simple_using_statement = false:none +csharp_style_prefer_switch_expression = true:suggestion + +# Code quality +dotnet_style_readonly_field = true:suggestion +dotnet_code_quality_unused_parameters = non_public:suggestion + +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:suggestion +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:suggestion +dotnet_style_prefer_conditional_expression_over_assignment = true +dotnet_style_prefer_conditional_expression_over_return = true +csharp_prefer_simple_default_expression = true:suggestion + +# Expression-bodied members +csharp_style_expression_bodied_methods = true +csharp_style_expression_bodied_constructors = true +csharp_style_expression_bodied_operators = true +csharp_style_expression_bodied_properties = true +csharp_style_expression_bodied_indexers = true +csharp_style_expression_bodied_accessors = true +csharp_style_expression_bodied_lambdas = true +csharp_style_expression_bodied_local_functions = true + +# Pattern matching +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion + +# Null checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion + +# Other features +csharp_style_prefer_index_operator = false:none +csharp_style_prefer_range_operator = false:none +csharp_style_pattern_local_over_anonymous_function = false:none + +# Space preferences +csharp_space_after_cast = false +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_after_comma = true +csharp_space_after_dot = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_after_semicolon_in_for_statement = true +csharp_space_around_binary_operators = before_and_after +csharp_space_around_declaration_statements = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_before_comma = false +csharp_space_before_dot = false +csharp_space_before_open_square_brackets = false +csharp_space_before_semicolon_in_for_statement = false +csharp_space_between_empty_square_brackets = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_declaration_name_and_open_parenthesis = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_between_square_brackets = false + +# Analyzers +dotnet_code_quality.ca1802.api_surface = private, internal +dotnet_code_quality.CA2208.api_surface = public + +# C++ Files +[*.{cpp,h,in}] +curly_bracket_next_line = true +indent_brace_style = Allman + +# Xml project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,nativeproj,locproj}] +indent_size = 2 + +[*.{csproj,vbproj,proj,nativeproj,locproj}] +charset = utf-8 + +# Xml build files +[*.builds] +indent_size = 2 + +# Xml files +[*.{xml,stylecop,resx,ruleset}] +indent_size = 2 + +# Xml config files +[*.{props,targets,config,nuspec}] +indent_size = 2 + +# Shell scripts +[*.sh] +end_of_line = lf +[*.{cmd, bat}] +end_of_line = crlf \ No newline at end of file diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..61c139c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,5 @@ +# Auto detect text files and perform LF normalization +* text=auto + +# Custom for csharp +*.cs diff=csharp diff --git a/.github/workflows/dotnet.yml b/.github/workflows/dotnet.yml new file mode 100644 index 0000000..db551d6 --- /dev/null +++ b/.github/workflows/dotnet.yml @@ -0,0 +1,109 @@ +name: Release + +on: + push: + branches: + - '**' + tags: + - '*.*.*' + +env: + DOTNET_NOLOGO: true + DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + filter: tree:0 + + - name: Setup dotnet + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 10.0.x + + - name: Compute version + shell: bash + run: | + BASE=$(sed -n 's|.*\(.*\).*|\1|p' Directory.Build.props | head -n1) + if [[ -z "$BASE" ]]; then + echo "Failed to read PackageBaseVersion from Directory.Build.props" >&2 + exit 1 + fi + + if [[ "${GITHUB_REF_TYPE}" == "tag" ]]; then + # Stable on tags + MINVER_VERSION="${BASE}" + PKG_VERSION="${BASE}" + elif [[ "${GITHUB_REF_NAME}" == "main" ]]; then + # Stable with 4th component on main (no -main) + MINVER_VERSION="${BASE}" # SemVer for MinVer + PKG_VERSION="${BASE}.${GITHUB_RUN_NUMBER}" # 4-part NuGet version + else + # Pre-release on other branches: base-branch.runNumber + SAFE_BRANCH=$(echo "${GITHUB_REF_NAME}" | tr '[:upper:]' '[:lower:]' | sed -E 's/[^0-9a-zA-Z.-]+/-/g' | sed -E 's/^-+|-+$//g') + MINVER_VERSION="${BASE}-${SAFE_BRANCH}.${GITHUB_RUN_NUMBER}" + PKG_VERSION="${MINVER_VERSION}" + fi + + echo "MINVER_VERSION=${MINVER_VERSION}" >> "$GITHUB_ENV" + echo "PKG_VERSION=${PKG_VERSION}" >> "$GITHUB_ENV" + echo "MinVer: ${MINVER_VERSION}" + echo "Package: ${PKG_VERSION}" + + - name: Prepare NuGet.config + run: | + cat > NuGet.config < + + + + + + + + + + + + + + + + + + + + + + + EOF + + - name: Restore + run: dotnet restore + + - name: Build + run: dotnet build --no-restore --configuration Release -p:MinVerVersionOverride=${MINVER_VERSION} + + - name: Test + run: dotnet test --no-build --configuration Release --verbosity normal -p:MinVerVersionOverride=${MINVER_VERSION} + + - name: Pack + run: dotnet pack --no-build --configuration Release --output ./artifacts -p:MinVerVersionOverride=${MINVER_VERSION} -p:PackageVersion=${PKG_VERSION} + + - name: Push to GitHub Packages + if: ${{ github.ref_name != 'main' }} + env: + GH_API_KEY: ${{ secrets.GH_PACKAGES_NUGET_API_KEY }} + run: dotnet nuget push ./artifacts/*.nupkg --source AuroraScienceHub --api-key $GH_API_KEY --skip-duplicate + + - name: Push to NuGet.org + if: ${{ github.ref_name == 'main' }} + env: + NUGET_URL: https://api.nuget.org/v3/index.json + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + run: dotnet nuget push ./artifacts/*.nupkg --source $NUGET_URL --api-key $NUGET_API_KEY --skip-duplicate diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4a6afa6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,356 @@ +## Ignore Visual Studio temporary files, build results, and +## files generated by popular Visual Studio add-ons. +## +## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore + +# Postgres mount folder +postgres-mnt/ + +# User-specific files +*.rsuser +*.suo +*.user +*.userosscache +*.sln.docstates + +# User-specific files (MonoDevelop/Xamarin Studio) +*.userprefs + +# Mono auto generated files +mono_crash.* + +# Build results +[Dd]ebug/ +[Dd]ebugPublic/ +[Rr]elease/ +[Rr]eleases/ +x64/ +x86/ +[Aa][Rr][Mm]/ +[Aa][Rr][Mm]64/ +bld/ +[Bb]in/ +[Oo]bj/ +[Ll]og/ +[Ll]ogs/ + +# Visual Studio 2015/2017 cache/options directory +.vs/ +# Uncomment if you have tasks that create the project's static files in wwwroot +#wwwroot/ + +# Visual Studio 2017 auto generated files +Generated\ Files/ + +# MSTest test Results +[Tt]est[Rr]esult*/ +[Bb]uild[Ll]og.* + +# NUnit +*.VisualState.xml +TestResult.xml +nunit-*.xml + +# Build Results of an ATL Project +[Dd]ebugPS/ +[Rr]eleasePS/ +dlldata.c + +# Benchmark Results +BenchmarkDotNet.Artifacts/ + +# .NET Core +project.lock.json +project.fragment.lock.json +artifacts/ + +# StyleCop +StyleCopReport.xml + +# Files built by Visual Studio +*_i.c +*_p.c +*_h.h +*.ilk +*.meta +*.obj +*.iobj +*.pch +*.pdb +*.ipdb +*.pgc +*.pgd +*.rsp +*.sbr +*.tlb +*.tli +*.tlh +*.tmp +*.tmp_proj +*_wpftmp.csproj +*.log +*.vspscc +*.vssscc +.builds +*.pidb +*.svclog +*.scc + +# Chutzpah Test files +_Chutzpah* + +# Visual C++ cache files +ipch/ +*.aps +*.ncb +*.opendb +*.opensdf +*.sdf +*.cachefile +*.VC.db +*.VC.VC.opendb + +# Visual Studio profiler +*.psess +*.vsp +*.vspx +*.sap + +# Visual Studio Trace Files +*.e2e + +# TFS 2012 Local Workspace +$tf/ + +# Guidance Automation Toolkit +*.gpState + +# ReSharper is a .NET coding add-in +_ReSharper*/ +*.[Rr]e[Ss]harper +*.DotSettings.user + +# TeamCity is a build add-in +_TeamCity* + +# DotCover is a Code Coverage Tool +*.dotCover + +# AxoCover is a Code Coverage Tool +.axoCover/* +!.axoCover/settings.json + +# Visual Studio code coverage results +*.coverage +*.coveragexml + +# NCrunch +_NCrunch_* +.*crunch*.local.xml +nCrunchTemp_* + +# MightyMoose +*.mm.* +AutoTest.Net/ + +# Web workbench (sass) +.sass-cache/ + +# Installshield output folder +[Ee]xpress/ + +# DocProject is a documentation generator add-in +DocProject/buildhelp/ +DocProject/Help/*.HxT +DocProject/Help/*.HxC +DocProject/Help/*.hhc +DocProject/Help/*.hhk +DocProject/Help/*.hhp +DocProject/Help/Html2 +DocProject/Help/html + +# Click-Once directory +publish/ + +# Publish Web Output +*.[Pp]ublish.xml +*.azurePubxml +# Note: Comment the next line if you want to checkin your web deploy settings, +# but database connection strings (with potential passwords) will be unencrypted +*.pubxml +*.publishproj + +# Microsoft Azure Web App publish settings. Comment the next line if you want to +# checkin your Azure Web App publish settings, but sensitive information contained +# in these scripts will be unencrypted +PublishScripts/ + +# NuGet Packages +*.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ +# Uncomment if necessary however generally it will be regenerated when needed +#!**/[Pp]ackages/repositories.config +# NuGet v3's project.json files produces more ignorable files +*.nuget.props +*.nuget.targets + +# Microsoft Azure Build Output +csx/ +*.build.csdef + +# Microsoft Azure Emulator +ecf/ +rcf/ + +# Windows Store app package directories and files +AppPackages/ +BundleArtifacts/ +Package.StoreAssociation.xml +_pkginfo.txt +*.appx +*.appxbundle +*.appxupload + +# Visual Studio cache files +# files ending in .cache can be ignored +*.[Cc]ache +# but keep track of directories ending in .cache +!?*.[Cc]ache/ + +# Others +ClientBin/ +~$* +*~ +*.dbmdl +*.dbproj.schemaview +*.jfm +*.pfx +*.publishsettings +orleans.codegen.cs + +# Including strong name files can present a security risk +# (https://github.com/github/gitignore/pull/2483#issue-259490424) +#*.snk + +# Since there are multiple workflows, uncomment next line to ignore bower_components +# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) +#bower_components/ + +# RIA/Silverlight projects +Generated_Code/ + +# Backup & report files from converting an old project file +# to a newer Visual Studio version. Backup files are not needed, +# because we have git ;-) +_UpgradeReport_Files/ +Backup*/ +UpgradeLog*.XML +UpgradeLog*.htm +ServiceFabricBackup/ +*.rptproj.bak + +# SQL Server files +*.mdf +*.ldf +*.ndf + +# Business Intelligence projects +*.rdl.data +*.bim.layout +*.bim_*.settings +*.rptproj.rsuser +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl + +# Microsoft Fakes +FakesAssemblies/ + +# GhostDoc plugin setting file +*.GhostDoc.xml + +# Node.js Tools for Visual Studio +.ntvs_analysis.dat +node_modules/ + +# Visual Studio 6 build log +*.plg + +# Visual Studio 6 workspace options file +*.opt + +# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) +*.vbw + +# Visual Studio LightSwitch build output +**/*.HTMLClient/GeneratedArtifacts +**/*.DesktopClient/GeneratedArtifacts +**/*.DesktopClient/ModelManifest.xml +**/*.Server/GeneratedArtifacts +**/*.Server/ModelManifest.xml +_Pvt_Extensions + +# Paket dependency manager +.paket/paket.exe +paket-files/ + +# FAKE - F# Make +.fake/ + +# CodeRush personal settings +.cr/personal + +# Python Tools for Visual Studio (PTVS) +__pycache__/ +*.pyc + +# Cake - Uncomment if you are using it +# tools/** +# !tools/packages.config + +# Tabs Studio +*.tss + +# Telerik's JustMock configuration file +*.jmconfig + +# BizTalk build output +*.btp.cs +*.btm.cs +*.odx.cs +*.xsd.cs + +# OpenCover UI analysis results +OpenCover/ + +# Azure Stream Analytics local run output +ASALocalRun/ + +# MSBuild Binary and Structured Log +*.binlog + +# NVidia Nsight GPU debugger configuration file +*.nvuser + +# MFractors (Xamarin productivity tool) working folder +.mfractor/ + +# Local History for Visual Studio +.localhistory/ + +# BeatPulse healthcheck temp database +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# JetBrains IDEA files +.idea/ diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..9d6bf4b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,51 @@ +# Changelog + +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.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.0.0] - 2026-02-05 + +### Added + +#### NoaaClient Package +- **ACE Client** - Integration with Advanced Composition Explorer (ACE) satellite data + - Magnetometer data access with real-time measurements + - Solar Wind Plasma data (SWEPAM) with comprehensive particle measurements + - Strongly-typed response models for all data types + +- **DSCOVR Client** - Deep Space Climate Observatory (DSCOVR) satellite integration + - Magnetometer data with multiple time ranges (2H, 1D, 3D, 7D) + - Solar Wind Plasma data with flexible time window selection + - Unified interface consistent with ACE client + +- **KP-Index Client** - Geomagnetic activity index monitoring + - Real-time nowcast data access + - 3-day forecast with hourly resolution + - 27-day forecast for long-term planning + - Parsed and validated response models + +#### Core Features +- HTTP client implementations with dependency injection support +- Configuration through Options pattern +- Strongly-typed response models for all API endpoints +- Built on .NET 10.0 with latest C# features +- Comprehensive unit test coverage +- Performance benchmarks for data parsing operations + +#### Documentation +- Complete API documentation with usage examples +- Package-specific README files +- Sample application demonstrating all client capabilities +- XML documentation for all public APIs + +#### Development Infrastructure +- EditorConfig for consistent code style +- Automated code formatting with `dotnet format` +- Warnings treated as errors for code quality +- Embedded debug symbols in NuGet packages +- GitHub Actions CI/CD pipeline for build and test + +[1.0.0]: https://github.com/Aurora-Science-Hub/Integrations/releases/tag/1.0.0 + diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..b16d8ca --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,59 @@ + + + + net10.0 + latest + False + enable + enable + + 9999 + True + True + $(NoWarn) + + + true + embedded + true + + + + AuroraScienceHub + $([System.DateTime]::Now.ToString(yyyy)) + + Copyright $(CurrentYear) © $(CompanyName). All rights reserved + $(CompanyName) + $(CompanyName) + + + + 1.0.0 + + + + + + + + + + + + + + + All + + + All + + + all + runtime; build; native; contentfiles; analyzers + + + diff --git a/Directory.Build.targets b/Directory.Build.targets new file mode 100644 index 0000000..52ee0fb --- /dev/null +++ b/Directory.Build.targets @@ -0,0 +1,19 @@ + + + + $(AssemblyName) + + + + + https://github.com/Aurora-Science-Hub/Integrations + https://github.com/Aurora-Science-Hub/Integrations.git + git + True + MIT + Framework;AuroraScienceHub;Science;Integrations + + + diff --git a/Directory.Packages.props b/Directory.Packages.props new file mode 100644 index 0000000..4b13d31 --- /dev/null +++ b/Directory.Packages.props @@ -0,0 +1,34 @@ + + + true + true + true + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + \ No newline at end of file diff --git a/Integrations.slnx b/Integrations.slnx new file mode 100644 index 0000000..06a7435 --- /dev/null +++ b/Integrations.slnx @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/LICENSE b/LICENSE index deb6ddd..2fb30de 100644 --- a/LICENSE +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2026 Aurora Science Hub +Copyright (c) 2025 Aurora Science Hub Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/NuGet.Config b/NuGet.Config new file mode 100644 index 0000000..25fdb36 --- /dev/null +++ b/NuGet.Config @@ -0,0 +1,20 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/README.md b/README.md index 533f660..da1250f 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,202 @@ -# Integrations -Integrations with third-party data sources +

+
+ + + + + +
+ Aurora Science Hub Integrations +
+

+
+ External data source integrations for space weather monitoring applications. +

+ +[![NuGet Version](https://img.shields.io/nuget/v/AuroraScienceHub.Integrations.NoaaClient?logo=nuget&label=NuGet)](https://www.nuget.org/packages/AuroraScienceHub.Integrations.NoaaClient/) +[![NuGet Downloads](https://img.shields.io/nuget/dt/AuroraScienceHub.Integrations.NoaaClient?logo=nuget&label=Downloads)](https://www.nuget.org/packages/AuroraScienceHub.Integrations.NoaaClient/) +[![](https://img.shields.io/badge/.NET-10.0-512BD4?logo=dotnet)](https://dotnet.microsoft.com/) +[![](https://img.shields.io/badge/C%23-13.0-239120?logo=csharp)](https://learn.microsoft.com/en-us/dotnet/csharp/) +
+[![Build & Test](https://github.com/Aurora-Science-Hub/Integrations/actions/workflows/dotnet.yml/badge.svg)](https://github.com/Aurora-Science-Hub/Integrations/actions/workflows/dotnet.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT) +[![GitHub Stars](https://img.shields.io/github/stars/Aurora-Science-Hub/Integrations?style=social)](https://github.com/Aurora-Science-Hub/Integrations) + +Getting Started • +Available Packages • +Architecture • +Development • +Code Style • +Testing • +License • +Contributing • +Links + +
+ + +## Getting Started + +All packages are distributed via NuGet and target **.NET 10.0**. Install individual packages as needed: + +```bash +dotnet add package AuroraScienceHub.Integrations.NoaaClient +``` + +### Quick Example + +```csharp +// Register services +builder.Services.AddNoaaClients(); + +// Inject and use clients +public class SpaceWeatherService +{ + private readonly IAceClient _aceClient; + + public SpaceWeatherService(IAceClient aceClient) + { + _aceClient = aceClient; + } + + public async Task> GetMagnetometerDataAsync() + { + return await _aceClient.GetMagnetometerDataAsync(); + } +} +``` + +Each package provides HTTP clients for accessing external data sources with strongly-typed response models and full dependency injection support. + +## Available Packages + +### NOAA Space Weather Integrations + +**[AuroraScienceHub.Integrations.NoaaClient](src/NoaaClient/)** - Comprehensive NOAA Space Weather data access + +| Component | Description | +|---------------------|-----------------------------------------------------------------------------------------------------------------------------| +| **ACE Client** | Advanced Composition Explorer satellite data - magnetometer and SWEPAM measurements | +| **DSCOVR Client** | Deep Space Climate Observatory data - solar wind and magnetic field measurements with multiple time ranges (2H, 1D, 3D, 7D) | +| **KP-Index Client** | Geomagnetic activity indices - nowcast and forecast data (3-day and 27-day) | + +See [detailed documentation](src/NoaaClient/README.md) for usage examples and API reference. + +## Architecture + +This repository provides production-ready HTTP clients for external space weather data sources with the following characteristics: + +- **Type-Safe Clients** - Strongly-typed HTTP clients with dependency injection support via `IHttpClientFactory` +- **Response Models** - Well-defined DTOs for all API responses with validation +- **Configuration** - Options pattern for configuring API endpoints and behavior +- **Modern .NET** - Built on .NET 10 with latest C# features and performance optimizations +- **Resilience** - Built-in retry policies and error handling +- **Testability** - Designed for easy unit testing with interface-based design + +### Design Principles + +- **Single Responsibility** - Each client focuses on a specific data source +- **Dependency Injection** - First-class DI support for ASP.NET Core and .NET applications +- **Performance** - Optimized parsing and minimal allocations +- **Extensibility** - Easy to extend with additional data sources + +## Development + +### Prerequisites + +- [.NET 10.0 SDK](https://dotnet.microsoft.com/download/dotnet/10.0) or later +- IDE: [Visual Studio 2025+](https://visualstudio.microsoft.com/), [Rider 2025+](https://www.jetbrains.com/rider/), or [VS Code](https://code.visualstudio.com/) + +### Building from Source + +```bash +# Clone the repository +git clone https://github.com/Aurora-Science-Hub/Integrations.git +cd Integrations + +# Restore dependencies +dotnet restore + +# Build the solution +dotnet build + +# Run tests +dotnet test +``` + +## Code Style + +The solution uses [EditorConfig](.editorconfig) based on [Azure SDK .NET](https://github.com/Azure/azure-sdk-for-net/blob/main/.editorconfig) to maintain consistent code style across all packages. + +### Formatting Commands + +```bash +# Format code before committing +dotnet format + +# Verify code style compliance +dotnet format --verify-no-changes +``` + +### Code Quality Standards + +- Nullable reference types enabled +- Warnings treated as errors +- Latest C# language version +- Code style enforcement in build +- Embedded debug symbols in packages + +## Testing + +Unit tests are located in the `tests/UnitTests/` directory. + +### Test Framework + +- **xUnit** - Test execution framework +- **Moq** - Mocking library for unit tests +- **Embedded Resources** - Test data stored as embedded resources for reproducibility + +### Running Tests + +```bash +# Run all tests +dotnet test + +# Run tests in watch mode +dotnet watch test --project tests/UnitTests/UnitTests.csproj +``` + +### Test Coverage + +The project maintains comprehensive test coverage for all data parsers and client implementations, ensuring data integrity and API reliability. + +## License + +This project is licensed under the MIT License. See [LICENSE](LICENSE) file for details. + +## Contributing + +We welcome contributions! When contributing to this repository: + +1. Follow the established code style (enforced by EditorConfig) +2. Run `dotnet format` before committing +3. Ensure all tests pass with `dotnet test` +4. Update relevant README files for your changes +5. Keep packages focused and loosely coupled +6. Add unit tests for new features + +### Reporting Issues + +Please report bugs and feature requests on the [GitHub Issues](https://github.com/Aurora-Science-Hub/Integrations/issues) page. + +## Links + +- **NuGet Package**: [AuroraScienceHub.Integrations.NoaaClient](https://www.nuget.org/packages/AuroraScienceHub.Integrations.NoaaClient/) +- **Source Code**: [GitHub Repository](https://github.com/Aurora-Science-Hub/Integrations) +- **Issue Tracker**: [GitHub Issues](https://github.com/Aurora-Science-Hub/Integrations/issues) +- **Changelog**: [CHANGELOG.md](CHANGELOG.md) +- **Release Notes**: [RELEASE_NOTES.md](docs/ReleaseNotes_Integrations_v1.md) + +## Acknowledgments + +This project integrates data from the **NOAA Space Weather Prediction Center**. We thank NOAA and NASA for providing free and open access to space weather data. diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/AuroraScienceHub.Integrations.Benchmarks.csproj b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/AuroraScienceHub.Integrations.Benchmarks.csproj new file mode 100644 index 0000000..11e3240 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/AuroraScienceHub.Integrations.Benchmarks.csproj @@ -0,0 +1,25 @@ + + + + AuroraScienceHub.Integrations.Benchmarks + Exe + false + + false + + + + + + + + + + + + + + + + + diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/IIntegrationsMarker.cs b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/IIntegrationsMarker.cs new file mode 100644 index 0000000..ce234d2 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/IIntegrationsMarker.cs @@ -0,0 +1,6 @@ +namespace AuroraScienceHub.Integrations.Benchmarks; + +/// +/// Marker interface for the Integrations benchmarks assembly. +/// +internal interface IBenchmarksMarker; diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/AceMagnetometerDataParserBenchmark.cs b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/AceMagnetometerDataParserBenchmark.cs new file mode 100644 index 0000000..57552ac --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/AceMagnetometerDataParserBenchmark.cs @@ -0,0 +1,31 @@ +using AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions; +using BenchmarkDotNet.Attributes; + +namespace AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Ace; + +/// +/// Benchmarks for (ACE). +/// +[MemoryDiagnoser(false)] +public class AceMagnetometerDataParserBenchmark +{ + const string ResourceName = "AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Ace.Samples.AceMagnetometerSample.txt"; + + private string _text = string.Empty; + + [GlobalSetup] + public async Task Setup() + { + await using var stream = typeof(IBenchmarksMarker).Assembly + .GetManifestResourceStream(ResourceName) + ?? throw new FileNotFoundException($"Resource '{ResourceName}' not found."); + using var reader = new StreamReader(stream); + _text = await reader.ReadToEndAsync(); + } + + [Benchmark] + public void Parse() + { + MagnetometerDataParser.Parse(_text); + } +} diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/AceSolarWindPlasmaDataParserBenchmark.cs b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/AceSolarWindPlasmaDataParserBenchmark.cs new file mode 100644 index 0000000..a8322dc --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/AceSolarWindPlasmaDataParserBenchmark.cs @@ -0,0 +1,31 @@ +using AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions; +using BenchmarkDotNet.Attributes; + +namespace AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Ace; + +/// +/// Benchmarks for (ACE). +/// +[MemoryDiagnoser(false)] +public class AceSolarWindPlasmaDataParserBenchmark +{ + const string ResourceName = "AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Ace.Samples.AceSwepamSample.txt"; + + private string _text = string.Empty; + + [GlobalSetup] + public async Task Setup() + { + await using var stream = typeof(IBenchmarksMarker).Assembly + .GetManifestResourceStream(ResourceName) + ?? throw new FileNotFoundException($"Resource '{ResourceName}' not found."); + using var reader = new StreamReader(stream); + _text = await reader.ReadToEndAsync(); + } + + [Benchmark] + public void Parse() + { + SolarWindPlasmaDataParser.Parse(_text); + } +} diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/Samples/AceMagnetometerSample.txt b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/Samples/AceMagnetometerSample.txt new file mode 100644 index 0000000..7f8c878 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/Samples/AceMagnetometerSample.txt @@ -0,0 +1,139 @@ +:Data_list: ace_mag_1m.txt +:Created: 2024 May 05 1046 UT +# Prepared by the U.S. Dept. of Commerce, NOAA, Space Weather Prediction Center +# Please send comments and suggestions to SWPC.Webmaster@noaa.gov +# +# Magnetometer values are in GSM coordinates. +# +# Units: Bx, By, Bz, Bt in nT +# Units: Latitude degrees +/- 90.0 +# Units: Longitude degrees 0.0 - 360.0 +# Status(S): 0 = nominal data, 1 to 8 = bad data record, 9 = no data +# Missing data values: -999.9 +# Source: ACE Satellite - Magnetometer +# +# 1-minute averaged Real-time Interplanetary Magnetic Field Values +# +# Modified Seconds +# UT Date Time Julian of the ---------------- GSM Coordinates --------------- +# YR MO DA HHMM Day Day S Bx By Bz Bt Lat. Long. +#------------------------------------------------------------------------------------ +2024 05 05 0846 60435 31560 0 -0.7 -0.8 -1.9 2.2 -60.5 225.8 +2024 05 05 0847 60435 31620 0 -0.7 -0.8 -1.7 2.0 -57.9 232.0 +2024 05 05 0848 60435 31680 0 -0.8 -1.0 -1.8 2.2 -53.6 229.6 +2024 05 05 0849 60435 31740 0 -0.8 -0.9 -1.8 2.2 -56.9 227.7 +2024 05 05 0850 60435 31800 0 -0.5 -0.6 -1.8 2.0 -65.9 229.0 +2024 05 05 0851 60435 31860 0 -0.5 -0.6 -1.6 1.8 -63.1 227.0 +2024 05 05 0852 60435 31920 0 -0.6 -0.6 -1.3 1.6 -55.9 226.6 +2024 05 05 0853 60435 31980 0 -0.6 -0.6 -1.1 1.4 -51.2 221.1 +2024 05 05 0854 60435 32040 0 -0.7 -0.5 -1.2 1.5 -55.0 217.1 +2024 05 05 0855 60435 32100 0 -0.6 -0.5 -1.4 1.6 -59.0 219.8 +2024 05 05 0856 60435 32160 0 -0.6 -0.6 -1.5 1.8 -59.9 227.2 +2024 05 05 0857 60435 32220 0 -0.6 -0.8 -1.6 1.8 -57.8 232.9 +2024 05 05 0858 60435 32280 0 -0.8 -0.1 -1.7 1.9 -64.4 185.2 +2024 05 05 0859 60435 32340 0 -0.8 0.0 -1.6 1.8 -62.6 177.4 +2024 05 05 0900 60435 32400 0 -0.9 -0.0 -1.6 1.9 -60.4 182.7 +2024 05 05 0901 60435 32460 0 -0.8 -0.1 -1.6 1.8 -62.3 184.7 +2024 05 05 0902 60435 32520 0 -0.7 0.2 -1.6 1.8 -66.5 162.9 +2024 05 05 0903 60435 32580 0 -0.7 0.1 -1.6 1.8 -65.2 169.9 +2024 05 05 0904 60435 32640 0 -0.8 0.4 -1.6 1.9 -60.0 151.5 +2024 05 05 0905 60435 32700 0 -1.0 0.3 -1.2 1.6 -50.0 161.8 +2024 05 05 0906 60435 32760 0 -1.0 0.7 -1.2 1.8 -42.8 144.4 +2024 05 05 0907 60435 32820 0 -1.0 0.9 -1.2 1.8 -42.9 138.0 +2024 05 05 0908 60435 32880 0 -1.1 0.8 -1.2 1.8 -40.6 141.3 +2024 05 05 0909 60435 32940 0 -0.9 0.7 -1.2 1.6 -46.1 142.0 +2024 05 05 0910 60435 33000 0 -0.8 0.8 -1.2 1.6 -47.0 134.6 +2024 05 05 0911 60435 33060 0 -1.0 0.6 -1.3 1.7 -48.5 146.2 +2024 05 05 0912 60435 33120 0 -1.0 0.7 -1.3 1.8 -45.5 146.4 +2024 05 05 0913 60435 33180 0 -1.0 0.9 -1.1 1.7 -38.8 138.7 +2024 05 05 0914 60435 33240 0 -1.0 1.1 -0.9 1.7 -30.1 130.0 +2024 05 05 0915 60435 33300 0 -1.0 1.3 -0.5 1.7 -18.8 127.6 +2024 05 05 0916 60435 33360 0 -0.8 1.3 -0.5 1.6 -18.8 122.4 +2024 05 05 0917 60435 33420 0 -0.8 1.3 -0.4 1.6 -16.1 120.4 +2024 05 05 0918 60435 33480 0 -1.0 1.2 -0.1 1.6 -3.6 129.5 +2024 05 05 0919 60435 33540 0 -1.1 1.2 0.0 1.6 0.7 133.9 +2024 05 05 0920 60435 33600 0 -1.2 1.1 0.1 1.7 3.0 138.6 +2024 05 05 0921 60435 33660 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0922 60435 33720 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0923 60435 33780 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0924 60435 33840 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0925 60435 33900 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0926 60435 33960 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0927 60435 34020 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0928 60435 34080 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0929 60435 34140 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0930 60435 34200 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0931 60435 34260 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0932 60435 34320 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0933 60435 34380 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0934 60435 34440 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0935 60435 34500 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0936 60435 34560 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0937 60435 34620 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0938 60435 34680 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0939 60435 34740 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0940 60435 34800 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0941 60435 34860 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0942 60435 34920 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0943 60435 34980 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0944 60435 35040 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0945 60435 35100 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0946 60435 35160 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0947 60435 35220 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0948 60435 35280 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0949 60435 35340 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0950 60435 35400 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0951 60435 35460 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0952 60435 35520 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0953 60435 35580 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0954 60435 35640 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0955 60435 35700 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0956 60435 35760 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0957 60435 35820 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0958 60435 35880 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 0959 60435 35940 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1000 60435 36000 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1001 60435 36060 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1002 60435 36120 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1003 60435 36180 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1004 60435 36240 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1005 60435 36300 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1006 60435 36360 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1007 60435 36420 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1008 60435 36480 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1009 60435 36540 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1010 60435 36600 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1011 60435 36660 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1012 60435 36720 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1013 60435 36780 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1014 60435 36840 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1015 60435 36900 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1016 60435 36960 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1017 60435 37020 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1018 60435 37080 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1019 60435 37140 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1020 60435 37200 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1021 60435 37260 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1022 60435 37320 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1023 60435 37380 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1024 60435 37440 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1025 60435 37500 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1026 60435 37560 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1027 60435 37620 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1028 60435 37680 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 +2024 05 05 1029 60435 37740 0 2.4 2.4 -0.3 3.4 -5.9 45.2 +2024 05 05 1030 60435 37800 0 2.4 2.6 -0.3 3.6 -5.2 46.6 +2024 05 05 1031 60435 37860 0 2.4 2.3 -0.1 3.3 -1.2 44.2 +2024 05 05 1032 60435 37920 0 2.6 2.6 -0.1 3.7 -1.6 44.4 +2024 05 05 1033 60435 37980 0 2.6 2.6 0.1 3.6 1.0 44.5 +2024 05 05 1034 60435 38040 0 2.6 2.5 0.1 3.6 2.3 43.1 +2024 05 05 1035 60435 38100 0 2.4 3.0 0.7 3.9 10.1 51.0 +2024 05 05 1036 60435 38160 0 2.3 3.0 1.0 3.9 14.1 52.3 +2024 05 05 1037 60435 38220 0 2.3 2.9 1.2 3.9 18.7 51.1 +2024 05 05 1038 60435 38280 0 2.2 2.8 1.4 3.9 21.8 52.1 +2024 05 05 1039 60435 38340 0 2.0 2.8 1.7 3.9 26.4 54.2 +2024 05 05 1040 60435 38400 0 2.1 2.6 1.9 3.8 29.8 51.4 +2024 05 05 1041 60435 38460 0 2.3 2.2 2.2 3.8 34.3 44.0 +2024 05 05 1042 60435 38520 0 2.3 1.8 2.2 3.7 36.3 38.0 +2024 05 05 1043 60435 38580 0 2.3 1.6 2.3 3.6 38.7 34.4 +2024 05 05 1044 60435 38640 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/Samples/AceSwepamSample.txt b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/Samples/AceSwepamSample.txt new file mode 100644 index 0000000..0053849 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Ace/Samples/AceSwepamSample.txt @@ -0,0 +1,137 @@ +:Data_list: ace_swepam_1m.txt +:Created: 2024 May 06 1857 UT +# Prepared by the U.S. Dept. of Commerce, NOAA, Space Weather Prediction Center +# Please send comments and suggestions to SWPC.Webmaster@noaa.gov +# +# Units: Proton density p/cc +# Units: Bulk speed km/s +# Units: Ion tempeture degrees K +# Status(S): 0 = nominal data, 1 to 8 = bad data record, 9 = no data +# Missing data values: Density and Speed = -9999.9, Temp. = -1.00e+05 +# Source: ACE Satellite - Solar Wind Electron Proton Alpha Monitor +# +# 1-minute averaged Real-time Bulk Parameters of the Solar Wind Plasma +# +# Modified Seconds ------------- Solar Wind ----------- +# UT Date Time Julian of the Proton Bulk Ion +# YR MO DA HHMM Day Day S Density Speed Temperature +#------------------------------------------------------------------------- +2024 05 06 1657 60436 61020 1 4.9 475.0 2.38e+05 +2024 05 06 1658 60436 61080 0 4.6 474.2 2.01e+05 +2024 05 06 1659 60436 61140 0 5.2 478.5 2.06e+05 +2024 05 06 1700 60436 61200 1 4.7 468.5 2.04e+05 +2024 05 06 1701 60436 61260 0 4.6 462.5 1.97e+05 +2024 05 06 1702 60436 61320 0 3.8 468.2 2.08e+05 +2024 05 06 1703 60436 61380 0 4.0 468.1 1.54e+05 +2024 05 06 1704 60436 61440 0 4.6 465.1 1.71e+05 +2024 05 06 1705 60436 61500 0 5.0 468.4 2.20e+05 +2024 05 06 1706 60436 61560 0 5.9 483.2 2.61e+05 +2024 05 06 1707 60436 61620 0 6.7 496.9 3.02e+05 +2024 05 06 1708 60436 61680 0 4.7 488.2 2.02e+05 +2024 05 06 1709 60436 61740 0 4.6 487.0 1.99e+05 +2024 05 06 1710 60436 61800 0 4.6 482.9 1.91e+05 +2024 05 06 1711 60436 61860 0 4.6 482.8 2.06e+05 +2024 05 06 1712 60436 61920 0 5.2 -9999.9 -1.00e+05 +2024 05 06 1713 60436 61980 0 5.2 486.9 2.03e+05 +2024 05 06 1714 60436 62040 1 4.9 488.1 1.97e+05 +2024 05 06 1715 60436 62100 1 5.1 487.6 2.21e+05 +2024 05 06 1716 60436 62160 0 5.2 482.7 2.05e+05 +2024 05 06 1717 60436 62220 0 5.7 482.2 2.17e+05 +2024 05 06 1718 60436 62280 0 5.5 480.1 2.28e+05 +2024 05 06 1719 60436 62340 0 5.8 492.0 3.00e+05 +2024 05 06 1720 60436 62400 1 5.5 498.1 3.00e+05 +2024 05 06 1721 60436 62460 1 4.5 469.5 1.89e+05 +2024 05 06 1722 60436 62520 0 4.2 459.7 1.53e+05 +2024 05 06 1723 60436 62580 0 6.5 484.4 2.17e+05 +2024 05 06 1724 60436 62640 0 8.0 485.9 2.23e+05 +2024 05 06 1725 60436 62700 0 4.6 477.3 2.04e+05 +2024 05 06 1726 60436 62760 0 4.1 463.5 1.86e+05 +2024 05 06 1727 60436 62820 0 5.5 489.7 2.70e+05 +2024 05 06 1728 60436 62880 0 5.0 470.3 2.16e+05 +2024 05 06 1729 60436 62940 0 5.9 501.0 2.86e+05 +2024 05 06 1730 60436 63000 0 4.5 463.6 1.84e+05 +2024 05 06 1731 60436 63060 1 5.8 479.3 1.94e+05 +2024 05 06 1732 60436 63120 0 5.5 491.9 2.26e+05 +2024 05 06 1733 60436 63180 1 5.4 476.3 2.02e+05 +2024 05 06 1734 60436 63240 0 5.0 508.8 2.69e+05 +2024 05 06 1735 60436 63300 0 3.9 517.0 2.98e+05 +2024 05 06 1736 60436 63360 0 3.7 512.9 3.04e+05 +2024 05 06 1737 60436 63420 0 4.2 509.3 3.12e+05 +2024 05 06 1738 60436 63480 0 4.0 503.9 2.86e+05 +2024 05 06 1739 60436 63540 0 3.1 490.9 2.05e+05 +2024 05 06 1740 60436 63600 1 4.4 488.4 1.37e+05 +2024 05 06 1741 60436 63660 0 7.6 519.9 1.96e+05 +2024 05 06 1742 60436 63720 0 4.0 492.0 2.75e+05 +2024 05 06 1743 60436 63780 1 3.4 475.0 1.16e+05 +2024 05 06 1744 60436 63840 0 2.7 477.5 1.84e+05 +2024 05 06 1745 60436 63900 9 -9999.9 -9999.9 -1.00e+05 +2024 05 06 1746 60436 63960 3 -9999.9 -9999.9 -1.00e+05 +2024 05 06 1747 60436 64020 1 1.7 443.4 4.39e+04 +2024 05 06 1748 60436 64080 0 4.0 485.9 2.27e+05 +2024 05 06 1749 60436 64140 0 4.8 497.0 2.64e+05 +2024 05 06 1750 60436 64200 0 4.8 498.0 2.62e+05 +2024 05 06 1751 60436 64260 0 4.7 494.9 2.59e+05 +2024 05 06 1752 60436 64320 0 5.1 497.4 2.51e+05 +2024 05 06 1753 60436 64380 0 5.2 499.9 2.41e+05 +2024 05 06 1754 60436 64440 0 5.0 498.2 2.41e+05 +2024 05 06 1755 60436 64500 1 4.8 497.5 2.54e+05 +2024 05 06 1756 60436 64560 0 4.6 496.1 2.66e+05 +2024 05 06 1757 60436 64620 0 4.9 492.7 2.67e+05 +2024 05 06 1758 60436 64680 0 5.2 499.6 2.84e+05 +2024 05 06 1759 60436 64740 0 5.1 512.4 3.22e+05 +2024 05 06 1800 60436 64800 0 4.6 507.0 3.06e+05 +2024 05 06 1801 60436 64860 0 5.2 491.5 2.51e+05 +2024 05 06 1802 60436 64920 0 4.6 454.5 1.86e+05 +2024 05 06 1803 60436 64980 0 0.7 407.5 7.84e+04 +2024 05 06 1804 60436 65040 0 3.9 485.8 2.63e+05 +2024 05 06 1805 60436 65100 1 4.8 488.0 2.38e+05 +2024 05 06 1806 60436 65160 0 4.0 462.0 1.70e+05 +2024 05 06 1807 60436 65220 0 4.4 486.6 2.41e+05 +2024 05 06 1808 60436 65280 0 5.0 510.4 3.35e+05 +2024 05 06 1809 60436 65340 0 4.9 503.4 3.46e+05 +2024 05 06 1810 60436 65400 0 4.4 472.2 2.56e+05 +2024 05 06 1811 60436 65460 0 4.7 465.3 2.23e+05 +2024 05 06 1812 60436 65520 0 5.6 501.5 3.23e+05 +2024 05 06 1813 60436 65580 0 5.6 500.6 3.25e+05 +2024 05 06 1814 60436 65640 0 5.1 488.8 3.29e+05 +2024 05 06 1815 60436 65700 0 4.3 468.7 2.33e+05 +2024 05 06 1816 60436 65760 0 4.3 511.3 3.59e+05 +2024 05 06 1817 60436 65820 0 4.3 524.7 3.25e+05 +2024 05 06 1818 60436 65880 0 4.3 -9999.9 -1.00e+05 +2024 05 06 1819 60436 65940 3 -9999.9 -9999.9 -1.00e+05 +2024 05 06 1820 60436 66000 0 2.1 516.0 1.61e+05 +2024 05 06 1821 60436 66060 0 2.8 538.6 2.18e+05 +2024 05 06 1822 60436 66120 0 2.7 538.1 2.10e+05 +2024 05 06 1823 60436 66180 0 2.6 538.3 2.23e+05 +2024 05 06 1824 60436 66240 0 2.2 526.2 1.76e+05 +2024 05 06 1825 60436 66300 0 3.7 541.5 1.43e+05 +2024 05 06 1826 60436 66360 0 4.5 556.9 1.54e+05 +2024 05 06 1827 60436 66420 0 2.8 543.2 1.61e+05 +2024 05 06 1828 60436 66480 0 2.2 540.7 1.84e+05 +2024 05 06 1829 60436 66540 0 2.5 536.2 1.91e+05 +2024 05 06 1830 60436 66600 0 2.4 543.3 1.79e+05 +2024 05 06 1831 60436 66660 0 2.2 545.2 1.88e+05 +2024 05 06 1832 60436 66720 0 2.1 543.4 1.75e+05 +2024 05 06 1833 60436 66780 0 3.2 544.2 1.59e+05 +2024 05 06 1834 60436 66840 0 3.1 540.1 1.54e+05 +2024 05 06 1835 60436 66900 0 3.6 546.6 1.73e+05 +2024 05 06 1836 60436 66960 0 2.5 538.2 1.64e+05 +2024 05 06 1837 60436 67020 0 2.2 522.6 1.43e+05 +2024 05 06 1838 60436 67080 0 2.4 511.6 1.31e+05 +2024 05 06 1839 60436 67140 0 2.7 537.3 2.11e+05 +2024 05 06 1840 60436 67200 0 2.8 549.6 2.52e+05 +2024 05 06 1841 60436 67260 0 2.7 540.9 2.18e+05 +2024 05 06 1842 60436 67320 0 2.4 514.7 1.55e+05 +2024 05 06 1843 60436 67380 0 2.3 516.2 1.72e+05 +2024 05 06 1844 60436 67440 0 2.5 556.3 2.66e+05 +2024 05 06 1845 60436 67500 0 2.4 533.0 2.39e+05 +2024 05 06 1846 60436 67560 0 2.9 521.0 2.25e+05 +2024 05 06 1847 60436 67620 0 1.6 486.8 1.52e+05 +2024 05 06 1848 60436 67680 0 2.2 545.4 2.46e+05 +2024 05 06 1849 60436 67740 0 3.4 547.5 2.12e+05 +2024 05 06 1850 60436 67800 0 6.7 550.4 1.98e+05 +2024 05 06 1851 60436 67860 0 6.8 550.5 1.97e+05 +2024 05 06 1852 60436 67920 3 -9999.9 -9999.9 -1.00e+05 +2024 05 06 1853 60436 67980 0 2.1 538.6 1.73e+05 +2024 05 06 1854 60436 68040 0 3.2 528.4 1.77e+05 +2024 05 06 1855 60436 68100 0 2.3 503.7 1.33e+05 diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/DscovrMagnetometerDataParserBenchmark.cs b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/DscovrMagnetometerDataParserBenchmark.cs new file mode 100644 index 0000000..6cc1821 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/DscovrMagnetometerDataParserBenchmark.cs @@ -0,0 +1,31 @@ +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Extensions; +using BenchmarkDotNet.Attributes; + +namespace AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Dscovr; + +/// +/// Benchmarks for (DSCOVR). +/// +[MemoryDiagnoser(false)] +public class DscovrMagnetometerDataParserBenchmark +{ + const string ResourceName = "AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Dscovr.Samples.DscovrMagnetometerSample.json"; + + private string _text = string.Empty; + + [GlobalSetup] + public async Task Setup() + { + await using var stream = typeof(IBenchmarksMarker).Assembly + .GetManifestResourceStream(ResourceName) + ?? throw new FileNotFoundException($"Resource '{ResourceName}' not found."); + using var reader = new StreamReader(stream); + _text = await reader.ReadToEndAsync(); + } + + [Benchmark] + public void Parse() + { + MagnetometerDataParser.Parse(_text); + } +} diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/DscovrSolarWindPlasmaDataParserBenchmark.cs b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/DscovrSolarWindPlasmaDataParserBenchmark.cs new file mode 100644 index 0000000..b9647f4 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/DscovrSolarWindPlasmaDataParserBenchmark.cs @@ -0,0 +1,31 @@ +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Extensions; +using BenchmarkDotNet.Attributes; + +namespace AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Dscovr; + +/// +/// Benchmarks for (DSCOVR). +/// +[MemoryDiagnoser(false)] +public class DscovrSolarWindPlasmaDataParserBenchmark +{ + const string ResourceName = "AuroraScienceHub.Integrations.Benchmarks.NoaaClient.Dscovr.Samples.DscovrSolarWindPlasmaSample.json"; + + private string _text = string.Empty; + + [GlobalSetup] + public async Task Setup() + { + await using var stream = typeof(IBenchmarksMarker).Assembly + .GetManifestResourceStream(ResourceName) + ?? throw new FileNotFoundException($"Resource '{ResourceName}' not found."); + using var reader = new StreamReader(stream); + _text = await reader.ReadToEndAsync(); + } + + [Benchmark] + public void Parse() + { + SolarWindPlasmaDataParser.Parse(_text); + } +} diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/Samples/DscovrMagnetometerSample.json b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/Samples/DscovrMagnetometerSample.json new file mode 100644 index 0000000..2072186 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/Samples/DscovrMagnetometerSample.json @@ -0,0 +1,1055 @@ +[ + [ + "time_tag", + "bx_gsm", + "by_gsm", + "bz_gsm", + "lon_gsm", + "lat_gsm", + "bt" + ], + [ + "2024-05-19 17:07:00.000", + "-2.15", + "8.51", + "-2.98", + "104.17", + "-18.76", + "9.27" + ], + [ + "2024-05-19 17:08:00.000", + "-2.38", + "8.31", + "-3.24", + "105.97", + "-20.54", + "9.23" + ], + [ + "2024-05-19 17:09:00.000", + "-2.15", + "8.59", + "-2.39", + "104.03", + "-15.13", + "9.17" + ], + [ + "2024-05-19 17:10:00.000", + "-1.89", + "8.83", + "-1.79", + "102.09", + "-11.24", + "9.20" + ], + [ + "2024-05-19 17:11:00.000", + "-1.45", + "8.89", + "-1.99", + "99.27", + "-12.45", + "9.22" + ], + [ + "2024-05-19 17:12:00.000", + "-0.84", + "9.07", + "-1.86", + "95.30", + "-11.57", + "9.29" + ], + [ + "2024-05-19 17:13:00.000", + "-1.08", + "9.13", + "-1.70", + "96.74", + "-10.50", + "9.35" + ], + [ + "2024-05-19 17:14:00.000", + "-0.77", + "9.23", + "-1.21", + "94.79", + "-7.46", + "9.34" + ], + [ + "2024-05-19 17:15:00.000", + "-0.58", + "9.23", + "-0.73", + "93.59", + "-4.53", + "9.28" + ], + [ + "2024-05-19 17:16:00.000", + "-1.63", + "8.76", + "-1.84", + "100.57", + "-11.66", + "9.09" + ], + [ + "2024-05-19 17:17:00.000", + "-2.04", + "8.42", + "-2.67", + "103.58", + "-17.11", + "9.07" + ], + [ + "2024-05-19 17:18:00.000", + "-2.26", + "8.32", + "-2.78", + "105.21", + "-17.90", + "9.06" + ], + [ + "2024-05-19 17:19:00.000", + "-2.22", + "8.39", + "-2.70", + "104.83", + "-17.27", + "9.09" + ], + [ + "2024-05-19 17:20:00.000", + "-2.29", + "8.36", + "-2.70", + "105.29", + "-17.27", + "9.08" + ], + [ + "2024-05-19 17:21:00.000", + "-2.52", + "8.23", + "-2.83", + "107.03", + "-18.19", + "9.06" + ], + [ + "2024-05-19 17:22:00.000", + "-2.64", + "8.14", + "-3.06", + "107.97", + "-19.65", + "9.09" + ], + [ + "2024-05-19 17:23:00.000", + "-2.61", + "7.99", + "-3.43", + "108.09", + "-22.23", + "9.08" + ], + [ + "2024-05-19 17:24:00.000", + "-2.71", + "8.02", + "-3.30", + "108.66", + "-21.29", + "9.09" + ], + [ + "2024-05-19 17:25:00.000", + "-2.36", + "7.79", + "-4.01", + "106.87", + "-26.25", + "9.07" + ], + [ + "2024-05-19 17:26:00.000", + "-2.67", + "8.07", + "-3.33", + "108.32", + "-21.39", + "9.13" + ], + [ + "2024-05-19 17:27:00.000", + "-2.86", + "8.22", + "-2.78", + "109.16", + "-17.72", + "9.14" + ], + [ + "2024-05-19 17:28:00.000", + "-2.80", + "8.19", + "-2.78", + "108.89", + "-17.79", + "9.09" + ], + [ + "2024-05-19 17:29:00.000", + "-2.59", + "8.12", + "-3.02", + "107.72", + "-19.54", + "9.04" + ], + [ + "2024-05-19 17:30:00.000", + "-2.38", + "8.29", + "-2.59", + "106.05", + "-16.72", + "9.00" + ], + [ + "2024-05-19 17:31:00.000", + "-2.18", + "8.38", + "-2.49", + "104.56", + "-16.03", + "9.01" + ], + [ + "2024-05-19 17:32:00.000", + "-2.28", + "8.41", + "-2.49", + "105.20", + "-15.98", + "9.06" + ], + [ + "2024-05-19 17:33:00.000", + "-2.24", + "8.40", + "-2.58", + "104.91", + "-16.52", + "9.07" + ], + [ + "2024-05-19 17:34:00.000", + "-2.38", + "8.29", + "-2.78", + "105.99", + "-17.89", + "9.06" + ], + [ + "2024-05-19 17:35:00.000", + "-2.92", + "7.80", + "-4.11", + "110.53", + "-26.27", + "9.29" + ], + [ + "2024-05-19 17:36:00.000", + "-3.23", + "7.34", + "-4.78", + "113.72", + "-30.81", + "9.34" + ], + [ + "2024-05-19 17:37:00.000", + "-2.94", + "7.47", + "-4.74", + "111.48", + "-30.56", + "9.33" + ], + [ + "2024-05-19 17:38:00.000", + "-2.86", + "7.37", + "-4.89", + "111.24", + "-31.75", + "9.30" + ], + [ + "2024-05-19 17:39:00.000", + "-2.79", + "7.34", + "-4.63", + "110.79", + "-30.55", + "9.11" + ], + [ + "2024-05-19 17:40:00.000", + "-2.54", + "7.52", + "-4.01", + "108.66", + "-26.82", + "8.89" + ], + [ + "2024-05-19 17:41:00.000", + "-2.48", + "8.00", + "-2.90", + "107.19", + "-19.12", + "8.87" + ], + [ + "2024-05-19 17:42:00.000", + "-2.40", + "8.31", + "-1.97", + "106.12", + "-12.85", + "8.87" + ], + [ + "2024-05-19 17:43:00.000", + "-2.32", + "8.44", + "-1.74", + "105.38", + "-11.22", + "8.93" + ], + [ + "2024-05-19 17:44:00.000", + "-2.21", + "8.49", + "-2.02", + "104.61", + "-12.94", + "9.01" + ], + [ + "2024-05-19 17:45:00.000", + "-2.13", + "8.57", + "-1.81", + "103.93", + "-11.58", + "9.01" + ], + [ + "2024-05-19 17:46:00.000", + "-2.24", + "8.60", + "-1.57", + "104.60", + "-10.02", + "9.03" + ], + [ + "2024-05-19 17:47:00.000", + "-2.43", + "8.58", + "-1.38", + "105.78", + "-8.77", + "9.02" + ], + [ + "2024-05-19 17:48:00.000", + "-2.45", + "8.60", + "-1.15", + "105.91", + "-7.33", + "9.02" + ], + [ + "2024-05-19 17:49:00.000", + "-2.17", + "8.61", + "-1.00", + "104.16", + "-6.41", + "8.93" + ], + [ + "2024-05-19 17:50:00.000", + "-2.24", + "8.49", + "-1.49", + "104.79", + "-9.62", + "8.91" + ], + [ + "2024-05-19 17:51:00.000", + "-2.18", + "8.49", + "-1.34", + "104.38", + "-8.67", + "8.87" + ], + [ + "2024-05-19 17:52:00.000", + "-1.66", + "8.59", + "-1.21", + "100.93", + "-7.85", + "8.83" + ], + [ + "2024-05-19 17:53:00.000", + "-2.09", + "8.57", + "-1.04", + "103.70", + "-6.71", + "8.89" + ], + [ + "2024-05-19 17:54:00.000", + "-2.00", + "8.63", + "-1.29", + "103.03", + "-8.31", + "8.96" + ], + [ + "2024-05-19 17:55:00.000", + "-1.97", + "8.67", + "-1.47", + "102.78", + "-9.41", + "9.02" + ], + [ + "2024-05-19 17:56:00.000", + "-1.92", + "8.69", + "-1.82", + "102.44", + "-11.55", + "9.09" + ], + [ + "2024-05-19 17:57:00.000", + "-1.98", + "8.71", + "-1.70", + "102.81", + "-10.77", + "9.10" + ], + [ + "2024-05-19 17:58:00.000", + "-2.04", + "8.72", + "-1.68", + "103.18", + "-10.64", + "9.11" + ], + [ + "2024-05-19 17:59:00.000", + "-1.80", + "8.74", + "-1.69", + "101.67", + "-10.75", + "9.08" + ], + [ + "2024-05-19 18:00:00.000", + "-1.80", + "8.74", + "-1.71", + "101.64", + "-10.82", + "9.09" + ], + [ + "2024-05-19 18:01:00.000", + "-1.63", + "8.72", + "-1.89", + "100.62", + "-12.03", + "9.07" + ], + [ + "2024-05-19 18:02:00.000", + "-1.86", + "8.73", + "-1.66", + "102.01", + "-10.53", + "9.08" + ], + [ + "2024-05-19 18:03:00.000", + "-1.89", + "8.73", + "-1.55", + "102.20", + "-9.88", + "9.06" + ], + [ + "2024-05-19 18:04:00.000", + "-2.13", + "8.70", + "-1.25", + "103.75", + "-7.97", + "9.05" + ], + [ + "2024-05-19 18:05:00.000", + "-2.19", + "8.67", + "-1.23", + "104.19", + "-7.81", + "9.03" + ], + [ + "2024-05-19 18:06:00.000", + "-2.33", + "8.60", + "-1.30", + "105.15", + "-8.28", + "9.00" + ], + [ + "2024-05-19 18:07:00.000", + "-2.56", + "8.58", + "-1.40", + "106.61", + "-8.86", + "9.06" + ], + [ + "2024-05-19 18:08:00.000", + "-2.63", + "8.62", + "-1.51", + "106.96", + "-9.54", + "9.13" + ], + [ + "2024-05-19 18:09:00.000", + "-2.86", + "8.55", + "-1.78", + "108.48", + "-11.16", + "9.19" + ], + [ + "2024-05-19 18:10:00.000", + "-2.50", + "8.70", + "-1.37", + "106.04", + "-8.59", + "9.16" + ], + [ + "2024-05-19 18:11:00.000", + "-2.43", + "8.76", + "-0.88", + "105.51", + "-5.55", + "9.14" + ], + [ + "2024-05-19 18:12:00.000", + "-2.13", + "8.82", + "-0.96", + "103.58", + "-6.02", + "9.12" + ], + [ + "2024-05-19 18:13:00.000", + "-2.09", + "8.85", + "-0.62", + "103.31", + "-3.89", + "9.11" + ], + [ + "2024-05-19 18:14:00.000", + "-2.29", + "8.78", + "-0.99", + "104.62", + "-6.21", + "9.13" + ], + [ + "2024-05-19 18:15:00.000", + "-2.77", + "8.53", + "-1.57", + "107.96", + "-9.93", + "9.11" + ], + [ + "2024-05-19 18:16:00.000", + "-2.74", + "8.55", + "-1.45", + "107.78", + "-9.16", + "9.09" + ], + [ + "2024-05-19 18:17:00.000", + "-2.76", + "8.21", + "-1.54", + "108.56", + "-10.07", + "8.80" + ], + [ + "2024-05-19 18:18:00.000", + "-3.25", + "8.05", + "-1.54", + "111.97", + "-10.08", + "8.81" + ], + [ + "2024-05-19 18:19:00.000", + "-3.05", + "8.24", + "-1.56", + "110.34", + "-10.09", + "8.92" + ], + [ + "2024-05-19 18:20:00.000", + "-2.58", + "8.35", + "-1.08", + "107.21", + "-7.07", + "8.80" + ], + [ + "2024-05-19 18:21:00.000", + "-1.79", + "8.59", + "0.20", + "101.78", + "1.32", + "8.77" + ], + [ + "2024-05-19 18:22:00.000", + "-0.80", + "7.98", + "1.27", + "95.74", + "8.97", + "8.12" + ], + [ + "2024-05-19 18:23:00.000", + "-0.79", + "7.96", + "1.71", + "95.67", + "12.08", + "8.18" + ], + [ + "2024-05-19 18:24:00.000", + "-0.75", + "7.81", + "2.16", + "95.51", + "15.41", + "8.14" + ], + [ + "2024-05-19 18:25:00.000", + "-0.40", + "7.94", + "1.46", + "92.91", + "10.38", + "8.08" + ], + [ + "2024-05-19 18:26:00.000", + "0.12", + "8.02", + "1.03", + "89.11", + "7.32", + "8.09" + ], + [ + "2024-05-19 18:27:00.000", + "0.10", + "7.88", + "1.65", + "89.30", + "11.83", + "8.05" + ], + [ + "2024-05-19 18:28:00.000", + "0.61", + "7.76", + "2.01", + "85.50", + "14.51", + "8.04" + ], + [ + "2024-05-19 18:29:00.000", + "0.68", + "7.59", + "2.56", + "84.88", + "18.56", + "8.04" + ], + [ + "2024-05-19 18:30:00.000", + "0.67", + "7.22", + "3.02", + "84.68", + "22.64", + "7.85" + ], + [ + "2024-05-19 18:31:00.000", + "0.53", + "7.18", + "2.77", + "85.80", + "21.03", + "7.71" + ], + [ + "2024-05-19 18:32:00.000", + "0.42", + "7.21", + "2.70", + "86.65", + "20.50", + "7.72" + ], + [ + "2024-05-19 18:33:00.000", + "0.55", + "7.17", + "2.72", + "85.63", + "20.69", + "7.69" + ], + [ + "2024-05-19 18:34:00.000", + "0.68", + "7.11", + "2.70", + "84.52", + "20.70", + "7.63" + ], + [ + "2024-05-19 18:35:00.000", + "0.58", + "7.07", + "2.74", + "85.28", + "21.15", + "7.60" + ], + [ + "2024-05-19 18:36:00.000", + "0.74", + "6.90", + "3.18", + "83.85", + "24.64", + "7.63" + ], + [ + "2024-05-19 18:37:00.000", + "0.88", + "6.83", + "3.25", + "82.64", + "25.26", + "7.62" + ], + [ + "2024-05-19 18:38:00.000", + "-0.02", + "7.30", + "2.40", + "90.13", + "18.24", + "7.68" + ], + [ + "2024-05-19 18:39:00.000", + "-0.12", + "7.57", + "1.81", + "90.93", + "13.47", + "7.78" + ], + [ + "2024-05-19 18:41:00.000", + "0.56", + "7.00", + "2.52", + "85.46", + "19.77", + "7.47" + ], + [ + "2024-05-19 18:42:00.000", + "0.97", + "6.54", + "3.10", + "81.60", + "25.16", + "7.30" + ], + [ + "2024-05-19 18:43:00.000", + "1.39", + "6.21", + "3.45", + "77.36", + "28.45", + "7.24" + ], + [ + "2024-05-19 18:44:00.000", + "1.40", + "6.24", + "3.63", + "77.35", + "29.58", + "7.36" + ], + [ + "2024-05-19 18:45:00.000", + "1.29", + "6.33", + "3.58", + "78.49", + "29.01", + "7.39" + ], + [ + "2024-05-19 18:46:00.000", + "1.64", + "6.05", + "4.04", + "74.87", + "32.82", + "7.46" + ], + [ + "2024-05-19 18:47:00.000", + "1.66", + "5.92", + "4.15", + "74.35", + "34.03", + "7.42" + ], + [ + "2024-05-19 18:48:00.000", + "1.62", + "5.93", + "4.18", + "74.74", + "34.25", + "7.43" + ], + [ + "2024-05-19 18:49:00.000", + "1.58", + "5.94", + "4.20", + "75.13", + "34.37", + "7.45" + ], + [ + "2024-05-19 18:50:00.000", + "1.68", + "5.75", + "4.41", + "73.70", + "36.39", + "7.44" + ], + [ + "2024-05-19 18:51:00.000", + "1.88", + "5.54", + "4.55", + "71.27", + "37.88", + "7.41" + ], + [ + "2024-05-19 18:52:00.000", + "1.91", + "5.40", + "4.66", + "70.51", + "39.16", + "7.38" + ], + [ + "2024-05-19 18:53:00.000", + "1.81", + "5.39", + "4.68", + "71.45", + "39.48", + "7.36" + ], + [ + "2024-05-19 18:54:00.000", + "1.94", + "5.34", + "4.70", + "70.00", + "39.59", + "7.37" + ], + [ + "2024-05-19 18:55:00.000", + "2.09", + "5.29", + "4.75", + "68.42", + "39.88", + "7.41" + ], + [ + "2024-05-19 18:56:00.000", + "1.52", + "5.95", + "4.34", + "75.62", + "35.29", + "7.52" + ], + [ + "2024-05-19 18:57:00.000", + "1.63", + "6.78", + "2.64", + "76.52", + "20.73", + "7.46" + ], + [ + "2024-05-19 18:58:00.000", + "1.37", + "6.79", + "2.76", + "78.55", + "21.76", + "7.46" + ], + [ + "2024-05-19 18:59:00.000", + "1.85", + "5.73", + "4.07", + "72.10", + "34.04", + "7.26" + ], + [ + "2024-05-19 19:00:00.000", + "2.22", + "5.14", + "5.02", + "66.60", + "41.92", + "7.52" + ], + [ + "2024-05-19 19:01:00.000", + "0.00", + "7.63", + "1.83", + "90.00", + "13.52", + "7.84" + ], + [ + "2024-05-19 19:02:00.000", + "0.49", + "5.12", + "4.04", + "84.53", + "38.13", + "6.55" + ], + [ + "2024-05-19 19:03:00.000", + "1.03", + "4.41", + "5.01", + "76.82", + "47.86", + "6.75" + ] +] \ No newline at end of file diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/Samples/DscovrSolarWindPlasmaSample.json b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/Samples/DscovrSolarWindPlasmaSample.json new file mode 100644 index 0000000..f9a8971 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/NoaaClient/Dscovr/Samples/DscovrSolarWindPlasmaSample.json @@ -0,0 +1,710 @@ +[ + [ + "time_tag", + "density", + "speed", + "temperature" + ], + [ + "2024-05-20 05:51:00.000", + "2.18", + "393.9", + "36967" + ], + [ + "2024-05-20 05:52:00.000", + "2.60", + "397.4", + "44618" + ], + [ + "2024-05-20 05:53:00.000", + "2.93", + "401.7", + "50222" + ], + [ + "2024-05-20 05:54:00.000", + "2.84", + "402.1", + "49766" + ], + [ + "2024-05-20 05:55:00.000", + "2.96", + "403.9", + "54639" + ], + [ + "2024-05-20 05:56:00.000", + "3.14", + "403.6", + "52343" + ], + [ + "2024-05-20 05:57:00.000", + "2.85", + "398.1", + "42124" + ], + [ + "2024-05-20 05:58:00.000", + "3.53", + "413.5", + "79750" + ], + [ + "2024-05-20 05:59:00.000", + "4.04", + "418.4", + "94403" + ], + [ + "2024-05-20 06:00:00.000", + "3.59", + "410.5", + "70257" + ], + [ + "2024-05-20 06:01:00.000", + "1.50", + "384.0", + "19719" + ], + [ + "2024-05-20 06:02:00.000", + "3.66", + "411.4", + "72816" + ], + [ + "2024-05-20 06:03:00.000", + "4.60", + "415.7", + "101427" + ], + [ + "2024-05-20 06:04:00.000", + "3.53", + "408.2", + "68041" + ], + [ + "2024-05-20 06:05:00.000", + "2.09", + "390.1", + "30888" + ], + [ + "2024-05-20 06:06:00.000", + "3.72", + "407.5", + "80549" + ], + [ + "2024-05-20 06:07:00.000", + "4.15", + "409.8", + "81947" + ], + [ + "2024-05-20 06:08:00.000", + "3.37", + "401.8", + "59059" + ], + [ + "2024-05-20 06:09:00.000", + "3.23", + "397.8", + "51255" + ], + [ + "2024-05-20 06:10:00.000", + "3.57", + "401.8", + "63432" + ], + [ + "2024-05-20 06:11:00.000", + "3.35", + "403.1", + "66628" + ], + [ + "2024-05-20 06:12:00.000", + "3.00", + "400.6", + "60324" + ], + [ + "2024-05-20 06:13:00.000", + "3.11", + "405.1", + "71978" + ], + [ + "2024-05-20 06:14:00.000", + "2.73", + "400.8", + "67951" + ], + [ + "2024-05-20 06:15:00.000", + "3.49", + "408.4", + "104656" + ], + [ + "2024-05-20 06:16:00.000", + "3.42", + "407.5", + "82744" + ], + [ + "2024-05-20 06:17:00.000", + "2.77", + "397.2", + "52336" + ], + [ + "2024-05-20 06:18:00.000", + "2.81", + "398.7", + "70191" + ], + [ + "2024-05-20 06:19:00.000", + "3.01", + "407.5", + "90879" + ], + [ + "2024-05-20 06:20:00.000", + "3.10", + "407.9", + "116975" + ], + [ + "2024-05-20 06:21:00.000", + "1.45", + "383.5", + "77142" + ], + [ + "2024-05-20 06:22:00.000", + "0.81", + "371.2", + "65281" + ], + [ + "2024-05-20 06:23:00.000", + "0.73", + "368.9", + "64933" + ], + [ + "2024-05-20 06:24:00.000", + "0.60", + "362.8", + "80676" + ], + [ + "2024-05-20 06:25:00.000", + "0.60", + "362.8", + "80676" + ], + [ + "2024-05-20 06:26:00.000", + "0.59", + "361.2", + "50890" + ], + [ + "2024-05-20 06:27:00.000", + "0.58", + "357.7", + "50132" + ], + [ + "2024-05-20 06:28:00.000", + "0.66", + "355.8", + "55686" + ], + [ + "2024-05-20 06:29:00.000", + "0.81", + "362.3", + "76460" + ], + [ + "2024-05-20 06:30:00.000", + "0.67", + "361.9", + "96422" + ], + [ + "2024-05-20 06:31:00.000", + "0.60", + "364.8", + "89577" + ], + [ + "2024-05-20 06:32:00.000", + "0.82", + "360.3", + "52768" + ], + [ + "2024-05-20 06:33:00.000", + "0.83", + "356.3", + "27929" + ], + [ + "2024-05-20 06:34:00.000", + "0.85", + "355.4", + "34516" + ], + [ + "2024-05-20 06:35:00.000", + "0.73", + "359.0", + "27452" + ], + [ + "2024-05-20 06:36:00.000", + "0.55", + "345.7", + "34811" + ], + [ + "2024-05-20 06:37:00.000", + "0.66", + "353.0", + "35145" + ], + [ + "2024-05-20 06:38:00.000", + "0.80", + "362.2", + "81131" + ], + [ + "2024-05-20 06:39:00.000", + "0.73", + "364.4", + "102429" + ], + [ + "2024-05-20 06:40:00.000", + "0.59", + "359.5", + "77349" + ], + [ + "2024-05-20 06:41:00.000", + "0.70", + "359.2", + "56324" + ], + [ + "2024-05-20 06:42:00.000", + "0.78", + "359.4", + "42705" + ], + [ + "2024-05-20 06:43:00.000", + "0.79", + "359.6", + "39952" + ], + [ + "2024-05-20 06:44:00.000", + "0.82", + "360.5", + "55489" + ], + [ + "2024-05-20 06:45:00.000", + "0.73", + "355.2", + "52068" + ], + [ + "2024-05-20 06:46:00.000", + "0.86", + "359.1", + "37104" + ], + [ + "2024-05-20 06:47:00.000", + "0.82", + "360.3", + "28677" + ], + [ + "2024-05-20 06:48:00.000", + "0.87", + "355.8", + "41992" + ], + [ + "2024-05-20 06:49:00.000", + "0.81", + "350.6", + "28254" + ], + [ + "2024-05-20 06:50:00.000", + "0.97", + "359.2", + "43745" + ], + [ + "2024-05-20 06:51:00.000", + "0.97", + "359.4", + "34669" + ], + [ + "2024-05-20 06:52:00.000", + "0.88", + "360.0", + "46481" + ], + [ + "2024-05-20 06:53:00.000", + "0.99", + "350.0", + "44946" + ], + [ + "2024-05-20 06:54:00.000", + "0.88", + "353.5", + "42201" + ], + [ + "2024-05-20 06:55:00.000", + "0.59", + "357.3", + "32648" + ], + [ + "2024-05-20 06:56:00.000", + "0.47", + "358.2", + "28213" + ], + [ + "2024-05-20 06:57:00.000", + "0.68", + "360.6", + "85443" + ], + [ + "2024-05-20 06:58:00.000", + "0.73", + "361.9", + "77703" + ], + [ + "2024-05-20 06:59:00.000", + "0.68", + "366.0", + "77807" + ], + [ + "2024-05-20 07:00:00.000", + "0.56", + "369.1", + "85208" + ], + [ + "2024-05-20 07:01:00.000", + "0.65", + "368.5", + "77965" + ], + [ + "2024-05-20 07:02:00.000", + "0.66", + "370.6", + "89680" + ], + [ + "2024-05-20 07:03:00.000", + "0.64", + "364.3", + "81903" + ], + [ + "2024-05-20 07:04:00.000", + "0.75", + "363.1", + "68216" + ], + [ + "2024-05-20 07:05:00.000", + "0.75", + "363.8", + "39073" + ], + [ + "2024-05-20 07:07:00.000", + "0.70", + "356.7", + "61982" + ], + [ + "2024-05-20 07:08:00.000", + "0.86", + "358.0", + "65453" + ], + [ + "2024-05-20 07:09:00.000", + "0.86", + "355.4", + "32897" + ], + [ + "2024-05-20 07:10:00.000", + "0.86", + "354.8", + "24625" + ], + [ + "2024-05-20 07:11:00.000", + "0.78", + "346.6", + "28705" + ], + [ + "2024-05-20 07:12:00.000", + "0.86", + "349.2", + "35810" + ], + [ + "2024-05-20 07:13:00.000", + "0.88", + "355.6", + "44336" + ], + [ + "2024-05-20 07:14:00.000", + "0.84", + "358.8", + "55106" + ], + [ + "2024-05-20 07:15:00.000", + "0.80", + "360.0", + "68165" + ], + [ + "2024-05-20 07:16:00.000", + "0.72", + "359.8", + "62071" + ], + [ + "2024-05-20 07:17:00.000", + "0.68", + "353.5", + "34599" + ], + [ + "2024-05-20 07:18:00.000", + "0.70", + "356.7", + "32300" + ], + [ + "2024-05-20 07:19:00.000", + "0.78", + "355.7", + "37786" + ], + [ + "2024-05-20 07:20:00.000", + "0.50", + "353.9", + "14791" + ], + [ + "2024-05-20 07:21:00.000", + "0.45", + "348.3", + "22851" + ], + [ + "2024-05-20 07:22:00.000", + "1.00", + "352.2", + "47357" + ], + [ + "2024-05-20 07:23:00.000", + "0.79", + "356.5", + "60315" + ], + [ + "2024-05-20 07:24:00.000", + "0.82", + "356.8", + "52856" + ], + [ + "2024-05-20 07:25:00.000", + "0.91", + "355.8", + "46918" + ], + [ + "2024-05-20 07:26:00.000", + "0.84", + "361.9", + "61155" + ], + [ + "2024-05-20 07:27:00.000", + "0.73", + "365.1", + "51227" + ], + [ + "2024-05-20 07:28:00.000", + "0.66", + "364.0", + "66302" + ], + [ + "2024-05-20 07:29:00.000", + "0.64", + "363.0", + "81527" + ], + [ + "2024-05-20 07:30:00.000", + "0.60", + "360.7", + "78187" + ], + [ + "2024-05-20 07:31:00.000", + "0.61", + "359.8", + "67269" + ], + [ + "2024-05-20 07:32:00.000", + "0.62", + "359.4", + "68661" + ], + [ + "2024-05-20 07:33:00.000", + "0.65", + "360.9", + "109078" + ], + [ + "2024-05-20 07:34:00.000", + "0.64", + "362.4", + "128036" + ], + [ + "2024-05-20 07:35:00.000", + "0.45", + "369.5", + "137350" + ], + [ + "2024-05-20 07:36:00.000", + "0.41", + "359.2", + "59948" + ], + [ + "2024-05-20 07:37:00.000", + "0.41", + "370.3", + "144742" + ], + [ + "2024-05-20 07:38:00.000", + "0.51", + "363.8", + "109968" + ], + [ + "2024-05-20 07:39:00.000", + "0.51", + "363.2", + "69724" + ], + [ + "2024-05-20 07:40:00.000", + "0.52", + "364.3", + "114640" + ], + [ + "2024-05-20 07:41:00.000", + "0.41", + "366.1", + "151774" + ], + [ + "2024-05-20 07:42:00.000", + "0.34", + "368.5", + "152852" + ], + [ + "2024-05-20 07:43:00.000", + "0.36", + "355.7", + "72072" + ], + [ + "2024-05-20 07:44:00.000", + "0.26", + "351.2", + "23548" + ], + [ + "2024-05-20 07:45:00.000", + "0.25", + "358.0", + "78363" + ], + [ + "2024-05-20 07:46:00.000", + "0.30", + "364.6", + "119952" + ], + [ + "2024-05-20 07:47:00.000", + "0.29", + "362.7", + "105402" + ], + [ + "2024-05-20 07:48:00.000", + "0.27", + "358.1", + "83667" + ] +] \ No newline at end of file diff --git a/benchmarks/AuroraScienceHub.Integrations.Benchmarks/Program.cs b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/Program.cs new file mode 100644 index 0000000..b797c92 --- /dev/null +++ b/benchmarks/AuroraScienceHub.Integrations.Benchmarks/Program.cs @@ -0,0 +1,9 @@ +using BenchmarkDotNet.Running; + +namespace AuroraScienceHub.Integrations.Benchmarks; + +public class Program +{ + public static void Main(string[] args) + => BenchmarkSwitcher.FromAssembly(typeof(Program).Assembly).Run(args); +} diff --git a/docs/ReleaseNotes_Integrations_v1.md b/docs/ReleaseNotes_Integrations_v1.md new file mode 100644 index 0000000..5757a3a --- /dev/null +++ b/docs/ReleaseNotes_Integrations_v1.md @@ -0,0 +1,105 @@ +# Release Notes - Version 1.0.0 + +**Release Date:** February 5, 2026 + +## Initial Release + +We're excited to announce the first stable release of **Aurora Science Hub Integrations** - a comprehensive suite of .NET clients for accessing space weather data from NOAA data sources. + +## Available Packages + +### AuroraScienceHub.Integrations.NoaaClient v1.0.0 + +The flagship package providing seamless integration with NOAA Space Weather Prediction Center data sources. + +## Key Features + +### ACE Spacecraft Integration +Access real-time data from the Advanced Composition Explorer satellite positioned at the L1 Lagrange point: +- **Magnetometer Data** - Interplanetary magnetic field measurements (Bx, By, Bz components) +- **Solar Wind Plasma Data** - Comprehensive particle measurements including density, speed, and temperature + +### DSCOVR Spacecraft Integration +Retrieve data from the Deep Space Climate Observatory with flexible time ranges: +- **Multiple Time Windows** - 2-hour, 1-day, 3-day, and 7-day data access +- **Magnetometer Measurements** - High-precision magnetic field data +- **Solar Wind Parameters** - Critical inputs for space weather forecasting + +### KP-Index Data Access +Monitor and forecast geomagnetic activity with three specialized endpoints: +- **Nowcast** - Real-time geomagnetic activity observations +- **3-Day Forecast** - Short-term predictions with 3-hour resolution +- **27-Day Forecast** - Extended outlook for planning and analysis + +## Technical Highlights + +- **Built on .NET 10.0** - Leveraging the latest .NET features and performance improvements +- **Strongly-Typed APIs** - Compile-time safety with comprehensive response models +- **Dependency Injection** - Native support for ASP.NET Core and modern .NET applications +- **High Performance** - Optimized parsing algorithms validated through extensive benchmarking +- **Production Ready** - Comprehensive unit tests and real-world validation + +## Getting Started + +Install via NuGet: +```bash +dotnet add package AuroraScienceHub.Integrations.NoaaClient +``` + +Register services in your application: +```csharp +builder.Services.AddNoaaClients(); +``` + +Start accessing space weather data: +```csharp +// Inject clients via dependency injection +public class SpaceWeatherService +{ + private readonly IAceClient _aceClient; + private readonly IDscovrClient _dscovrClient; + private readonly IKpIndexClient _kpIndexClient; + + public SpaceWeatherService( + IAceClient aceClient, + IDscovrClient dscovrClient, + IKpIndexClient kpIndexClient) + { + _aceClient = aceClient; + _dscovrClient = dscovrClient; + _kpIndexClient = kpIndexClient; + } + + public async Task GetLatestDataAsync() + { + return await _aceClient.GetMagnetometerDataAsync(); + } +} +``` + +## Documentation + +- [Main README](README.md) - Repository overview and development guidelines +- [NoaaClient Documentation](src/NoaaClient/README.md) - Package-specific documentation +- [Sample Application](samples/NoaaClientSample/) - Complete working examples +- [CHANGELOG](CHANGELOG.md) - Detailed change history + +## Resources + +- **GitHub Repository:** https://github.com/Aurora-Science-Hub/Integrations +- **NuGet Package:** https://www.nuget.org/packages/AuroraScienceHub.Integrations.NoaaClient +- **Issue Tracker:** https://github.com/Aurora-Science-Hub/Integrations/issues +- **Discussions:** https://github.com/Aurora-Science-Hub/Integrations/discussions + +## Acknowledgments + +This project integrates data from the NOAA Space Weather Prediction Center. We are grateful to NOAA and NASA for providing free and open access to space weather data. + +## License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +--- + +**Questions or feedback?** Open an issue on [GitHub](https://github.com/Aurora-Science-Hub/Integrations/issues) or start a [discussion](https://github.com/Aurora-Science-Hub/Integrations/discussions). + diff --git a/docs/logo/In_black.png b/docs/logo/In_black.png new file mode 100644 index 0000000..2ec306b Binary files /dev/null and b/docs/logo/In_black.png differ diff --git a/docs/logo/In_icon.png b/docs/logo/In_icon.png new file mode 100644 index 0000000..204de9e Binary files /dev/null and b/docs/logo/In_icon.png differ diff --git a/docs/logo/In_white.png b/docs/logo/In_white.png new file mode 100644 index 0000000..2094481 Binary files /dev/null and b/docs/logo/In_white.png differ diff --git a/global.json b/global.json new file mode 100644 index 0000000..895d51b --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "10.0.100", + "rollForward": "patch", + "allowPrerelease": false + } +} diff --git a/samples/NoaaClientSample/NoaaClientSample.csproj b/samples/NoaaClientSample/NoaaClientSample.csproj new file mode 100644 index 0000000..9161629 --- /dev/null +++ b/samples/NoaaClientSample/NoaaClientSample.csproj @@ -0,0 +1,23 @@ + + + + Exe + AuroraScienceHub.Integrations.Samples.NoaaClientSample + + + + + + + + + + PreserveNewest + + + + + + + + diff --git a/samples/NoaaClientSample/OutputFormatter.cs b/samples/NoaaClientSample/OutputFormatter.cs new file mode 100644 index 0000000..792aa56 --- /dev/null +++ b/samples/NoaaClientSample/OutputFormatter.cs @@ -0,0 +1,201 @@ +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; +using Spectre.Console; +using AceMagnetometerRecord = AuroraScienceHub.Integrations.NoaaClient.Ace.Responses.MagnetometerRecord; +using DscovrMagnetometerRecord = AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses.MagnetometerRecord; +using AceSolarWindPlasmaRecord = AuroraScienceHub.Integrations.NoaaClient.Ace.Responses.SolarWindPlasmaRecord; +using DscovrSolarWindPlasmaRecord = AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses.SolarWindPlasmaRecord; + +namespace AuroraScienceHub.Integrations.Samples.NoaaClientSample; + +/// +/// Formats NOAA API data for console output using Spectre.Console +/// +internal static class OutputFormatter +{ + /// + /// Displays ACE magnetometer data in table format + /// + public static void DisplayAceMagnetometer(IReadOnlyList data, int limit) + { + var table = new Table() + .Border(TableBorder.Rounded) + .Title($"[yellow]ACE Magnetometer Data[/] [dim](showing {Math.Min(limit, data.Count)} of {data.Count})[/]") + .AddColumn("[cyan]DateTime[/]") + .AddColumn("[cyan]Status[/]") + .AddColumn("[cyan]Bx (nT)[/]") + .AddColumn("[cyan]By (nT)[/]") + .AddColumn("[cyan]Bz (nT)[/]") + .AddColumn("[cyan]Bt (nT)[/]") + .AddColumn("[cyan]Lat[/]") + .AddColumn("[cyan]Lon[/]"); + + foreach (var record in data.TakeLast(limit)) + { + table.AddRow( + record.DateTime.ToString("yyyy-MM-dd HH:mm:ss"), + record.Status.ToString(), + Format(record.Bx), + Format(record.By), + Format(record.Bz), + Format(record.Bt), + Format(record.Latitude), + Format(record.Longitude)); + } + + AnsiConsole.Write(table); + } + + /// + /// Displays ACE SWEPAM data in table format + /// + public static void DisplayAceSwepam(IReadOnlyList data, int limit) + { + var table = new Table() + .Border(TableBorder.Rounded) + .Title($"[yellow]ACE SWEPAM Data[/] [dim](showing {Math.Min(limit, data.Count)} of {data.Count})[/]") + .AddColumn("[cyan]DateTime[/]") + .AddColumn("[cyan]Status[/]") + .AddColumn("[cyan]Proton Density[/]") + .AddColumn("[cyan]Bulk Speed[/]") + .AddColumn("[cyan]Ion Temp[/]"); + + foreach (var record in data.TakeLast(limit)) + { + table.AddRow( + record.DateTime.ToString("yyyy-MM-dd HH:mm:ss"), + record.Status.ToString(), + Format(record.ProtonDensity), + Format(record.BulkSpeed), + Format(record.IonTemperature)); + } + + AnsiConsole.Write(table); + } + + /// + /// Displays DSCOVR magnetometer data in table format + /// + public static void DisplayDscovrMagnetometer(IReadOnlyList data, int limit) + { + var table = new Table() + .Border(TableBorder.Rounded) + .Title($"[yellow]DSCOVR Magnetometer Data[/] [dim](showing {Math.Min(limit, data.Count)} of {data.Count})[/]") + .AddColumn("[cyan]DateTime[/]") + .AddColumn("[cyan]Bx (nT)[/]") + .AddColumn("[cyan]By (nT)[/]") + .AddColumn("[cyan]Bz (nT)[/]") + .AddColumn("[cyan]Bt (nT)[/]") + .AddColumn("[cyan]Lat[/]") + .AddColumn("[cyan]Lon[/]"); + + foreach (var record in data.TakeLast(limit)) + { + table.AddRow( + record.DateTime.ToString("yyyy-MM-dd HH:mm:ss"), + Format(record.Bx), + Format(record.By), + Format(record.Bz), + Format(record.Bt), + Format(record.Latitude), + Format(record.Longitude)); + } + + AnsiConsole.Write(table); + } + + /// + /// Displays DSCOVR solar wind plasma data in table format + /// + public static void DisplayDscovrPlasma(IReadOnlyList data, int limit) + { + var table = new Table() + .Border(TableBorder.Rounded) + .Title($"[yellow]DSCOVR Solar Wind Plasma Data[/] [dim](showing {Math.Min(limit, data.Count)} of {data.Count})[/]") + .AddColumn("[cyan]DateTime[/]") + .AddColumn("[cyan]Proton Density[/]") + .AddColumn("[cyan]Bulk Speed[/]") + .AddColumn("[cyan]Ion Temp[/]"); + + foreach (var record in data.TakeLast(limit)) + { + table.AddRow( + record.DateTime.ToString("yyyy-MM-dd HH:mm:ss"), + Format(record.ProtonDensity), + Format(record.BulkSpeed), + Format(record.IonTemperature)); + } + + AnsiConsole.Write(table); + } + + /// + /// Displays KP index forecast data in table format + /// + public static void DisplayKpForecast(IReadOnlyList data, int limit, Func formatter) + { + var title = typeof(T).Name.Contains("27Day") ? "27-Day Forecast" : "3-Day Forecast"; + var table = new Table() + .Border(TableBorder.Rounded) + .Title($"[yellow]KP Index {title}[/] [dim](showing {Math.Min(limit, data.Count)} of {data.Count})[/]") + .AddColumn("[cyan]Date/Time[/]") + .AddColumn("[cyan]KP Index[/]") + .AddColumn("[cyan]Activity Level[/]"); + + var records = typeof(T).Name.Contains("27Day") ? data.Take(limit) : data.TakeLast(limit); + foreach (var record in records) + { + var (date, kp, level) = formatter(record); + table.AddRow(date, kp, GetColoredLevel(level)); + } + + AnsiConsole.Write(table); + } + + /// + /// Displays KP index nowcast data in table format + /// + public static void DisplayKpNowcast(IReadOnlyList data, int limit) + { + var table = new Table() + .Border(TableBorder.Rounded) + .Title($"[yellow]KP Index Nowcast[/] [dim](showing {Math.Min(limit, data.Count)} of {data.Count})[/]") + .AddColumn("[cyan]DateTime[/]") + .AddColumn("[cyan]KP Index[/]") + .AddColumn("[cyan]Stations[/]") + .AddColumn("[cyan]Activity Level[/]"); + + foreach (var record in data.TakeLast(limit)) + { + var level = GetActivityLevel((int)record.KpIndex); + table.AddRow( + record.DateTime.ToString("yyyy-MM-dd HH:mm:ss"), + record.KpIndex.ToString("F2"), + record.StationsCount.ToString(), + GetColoredLevel(level)); + } + + AnsiConsole.Write(table); + } + + // Helper methods + private static string Format(float? value) => value?.ToString("F2") ?? "[dim]N/A[/]"; + + private static string GetActivityLevel(int kpIndex) => kpIndex switch + { + <= 2 => "Low", + <= 4 => "Moderate", + <= 6 => "Elevated", + <= 8 => "High", + _ => "Extreme" + }; + + private static string GetColoredLevel(string level) => level switch + { + "Low" => "[green]Low[/]", + "Moderate" => "[yellow]Moderate[/]", + "Elevated" => "[orange1]Elevated[/]", + "High" => "[red]High[/]", + "Extreme" => "[bold red]Extreme[/]", + _ => level + }; +} diff --git a/samples/NoaaClientSample/Program.cs b/samples/NoaaClientSample/Program.cs new file mode 100644 index 0000000..2c10240 --- /dev/null +++ b/samples/NoaaClientSample/Program.cs @@ -0,0 +1,186 @@ +using AuroraScienceHub.Integrations.NoaaClient; +using AuroraScienceHub.Integrations.NoaaClient.Ace; +using AuroraScienceHub.Integrations.NoaaClient.Dscovr; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex; +using AuroraScienceHub.Integrations.Samples.NoaaClientSample; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Hosting; +using Spectre.Console; + +// Setup DI and configuration +var builder = Host.CreateApplicationBuilder(args); +builder.Configuration.AddJsonFile("appsettings.json", optional: false); +builder.Services.AddNoaaClients(); +var host = builder.Build(); + +// Get NOAA clients from DI +var aceClient = host.Services.GetRequiredService(); +var dscovrClient = host.Services.GetRequiredService(); +var kpIndexClient = host.Services.GetRequiredService(); + +// Display header +AnsiConsole.Write(new FigletText("NOAA Client").Color(Color.Blue)); +AnsiConsole.MarkupLine("[dim]Interactive sample application for NOAA API clients[/]\n"); + +// Interactive menu loop +while (true) +{ + var choice = AnsiConsole.Prompt( + new SelectionPrompt() + .Title("[green]Select an option:[/]") + .PageSize(10) + .AddChoices( + "ACE Magnetometer", + "ACE SWEPAM", + "DSCOVR Magnetometer", + "DSCOVR Solar Wind Plasma", + "KP Index 27-Day Forecast", + "KP Index 3-Day Forecast", + "KP Index Nowcast", + new string('-', 30), + "Execute All Requests", + "Exit")); + + if (choice == "Exit") break; + if (choice.StartsWith("---")) continue; + + AnsiConsole.WriteLine(); + + try + { + await AnsiConsole.Status() + .Spinner(Spinner.Known.Dots) + .SpinnerStyle(Style.Parse("green")) + .StartAsync("Fetching data...", async ctx => + { + await (choice switch + { + "ACE Magnetometer" => FetchAndDisplay(async () => + { + var data = await aceClient.GetMagnetometerDataAsync(CancellationToken.None); + OutputFormatter.DisplayAceMagnetometer(data, 10); + }), + "ACE SWEPAM" => FetchAndDisplay(async () => + { + var data = await aceClient.GetSwepamDataAsync(CancellationToken.None); + OutputFormatter.DisplayAceSwepam(data, 10); + }), + "DSCOVR Magnetometer" => FetchAndDisplay(async () => + { + var data = await dscovrClient.GetMagnetometerData2HAsync(CancellationToken.None); + OutputFormatter.DisplayDscovrMagnetometer(data, 10); + }), + "DSCOVR Solar Wind Plasma" => FetchAndDisplay(async () => + { + var data = await dscovrClient.GetSolarWindPlasmaData2HAsync(CancellationToken.None); + OutputFormatter.DisplayDscovrPlasma(data, 10); + }), + "KP Index 27-Day Forecast" => FetchAndDisplay(async () => + { + var data = await kpIndexClient.GetKpIndex27DayForecastAsync(CancellationToken.None); + OutputFormatter.DisplayKpForecast(data, 10, r => ( + r.Date.ToString("yyyy-MM-dd"), + r.KpIndex.ToString(), + GetActivityLevel(r.KpIndex))); + }), + "KP Index 3-Day Forecast" => FetchAndDisplay(async () => + { + var data = await kpIndexClient.GetKpIndex3DayForecastAsync(CancellationToken.None); + OutputFormatter.DisplayKpForecast(data, 10, r => ( + r.DateTime.ToString("yyyy-MM-dd HH:mm:ss"), + r.KpIndex.ToString("F2"), + GetActivityLevel((int)r.KpIndex))); + }), + "KP Index Nowcast" => FetchAndDisplay(async () => + { + var data = await kpIndexClient.GetKpIndexNowcastAsync(CancellationToken.None); + OutputFormatter.DisplayKpNowcast(data, 10); + }), + "Execute All Requests" => ExecuteAllRequests(), + _ => Task.CompletedTask + }); + }); + } + catch (Exception ex) + { + AnsiConsole.MarkupLine($"[red]Error: {ex.Message.EscapeMarkup()}[/]"); + } + + AnsiConsole.WriteLine(); + AnsiConsole.MarkupLine("[dim]Press any key to continue...[/]"); + Console.ReadKey(true); + AnsiConsole.Clear(); +} + +AnsiConsole.MarkupLine("[blue]Goodbye![/]"); +return 0; + +async Task ExecuteAllRequests() +{ + AnsiConsole.MarkupLine("[yellow]Executing all NOAA API requests...[/]\n"); + + var tasks = new (string name, Func action)[] + { + ("ACE Magnetometer", async () => + { + var data = await aceClient.GetMagnetometerDataAsync(CancellationToken.None); + OutputFormatter.DisplayAceMagnetometer(data, 5); + }), + ("ACE SWEPAM", async () => + { + var data = await aceClient.GetSwepamDataAsync(CancellationToken.None); + OutputFormatter.DisplayAceSwepam(data, 5); + }), + ("DSCOVR Magnetometer", async () => + { + var data = await dscovrClient.GetMagnetometerData2HAsync(CancellationToken.None); + OutputFormatter.DisplayDscovrMagnetometer(data, 5); + }), + ("DSCOVR Solar Wind", async () => + { + var data = await dscovrClient.GetSolarWindPlasmaData2HAsync(CancellationToken.None); + OutputFormatter.DisplayDscovrPlasma(data, 5); + }), + ("KP Index Nowcast", async () => + { + var data = await kpIndexClient.GetKpIndexNowcastAsync(CancellationToken.None); + OutputFormatter.DisplayKpNowcast(data, 5); + }) + }; + + await AnsiConsole.Progress() + .AutoClear(false) + .Columns( + new TaskDescriptionColumn(), + new ProgressBarColumn(), + new PercentageColumn(), + new SpinnerColumn()) + .StartAsync(async ctx => + { + for (int i = 0; i < tasks.Length; i++) + { + var progressTask = ctx.AddTask($"[green]{tasks[i].name}[/]"); + await tasks[i].action(); + progressTask.Increment(100); + AnsiConsole.WriteLine(); + } + }); + + AnsiConsole.MarkupLine("\n[green]✓ All requests completed successfully![/]"); +} + +// Helper to fetch and display data +static async Task FetchAndDisplay(Func action) => await action(); + +// Helper method for activity level +static string GetActivityLevel(int kpIndex) => kpIndex switch +{ + <= 2 => "Low", + <= 4 => "Moderate", + <= 6 => "Elevated", + <= 8 => "High", + _ => "Extreme" +}; + + diff --git a/samples/NoaaClientSample/README.md b/samples/NoaaClientSample/README.md new file mode 100644 index 0000000..925e01c --- /dev/null +++ b/samples/NoaaClientSample/README.md @@ -0,0 +1,133 @@ +# NOAA Client Sample + +An interactive console application demonstrating the NOAA API clients using Spectre.Console. + +## Overview + +This sample demonstrates all available NOAA client methods through an interactive menu-driven interface: + +### ACE (Advanced Composition Explorer) Spacecraft +- **Magnetometer Data** - 1-minute averaged magnetic field measurements +- **SWEPAM Data** - Solar Wind Electron Proton Alpha Monitor measurements + +### DSCOVR (Deep Space Climate Observatory) Spacecraft +- **Magnetometer Data** - Magnetic field measurements (2-hour window) +- **Solar Wind Plasma Data** - Solar wind measurements (2-hour window) + +### KP Index +- **27-Day Forecast** - Extended KP index forecast +- **3-Day Forecast** - Short-term KP index forecast +- **Nowcast** - Real-time KP index measurements + +## Features + +**Interactive Menu** - Easy-to-use selection interface powered by Spectre.Console + +**Formatted Tables** - Beautiful table output with colors and borders + +**Activity Levels** - KP index displayed with color-coded activity levels: +- Low (0-2) - Green +- Moderate (3-4) - Yellow +- Elevated (5-6) - Orange +- High (7-8) - Red +- Extreme (9+) - Bold Red + +**Bulk Execution** - Execute all API requests with a single menu option + +**Loading Indicators** - Visual feedback with spinners and progress bars + +## Requirements + +- .NET 8.0 or higher +- Internet connection (for NOAA API access) + +## Configuration + +The application uses `appsettings.json` for configuration: + +```json +{ + "Noaa": { + "ServerUrl": "https://services.swpc.noaa.gov" + } +} +``` + +## Usage + +Run the application: + +```bash +dotnet run +``` + +You'll see an interactive menu with the following options: + +1. **ACE Magnetometer** - Fetch ACE spacecraft magnetometer data +2. **ACE SWEPAM** - Fetch ACE solar wind plasma data +3. **DSCOVR Magnetometer** - Fetch DSCOVR magnetometer data +4. **DSCOVR Solar Wind Plasma** - Fetch DSCOVR plasma data +5. **KP Index 27-Day Forecast** - View extended KP forecast +6. **KP Index 3-Day Forecast** - View short-term KP forecast +7. **KP Index Nowcast** - View current KP measurements +8. **Execute All Requests** - Run all queries sequentially with progress tracking +9. **Exit** - Quit the application + +Simply use arrow keys to navigate and press Enter to select an option. + +## Architecture + +The application follows best practices: + +- **Dependency Injection** - Uses Microsoft.Extensions.DependencyInjection +- **Configuration Management** - Leverages Microsoft.Extensions.Configuration +- **Typed HTTP Clients** - Registered via AddHttpClient +- **Async/Await** - All API calls are asynchronous +- **Error Handling** - Graceful error handling with user-friendly messages +- **Separation of Concerns** - UI formatting separated in OutputFormatter class + +## Project Structure + +``` +NoaaClientSample/ +├── Program.cs # Main application with interactive menu +├── OutputFormatter.cs # Table formatting utilities +├── appsettings.json # Application configuration +└── NoaaClientSample.csproj # Project file +``` + +## Dependencies + +- **AuroraScienceHub.Integrations.Noaa** - NOAA API client library +- **Microsoft.Extensions.Hosting** - For DI and configuration +- **Spectre.Console** - For beautiful console UI + +## Sample Output + +``` + _ _ ___ _ _ ____ _ _ _ + | \ | |/ _ \ / \ / \ / ___| (_) ___ _ __ | |_ + | \| | | | |/ _ \/ _ \ | | | | |/ _ \ '_ \| __| + | |\ | |_| / ___ \ ___ \ | |___| | | __/ | | | |_ + |_| \_|\___/_/ \_\ \_\ \____|_|_|\___|_| |_|\__| + +Interactive sample application for NOAA API clients + +? Select an option: › +❯ ACE Magnetometer + ACE SWEPAM + DSCOVR Magnetometer + DSCOVR Solar Wind Plasma + KP Index 27-Day Forecast + KP Index 3-Day Forecast + KP Index Nowcast + ────────────────────────────── + Execute All Requests + Exit +``` + +## License + +See LICENSE file in the repository root. + + diff --git a/samples/NoaaClientSample/appsettings.json b/samples/NoaaClientSample/appsettings.json new file mode 100644 index 0000000..b89b4ad --- /dev/null +++ b/samples/NoaaClientSample/appsettings.json @@ -0,0 +1,5 @@ +{ + "Noaa": { + "ServerUrl": "https://services.swpc.noaa.gov" + } +} diff --git a/src/Directory.Build.props b/src/Directory.Build.props new file mode 100644 index 0000000..f435be9 --- /dev/null +++ b/src/Directory.Build.props @@ -0,0 +1,30 @@ + + + + + + True + + + + Aleksei Ermilov;Alexander Nikolaev;Aurora Science Hub + README.md + https://github.com/Aurora-Science-Hub/Integrations + https://github.com/Aurora-Science-Hub/Integrations.git + git + MIT + true + In_icon.png + Copyright 2024-$([System.DateTime]::Now.ToString(yyyy)) Aurora Science Hub + + + + + + + + + + + + diff --git a/src/NoaaClient/Ace/AceClient.cs b/src/NoaaClient/Ace/AceClient.cs new file mode 100644 index 0000000..7c0751b --- /dev/null +++ b/src/NoaaClient/Ace/AceClient.cs @@ -0,0 +1,39 @@ +using AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; +using Microsoft.Extensions.Options; + +namespace AuroraScienceHub.Integrations.NoaaClient.Ace; + +internal sealed class AceClient : IAceClient +{ + private readonly HttpClient _httpClient; + private readonly Uri _baseUrl; + + public AceClient( + HttpClient httpClient, + IOptions options) + { + _httpClient = httpClient; + _baseUrl = options.Value.RequiredServerUrl; + } + + public async Task> GetMagnetometerDataAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "text/ace-magnetometer.txt"); + var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + return MagnetometerDataParser.Parse(text); + } + + public async Task> GetSwepamDataAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "text/ace-swepam.txt"); + var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + return SolarWindPlasmaDataParser.Parse(text); + } +} diff --git a/src/NoaaClient/Ace/Extensions/MagnetometerDataParser.cs b/src/NoaaClient/Ace/Extensions/MagnetometerDataParser.cs new file mode 100644 index 0000000..3ca0201 --- /dev/null +++ b/src/NoaaClient/Ace/Extensions/MagnetometerDataParser.cs @@ -0,0 +1,67 @@ +using AuroraScienceHub.Framework.Utilities.System; +using AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions; + +// ACE Magnetometer data example: +// # Status(S): 0 = nominal data, 1 to 8 = bad data record, 9 = no data +// # Missing data values: -999.9 +// # Modified Seconds +// # UT Date Time Julian of the ---------------- GSM Coordinates --------------- +// # YR MO DA HHMM Day Day S Bx By Bz Bt Lat. Long. +// #------------------------------------------------------------------------------------ +// 2024 05 05 0846 60435 31560 0 -0.7 -0.8 -1.9 2.2 -60.5 225.8 +internal static class MagnetometerDataParser +{ + private const string MissingDataValue = "-999.9"; + + public static IReadOnlyList Parse(string text) + { + var records = new List(); + var lines = text.SplitLines(); + Span fieldsRange = stackalloc Range[32]; + + foreach (var line in lines) + { + var trimmedLine = line.Line.Trim(); + if (trimmedLine.Length == 0 || trimmedLine[0] == '#') + { + continue; + } + var rangesCount = trimmedLine.Split(fieldsRange, ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // check if line contains data + if (rangesCount != 13 || !trimmedLine[fieldsRange[0]].IsParsableAsInt()) + { + continue; + } + + var record = new MagnetometerRecord + ( + DateTime: new DateTime( + year: trimmedLine[fieldsRange[0]].ParseIntInvariant(), + month: trimmedLine[fieldsRange[1]].ParseIntInvariant(), + day: trimmedLine[fieldsRange[2]].ParseIntInvariant(), + hour: trimmedLine[fieldsRange[3]][..2].ParseIntInvariant(), + minute: trimmedLine[fieldsRange[3]][2..].ParseIntInvariant(), + second: 0, + DateTimeKind.Utc), + Status: trimmedLine[fieldsRange[6]].ParseIntInvariant(), + Bx: GetFloatOrNull(trimmedLine[fieldsRange[7]]), + By: GetFloatOrNull(trimmedLine[fieldsRange[8]]), + Bz: GetFloatOrNull(trimmedLine[fieldsRange[9]]), + Bt: GetFloatOrNull(trimmedLine[fieldsRange[10]]), + Latitude: GetFloatOrNull(trimmedLine[fieldsRange[11]]), + Longitude: GetFloatOrNull(trimmedLine[fieldsRange[12]]) + ); + records.Add(record); + } + + return records; + } + + private static float? GetFloatOrNull(ReadOnlySpan value) + => value.SequenceEqual(MissingDataValue) + ? null + : value.ParseFloatInvariant(); +} diff --git a/src/NoaaClient/Ace/Extensions/SolarWindPlasmaDataParser.cs b/src/NoaaClient/Ace/Extensions/SolarWindPlasmaDataParser.cs new file mode 100644 index 0000000..05ac4a7 --- /dev/null +++ b/src/NoaaClient/Ace/Extensions/SolarWindPlasmaDataParser.cs @@ -0,0 +1,70 @@ +using AuroraScienceHub.Framework.Utilities.System; +using AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions; + +// # 1-minute averaged Real-time Bulk Parameters of the Solar Wind Plasma +// # Status(S): 0 = nominal data, 1 to 8 = bad data record, 9 = no data +// # Missing data values: Density and Speed = -9999.9, Temp. = -1.00e+05 +// # Modified Seconds ------------- Solar Wind ----------- +// # UT Date Time Julian of the Proton Bulk Ion +// # YR MO DA HHMM Day Day S Density Speed Temperature +// #------------------------------------------------------------------------- +// 2024 05 06 1657 60436 61020 1 4.9 475.0 2.38e+05 +internal static class SolarWindPlasmaDataParser +{ + private const string MissingDataValue = "-9999.9"; + private const string MissingTemperatureValue = "-1.00e+05"; + + public static IReadOnlyList Parse(string text) + { + var records = new List(); + var lines = text.SplitLines(); + Span fieldsRange = stackalloc Range[32]; + + foreach (var line in lines) + { + var trimmedLine = line.Line.Trim(); + if (trimmedLine.Length == 0 || trimmedLine[0] == '#') + { + continue; + } + var rangesCount = trimmedLine.Split(fieldsRange, ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + // check if line contains data + if (rangesCount != 10 || !trimmedLine[fieldsRange[0]].IsParsableAsInt()) + { + continue; + } + + var record = new SolarWindPlasmaRecord + ( + DateTime: new DateTime( + year: trimmedLine[fieldsRange[0]].ParseIntInvariant(), + month: trimmedLine[fieldsRange[1]].ParseIntInvariant(), + day: trimmedLine[fieldsRange[2]].ParseIntInvariant(), + hour: trimmedLine[fieldsRange[3]][..2].ParseIntInvariant(), + minute: trimmedLine[fieldsRange[3]][2..].ParseIntInvariant(), + second: 0, + DateTimeKind.Utc), + Status: trimmedLine[fieldsRange[6]].ParseIntInvariant(), + ProtonDensity: GetFloatOrNull(trimmedLine[fieldsRange[7]]), + BulkSpeed: GetFloatOrNull(trimmedLine[fieldsRange[8]]), + IonTemperature: GetTemperatureOrNull(trimmedLine[fieldsRange[9]]) + ); + records.Add(record); + } + + return records; + } + + private static float? GetFloatOrNull(ReadOnlySpan value) + => value.SequenceEqual(MissingDataValue) + ? null + : value.ParseFloatInvariant(); + + private static float? GetTemperatureOrNull(ReadOnlySpan value) + => value.SequenceEqual(MissingTemperatureValue) + ? null + : value.ParseFloatInvariant(); +} diff --git a/src/NoaaClient/Ace/IAceClient.cs b/src/NoaaClient/Ace/IAceClient.cs new file mode 100644 index 0000000..1d10d15 --- /dev/null +++ b/src/NoaaClient/Ace/IAceClient.cs @@ -0,0 +1,19 @@ +using AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.Ace; + +/// +/// ACE Spacecraft client +/// +public interface IAceClient +{ + /// + /// Get ACE Magnetometer data + /// + Task> GetMagnetometerDataAsync(CancellationToken cancellationToken); + + /// + /// Get ACE Solar Wind Electron Proton Alpha Monitor data + /// + Task> GetSwepamDataAsync(CancellationToken cancellationToken); +} diff --git a/src/NoaaClient/Ace/Responses/MagnetometerRecord.cs b/src/NoaaClient/Ace/Responses/MagnetometerRecord.cs new file mode 100644 index 0000000..f4653d9 --- /dev/null +++ b/src/NoaaClient/Ace/Responses/MagnetometerRecord.cs @@ -0,0 +1,22 @@ +namespace AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; + +/// +/// 1-minute averaged Real-time Interplanetary Magnetic Field Values +/// +/// Date and time of record (UTC) +/// Data Status. 0 = nominal data 1 to 8 = bad data record 9 = no data +/// IMF GSM x-component +/// IMF GSM y-component +/// IMF GSM z-component +/// IMF module +/// Spacecraft latitude +/// Spacecraft longitude +public record MagnetometerRecord( + DateTime DateTime, + int Status, + float? Bx, + float? By, + float? Bz, + float? Bt, + float? Latitude, + float? Longitude); diff --git a/src/NoaaClient/Ace/Responses/SolarWindPlasmaRecord.cs b/src/NoaaClient/Ace/Responses/SolarWindPlasmaRecord.cs new file mode 100644 index 0000000..793f3a9 --- /dev/null +++ b/src/NoaaClient/Ace/Responses/SolarWindPlasmaRecord.cs @@ -0,0 +1,17 @@ +namespace AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; + +/// +/// 1-minute averaged Real-time Bulk Parameters of the Solar Wind Plasma +/// +/// Date and time of record (UTC) +/// Data Status. 0 = nominal data 1 to 8 = bad data record 9 = no data +/// Solar Wind proton density +/// Solar Wind plasma speed +/// Solar Wind Ion Temp +public record SolarWindPlasmaRecord( + DateTime DateTime, + int Status, + float? ProtonDensity, + float? BulkSpeed, + float? IonTemperature +); diff --git a/src/NoaaClient/Dscovr/DscovrClient.cs b/src/NoaaClient/Dscovr/DscovrClient.cs new file mode 100644 index 0000000..7b1a737 --- /dev/null +++ b/src/NoaaClient/Dscovr/DscovrClient.cs @@ -0,0 +1,97 @@ +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; +using Microsoft.Extensions.Options; + +namespace AuroraScienceHub.Integrations.NoaaClient.Dscovr; + +internal sealed class DscovrClient : IDscovrClient +{ + private readonly HttpClient _httpClient; + private readonly Uri _baseUrl; + + public DscovrClient( + HttpClient httpClient, + IOptions options) + { + _httpClient = httpClient; + _baseUrl = options.Value.RequiredServerUrl; + } + + public async Task> GetMagnetometerData2HAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/mag-2-hour.json"); + return await GetMagnetometerDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetMagnetometerData1DAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/mag-1-day.json"); + return await GetMagnetometerDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetMagnetometerData3DAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/mag-3-day.json"); + return await GetMagnetometerDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetMagnetometerData7DAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/mag-7-day.json"); + return await GetMagnetometerDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetSolarWindPlasmaData2HAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/plasma-2-hour.json"); + return await GetSolarWindPlasmaDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetSolarWindPlasmaData1DAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/plasma-1-day.json"); + return await GetSolarWindPlasmaDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetSolarWindPlasmaData3DAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/plasma-3-day.json"); + return await GetSolarWindPlasmaDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + public async Task> GetSolarWindPlasmaData7DAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/solar-wind/plasma-7-day.json"); + return await GetSolarWindPlasmaDataAsync(url, cancellationToken) + .ConfigureAwait(false); + } + + private async Task> GetMagnetometerDataAsync( + Uri url, + CancellationToken cancellationToken) + { + var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var text = await response.Content.ReadAsStringAsync(cancellationToken); + + return MagnetometerDataParser.Parse(text); + } + + private async Task> GetSolarWindPlasmaDataAsync( + Uri url, + CancellationToken cancellationToken) + { + var response = await _httpClient.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + var text = await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + + return SolarWindPlasmaDataParser.Parse(text); + } +} diff --git a/src/NoaaClient/Dscovr/Extensions/MagnetometerDataParser.cs b/src/NoaaClient/Dscovr/Extensions/MagnetometerDataParser.cs new file mode 100644 index 0000000..35625f3 --- /dev/null +++ b/src/NoaaClient/Dscovr/Extensions/MagnetometerDataParser.cs @@ -0,0 +1,93 @@ +using System.Text.Json; +using AuroraScienceHub.Framework.Utilities.System; +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.Dscovr.Extensions; + +// DSCOVR Magnetometer data example: +// [ +// [ +// "time_tag", +// "bx_gsm", +// "by_gsm", +// "bz_gsm", +// "lon_gsm", +// "lat_gsm", +// "bt" +// ], +// [ +// "2024-05-19 16:50:00.000", +// "-2.25", +// "8.71", +// "-1.79", +// "104.50", +// "-11.25", +// "9.17" +// ] +// ] +internal static class MagnetometerDataParser +{ + private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss.fff"; + + public static IReadOnlyList Parse(string text) + { + using var jsonDoc = JsonDocument.Parse(text); + var rootElement = jsonDoc.RootElement; + + if (rootElement.ValueKind != JsonValueKind.Array) + { + return Array.Empty(); + } + + var arrayLength = rootElement.GetArrayLength(); + if (rootElement.GetArrayLength() < 2) + { + return Array.Empty(); + } + + var fieldNames = rootElement[0]; + var magnetometerRecords = new List(arrayLength); + + var timeTagIndex = GetIndex(fieldNames, "time_tag"); + var bxGsmIndex = GetIndex(fieldNames, "bx_gsm"); + var byGsmIndex = GetIndex(fieldNames, "by_gsm"); + var bzGsmIndex = GetIndex(fieldNames, "bz_gsm"); + var btIndex = GetIndex(fieldNames, "bt"); + var latGsmIndex = GetIndex(fieldNames, "lat_gsm"); + var lonGsmIndex = GetIndex(fieldNames, "lon_gsm"); + + foreach (var record in rootElement.EnumerateArray().Skip(1)) + { + magnetometerRecords.Add(new MagnetometerRecord( + DateTime: DateTime.ParseExact( + record[timeTagIndex].GetString().Required(), DateTimeFormat, + provider: null, + style: System.Globalization.DateTimeStyles.AssumeUniversal | + System.Globalization.DateTimeStyles.AdjustToUniversal), + Bx: record[bxGsmIndex].GetString()?.ParseFloatInvariant(), + By: record[byGsmIndex].GetString()?.ParseFloatInvariant(), + Bz: record[bzGsmIndex].GetString()?.ParseFloatInvariant(), + Bt: record[btIndex].GetString()?.ParseFloatInvariant(), + Latitude: record[latGsmIndex].GetString()?.ParseFloatInvariant(), + Longitude: record[lonGsmIndex].GetString()?.ParseFloatInvariant() + )); + } + + return magnetometerRecords; + } + + private static int GetIndex(JsonElement fieldNames, string fieldName) + { + var index = 0; + foreach (var name in fieldNames.EnumerateArray()) + { + if (string.Equals(name.GetString(), fieldName, StringComparison.OrdinalIgnoreCase)) + { + return index; + } + index++; + } + + throw new InvalidOperationException($"Field '{fieldName}' not found."); + } +} diff --git a/src/NoaaClient/Dscovr/Extensions/SolarWindPlasmaDataParser.cs b/src/NoaaClient/Dscovr/Extensions/SolarWindPlasmaDataParser.cs new file mode 100644 index 0000000..6f121cd --- /dev/null +++ b/src/NoaaClient/Dscovr/Extensions/SolarWindPlasmaDataParser.cs @@ -0,0 +1,87 @@ +using System.Text.Json; +using AuroraScienceHub.Framework.Utilities.System; +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.Dscovr.Extensions; + +// DSCOVR Solar wind plasma data example: +// [ +// [ +// "time_tag", +// "density", +// "speed", +// "temperature" +// ], +// [ +// "2024-05-20 05:51:00.000", +// "2.18", +// "393.9", +// "36967" +// ], +// [ +// "2024-05-20 05:52:00.000", +// "2.60", +// "397.4", +// "44618" +// ] +// ] +internal static class SolarWindPlasmaDataParser +{ + private const string DateTimeFormat = "yyyy-MM-dd HH:mm:ss.fff"; + + public static IReadOnlyList Parse(string text) + { + using var jsonDoc = JsonDocument.Parse(text); + var rootElement = jsonDoc.RootElement; + + if (rootElement.ValueKind != JsonValueKind.Array) + { + return Array.Empty(); + } + + var arrayLength = rootElement.GetArrayLength(); + if (rootElement.GetArrayLength() < 2) + { + return Array.Empty(); + } + + var fieldNames = rootElement[0]; + var solarWindPlasmaRecords = new List(arrayLength); + + var timeTagIndex = GetIndex(fieldNames, "time_tag"); + var densityIndex = GetIndex(fieldNames, "density"); + var speedIndex = GetIndex(fieldNames, "speed"); + var temperatureIndex = GetIndex(fieldNames, "temperature"); + + foreach (var record in rootElement.EnumerateArray().Skip(1)) + { + solarWindPlasmaRecords.Add(new SolarWindPlasmaRecord( + DateTime: DateTime.ParseExact( + record[timeTagIndex].GetString().Required(), DateTimeFormat, + provider: null, + style: System.Globalization.DateTimeStyles.AssumeUniversal | + System.Globalization.DateTimeStyles.AdjustToUniversal), + ProtonDensity: record[densityIndex].GetString()?.ParseFloatInvariant(), + BulkSpeed: record[speedIndex].GetString()?.ParseFloatInvariant(), + IonTemperature: record[temperatureIndex].GetString()?.ParseFloatInvariant() + )); + } + + return solarWindPlasmaRecords; + } + + private static int GetIndex(JsonElement fieldNames, string fieldName) + { + var index = 0; + foreach (var name in fieldNames.EnumerateArray()) + { + if (string.Equals(name.GetString(), fieldName, StringComparison.OrdinalIgnoreCase)) + { + return index; + } + index++; + } + + throw new InvalidOperationException($"Field '{fieldName}' not found."); + } +} diff --git a/src/NoaaClient/Dscovr/IDscovrClient.cs b/src/NoaaClient/Dscovr/IDscovrClient.cs new file mode 100644 index 0000000..71f5726 --- /dev/null +++ b/src/NoaaClient/Dscovr/IDscovrClient.cs @@ -0,0 +1,49 @@ +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.Dscovr; + +/// +/// DSCOVR Spacecraft client +/// +public interface IDscovrClient +{ + /// + /// Get DSCOVR Magnetometer data (2 hours) + /// + Task> GetMagnetometerData2HAsync(CancellationToken cancellationToken); + + /// + /// Get DSCOVR Magnetometer data (1 day) + /// + Task> GetMagnetometerData1DAsync(CancellationToken cancellationToken); + + /// + /// Get DSCOVR Magnetometer data (3 days) + /// + Task> GetMagnetometerData3DAsync(CancellationToken cancellationToken); + + /// + /// Get DSCOVR Magnetometer data (7 days) + /// + Task> GetMagnetometerData7DAsync(CancellationToken cancellationToken); + + /// + /// Get DSCOVR Solar Wind Electron Proton Alpha Monitor data (2 hours) + /// + Task> GetSolarWindPlasmaData2HAsync(CancellationToken cancellationToken); + + /// + /// Get DSCOVR Solar Wind Electron Proton Alpha Monitor data (1 day) + /// + Task> GetSolarWindPlasmaData1DAsync(CancellationToken cancellationToken); + + /// + /// Get DSCOVR Solar Wind Electron Proton Alpha Monitor data (3 days) + /// + Task> GetSolarWindPlasmaData3DAsync(CancellationToken cancellationToken); + + /// + /// Get DSCOVR Solar Wind Electron Proton Alpha Monitor data (7 days) + /// + Task> GetSolarWindPlasmaData7DAsync(CancellationToken cancellationToken); +} diff --git a/src/NoaaClient/Dscovr/Responses/MagnetometerRecord.cs b/src/NoaaClient/Dscovr/Responses/MagnetometerRecord.cs new file mode 100644 index 0000000..6ef18b7 --- /dev/null +++ b/src/NoaaClient/Dscovr/Responses/MagnetometerRecord.cs @@ -0,0 +1,20 @@ +namespace AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; + +/// +/// 1-minute averaged Real-time Interplanetary Magnetic Field Values (DSCOVR) +/// +/// Date and time of record (UTC) +/// IMF GSM x-component +/// IMF GSM y-component +/// IMF GSM z-component +/// IMF module +/// Spacecraft latitude +/// Spacecraft longitude +public record MagnetometerRecord( + DateTime DateTime, + float? Bx, + float? By, + float? Bz, + float? Bt, + float? Latitude, + float? Longitude); diff --git a/src/NoaaClient/Dscovr/Responses/SolarWindPlasmaRecord.cs b/src/NoaaClient/Dscovr/Responses/SolarWindPlasmaRecord.cs new file mode 100644 index 0000000..9659fd0 --- /dev/null +++ b/src/NoaaClient/Dscovr/Responses/SolarWindPlasmaRecord.cs @@ -0,0 +1,15 @@ +namespace AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; + +/// +/// 1-minute averaged Real-time Bulk Parameters of the Solar Wind Plasma (DSCOVR) +/// +/// Date and time of record (UTC) +/// Solar Wind proton density +/// Solar Wind plasma speed +/// Solar Wind Ion Temp +public record SolarWindPlasmaRecord( + DateTime DateTime, + float? ProtonDensity, + float? BulkSpeed, + float? IonTemperature +); diff --git a/src/NoaaClient/KpIndex/Extensions/KpIndex27DayDataParser.cs b/src/NoaaClient/KpIndex/Extensions/KpIndex27DayDataParser.cs new file mode 100644 index 0000000..10fd385 --- /dev/null +++ b/src/NoaaClient/KpIndex/Extensions/KpIndex27DayDataParser.cs @@ -0,0 +1,84 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using AuroraScienceHub.Framework.Utilities.System; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions; + +// 27-day KP-index forecast data example: +// :Product: 27-day Space Weather Outlook Table 27DO.txt +// :Issued: 2025 Jan 06 0242 UTC +// # Prepared by the US Dept. of Commerce, NOAA, Space Weather Prediction Center +// # Product description and SWPC contact on the Web +// # https://www.swpc.noaa.gov/content/subscription-services +// # +// # 27-day Space Weather Outlook Table +// # Issued 2025-01-06 +// # +// # UTC Radio Flux Planetary Largest +// # Date 10.7 cm A Index Kp Index +// 2025 Jan 06 172 22 5 +// 2025 Jan 07 165 12 4 +// 2025 Jan 08 165 8 3 + +internal static class KpIndex27DayDataParser +{ + private const int ForecastDaysCount = 27; + + public static IReadOnlyList Parse(string text) + { + var responses = new List(ForecastDaysCount); + + var lineEntries = text.SplitLines(); + foreach (var lineEntry in lineEntries) + { + if (TryParseLine(lineEntry.Line, out var response)) + { + responses.Add(response); + } + } + + return responses; + } + + private static bool TryParseLine(ReadOnlySpan line, [NotNullWhen(true)] out KpIndex27DayResponse? response) + { + var trimmedLine = line.Trim(); + if (trimmedLine.IsEmpty || trimmedLine[0] == '#' || trimmedLine[0] == ':') + { + response = null; + return false; + } + + const int fieldsInRowCount = 6; + Span fieldsRange = stackalloc Range[fieldsInRowCount]; + var rangesCount = trimmedLine.Split(fieldsRange, ' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + + if (rangesCount != fieldsInRowCount) + { + response = null; + return false; + } + + var yearString = trimmedLine[fieldsRange[0]]; + var monthString = trimmedLine[fieldsRange[1]]; + var dayString = trimmedLine[fieldsRange[2]]; + + var dateString = string.Concat(yearString, monthString, dayString); + if (!DateOnly.TryParseExact(dateString, "yyyyMMMdd", CultureInfo.InvariantCulture, DateTimeStyles.None, out var date)) + { + response = null; + return false; + } + + var kpIndexString = trimmedLine[fieldsRange[5]]; + if (!int.TryParse(kpIndexString, out var kpIndex)) + { + response = null; + return false; + } + + response = new KpIndex27DayResponse(date, kpIndex); + return true; + } +} diff --git a/src/NoaaClient/KpIndex/Extensions/KpIndex3DayDataParser.cs b/src/NoaaClient/KpIndex/Extensions/KpIndex3DayDataParser.cs new file mode 100644 index 0000000..2bb75d6 --- /dev/null +++ b/src/NoaaClient/KpIndex/Extensions/KpIndex3DayDataParser.cs @@ -0,0 +1,80 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions; + +// 3-day KP-index forecast data example: +// [ +// [ +// "time_tag", +// "kp", +// "observed", +// "noaa_scale" +// ], +// [ +// "2025-01-04 00:00:00", +// "2.67", +// "observed", +// null +// ] +// ] + +internal static class KpIndex3DayDataParser +{ + public static IReadOnlyList Parse(string text) + { + using var jsonDocument = JsonDocument.Parse(text); + var rootElement = jsonDocument.RootElement; + + if (rootElement.ValueKind != JsonValueKind.Array) + { + return Array.Empty(); + } + + var arrayLength = rootElement.GetArrayLength(); + if (arrayLength < 2) + { + return Array.Empty(); + } + + var responses = new List(arrayLength); + foreach (var jsonElement in rootElement.EnumerateArray().Skip(1)) + { + if (TryParseElement(jsonElement, out var response)) + { + responses.Add(response); + } + } + + return responses; + } + + private static bool TryParseElement(JsonElement jsonElement, [NotNullWhen(true)] out KpIndex3DayResponse? response) + { + if (jsonElement.ValueKind != JsonValueKind.Array || + jsonElement.GetArrayLength() < 2) + { + response = null; + return false; + } + + var dateTimeString = jsonElement[0].GetString(); + if (!DateTime.TryParse(dateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)) + { + response = null; + return false; + } + + var kpIndexString = jsonElement[1].GetString(); + if (!float.TryParse(kpIndexString, NumberFormatInfo.InvariantInfo, out var kpIndex)) + { + response = null; + return false; + } + + response = new KpIndex3DayResponse(dateTime, kpIndex); + return true; + } +} diff --git a/src/NoaaClient/KpIndex/Extensions/KpIndexNowcastDataParser.cs b/src/NoaaClient/KpIndex/Extensions/KpIndexNowcastDataParser.cs new file mode 100644 index 0000000..17fe110 --- /dev/null +++ b/src/NoaaClient/KpIndex/Extensions/KpIndexNowcastDataParser.cs @@ -0,0 +1,87 @@ +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Text.Json; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions; + +// Current KP-index forecast data example: +// [ +// [ +// "time_tag", +// "Kp", +// "a_running", +// "station_count" +// ], +// [ +// "2025-01-05 00:00:00.000", +// "3.67", +// "22", +// "8" +// ] +// ] + +internal static class KpIndexNowcastDataParser +{ + public static IReadOnlyList Parse(string text) + { + using var jsonDocument = JsonDocument.Parse(text); + var rootElement = jsonDocument.RootElement; + + if (rootElement.ValueKind != JsonValueKind.Array) + { + return Array.Empty(); + } + + var arrayLength = rootElement.GetArrayLength(); + if (arrayLength < 2) + { + return Array.Empty(); + } + + var responses = new List(arrayLength); + foreach (var jsonElement in rootElement.EnumerateArray().Skip(1)) + { + if (TryParseElement(jsonElement, out var response)) + { + responses.Add(response); + } + } + + return responses; + } + + private static bool TryParseElement(JsonElement jsonElement, [NotNullWhen(true)] out KpIndexNowcastResponse? response) + { + if (jsonElement.ValueKind != JsonValueKind.Array || + jsonElement.GetArrayLength() < 4) + { + response = null; + return false; + } + + var dateTimeString = jsonElement[0].GetString(); + if (!DateTime.TryParse(dateTimeString, CultureInfo.InvariantCulture, DateTimeStyles.AssumeUniversal | DateTimeStyles.AdjustToUniversal, out var dateTime)) + { + response = null; + return false; + } + + var kpIndexString = jsonElement[1].GetString(); + if (!float.TryParse(kpIndexString, NumberFormatInfo.InvariantInfo, out var kpIndex)) + { + response = null; + return false; + } + + var stationsCountString = jsonElement[3].GetString(); + if (!int.TryParse(stationsCountString, NumberFormatInfo.InvariantInfo, out var stationsCount)) + { + response = null; + return false; + } + + response = new KpIndexNowcastResponse(dateTime, kpIndex, stationsCount); + return true; + } +} diff --git a/src/NoaaClient/KpIndex/IKpIndexClient.cs b/src/NoaaClient/KpIndex/IKpIndexClient.cs new file mode 100644 index 0000000..25b444b --- /dev/null +++ b/src/NoaaClient/KpIndex/IKpIndexClient.cs @@ -0,0 +1,24 @@ +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; + +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex; + +/// +/// KP-index client +/// +public interface IKpIndexClient +{ + /// + /// Get 27-day KP-index forecast + /// + Task> GetKpIndex27DayForecastAsync(CancellationToken cancellationToken); + + /// + /// Get 3-day KP-index forecast + /// + Task> GetKpIndex3DayForecastAsync(CancellationToken cancellationToken); + + /// + /// Get KP-index nowcast + /// + Task> GetKpIndexNowcastAsync(CancellationToken cancellationToken); +} diff --git a/src/NoaaClient/KpIndex/KpIndexClient.cs b/src/NoaaClient/KpIndex/KpIndexClient.cs new file mode 100644 index 0000000..943be75 --- /dev/null +++ b/src/NoaaClient/KpIndex/KpIndexClient.cs @@ -0,0 +1,48 @@ +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; +using Microsoft.Extensions.Options; + +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex; + +internal sealed class KpIndexClient : IKpIndexClient +{ + private readonly HttpClient _client; + private readonly Uri _baseUrl; + + public KpIndexClient( + HttpClient client, + IOptions options) + { + _client = client; + _baseUrl = options.Value.RequiredServerUrl; + } + + public async Task> GetKpIndex27DayForecastAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "text/27-day-outlook.txt"); + var text = await GetStringOrDefaultAsync(url, cancellationToken).ConfigureAwait(false); + return string.IsNullOrWhiteSpace(text) ? [] : KpIndex27DayDataParser.Parse(text); + } + + public async Task> GetKpIndex3DayForecastAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/noaa-planetary-k-index-forecast.json"); + var text = await GetStringOrDefaultAsync(url, cancellationToken).ConfigureAwait(false); + return string.IsNullOrWhiteSpace(text) ? [] : KpIndex3DayDataParser.Parse(text); + } + + public async Task> GetKpIndexNowcastAsync(CancellationToken cancellationToken) + { + var url = new Uri(_baseUrl, "products/noaa-planetary-k-index.json"); + var text = await GetStringOrDefaultAsync(url, cancellationToken).ConfigureAwait(false); + return string.IsNullOrWhiteSpace(text) ? [] : KpIndexNowcastDataParser.Parse(text); + } + + private async Task GetStringOrDefaultAsync(Uri url, CancellationToken cancellationToken) + { + var response = await _client.GetAsync(url, cancellationToken).ConfigureAwait(false); + response.EnsureSuccessStatusCode(); + + return await response.Content.ReadAsStringAsync(cancellationToken).ConfigureAwait(false); + } +} diff --git a/src/NoaaClient/KpIndex/Responses/KpIndex27DayResponse.cs b/src/NoaaClient/KpIndex/Responses/KpIndex27DayResponse.cs new file mode 100644 index 0000000..7f6322f --- /dev/null +++ b/src/NoaaClient/KpIndex/Responses/KpIndex27DayResponse.cs @@ -0,0 +1,10 @@ +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; + +/// +/// 27-day KP-index data +/// +/// Date of record +/// KP-index +public sealed record KpIndex27DayResponse( + DateOnly Date, + int KpIndex); diff --git a/src/NoaaClient/KpIndex/Responses/KpIndex3DayResponse.cs b/src/NoaaClient/KpIndex/Responses/KpIndex3DayResponse.cs new file mode 100644 index 0000000..d964ad5 --- /dev/null +++ b/src/NoaaClient/KpIndex/Responses/KpIndex3DayResponse.cs @@ -0,0 +1,8 @@ +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; + +/// +/// 3-day KP-index data +/// +/// Date and time of record (UTC) +/// KP-index +public sealed record KpIndex3DayResponse(DateTime DateTime, float KpIndex); diff --git a/src/NoaaClient/KpIndex/Responses/KpIndexNowcastResponse.cs b/src/NoaaClient/KpIndex/Responses/KpIndexNowcastResponse.cs new file mode 100644 index 0000000..2249e05 --- /dev/null +++ b/src/NoaaClient/KpIndex/Responses/KpIndexNowcastResponse.cs @@ -0,0 +1,12 @@ +namespace AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; + +/// +/// Current KP-index data +/// +/// Date and time of record (UTC) +/// KP-index +/// KP-index calculated by stations count +public sealed record KpIndexNowcastResponse( + DateTime DateTime, + float KpIndex, + int StationsCount); diff --git a/src/NoaaClient/NoaaClient.csproj b/src/NoaaClient/NoaaClient.csproj new file mode 100644 index 0000000..785fbc0 --- /dev/null +++ b/src/NoaaClient/NoaaClient.csproj @@ -0,0 +1,23 @@ + + + + AuroraScienceHub.Integrations.NoaaClient + AuroraScienceHub.Integrations.NoaaClient + NOAA space weather data integration client for ACE, DSCOVR spacecraft and KP-index data + + + + + + + + + + + + Provides unified HTTP client interfaces for accessing NOAA space weather data including solar wind measurements, magnetometer data, and geomagnetic activity indices from ACE and DSCOVR spacecraft with KP-index forecasts and nowcast data. + + noaa;space-weather;ace;dscovr;kp-index;solar-wind;magnetometer;geomagnetic;aurora;integration + + + diff --git a/src/NoaaClient/NoaaClientOptions.cs b/src/NoaaClient/NoaaClientOptions.cs new file mode 100644 index 0000000..0f2ce28 --- /dev/null +++ b/src/NoaaClient/NoaaClientOptions.cs @@ -0,0 +1,24 @@ +namespace AuroraScienceHub.Integrations.NoaaClient; + +/// +/// NOAA client options +/// +public sealed class NoaaClientOptions +{ + /// + /// Configuration option key + /// + public const string OptionKey = "NoaaClient"; + + private const string EmptyServerUrlMessage = $"Configuration value '{OptionKey}:{nameof(ServerUrl)}' is not set."; + + /// + /// NOAA server URL + /// + public Uri? ServerUrl { get; set; } + + /// + /// Gets the required server URL. Throws if not set. + /// + public Uri RequiredServerUrl => ServerUrl ?? throw new ArgumentNullException(nameof(ServerUrl), EmptyServerUrlMessage); +} diff --git a/src/NoaaClient/README.md b/src/NoaaClient/README.md new file mode 100644 index 0000000..0a7a2ed --- /dev/null +++ b/src/NoaaClient/README.md @@ -0,0 +1,72 @@ +# AuroraScienceHub.Integrations.Noaa + +NOAA space weather data integration with support for ACE, DSCOVR spacecraft and KP-index data. + +## Overview + +Provides unified interfaces for accessing NOAA space weather data including solar wind measurements, magnetometer data, and geomagnetic activity indices. + +## Key Features + +- **ACE Spacecraft** - Access to Magnetometer and SWEPAM (Solar Wind) data +- **DSCOVR Spacecraft** - Magnetometer and Solar Wind Plasma data with multiple time ranges +- **KP-Index** - Geomagnetic activity forecasts and nowcast data +- **Unified Interfaces** - Consistent API across all NOAA data sources + +## Installation + +```bash +dotnet add package AuroraScienceHub.Integrations.Noaa +``` + +## Usage + +### Service Registration + +```csharp +// Configuration +builder.Services.AddNoaaClients(); +``` + +### ACE Client + +```csharp +var magnetometerData = await aceClient.GetMagnetometerDataAsync(cancellationToken); +var solarWindData = await aceClient.GetSwepamDataAsync(cancellationToken); +``` + +### DSCOVR Client + +```csharp +// Available time ranges: 2H, 1D, 3D, 7D +var magnetometerData = await dscovrClient.GetMagnetometerData1DAsync(cancellationToken); +var solarWindData = await dscovrClient.GetSolarWindPlasmaData1DAsync(cancellationToken); +``` + +### KP-Index Client + +```csharp +var nowcast = await kpIndexClient.GetKpIndexNowcastAsync(cancellationToken); +var forecast3Day = await kpIndexClient.GetKpIndex3DayForecastAsync(cancellationToken); +var forecast27Day = await kpIndexClient.GetKpIndex27DayForecastAsync(cancellationToken); +``` + +### Configuration + +```json +{ + "Noaa": { + "ServerUrl": "https://services.swpc.noaa.gov" + } +} +``` + + +## License + +See [LICENSE](../../LICENSE) file in the repository root. + +## Related Packages + +- `AuroraScienceHub.Framework.Http` - HTTP utilities +- `AuroraScienceHub.Framework.Utilities` - Common utilities diff --git a/src/NoaaClient/ServiceCollectionExtensions.cs b/src/NoaaClient/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..a1b752a --- /dev/null +++ b/src/NoaaClient/ServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using AuroraScienceHub.Integrations.NoaaClient.Ace; +using AuroraScienceHub.Integrations.NoaaClient.Dscovr; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex; +using Microsoft.Extensions.DependencyInjection; + +namespace AuroraScienceHub.Integrations.NoaaClient; + +/// +/// to register NOAA clients. +/// +public static class ServiceCollectionExtensions +{ + /// + /// Adds NOAA clients to the service collection. + /// + public static IServiceCollection AddNoaaClients(this IServiceCollection services) + { + services.AddOptions() + .BindConfiguration(NoaaClientOptions.OptionKey); + + services.AddHttpClient(); + services.AddHttpClient(); + services.AddHttpClient(); + + return services; + } +} diff --git a/tests/Directory.Build.props b/tests/Directory.Build.props new file mode 100644 index 0000000..4e78215 --- /dev/null +++ b/tests/Directory.Build.props @@ -0,0 +1,25 @@ + + + + + + true + $(NoWarn) + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + all + runtime; build; native; contentfiles; analyzers + + + + + + diff --git a/tests/UnitTests/NoaaClient/Ace/MagnetometerDataParserTests.cs b/tests/UnitTests/NoaaClient/Ace/MagnetometerDataParserTests.cs new file mode 100644 index 0000000..dee856a --- /dev/null +++ b/tests/UnitTests/NoaaClient/Ace/MagnetometerDataParserTests.cs @@ -0,0 +1,58 @@ +using AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; +using AuroraScienceHub.Integrations.UnitTests.Utils; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient.Ace; + +/// +/// Unit tests for . +/// +public sealed class MagnetometerDataParserTests +{ + [Theory] + [EmbeddedResourceData("AuroraScienceHub.Integrations.UnitTests.NoaaClient/Ace/Samples/AceMagnetometerSample.txt")] + public void Parse_WhenTextIsValid_ReturnsRecords(string text) + { + // Arrange, Act + var result = MagnetometerDataParser.Parse(text); + + // Assert + result.ShouldNotBeEmpty(); + result.ShouldBe([ + new MagnetometerRecord + ( + DateTime: new DateTime(2024, 5, 5, 10, 42, 0, DateTimeKind.Utc), + Status: 0, + Bx: 2.3f, + By: 1.8f, + Bz: 2.2f, + Bt: 3.7f, + Latitude: 36.3f, + Longitude: 38.0f + ), + new MagnetometerRecord + ( + DateTime: new DateTime(2024, 5, 5, 10, 43, 0, DateTimeKind.Utc), + Status: 0, + Bx: 2.3f, + By: 1.6f, + Bz: 2.3f, + Bt: 3.6f, + Latitude: 38.7f, + Longitude: 34.4f + ), + new MagnetometerRecord + ( + DateTime: new DateTime(2024, 5, 5, 10, 44, 0, DateTimeKind.Utc), + Status: 9, + Bx: null, + By: null, + Bz: null, + Bt: null, + Latitude: null, + Longitude: null + ) + ]); + } +} diff --git a/tests/UnitTests/NoaaClient/Ace/Samples/AceMagnetometerSample.txt b/tests/UnitTests/NoaaClient/Ace/Samples/AceMagnetometerSample.txt new file mode 100644 index 0000000..5aedd98 --- /dev/null +++ b/tests/UnitTests/NoaaClient/Ace/Samples/AceMagnetometerSample.txt @@ -0,0 +1,23 @@ +:Data_list: ace_mag_1m.txt +:Created: 2024 May 05 1046 UT +# Prepared by the U.S. Dept. of Commerce, NOAA, Space Weather Prediction Center +# Please send comments and suggestions to SWPC.Webmaster@noaa.gov +# +# Magnetometer values are in GSM coordinates. +# +# Units: Bx, By, Bz, Bt in nT +# Units: Latitude degrees +/- 90.0 +# Units: Longitude degrees 0.0 - 360.0 +# Status(S): 0 = nominal data, 1 to 8 = bad data record, 9 = no data +# Missing data values: -999.9 +# Source: ACE Satellite - Magnetometer +# +# 1-minute averaged Real-time Interplanetary Magnetic Field Values +# +# Modified Seconds +# UT Date Time Julian of the ---------------- GSM Coordinates --------------- +# YR MO DA HHMM Day Day S Bx By Bz Bt Lat. Long. +#------------------------------------------------------------------------------------ +2024 05 05 1042 60435 38520 0 2.3 1.8 2.2 3.7 36.3 38.0 +2024 05 05 1043 60435 38580 0 2.3 1.6 2.3 3.6 38.7 34.4 +2024 05 05 1044 60435 38640 9 -999.9 -999.9 -999.9 -999.9 -999.9 -999.9 diff --git a/tests/UnitTests/NoaaClient/Ace/Samples/AceSwepamSample.txt b/tests/UnitTests/NoaaClient/Ace/Samples/AceSwepamSample.txt new file mode 100644 index 0000000..73558ca --- /dev/null +++ b/tests/UnitTests/NoaaClient/Ace/Samples/AceSwepamSample.txt @@ -0,0 +1,21 @@ +:Data_list: ace_swepam_1m.txt +:Created: 2024 May 06 1857 UT +# Prepared by the U.S. Dept. of Commerce, NOAA, Space Weather Prediction Center +# Please send comments and suggestions to SWPC.Webmaster@noaa.gov +# +# Units: Proton density p/cc +# Units: Bulk speed km/s +# Units: Ion tempeture degrees K +# Status(S): 0 = nominal data, 1 to 8 = bad data record, 9 = no data +# Missing data values: Density and Speed = -9999.9, Temp. = -1.00e+05 +# Source: ACE Satellite - Solar Wind Electron Proton Alpha Monitor +# +# 1-minute averaged Real-time Bulk Parameters of the Solar Wind Plasma +# +# Modified Seconds ------------- Solar Wind ----------- +# UT Date Time Julian of the Proton Bulk Ion +# YR MO DA HHMM Day Day S Density Speed Temperature +#------------------------------------------------------------------------- +2024 05 06 1657 60436 61020 1 4.9 475.0 2.38e+05 +2024 05 06 1658 60436 61080 0 4.6 474.2 2.01e+05 +2024 05 06 1659 60436 61140 3 -9999.9 -9999.9 -1.00e+05 diff --git a/tests/UnitTests/NoaaClient/Ace/SolarWindPlasmaDataParserTests.cs b/tests/UnitTests/NoaaClient/Ace/SolarWindPlasmaDataParserTests.cs new file mode 100644 index 0000000..49c3438 --- /dev/null +++ b/tests/UnitTests/NoaaClient/Ace/SolarWindPlasmaDataParserTests.cs @@ -0,0 +1,49 @@ +using AuroraScienceHub.Integrations.NoaaClient.Ace.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.Ace.Responses; +using AuroraScienceHub.Integrations.UnitTests.Utils; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient.Ace; + +/// +/// Unit tests for . +/// +public sealed class SolarWindPlasmaDataParserTests +{ + [Theory] + [EmbeddedResourceData("AuroraScienceHub.Integrations.UnitTests.NoaaClient/Ace/Samples/AceSwepamSample.txt")] + public void Parse_WhenTextIsValid_ReturnsRecords(string text) + { + // Arrange, Act + var result = SolarWindPlasmaDataParser.Parse(text); + + // Assert + result.ShouldNotBeEmpty(); + result.ShouldBe([ + new SolarWindPlasmaRecord + ( + DateTime: new DateTime(2024, 5, 6, 16, 57, 0, DateTimeKind.Utc), + Status: 1, + ProtonDensity: 4.9f, + BulkSpeed: 475.0f, + IonTemperature: 2.38e+05f + ), + new SolarWindPlasmaRecord + ( + DateTime: new DateTime(2024, 5, 6, 16, 58, 0, DateTimeKind.Utc), + Status: 0, + ProtonDensity: 4.6f, + BulkSpeed: 474.2f, + IonTemperature: 2.01e+05f + ), + new SolarWindPlasmaRecord + ( + DateTime: new DateTime(2024, 5, 6, 16, 59, 0, DateTimeKind.Utc), + Status: 3, + ProtonDensity: null, + BulkSpeed: null, + IonTemperature: null + ) + ]); + } +} diff --git a/tests/UnitTests/NoaaClient/Dscovr/MagnetometerDataParserTests.cs b/tests/UnitTests/NoaaClient/Dscovr/MagnetometerDataParserTests.cs new file mode 100644 index 0000000..f927dcd --- /dev/null +++ b/tests/UnitTests/NoaaClient/Dscovr/MagnetometerDataParserTests.cs @@ -0,0 +1,45 @@ +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; +using AuroraScienceHub.Integrations.UnitTests.Utils; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient.Dscovr; + +/// +/// Unit tests for . +/// +public sealed class MagnetometerDataParserTests +{ + [Theory] + [EmbeddedResourceData("AuroraScienceHub.Integrations.UnitTests.NoaaClient/Dscovr/Samples/DscovrMagnetometerSample.json")] + public void Parse_WhenTextIsValid_ReturnsRecords(string text) + { + // Arrange, Act + var result = MagnetometerDataParser.Parse(text); + + // Assert + result.ShouldNotBeEmpty(); + result.ShouldBe([ + new MagnetometerRecord + ( + DateTime: new DateTime(2024, 5, 19, 17, 7, 0, DateTimeKind.Utc), + Bx: -2.15f, + By: 8.51f, + Bz: -2.98f, + Bt: 9.27f, + Latitude: -18.76f, + Longitude: 104.17f + ), + new MagnetometerRecord + ( + DateTime: new DateTime(2024, 5, 19, 17, 8, 0, DateTimeKind.Utc), + Bx: -2.38f, + By: 8.31f, + Bz: -3.24f, + Bt: 9.23f, + Latitude: -20.54f, + Longitude: 105.97f + ) + ]); + } +} diff --git a/tests/UnitTests/NoaaClient/Dscovr/Samples/DscovrMagnetometerSample.json b/tests/UnitTests/NoaaClient/Dscovr/Samples/DscovrMagnetometerSample.json new file mode 100644 index 0000000..9de4324 --- /dev/null +++ b/tests/UnitTests/NoaaClient/Dscovr/Samples/DscovrMagnetometerSample.json @@ -0,0 +1,29 @@ +[ + [ + "time_tag", + "bx_gsm", + "by_gsm", + "bz_gsm", + "lon_gsm", + "lat_gsm", + "bt" + ], + [ + "2024-05-19 17:07:00.000", + "-2.15", + "8.51", + "-2.98", + "104.17", + "-18.76", + "9.27" + ], + [ + "2024-05-19 17:08:00.000", + "-2.38", + "8.31", + "-3.24", + "105.97", + "-20.54", + "9.23" + ] +] diff --git a/tests/UnitTests/NoaaClient/Dscovr/Samples/DscovrSolarWindPlasmaSample.json b/tests/UnitTests/NoaaClient/Dscovr/Samples/DscovrSolarWindPlasmaSample.json new file mode 100644 index 0000000..cb5af74 --- /dev/null +++ b/tests/UnitTests/NoaaClient/Dscovr/Samples/DscovrSolarWindPlasmaSample.json @@ -0,0 +1,20 @@ +[ + [ + "time_tag", + "density", + "speed", + "temperature" + ], + [ + "2024-05-19 05:51:00.000", + "2.18", + "393.9", + "36967" + ], + [ + "2024-05-19 05:52:00.000", + "2.60", + "397.4", + "44618" + ] +] diff --git a/tests/UnitTests/NoaaClient/Dscovr/SolarWindPlasmaDataParserTests.cs b/tests/UnitTests/NoaaClient/Dscovr/SolarWindPlasmaDataParserTests.cs new file mode 100644 index 0000000..b64e7f9 --- /dev/null +++ b/tests/UnitTests/NoaaClient/Dscovr/SolarWindPlasmaDataParserTests.cs @@ -0,0 +1,39 @@ +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.Dscovr.Responses; +using AuroraScienceHub.Integrations.UnitTests.Utils; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient.Dscovr; + +/// +/// Unit tests for . +/// +public sealed class SolarWindPlasmaDataParserTests +{ + [Theory] + [EmbeddedResourceData("AuroraScienceHub.Integrations.UnitTests.NoaaClient/Dscovr/Samples/DscovrSolarWindPlasmaSample.json")] + public void Parse_WhenTextIsValid_ReturnsRecords(string text) + { + // Arrange, Act + var result = SolarWindPlasmaDataParser.Parse(text); + + // Assert + result.ShouldNotBeEmpty(); + result.ShouldBe([ + new SolarWindPlasmaRecord + ( + DateTime: new DateTime(2024, 5, 19, 5, 51, 0, DateTimeKind.Utc), + ProtonDensity: 2.18f, + BulkSpeed: 393.9f, + IonTemperature: 36967f + ), + new SolarWindPlasmaRecord + ( + DateTime: new DateTime(2024, 5, 19, 5, 52, 0, DateTimeKind.Utc), + ProtonDensity: 2.60f, + BulkSpeed: 397.4f, + IonTemperature: 44618f + ) + ]); + } +} diff --git a/tests/UnitTests/NoaaClient/KpIndex/KpIndex27DayDataParserTests.cs b/tests/UnitTests/NoaaClient/KpIndex/KpIndex27DayDataParserTests.cs new file mode 100644 index 0000000..53c7154 --- /dev/null +++ b/tests/UnitTests/NoaaClient/KpIndex/KpIndex27DayDataParserTests.cs @@ -0,0 +1,50 @@ +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; +using AuroraScienceHub.Integrations.UnitTests.Utils; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient.KpIndex; + +/// +/// Unit tests for . +/// +public sealed class KpIndex27DayDataParserTests +{ + [Theory] + [EmbeddedResourceData("AuroraScienceHub.Integrations.UnitTests.NoaaClient/KpIndex/Samples/KpIndex27DayForecastSample.txt")] + public void Parse_WhenTextIsValid_ReturnsRecords(string text) + { + // Arrange, Act + var result = KpIndex27DayDataParser.Parse(text); + + // Assert + result.ShouldBe( + [ + new KpIndex27DayResponse + ( + Date: new DateOnly(2025, 1, 20), + KpIndex: 4 + ), + new KpIndex27DayResponse + ( + Date: new DateOnly(2025, 1, 21), + KpIndex: 3 + ), + new KpIndex27DayResponse + ( + Date: new DateOnly(2025, 1, 22), + KpIndex: 2 + ), + new KpIndex27DayResponse + ( + Date: new DateOnly(2025, 1, 23), + KpIndex: 2 + ), + new KpIndex27DayResponse + ( + Date: new DateOnly(2025, 1, 24), + KpIndex: 2 + ), + ]); + } +} diff --git a/tests/UnitTests/NoaaClient/KpIndex/KpIndex3DayDataParserTests.cs b/tests/UnitTests/NoaaClient/KpIndex/KpIndex3DayDataParserTests.cs new file mode 100644 index 0000000..8d6844a --- /dev/null +++ b/tests/UnitTests/NoaaClient/KpIndex/KpIndex3DayDataParserTests.cs @@ -0,0 +1,50 @@ +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; +using AuroraScienceHub.Integrations.UnitTests.Utils; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient.KpIndex; + +/// +/// Unit tests for . +/// +public sealed class KpIndex3DayDataParserTests +{ + [Theory] + [EmbeddedResourceData("AuroraScienceHub.Integrations.UnitTests.NoaaClient/KpIndex/Samples/KpIndex3DayForecastSample.json")] + public void Parse_WhenTextIsValid_ReturnsRecords(string text) + { + // Arrange, Act + var result = KpIndex3DayDataParser.Parse(text); + + // Assert + result.ShouldBe( + [ + new KpIndex3DayResponse + ( + DateTime: new DateTime(2025, 1, 18, 0, 0, 0), + KpIndex: 2.67F + ), + new KpIndex3DayResponse + ( + DateTime: new DateTime(2025, 1, 18, 3, 0, 0), + KpIndex: 2.00F + ), + new KpIndex3DayResponse + ( + DateTime: new DateTime(2025, 1, 18, 6, 0, 0), + KpIndex: 2.67F + ), + new KpIndex3DayResponse + ( + DateTime: new DateTime(2025, 1, 18, 9, 0, 0), + KpIndex: 2.00F + ), + new KpIndex3DayResponse + ( + DateTime: new DateTime(2025, 1, 18, 12, 0, 0), + KpIndex: 2.67F + ), + ]); + } +} diff --git a/tests/UnitTests/NoaaClient/KpIndex/KpIndexNowcastDataParserTests.cs b/tests/UnitTests/NoaaClient/KpIndex/KpIndexNowcastDataParserTests.cs new file mode 100644 index 0000000..b3d4c94 --- /dev/null +++ b/tests/UnitTests/NoaaClient/KpIndex/KpIndexNowcastDataParserTests.cs @@ -0,0 +1,55 @@ +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Extensions; +using AuroraScienceHub.Integrations.NoaaClient.KpIndex.Responses; +using AuroraScienceHub.Integrations.UnitTests.Utils; +using Shouldly; + +namespace AuroraScienceHub.Integrations.UnitTests.NoaaClient.KpIndex; + +/// +/// Unit tests for . +/// +public sealed class KpIndexNowcastDataParserTests +{ + [Theory] + [EmbeddedResourceData("AuroraScienceHub.Integrations.UnitTests.NoaaClient/KpIndex/Samples/KpIndexNowcastSample.json")] + public void Parse_WhenTextIsValid_ReturnsRecords(string text) + { + // Arrange, Act + var result = KpIndexNowcastDataParser.Parse(text); + + // Assert + result.ShouldBe( + [ + new KpIndexNowcastResponse + ( + DateTime: new DateTime(2025, 1, 18, 0, 0, 0), + KpIndex: 2.67F, + StationsCount: 8 + ), + new KpIndexNowcastResponse + ( + DateTime: new DateTime(2025, 1, 18, 3, 0, 0), + KpIndex: 2.00F, + StationsCount: 10 + ), + new KpIndexNowcastResponse + ( + DateTime: new DateTime(2025, 1, 18, 6, 0, 0), + KpIndex: 2.67F, + StationsCount: 8 + ), + new KpIndexNowcastResponse + ( + DateTime: new DateTime(2025, 1, 18, 9, 0, 0), + KpIndex: 2.00F, + StationsCount: 8 + ), + new KpIndexNowcastResponse + ( + DateTime: new DateTime(2025, 1, 18, 12, 0, 0), + KpIndex: 2.67F, + StationsCount: 9 + ), + ]); + } +} diff --git a/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndex27DayForecastSample.txt b/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndex27DayForecastSample.txt new file mode 100644 index 0000000..470615b --- /dev/null +++ b/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndex27DayForecastSample.txt @@ -0,0 +1,18 @@ +:Product: 27-day Space Weather Outlook Table 27DO.txt +:Issued: 2025 Jan 20 0136 UTC +# Prepared by the US Dept. of Commerce, NOAA, Space Weather Prediction Center +# Product description and SWPC contact on the Web +# https://www.swpc.noaa.gov/content/subscription-services +# +# 27-day Space Weather Outlook Table +# Issued 2025-01-20 +# +# UTC Radio Flux Planetary Largest +# Date 10.7 cm A Index Kp Index +2025 Jan 20 235 12 4 +2025 Jan 21 235 8 3 +2025 Jan 22 235 5 2 +2025 Jan 23 230 5 2 +2025 Jan 24 230 5 2 + + diff --git a/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndex3DayForecastSample.json b/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndex3DayForecastSample.json new file mode 100644 index 0000000..16cc09f --- /dev/null +++ b/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndex3DayForecastSample.json @@ -0,0 +1,8 @@ +[ + ["time_tag","kp","observed","noaa_scale"], + ["2025-01-18 00:00:00","2.67","observed",null], + ["2025-01-18 03:00:00","2.00","observed",null], + ["2025-01-18 06:00:00","2.67","observed",null], + ["2025-01-18 09:00:00","2.00","observed",null], + ["2025-01-18 12:00:00","2.67","observed",null] +] diff --git a/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndexNowcastSample.json b/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndexNowcastSample.json new file mode 100644 index 0000000..4b376f3 --- /dev/null +++ b/tests/UnitTests/NoaaClient/KpIndex/Samples/KpIndexNowcastSample.json @@ -0,0 +1,8 @@ +[ + ["time_tag","Kp","a_running","station_count"], + ["2025-01-18 00:00:00.000","2.67","12","8"], + ["2025-01-18 03:00:00.000","2.00","7","10"], + ["2025-01-18 06:00:00.000","2.67","12","8"], + ["2025-01-18 09:00:00.000","2.00","7","8"], + ["2025-01-18 12:00:00.000","2.67","12","9"] +] diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj new file mode 100644 index 0000000..b629956 --- /dev/null +++ b/tests/UnitTests/UnitTests.csproj @@ -0,0 +1,19 @@ + + + + Exe + AuroraScienceHub.Integrations.UnitTests + true + + + + + + + + + + + + + diff --git a/tests/UnitTests/Usings.cs b/tests/UnitTests/Usings.cs new file mode 100644 index 0000000..c802f44 --- /dev/null +++ b/tests/UnitTests/Usings.cs @@ -0,0 +1 @@ +global using Xunit; diff --git a/tests/UnitTests/Utils/EmbeddedResourceDataAttribute.cs b/tests/UnitTests/Utils/EmbeddedResourceDataAttribute.cs new file mode 100644 index 0000000..36e7151 --- /dev/null +++ b/tests/UnitTests/Utils/EmbeddedResourceDataAttribute.cs @@ -0,0 +1,46 @@ +// Source code from: https://patriksvensson.se/posts/2017/11/using-embedded-resources-in-xunit-tests + +using System.Reflection; +using Xunit.Sdk; +using Xunit.v3; + +namespace AuroraScienceHub.Integrations.UnitTests.Utils; + +/// +/// Attribute to load test data from embedded resources. +/// +public sealed class EmbeddedResourceDataAttribute : DataAttribute +{ + private readonly string[] _args; + + public EmbeddedResourceDataAttribute(params string[] args) + { + _args = args; + } + + public override ValueTask> GetData(MethodInfo testMethod, DisposalTracker disposalTracker) + { + var result = new object[_args.Length]; + for (var index = 0; index < _args.Length; index++) + { + result[index] = ReadManifestData(_args[index]); + } + return new ValueTask>([new TheoryDataRow(result)]); + } + + private static string ReadManifestData(string resourceName) + { + var assembly = typeof(EmbeddedResourceDataAttribute).GetTypeInfo().Assembly; + resourceName = resourceName.Replace("/", "."); + using var stream = assembly.GetManifestResourceStream(resourceName); + if (stream == null) + { + throw new InvalidOperationException($"Could not load manifest resource stream: {resourceName}"); + } + + using var reader = new StreamReader(stream); + return reader.ReadToEnd(); + } + + public override bool SupportsDiscoveryEnumeration() => true; +}