Bläddra i källkod

Merge uv_meter_as7331 from https://github.com/michaelbaisch/uv_meter

# Conflicts:
#	uv_meter_as7331/application.fam
Willy-JL 8 månader sedan
förälder
incheckning
4a2371d70b

+ 43 - 0
uv_meter_as7331/README.md

@@ -4,6 +4,26 @@ A Flipper Zero application designed to measure ultraviolet (UV) radiation levels
 
 ![wiring](images/flipper_with_sensor.jpeg)
 
+
+
+## Motivation
+
+Have you heard that sitting behind a window protects you from sunburn? Sounds good, right? Unless you do a bit of research and find it's only *kind of* true. While potentially some UV-B (the stuff that causes immediate sunburn) gets blocked, a lot of UV-A could still get through. And guess what? UV-A plays a role in developing melanoma—a deadly form of skin cancer. Now it almost sounds even *more* dangerous: you lose the immediate “sunburn feedback” yet still face a long-term risk.
+
+It gets even more complicated because it depends on the specific type of window and possible surface treatments; I have also heard that some car windows might be better in this regard. In the end, things seem less predictable, with more questions than before. And it doesn't stop there: do my sunglasses really work? Does my shirt actually protect me? How bad is it really in the shade or on a cloudy day?
+
+What we need is **data**. Being able to actually *measure* something can be surprisingly empowering. I found the AS7331 sensor, which can independently measure UV-A, UV-B, and UV-C—and this project was born.
+
+In general, it's surprising how low the maximum daily exposure durations actually are (based on the 2024 TLVs and BEIs by the [ACGIH](https://en.wikipedia.org/wiki/American_Conference_of_Governmental_Industrial_Hygienists)). Sure, compared to direct sunlight, you're better off behind a window, in the shade, or under clouds—but probably not *as much* as you'd expect.
+
+In some of my measurements, the safe daily exposure duration, for example, tripled—but when you're starting with just 3 minutes, tripling still leaves you under 10 minutes. My takeaway? I should protect my eyes and skin more than I once thought necessary.
+
+
+
+https://github.com/user-attachments/assets/cabb948c-9c79-4a1d-aab7-8789f0833f28
+
+
+
 ## Wiring
 
 Connect the AS7331 sensor to your Flipper Zero via I²C:
@@ -20,6 +40,7 @@ Connect the AS7331 sensor to your Flipper Zero via I²C:
 By default, the application scans all possible I²C addresses for the sensor. However, you can manually set a specific address in the settings menu, accessible by pressing the **Enter** button.
 
 
+
 ## Usage
 
 Once connected, the application automatically displays real-time UV measurements. The main screen shows individual UV-A, UV-B, and UV-C readings. Beneath the numbers you see the currently used unit (µW/cm², W/m² or mW/m²).
@@ -39,6 +60,28 @@ When following the maximum daily exposure duration, the TLV/BEI guidelines ensur
 > “[...] nearly all healthy workers may be repeatedly exposed without acute adverse health effects such as erythema and photokeratitis.”
 
 
+
+## Magic Numbers
+
+In the source code, specifically the [`uv_meter_data_calculate_effective_results()`](views/uv_meter_data.cpp#L668) function, you'll find some numbers that might look mysterious ("magic numbers"). They're used to calculate the maximum daily UV exposure duration based on sensor readings:
+
+```cpp
+// Weighted Spectral Effectiveness
+double w_spectral_eff_uv_a = 0.0002824;
+double w_spectral_eff_uv_b = 0.3814;
+double w_spectral_eff_uv_c = 0.6047;
+
+if(eyes_protected) { // 😎
+    // w_spectral_eff_uv_a is the same
+    w_spectral_eff_uv_b = 0.2009;
+    w_spectral_eff_uv_c = 0.2547;
+}
+```
+
+You might wonder, "Where do these numbers come from?" Good question! To uncover the full story behind these values, check out the detailed explanation in the [Magic Numbers documentation](magic_numbers/README.md).
+
+
+
 ## Disclaimer
 
 This application is provided for informational purposes only and should not be used as a sole basis for safety-critical decisions. Always follow official guidelines, regulations, and professional advice regarding UV exposure. The developer assumes no responsibility for any damages, injuries, or consequences arising from decisions or actions based on the information provided by this application.

+ 1 - 1
uv_meter_as7331/application.fam

@@ -6,7 +6,7 @@ App(
     requires=["gui"],
     stack_size=2 * 1024,
     fap_category="GPIO/Sensors",
-    sources=["*.c*", "!documentation"],
+    sources=["*.c*", "!magic_numbers"],
     fap_version="1.0",
     fap_icon="uv_meter.png",
     fap_description="Measure UV radiation using the AS7331 sensor",

+ 13 - 0
uv_meter_as7331/magic_numbers/Pipfile

@@ -0,0 +1,13 @@
+[[source]]
+url = "https://pypi.org/simple"
+verify_ssl = true
+name = "pypi"
+
+[packages]
+pandas = "*"
+matplotlib = "*"
+
+[dev-packages]
+
+[requires]
+python_version = "3.13"

+ 506 - 0
uv_meter_as7331/magic_numbers/Pipfile.lock

@@ -0,0 +1,506 @@
+{
+    "_meta": {
+        "hash": {
+            "sha256": "e2e6aa7880d2c85b335c23cbe1bd6038a91e7538c7a8a58bfa23aeb33a4eb2bd"
+        },
+        "pipfile-spec": 6,
+        "requires": {
+            "python_version": "3.13"
+        },
+        "sources": [
+            {
+                "name": "pypi",
+                "url": "https://pypi.org/simple",
+                "verify_ssl": true
+            }
+        ]
+    },
+    "default": {
+        "contourpy": {
+            "hashes": [
+                "sha256:041b640d4ec01922083645a94bb3b2e777e6b626788f4095cf21abbe266413c1",
+                "sha256:05e806338bfeaa006acbdeba0ad681a10be63b26e1b17317bfac3c5d98f36cda",
+                "sha256:08d9d449a61cf53033612cb368f3a1b26cd7835d9b8cd326647efe43bca7568d",
+                "sha256:0ffa84be8e0bd33410b17189f7164c3589c229ce5db85798076a3fa136d0e509",
+                "sha256:113231fe3825ebf6f15eaa8bc1f5b0ddc19d42b733345eae0934cb291beb88b6",
+                "sha256:14c102b0eab282427b662cb590f2e9340a9d91a1c297f48729431f2dcd16e14f",
+                "sha256:174e758c66bbc1c8576992cec9599ce8b6672b741b5d336b5c74e35ac382b18e",
+                "sha256:19c1555a6801c2f084c7ddc1c6e11f02eb6a6016ca1318dd5452ba3f613a1751",
+                "sha256:19d40d37c1c3a4961b4619dd9d77b12124a453cc3d02bb31a07d58ef684d3d86",
+                "sha256:1bf98051f1045b15c87868dbaea84f92408337d4f81d0e449ee41920ea121d3b",
+                "sha256:20914c8c973f41456337652a6eeca26d2148aa96dd7ac323b74516988bea89fc",
+                "sha256:287ccc248c9e0d0566934e7d606201abd74761b5703d804ff3df8935f523d546",
+                "sha256:2ba94a401342fc0f8b948e57d977557fbf4d515f03c67682dd5c6191cb2d16ec",
+                "sha256:31c1b55c1f34f80557d3830d3dd93ba722ce7e33a0b472cba0ec3b6535684d8f",
+                "sha256:36987a15e8ace5f58d4d5da9dca82d498c2bbb28dff6e5d04fbfcc35a9cb3a82",
+                "sha256:3a04ecd68acbd77fa2d39723ceca4c3197cb2969633836ced1bea14e219d077c",
+                "sha256:3e8b974d8db2c5610fb4e76307e265de0edb655ae8169e8b21f41807ccbeec4b",
+                "sha256:3ea9924d28fc5586bf0b42d15f590b10c224117e74409dd7a0be3b62b74a501c",
+                "sha256:4318af1c925fb9a4fb190559ef3eec206845f63e80fb603d47f2d6d67683901c",
+                "sha256:44a29502ca9c7b5ba389e620d44f2fbe792b1fb5734e8b931ad307071ec58c53",
+                "sha256:47734d7073fb4590b4a40122b35917cd77be5722d80683b249dac1de266aac80",
+                "sha256:4d76d5993a34ef3df5181ba3c92fabb93f1eaa5729504fb03423fcd9f3177242",
+                "sha256:4dbbc03a40f916a8420e420d63e96a1258d3d1b58cbdfd8d1f07b49fcbd38e85",
+                "sha256:500360b77259914f7805af7462e41f9cb7ca92ad38e9f94d6c8641b089338124",
+                "sha256:523a8ee12edfa36f6d2a49407f705a6ef4c5098de4f498619787e272de93f2d5",
+                "sha256:573abb30e0e05bf31ed067d2f82500ecfdaec15627a59d63ea2d95714790f5c2",
+                "sha256:5b75aa69cb4d6f137b36f7eb2ace9280cfb60c55dc5f61c731fdf6f037f958a3",
+                "sha256:61332c87493b00091423e747ea78200659dc09bdf7fd69edd5e98cef5d3e9a8d",
+                "sha256:805617228ba7e2cbbfb6c503858e626ab528ac2a32a04a2fe88ffaf6b02c32bc",
+                "sha256:841ad858cff65c2c04bf93875e384ccb82b654574a6d7f30453a04f04af71342",
+                "sha256:89785bb2a1980c1bd87f0cb1517a71cde374776a5f150936b82580ae6ead44a1",
+                "sha256:8eb96e79b9f3dcadbad2a3891672f81cdcab7f95b27f28f1c67d75f045b6b4f1",
+                "sha256:974d8145f8ca354498005b5b981165b74a195abfae9a8129df3e56771961d595",
+                "sha256:9ddeb796389dadcd884c7eb07bd14ef12408aaae358f0e2ae24114d797eede30",
+                "sha256:a045f341a77b77e1c5de31e74e966537bba9f3c4099b35bf4c2e3939dd54cdab",
+                "sha256:a0cffcbede75c059f535725c1680dfb17b6ba8753f0c74b14e6a9c68c29d7ea3",
+                "sha256:a761d9ccfc5e2ecd1bf05534eda382aa14c3e4f9205ba5b1684ecfe400716ef2",
+                "sha256:a7895f46d47671fa7ceec40f31fae721da51ad34bdca0bee83e38870b1f47ffd",
+                "sha256:a9fa36448e6a3a1a9a2ba23c02012c43ed88905ec80163f2ffe2421c7192a5d7",
+                "sha256:ab29962927945d89d9b293eabd0d59aea28d887d4f3be6c22deaefbb938a7277",
+                "sha256:abbb49fb7dac584e5abc6636b7b2a7227111c4f771005853e7d25176daaf8453",
+                "sha256:ac4578ac281983f63b400f7fe6c101bedc10651650eef012be1ccffcbacf3697",
+                "sha256:adce39d67c0edf383647a3a007de0a45fd1b08dedaa5318404f1a73059c2512b",
+                "sha256:ade08d343436a94e633db932e7e8407fe7de8083967962b46bdfc1b0ced39454",
+                "sha256:b2bdca22a27e35f16794cf585832e542123296b4687f9fd96822db6bae17bfc9",
+                "sha256:b2f926efda994cdf3c8d3fdb40b9962f86edbc4457e739277b961eced3d0b4c1",
+                "sha256:b457d6430833cee8e4b8e9b6f07aa1c161e5e0d52e118dc102c8f9bd7dd060d6",
+                "sha256:c414fc1ed8ee1dbd5da626cf3710c6013d3d27456651d156711fa24f24bd1291",
+                "sha256:cb76c1a154b83991a3cbbf0dfeb26ec2833ad56f95540b442c73950af2013750",
+                "sha256:dfd97abd83335045a913e3bcc4a09c0ceadbe66580cf573fe961f4a825efa699",
+                "sha256:e914a8cb05ce5c809dd0fe350cfbb4e881bde5e2a38dc04e3afe1b3e58bd158e",
+                "sha256:ece6df05e2c41bd46776fbc712e0996f7c94e0d0543af1656956d150c4ca7c81",
+                "sha256:efa874e87e4a647fd2e4f514d5e91c7d493697127beb95e77d2f7561f6905bd9",
+                "sha256:f611e628ef06670df83fce17805c344710ca5cde01edfdc72751311da8585375"
+            ],
+            "markers": "python_version >= '3.10'",
+            "version": "==1.3.1"
+        },
+        "cycler": {
+            "hashes": [
+                "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30",
+                "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==0.12.1"
+        },
+        "fonttools": {
+            "hashes": [
+                "sha256:003548eadd674175510773f73fb2060bb46adb77c94854af3e0cc5bc70260049",
+                "sha256:0073b62c3438cf0058488c002ea90489e8801d3a7af5ce5f7c05c105bee815c3",
+                "sha256:1088182f68c303b50ca4dc0c82d42083d176cba37af1937e1a976a31149d4d14",
+                "sha256:133bedb9a5c6376ad43e6518b7e2cd2f866a05b1998f14842631d5feb36b5786",
+                "sha256:14a3e3e6b211660db54ca1ef7006401e4a694e53ffd4553ab9bc87ead01d0f05",
+                "sha256:17f39313b649037f6c800209984a11fc256a6137cbe5487091c6c7187cae4685",
+                "sha256:193b86e9f769320bc98ffdb42accafb5d0c8c49bd62884f1c0702bc598b3f0a2",
+                "sha256:2d351275f73ebdd81dd5b09a8b8dac7a30f29a279d41e1c1192aedf1b6dced40",
+                "sha256:300c310bb725b2bdb4f5fc7e148e190bd69f01925c7ab437b9c0ca3e1c7cd9ba",
+                "sha256:331954d002dbf5e704c7f3756028e21db07097c19722569983ba4d74df014000",
+                "sha256:38b947de71748bab150259ee05a775e8a0635891568e9fdb3cdd7d0e0004e62f",
+                "sha256:3cf4f8d2a30b454ac682e12c61831dcb174950c406011418e739de592bbf8f76",
+                "sha256:3fd3fccb7b9adaaecfa79ad51b759f2123e1aba97f857936ce044d4f029abd71",
+                "sha256:442ad4122468d0e47d83bc59d0e91b474593a8c813839e1872e47c7a0cb53b10",
+                "sha256:47b5e4680002ae1756d3ae3b6114e20aaee6cc5c69d1e5911f5ffffd3ee46c6b",
+                "sha256:53f5e9767978a4daf46f28e09dbeb7d010319924ae622f7b56174b777258e5ba",
+                "sha256:62b4c6802fa28e14dba010e75190e0e6228513573f1eeae57b11aa1a39b7e5b1",
+                "sha256:62cc1253827d1e500fde9dbe981219fea4eb000fd63402283472d38e7d8aa1c6",
+                "sha256:654ac4583e2d7c62aebc6fc6a4c6736f078f50300e18aa105d87ce8925cfac31",
+                "sha256:661a8995d11e6e4914a44ca7d52d1286e2d9b154f685a4d1f69add8418961563",
+                "sha256:6c1d38642ca2dddc7ae992ef5d026e5061a84f10ff2b906be5680ab089f55bb8",
+                "sha256:6e81c1cc80c1d8bf071356cc3e0e25071fbba1c75afc48d41b26048980b3c771",
+                "sha256:705837eae384fe21cee5e5746fd4f4b2f06f87544fa60f60740007e0aa600311",
+                "sha256:7ef04bc7827adb7532be3d14462390dd71287644516af3f1e67f1e6ff9c6d6df",
+                "sha256:86b2a1013ef7a64d2e94606632683f07712045ed86d937c11ef4dde97319c086",
+                "sha256:8d1613abd5af2f93c05867b3a3759a56e8bf97eb79b1da76b2bc10892f96ff16",
+                "sha256:965d0209e6dbdb9416100123b6709cb13f5232e2d52d17ed37f9df0cc31e2b35",
+                "sha256:96a4271f63a615bcb902b9f56de00ea225d6896052c49f20d0c91e9f43529a29",
+                "sha256:9d94449ad0a5f2a8bf5d2f8d71d65088aee48adbe45f3c5f8e00e3ad861ed81a",
+                "sha256:9da650cb29bc098b8cfd15ef09009c914b35c7986c8fa9f08b51108b7bc393b4",
+                "sha256:a05d1f07eb0a7d755fbe01fee1fd255c3a4d3730130cf1bfefb682d18fd2fcea",
+                "sha256:a114d1567e1a1586b7e9e7fc2ff686ca542a82769a296cef131e4c4af51e58f4",
+                "sha256:a1af375734018951c31c0737d04a9d5fd0a353a0253db5fbed2ccd44eac62d8c",
+                "sha256:b23d30a2c0b992fb1c4f8ac9bfde44b5586d23457759b6cf9a787f1a35179ee0",
+                "sha256:bc871904a53a9d4d908673c6faa15689874af1c7c5ac403a8e12d967ebd0c0dc",
+                "sha256:bce60f9a977c9d3d51de475af3f3581d9b36952e1f8fc19a1f2254f1dda7ce9c",
+                "sha256:bd9825822e7bb243f285013e653f6741954d8147427aaa0324a862cdbf4cbf62",
+                "sha256:ca7962e8e5fc047cc4e59389959843aafbf7445b6c08c20d883e60ced46370a5",
+                "sha256:d0cb73ccf7f6d7ca8d0bc7ea8ac0a5b84969a41c56ac3ac3422a24df2680546f",
+                "sha256:d54a45d30251f1d729e69e5b675f9a08b7da413391a1227781e2a297fa37f6d2",
+                "sha256:d6ca96d1b61a707ba01a43318c9c40aaf11a5a568d1e61146fafa6ab20890793",
+                "sha256:d6f195c14c01bd057bc9b4f70756b510e009c83c5ea67b25ced3e2c38e6ee6e9",
+                "sha256:e2cad98c94833465bcf28f51c248aaf07ca022efc6a3eba750ad9c1e0256d278",
+                "sha256:e2e993e8db36306cc3f1734edc8ea67906c55f98683d6fd34c3fc5593fdbba4c",
+                "sha256:e9270505a19361e81eecdbc2c251ad1e1a9a9c2ad75fa022ccdee533f55535dc",
+                "sha256:f20e2c0dfab82983a90f3d00703ac0960412036153e5023eed2b4641d7d5e692",
+                "sha256:f36a0868f47b7566237640c026c65a86d09a3d9ca5df1cd039e30a1da73098a0",
+                "sha256:f59746f7953f69cc3290ce2f971ab01056e55ddd0fb8b792c31a8acd7fee2d28",
+                "sha256:fa760e5fe8b50cbc2d71884a1eff2ed2b95a005f02dda2fa431560db0ddd927f",
+                "sha256:ffda9b8cd9cb8b301cae2602ec62375b59e2e2108a117746f12215145e3f786c"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==4.56.0"
+        },
+        "kiwisolver": {
+            "hashes": [
+                "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50",
+                "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c",
+                "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8",
+                "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc",
+                "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f",
+                "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79",
+                "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6",
+                "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2",
+                "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605",
+                "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09",
+                "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab",
+                "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e",
+                "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc",
+                "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8",
+                "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7",
+                "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880",
+                "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b",
+                "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b",
+                "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff",
+                "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3",
+                "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c",
+                "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0",
+                "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6",
+                "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30",
+                "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47",
+                "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0",
+                "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1",
+                "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90",
+                "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d",
+                "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b",
+                "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c",
+                "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a",
+                "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e",
+                "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc",
+                "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16",
+                "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a",
+                "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712",
+                "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c",
+                "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3",
+                "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc",
+                "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561",
+                "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d",
+                "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc",
+                "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db",
+                "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed",
+                "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751",
+                "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957",
+                "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165",
+                "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2",
+                "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476",
+                "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84",
+                "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246",
+                "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4",
+                "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25",
+                "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d",
+                "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271",
+                "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb",
+                "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31",
+                "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e",
+                "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85",
+                "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b",
+                "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7",
+                "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03",
+                "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b",
+                "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d",
+                "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a",
+                "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d",
+                "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3",
+                "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67",
+                "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f",
+                "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c",
+                "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502",
+                "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062",
+                "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954",
+                "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb",
+                "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a",
+                "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b",
+                "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed",
+                "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34",
+                "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794"
+            ],
+            "markers": "python_version >= '3.10'",
+            "version": "==1.4.8"
+        },
+        "matplotlib": {
+            "hashes": [
+                "sha256:01e63101ebb3014e6e9f80d9cf9ee361a8599ddca2c3e166c563628b39305dbb",
+                "sha256:02582304e352f40520727984a5a18f37e8187861f954fea9be7ef06569cf85b4",
+                "sha256:057206ff2d6ab82ff3e94ebd94463d084760ca682ed5f150817b859372ec4401",
+                "sha256:0721a3fd3d5756ed593220a8b86808a36c5031fce489adb5b31ee6dbb47dd5b2",
+                "sha256:0f69dc9713e4ad2fb21a1c30e37bd445d496524257dfda40ff4a8efb3604ab5c",
+                "sha256:11b65088c6f3dae784bc72e8d039a2580186285f87448babb9ddb2ad0082993a",
+                "sha256:1985ad3d97f51307a2cbfc801a930f120def19ba22864182dacef55277102ba6",
+                "sha256:19b06241ad89c3ae9469e07d77efa87041eac65d78df4fcf9cac318028009b01",
+                "sha256:2589659ea30726284c6c91037216f64a506a9822f8e50592d48ac16a2f29e044",
+                "sha256:35e87384ee9e488d8dd5a2dd7baf471178d38b90618d8ea147aced4ab59c9bea",
+                "sha256:3f06bad951eea6422ac4e8bdebcf3a70c59ea0a03338c5d2b109f57b64eb3972",
+                "sha256:4c59af3e8aca75d7744b68e8e78a669e91ccbcf1ac35d0102a7b1b46883f1dd7",
+                "sha256:4f0647b17b667ae745c13721602b540f7aadb2a32c5b96e924cd4fea5dcb90f1",
+                "sha256:56c5d9fcd9879aa8040f196a235e2dcbdf7dd03ab5b07c0696f80bc6cf04bedd",
+                "sha256:5d45d3f5245be5b469843450617dcad9af75ca50568acf59997bed9311131a0b",
+                "sha256:648406f1899f9a818cef8c0231b44dcfc4ff36f167101c3fd1c9151f24220fdc",
+                "sha256:66e907a06e68cb6cfd652c193311d61a12b54f56809cafbed9736ce5ad92f107",
+                "sha256:7e496c01441be4c7d5f96d4e40f7fca06e20dcb40e44c8daa2e740e1757ad9e6",
+                "sha256:8e875b95ac59a7908978fe307ecdbdd9a26af7fa0f33f474a27fcf8c99f64a19",
+                "sha256:8e8e25b1209161d20dfe93037c8a7f7ca796ec9aa326e6e4588d8c4a5dd1e473",
+                "sha256:a144867dd6bf8ba8cb5fc81a158b645037e11b3e5cf8a50bd5f9917cb863adfe",
+                "sha256:a3dfb036f34873b46978f55e240cff7a239f6c4409eac62d8145bad3fc6ba5a3",
+                "sha256:a97ff127f295817bc34517255c9db6e71de8eddaab7f837b7d341dee9f2f587f",
+                "sha256:aa3854b5f9473564ef40a41bc922be978fab217776e9ae1545c9b3a5cf2092a3",
+                "sha256:bc411ebd5889a78dabbc457b3fa153203e22248bfa6eedc6797be5df0164dbf9",
+                "sha256:c42eee41e1b60fd83ee3292ed83a97a5f2a8239b10c26715d8a6172226988d7b",
+                "sha256:c96f2c2f825d1257e437a1482c5a2cf4fee15db4261bd6fc0750f81ba2b4ba3d",
+                "sha256:cfd414bce89cc78a7e1d25202e979b3f1af799e416010a20ab2b5ebb3a02425c",
+                "sha256:d0673b4b8f131890eb3a1ad058d6e065fb3c6e71f160089b65f8515373394698",
+                "sha256:d3809916157ba871bcdd33d3493acd7fe3037db5daa917ca6e77975a94cef779",
+                "sha256:dc6ab14a7ab3b4d813b88ba957fc05c79493a037f54e246162033591e770de6f",
+                "sha256:e8d2d0e3881b129268585bf4765ad3ee73a4591d77b9a18c214ac7e3a79fb2ba",
+                "sha256:e9b4bb156abb8fa5e5b2b460196f7db7264fc6d62678c03457979e7d5254b7be",
+                "sha256:ff2ae14910be903f4a24afdbb6d7d3a6c44da210fc7d42790b87aeac92238a16"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.10'",
+            "version": "==3.10.1"
+        },
+        "numpy": {
+            "hashes": [
+                "sha256:0391ea3622f5c51a2e29708877d56e3d276827ac5447d7f45e9bc4ade8923c52",
+                "sha256:12c045f43b1d2915eca6b880a7f4a256f59d62df4f044788c8ba67709412128d",
+                "sha256:136553f123ee2951bfcfbc264acd34a2fc2f29d7cdf610ce7daf672b6fbaa693",
+                "sha256:1402da8e0f435991983d0a9708b779f95a8c98c6b18a171b9f1be09005e64d9d",
+                "sha256:16372619ee728ed67a2a606a614f56d3eabc5b86f8b615c79d01957062826ca8",
+                "sha256:1ad78ce7f18ce4e7df1b2ea4019b5817a2f6a8a16e34ff2775f646adce0a5027",
+                "sha256:1b416af7d0ed3271cad0f0a0d0bee0911ed7eba23e66f8424d9f3dfcdcae1304",
+                "sha256:1f45315b2dc58d8a3e7754fe4e38b6fce132dab284a92851e41b2b344f6441c5",
+                "sha256:2376e317111daa0a6739e50f7ee2a6353f768489102308b0d98fcf4a04f7f3b5",
+                "sha256:23c9f4edbf4c065fddb10a4f6e8b6a244342d95966a48820c614891e5059bb50",
+                "sha256:246535e2f7496b7ac85deffe932896a3577be7af8fb7eebe7146444680297e9a",
+                "sha256:2e8da03bd561504d9b20e7a12340870dfc206c64ea59b4cfee9fceb95070ee94",
+                "sha256:34c1b7e83f94f3b564b35f480f5652a47007dd91f7c839f404d03279cc8dd021",
+                "sha256:39261798d208c3095ae4f7bc8eaeb3481ea8c6e03dc48028057d3cbdbdb8937e",
+                "sha256:3b787adbf04b0db1967798dba8da1af07e387908ed1553a0d6e74c084d1ceafe",
+                "sha256:3c2ec8a0f51d60f1e9c0c5ab116b7fc104b165ada3f6c58abf881cb2eb16044d",
+                "sha256:435e7a933b9fda8126130b046975a968cc2d833b505475e588339e09f7672890",
+                "sha256:4d8335b5f1b6e2bce120d55fb17064b0262ff29b459e8493d1785c18ae2553b8",
+                "sha256:4d9828d25fb246bedd31e04c9e75714a4087211ac348cb39c8c5f99dbb6683fe",
+                "sha256:52659ad2534427dffcc36aac76bebdd02b67e3b7a619ac67543bc9bfe6b7cdb1",
+                "sha256:5266de33d4c3420973cf9ae3b98b54a2a6d53a559310e3236c4b2b06b9c07d4e",
+                "sha256:5521a06a3148686d9269c53b09f7d399a5725c47bbb5b35747e1cb76326b714b",
+                "sha256:596140185c7fa113563c67c2e894eabe0daea18cf8e33851738c19f70ce86aeb",
+                "sha256:5b732c8beef1d7bc2d9e476dbba20aaff6167bf205ad9aa8d30913859e82884b",
+                "sha256:5ebeb7ef54a7be11044c33a17b2624abe4307a75893c001a4800857956b41094",
+                "sha256:712a64103d97c404e87d4d7c47fb0c7ff9acccc625ca2002848e0d53288b90ea",
+                "sha256:7678556eeb0152cbd1522b684dcd215250885993dd00adb93679ec3c0e6e091c",
+                "sha256:77974aba6c1bc26e3c205c2214f0d5b4305bdc719268b93e768ddb17e3fdd636",
+                "sha256:783145835458e60fa97afac25d511d00a1eca94d4a8f3ace9fe2043003c678e4",
+                "sha256:7bfdb06b395385ea9b91bf55c1adf1b297c9fdb531552845ff1d3ea6e40d5aba",
+                "sha256:7c8dde0ca2f77828815fd1aedfdf52e59071a5bae30dac3b4da2a335c672149a",
+                "sha256:83807d445817326b4bcdaaaf8e8e9f1753da04341eceec705c001ff342002e5d",
+                "sha256:87eed225fd415bbae787f93a457af7f5990b92a334e346f72070bf569b9c9c95",
+                "sha256:8fb62fe3d206d72fe1cfe31c4a1106ad2b136fcc1606093aeab314f02930fdf2",
+                "sha256:95172a21038c9b423e68be78fd0be6e1b97674cde269b76fe269a5dfa6fadf0b",
+                "sha256:9f48ba6f6c13e5e49f3d3efb1b51c8193215c42ac82610a04624906a9270be6f",
+                "sha256:a0c03b6be48aaf92525cccf393265e02773be8fd9551a2f9adbe7db1fa2b60f1",
+                "sha256:a5ae282abe60a2db0fd407072aff4599c279bcd6e9a2475500fc35b00a57c532",
+                "sha256:aee2512827ceb6d7f517c8b85aa5d3923afe8fc7a57d028cffcd522f1c6fd082",
+                "sha256:c8b0451d2ec95010d1db8ca733afc41f659f425b7f608af569711097fd6014e2",
+                "sha256:c9aa4496fd0e17e3843399f533d62857cef5900facf93e735ef65aa4bbc90ef0",
+                "sha256:cbc6472e01952d3d1b2772b720428f8b90e2deea8344e854df22b0618e9cce71",
+                "sha256:cdfe0c22692a30cd830c0755746473ae66c4a8f2e7bd508b35fb3b6a0813d787",
+                "sha256:cf802eef1f0134afb81fef94020351be4fe1d6681aadf9c5e862af6602af64ef",
+                "sha256:d42f9c36d06440e34226e8bd65ff065ca0963aeecada587b937011efa02cdc9d",
+                "sha256:d5b47c440210c5d1d67e1cf434124e0b5c395eee1f5806fdd89b553ed1acd0a3",
+                "sha256:d9b4a8148c57ecac25a16b0e11798cbe88edf5237b0df99973687dd866f05e1b",
+                "sha256:daf43a3d1ea699402c5a850e5313680ac355b4adc9770cd5cfc2940e7861f1bf",
+                "sha256:dbdc15f0c81611925f382dfa97b3bd0bc2c1ce19d4fe50482cb0ddc12ba30020",
+                "sha256:deaa09cd492e24fd9b15296844c0ad1b3c976da7907e1c1ed3a0ad21dded6f76",
+                "sha256:e37242f5324ffd9f7ba5acf96d774f9276aa62a966c0bad8dae692deebec7716",
+                "sha256:ed2cf9ed4e8ebc3b754d398cba12f24359f018b416c380f577bbae112ca52fc9",
+                "sha256:f2712c5179f40af9ddc8f6727f2bd910ea0eb50206daea75f58ddd9fa3f715bb",
+                "sha256:f4ca91d61a4bf61b0f2228f24bbfa6a9facd5f8af03759fe2a655c50ae2c6610",
+                "sha256:f6b3dfc7661f8842babd8ea07e9897fe3d9b69a1d7e5fbb743e4160f9387833b"
+            ],
+            "markers": "python_version >= '3.10'",
+            "version": "==2.2.3"
+        },
+        "packaging": {
+            "hashes": [
+                "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759",
+                "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"
+            ],
+            "markers": "python_version >= '3.8'",
+            "version": "==24.2"
+        },
+        "pandas": {
+            "hashes": [
+                "sha256:062309c1b9ea12a50e8ce661145c6aab431b1e99530d3cd60640e255778bd43a",
+                "sha256:15c0e1e02e93116177d29ff83e8b1619c93ddc9c49083f237d4312337a61165d",
+                "sha256:1948ddde24197a0f7add2bdc4ca83bf2b1ef84a1bc8ccffd95eda17fd836ecb5",
+                "sha256:1db71525a1538b30142094edb9adc10be3f3e176748cd7acc2240c2f2e5aa3a4",
+                "sha256:22a9d949bfc9a502d320aa04e5d02feab689d61da4e7764b62c30b991c42c5f0",
+                "sha256:29401dbfa9ad77319367d36940cd8a0b3a11aba16063e39632d98b0e931ddf32",
+                "sha256:31d0ced62d4ea3e231a9f228366919a5ea0b07440d9d4dac345376fd8e1477ea",
+                "sha256:3508d914817e153ad359d7e069d752cdd736a247c322d932eb89e6bc84217f28",
+                "sha256:37e0aced3e8f539eccf2e099f65cdb9c8aa85109b0be6e93e2baff94264bdc6f",
+                "sha256:381175499d3802cde0eabbaf6324cce0c4f5d52ca6f8c377c29ad442f50f6348",
+                "sha256:38cf8125c40dae9d5acc10fa66af8ea6fdf760b2714ee482ca691fc66e6fcb18",
+                "sha256:3b71f27954685ee685317063bf13c7709a7ba74fc996b84fc6821c59b0f06468",
+                "sha256:3fc6873a41186404dad67245896a6e440baacc92f5b716ccd1bc9ed2995ab2c5",
+                "sha256:4850ba03528b6dd51d6c5d273c46f183f39a9baf3f0143e566b89450965b105e",
+                "sha256:4f18ba62b61d7e192368b84517265a99b4d7ee8912f8708660fb4a366cc82667",
+                "sha256:56534ce0746a58afaf7942ba4863e0ef81c9c50d3f0ae93e9497d6a41a057645",
+                "sha256:59ef3764d0fe818125a5097d2ae867ca3fa64df032331b7e0917cf5d7bf66b13",
+                "sha256:5dbca4c1acd72e8eeef4753eeca07de9b1db4f398669d5994086f788a5d7cc30",
+                "sha256:5de54125a92bb4d1c051c0659e6fcb75256bf799a732a87184e5ea503965bce3",
+                "sha256:61c5ad4043f791b61dd4752191d9f07f0ae412515d59ba8f005832a532f8736d",
+                "sha256:6374c452ff3ec675a8f46fd9ab25c4ad0ba590b71cf0656f8b6daa5202bca3fb",
+                "sha256:63cc132e40a2e084cf01adf0775b15ac515ba905d7dcca47e9a251819c575ef3",
+                "sha256:66108071e1b935240e74525006034333f98bcdb87ea116de573a6a0dccb6c039",
+                "sha256:6dfcb5ee8d4d50c06a51c2fffa6cff6272098ad6540aed1a76d15fb9318194d8",
+                "sha256:7c2875855b0ff77b2a64a0365e24455d9990730d6431b9e0ee18ad8acee13dbd",
+                "sha256:7eee9e7cea6adf3e3d24e304ac6b8300646e2a5d1cd3a3c2abed9101b0846761",
+                "sha256:800250ecdadb6d9c78eae4990da62743b857b470883fa27f652db8bdde7f6659",
+                "sha256:86976a1c5b25ae3f8ccae3a5306e443569ee3c3faf444dfd0f41cda24667ad57",
+                "sha256:8cd6d7cc958a3910f934ea8dbdf17b2364827bb4dafc38ce6eef6bb3d65ff09c",
+                "sha256:99df71520d25fade9db7c1076ac94eb994f4d2673ef2aa2e86ee039b6746d20c",
+                "sha256:a5a1595fe639f5988ba6a8e5bc9649af3baf26df3998a0abe56c02609392e0a4",
+                "sha256:ad5b65698ab28ed8d7f18790a0dc58005c7629f227be9ecc1072aa74c0c1d43a",
+                "sha256:b1d432e8d08679a40e2a6d8b2f9770a5c21793a6f9f47fdd52c5ce1948a5a8a9",
+                "sha256:b8661b0238a69d7aafe156b7fa86c44b881387509653fdf857bebc5e4008ad42",
+                "sha256:ba96630bc17c875161df3818780af30e43be9b166ce51c9a18c1feae342906c2",
+                "sha256:bc6b93f9b966093cb0fd62ff1a7e4c09e6d546ad7c1de191767baffc57628f39",
+                "sha256:c124333816c3a9b03fbeef3a9f230ba9a737e9e5bb4060aa2107a86cc0a497fc",
+                "sha256:cd8d0c3be0515c12fed0bdbae072551c8b54b7192c7b1fda0ba56059a0179698",
+                "sha256:d9c45366def9a3dd85a6454c0e7908f2b3b8e9c138f5dc38fed7ce720d8453ed",
+                "sha256:f00d1345d84d8c86a63e476bb4955e46458b304b9575dcf71102b5c705320015",
+                "sha256:f3a255b2c19987fbbe62a9dfd6cff7ff2aa9ccab3fc75218fd4b7530f01efa24",
+                "sha256:fffb8ae78d8af97f849404f21411c95062db1496aeb3e56f146f0355c9989319"
+            ],
+            "index": "pypi",
+            "markers": "python_version >= '3.9'",
+            "version": "==2.2.3"
+        },
+        "pillow": {
+            "hashes": [
+                "sha256:015c6e863faa4779251436db398ae75051469f7c903b043a48f078e437656f83",
+                "sha256:0a2f91f8a8b367e7a57c6e91cd25af510168091fb89ec5146003e424e1558a96",
+                "sha256:11633d58b6ee5733bde153a8dafd25e505ea3d32e261accd388827ee987baf65",
+                "sha256:2062ffb1d36544d42fcaa277b069c88b01bb7298f4efa06731a7fd6cc290b81a",
+                "sha256:31eba6bbdd27dde97b0174ddf0297d7a9c3a507a8a1480e1e60ef914fe23d352",
+                "sha256:3362c6ca227e65c54bf71a5f88b3d4565ff1bcbc63ae72c34b07bbb1cc59a43f",
+                "sha256:368da70808b36d73b4b390a8ffac11069f8a5c85f29eff1f1b01bcf3ef5b2a20",
+                "sha256:36ba10b9cb413e7c7dfa3e189aba252deee0602c86c309799da5a74009ac7a1c",
+                "sha256:3764d53e09cdedd91bee65c2527815d315c6b90d7b8b79759cc48d7bf5d4f114",
+                "sha256:3a5fe20a7b66e8135d7fd617b13272626a28278d0e578c98720d9ba4b2439d49",
+                "sha256:3cdcdb0b896e981678eee140d882b70092dac83ac1cdf6b3a60e2216a73f2b91",
+                "sha256:4637b88343166249fe8aa94e7c4a62a180c4b3898283bb5d3d2fd5fe10d8e4e0",
+                "sha256:4db853948ce4e718f2fc775b75c37ba2efb6aaea41a1a5fc57f0af59eee774b2",
+                "sha256:4dd43a78897793f60766563969442020e90eb7847463eca901e41ba186a7d4a5",
+                "sha256:54251ef02a2309b5eec99d151ebf5c9904b77976c8abdcbce7891ed22df53884",
+                "sha256:54ce1c9a16a9561b6d6d8cb30089ab1e5eb66918cb47d457bd996ef34182922e",
+                "sha256:593c5fd6be85da83656b93ffcccc2312d2d149d251e98588b14fbc288fd8909c",
+                "sha256:5bb94705aea800051a743aa4874bb1397d4695fb0583ba5e425ee0328757f196",
+                "sha256:67cd427c68926108778a9005f2a04adbd5e67c442ed21d95389fe1d595458756",
+                "sha256:70ca5ef3b3b1c4a0812b5c63c57c23b63e53bc38e758b37a951e5bc466449861",
+                "sha256:73ddde795ee9b06257dac5ad42fcb07f3b9b813f8c1f7f870f402f4dc54b5269",
+                "sha256:758e9d4ef15d3560214cddbc97b8ef3ef86ce04d62ddac17ad39ba87e89bd3b1",
+                "sha256:7d33d2fae0e8b170b6a6c57400e077412240f6f5bb2a342cf1ee512a787942bb",
+                "sha256:7fdadc077553621911f27ce206ffcbec7d3f8d7b50e0da39f10997e8e2bb7f6a",
+                "sha256:8000376f139d4d38d6851eb149b321a52bb8893a88dae8ee7d95840431977081",
+                "sha256:837060a8599b8f5d402e97197d4924f05a2e0d68756998345c829c33186217b1",
+                "sha256:89dbdb3e6e9594d512780a5a1c42801879628b38e3efc7038094430844e271d8",
+                "sha256:8c730dc3a83e5ac137fbc92dfcfe1511ce3b2b5d7578315b63dbbb76f7f51d90",
+                "sha256:8e275ee4cb11c262bd108ab2081f750db2a1c0b8c12c1897f27b160c8bd57bbc",
+                "sha256:9044b5e4f7083f209c4e35aa5dd54b1dd5b112b108648f5c902ad586d4f945c5",
+                "sha256:93a18841d09bcdd774dcdc308e4537e1f867b3dec059c131fde0327899734aa1",
+                "sha256:9409c080586d1f683df3f184f20e36fb647f2e0bc3988094d4fd8c9f4eb1b3b3",
+                "sha256:96f82000e12f23e4f29346e42702b6ed9a2f2fea34a740dd5ffffcc8c539eb35",
+                "sha256:9aa9aeddeed452b2f616ff5507459e7bab436916ccb10961c4a382cd3e03f47f",
+                "sha256:9ee85f0696a17dd28fbcfceb59f9510aa71934b483d1f5601d1030c3c8304f3c",
+                "sha256:a07dba04c5e22824816b2615ad7a7484432d7f540e6fa86af60d2de57b0fcee2",
+                "sha256:a3cd561ded2cf2bbae44d4605837221b987c216cff94f49dfeed63488bb228d2",
+                "sha256:a697cd8ba0383bba3d2d3ada02b34ed268cb548b369943cd349007730c92bddf",
+                "sha256:a76da0a31da6fcae4210aa94fd779c65c75786bc9af06289cd1c184451ef7a65",
+                "sha256:a85b653980faad27e88b141348707ceeef8a1186f75ecc600c395dcac19f385b",
+                "sha256:a8d65b38173085f24bc07f8b6c505cbb7418009fa1a1fcb111b1f4961814a442",
+                "sha256:aa8dd43daa836b9a8128dbe7d923423e5ad86f50a7a14dc688194b7be5c0dea2",
+                "sha256:ab8a209b8485d3db694fa97a896d96dd6533d63c22829043fd9de627060beade",
+                "sha256:abc56501c3fd148d60659aae0af6ddc149660469082859fa7b066a298bde9482",
+                "sha256:ad5db5781c774ab9a9b2c4302bbf0c1014960a0a7be63278d13ae6fdf88126fe",
+                "sha256:ae98e14432d458fc3de11a77ccb3ae65ddce70f730e7c76140653048c71bfcbc",
+                "sha256:b20be51b37a75cc54c2c55def3fa2c65bb94ba859dde241cd0a4fd302de5ae0a",
+                "sha256:b523466b1a31d0dcef7c5be1f20b942919b62fd6e9a9be199d035509cbefc0ec",
+                "sha256:b5d658fbd9f0d6eea113aea286b21d3cd4d3fd978157cbf2447a6035916506d3",
+                "sha256:b6123aa4a59d75f06e9dd3dac5bf8bc9aa383121bb3dd9a7a612e05eabc9961a",
+                "sha256:bd165131fd51697e22421d0e467997ad31621b74bfc0b75956608cb2906dda07",
+                "sha256:bf902d7413c82a1bfa08b06a070876132a5ae6b2388e2712aab3a7cbc02205c6",
+                "sha256:c12fc111ef090845de2bb15009372175d76ac99969bdf31e2ce9b42e4b8cd88f",
+                "sha256:c1eec9d950b6fe688edee07138993e54ee4ae634c51443cfb7c1e7613322718e",
+                "sha256:c640e5a06869c75994624551f45e5506e4256562ead981cce820d5ab39ae2192",
+                "sha256:cc1331b6d5a6e144aeb5e626f4375f5b7ae9934ba620c0ac6b3e43d5e683a0f0",
+                "sha256:cfd5cd998c2e36a862d0e27b2df63237e67273f2fc78f47445b14e73a810e7e6",
+                "sha256:d3d8da4a631471dfaf94c10c85f5277b1f8e42ac42bade1ac67da4b4a7359b73",
+                "sha256:d44ff19eea13ae4acdaaab0179fa68c0c6f2f45d66a4d8ec1eda7d6cecbcc15f",
+                "sha256:dd0052e9db3474df30433f83a71b9b23bd9e4ef1de13d92df21a52c0303b8ab6",
+                "sha256:dd0e081319328928531df7a0e63621caf67652c8464303fd102141b785ef9547",
+                "sha256:dda60aa465b861324e65a78c9f5cf0f4bc713e4309f83bc387be158b077963d9",
+                "sha256:e06695e0326d05b06833b40b7ef477e475d0b1ba3a6d27da1bb48c23209bf457",
+                "sha256:e1abe69aca89514737465752b4bcaf8016de61b3be1397a8fc260ba33321b3a8",
+                "sha256:e267b0ed063341f3e60acd25c05200df4193e15a4a5807075cd71225a2386e26",
+                "sha256:e5449ca63da169a2e6068dd0e2fcc8d91f9558aba89ff6d02121ca8ab11e79e5",
+                "sha256:e63e4e5081de46517099dc30abe418122f54531a6ae2ebc8680bcd7096860eab",
+                "sha256:f189805c8be5ca5add39e6f899e6ce2ed824e65fb45f3c28cb2841911da19070",
+                "sha256:f7955ecf5609dee9442cbface754f2c6e541d9e6eda87fad7f7a989b0bdb9d71",
+                "sha256:f86d3a7a9af5d826744fabf4afd15b9dfef44fe69a98541f666f66fbb8d3fef9",
+                "sha256:fbd43429d0d7ed6533b25fc993861b8fd512c42d04514a0dd6337fb3ccf22761"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==11.1.0"
+        },
+        "pyparsing": {
+            "hashes": [
+                "sha256:506ff4f4386c4cec0590ec19e6302d3aedb992fdc02c761e90416f158dacf8e1",
+                "sha256:61980854fd66de3a90028d679a954d5f2623e83144b5afe5ee86f43d762e5f0a"
+            ],
+            "markers": "python_version >= '3.9'",
+            "version": "==3.2.1"
+        },
+        "python-dateutil": {
+            "hashes": [
+                "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3",
+                "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+            "version": "==2.9.0.post0"
+        },
+        "pytz": {
+            "hashes": [
+                "sha256:89dd22dca55b46eac6eda23b2d72721bf1bdfef212645d81513ef5d03038de57",
+                "sha256:c2db42be2a2518b28e65f9207c4d05e6ff547d1efa4086469ef855e4ab70178e"
+            ],
+            "version": "==2025.1"
+        },
+        "six": {
+            "hashes": [
+                "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274",
+                "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"
+            ],
+            "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'",
+            "version": "==1.17.0"
+        },
+        "tzdata": {
+            "hashes": [
+                "sha256:24894909e88cdb28bd1636c6887801df64cb485bd593f2fd83ef29075a81d694",
+                "sha256:7e127113816800496f027041c570f50bcd464a020098a3b6b199517772303639"
+            ],
+            "markers": "python_version >= '2'",
+            "version": "==2025.1"
+        }
+    },
+    "develop": {}
+}

+ 95 - 0
uv_meter_as7331/magic_numbers/README.md

@@ -0,0 +1,95 @@
+# Magic Numbers Explained
+
+In the source code, within the [`uv_meter_data_calculate_effective_results()`](../views/uv_meter_data.cpp#L668) function, you'll see some seemingly arbitrary ("magic") numbers. They’re essential for converting raw UV sensor values into what's called "Effective Irradiance", which is then used to calculate the maximum daily exposure duration shown in the application:
+
+```cpp
+UVMeterEffectiveResults
+    uv_meter_data_calculate_effective_results(const AS7331::Results* results, bool eyes_protected) {
+    // Weighted Spectral Effectiveness
+    double w_spectral_eff_uv_a = 0.0002824;
+    double w_spectral_eff_uv_b = 0.3814;
+    double w_spectral_eff_uv_c = 0.6047;
+
+    if(eyes_protected) { // 😎
+        // w_spectral_eff_uv_a is the same
+        w_spectral_eff_uv_b = 0.2009;
+        w_spectral_eff_uv_c = 0.2547;
+    }
+    UVMeterEffectiveResults effective_results;
+    // Effective Irradiance
+    effective_results.uv_a_eff = results->uv_a * w_spectral_eff_uv_a;
+    effective_results.uv_b_eff = results->uv_b * w_spectral_eff_uv_b;
+    effective_results.uv_c_eff = results->uv_c * w_spectral_eff_uv_c;
+    effective_results.uv_total_eff =
+        effective_results.uv_a_eff + effective_results.uv_b_eff + effective_results.uv_c_eff;
+
+    // Daily dose (seconds) based on the total effective irradiance
+    double daily_dose = 0.003; // J/cm^2
+    double uW_to_W = 1e-6;
+    effective_results.t_max = daily_dose / (effective_results.uv_total_eff * uW_to_W);
+    return effective_results;
+}
+```
+
+
+
+## Why These Numbers?
+
+Before diving deeper, here's a quick explanation of what we're doing:
+
+1. We start with raw UV-A, UV-B, and UV-C values from the sensor (in µW/cm²).
+2. Multiply these raw values by the magic numbers to get "Effective Irradiance".
+3. Sum these results to get a total effective irradiance.
+4. Use a defined daily dose (`0.003 J/cm²`) to determine how long you'd need to be exposed at this irradiance level to reach that safe daily limit.
+5. Finally, this calculated duration is displayed on the Flipper screen.
+
+
+
+## But Where Did They Come From?
+
+These numbers are derived using two key documents:
+
+- The datasheet of the [AS7331 UV sensor](https://ams-osram.com/products/sensor-solutions/ambient-light-color-spectral-proximity-sensors/ams-as7331-spectral-uv-sensor).
+- The 2024 guidelines for [Threshold Limit Values (TLVs) and Biological Exposure Indices (BEIs)](https://en.wikipedia.org/wiki/Threshold_limit_value) published by the [American Conference of Governmental Industrial Hygienists (ACGIH)](https://en.wikipedia.org/wiki/American_Conference_of_Governmental_Industrial_Hygienists). Specifically, the chapter on "Ultraviolet Radiation", which, among other things, defines the daily exposure limit as `0.003 J/cm²`. Unfortunately, this document isn't freely accessible.
+
+
+
+## Effective Irradiance Explained
+
+"Effective Irradiance" isn't just the raw UV sensor data–it's adjusted based on the human body's sensitivity to different UV wavelengths. Some wavelengths are more harmful than others, with the peak danger at 270 nm (in UV-C). At this peak, the "Relative Spectral Effectiveness" value is 1.0, and it decreases as wavelengths get shorter or longer.
+
+But here's the catch: the AS7331 sensor provides just one reading for each UV range:
+
+- UV-A (315–410 nm)
+- UV-B (280–315 nm)
+- UV-C (240–280 nm)
+
+However, the Relative Spectral Effectiveness values are listed individually every 5 nm. So, how do we pick a representative number? That’s exactly the puzzle I had to solve—and the magic numbers are my solution.
+
+
+
+## The Method Behind the Magic
+
+![](visualize_data.jpeg)
+
+My approach was to create a weighted spectral effectiveness curve by merging the sensor's Responsivity curves with the "Relative Spectral Effectiveness" curves, effectively combining both the sensor's sensitivity and the biological impact of each UV wavelength.
+
+The initial results seemed quite conservative, so I refined the approach by considering only the weighted effectiveness within clearly defined wavelength bands, ensuring there was no overlap between the UV channels. However, this meant that I also needed to account for the parts of the UV readings that fell outside those bands. To do this, I calculated a ratio: the total area under the original Responsivity curve compared to the area within the selected bands. This ratio is then applied to the UV readings to adjust for the excluded areas.
+
+The result of these steps is visualized in the two sets of plots below. There are two sets because the "Relative Spectral Effectiveness" varies depending on whether eyes are protected or not:
+
+![](weighted_spectral_effectiveness_eyes_not_protected.jpeg)
+
+![](weighted_spectral_effectiveness_eyes_protected.jpeg)
+
+At the top of each plot, the calculated Weighted Spectral Effectiveness value is displayed. These final numbers became the magic numbers in the code—hopefully providing a balanced and scientifically justified conversion from raw sensor data to Effective Irradiance.
+
+If you're curious or want to double-check my math, feel free to run the `weighted_spectral_effectiveness.py` script included here.
+
+
+
+## Final Note and Sanity Check
+
+If you think the maximum daily exposure durations seem quite short, you're not alone. As a sanity check, I considered using the Spectral Effectiveness values at the sensor's peak response (you can see these in the first graph). However, this simpler approach results in similar, and often shorter, daily exposure durations, which suggests that my chosen method isn't far off.
+
+Finally, while these magic numbers might not be the only possible solution, I believe they are reasonable. If you have any insights or alternative approaches I may have missed, feel free to let me know.

+ 15 - 0
uv_meter_as7331/magic_numbers/requirements.txt

@@ -0,0 +1,15 @@
+-i https://pypi.org/simple
+contourpy==1.3.1; python_version >= '3.10'
+cycler==0.12.1; python_version >= '3.8'
+fonttools==4.56.0; python_version >= '3.8'
+kiwisolver==1.4.8; python_version >= '3.10'
+matplotlib==3.10.1; python_version >= '3.10'
+numpy==2.2.3; python_version >= '3.10'
+packaging==24.2; python_version >= '3.8'
+pandas==2.2.3; python_version >= '3.9'
+pillow==11.1.0; python_version >= '3.9'
+pyparsing==3.2.1; python_version >= '3.9'
+python-dateutil==2.9.0.post0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
+pytz==2025.1
+six==1.17.0; python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2'
+tzdata==2025.1; python_version >= '2'

+ 42 - 0
uv_meter_as7331/magic_numbers/uv_responsivity_and_spectral_effectiveness_data.csv

@@ -0,0 +1,42 @@
+Wavelength (nm),UVC,UVB,UVA,Relative Spectral Effectiveness,Relative Spectral Effectiveness (eyes protected)
+230,0.0,0.0,0.03,0.19,0.0189
+235,0.0,0.0,0.03,0.24,0.038
+240,0.25,0.0,0.02,0.3,0.075
+245,0.88,0.0,0.02,0.36,0.15
+250,1.0,0.0,0.02,0.43,0.3
+255,0.92,0.0,0.0,0.52,0.3
+260,0.89,0.0,0.0,0.65,0.3
+265,0.86,0.03,0.0,0.81,0.3
+270,0.8,0.04,0.0,1.0,0.3
+275,0.67,0.08,0.0,0.96,0.3
+280,0.48,0.37,0.0,0.88,0.3
+285,0.15,0.83,0.0,0.77,0.3
+290,0.0,0.9,0.0,0.64,0.3
+295,0.0,1.0,0.0,0.54,0.3
+300,0.0,0.95,0.02,0.3,0.3
+305,0.0,0.89,0.03,0.06,0.06
+310,0.0,0.8,0.06,0.015,0.015
+315,0.0,0.4,0.31,0.003,0.003
+320,0.0,0.1,0.82,0.001,0.001
+325,0.0,0.0,0.89,0.0005,0.0005
+330,0.0,0.0,0.9,0.00041,0.00041
+335,0.0,0.0,1.0,0.00034,0.00034
+340,0.0,0.0,0.9,0.00028,0.00028
+345,0.0,0.0,1.0,0.00024,0.00024
+350,0.0,0.0,0.86,0.0002,0.0002
+355,0.0,0.0,0.75,0.00016,0.00016
+360,0.0,0.0,0.75,0.00013,0.00013
+365,0.0,0.0,0.62,0.00011,0.00011
+370,0.0,0.0,0.75,9.3e-05,9.3e-05
+375,0.0,0.0,0.61,7.7e-05,7.7e-05
+380,0.0,0.0,0.75,6.4e-05,6.4e-05
+385,0.0,0.0,0.6,5.3e-05,5.3e-05
+390,0.0,0.0,0.67,4.4e-05,4.4e-05
+395,0.0,0.0,0.52,3.6e-05,3.6e-05
+400,0.0,0.0,0.55,3e-05,3e-05
+405,0.0,0.0,0.43,3e-05,3e-05
+410,0.0,0.0,0.4,3e-05,3e-05
+415,0.0,0.0,0.2,3e-05,3e-05
+420,0.0,0.0,0.03,3e-05,3e-05
+425,0.0,0.0,0.0,3e-05,3e-05
+430,0.0,0.0,0.0,3e-05,3e-05

BIN
uv_meter_as7331/magic_numbers/visualize_data.jpeg


+ 114 - 0
uv_meter_as7331/magic_numbers/visualize_data.py

@@ -0,0 +1,114 @@
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+# -----------------------------------------------------
+# 1) Read and prepare data
+# -----------------------------------------------------
+file_path = "./uv_responsivity_and_spectral_effectiveness_data.csv"
+df = pd.read_csv(file_path)
+
+w = df["Wavelength (nm)"].values
+uvc = df["UVC"].values
+uvb = df["UVB"].values
+uva = df["UVA"].values
+eff_std = df["Relative Spectral Effectiveness"].values
+eff_prot = df["Relative Spectral Effectiveness (eyes protected)"].values
+
+# -----------------------------------------------------
+# 2) Peak finding (with UVA “double-peak” averaging)
+# -----------------------------------------------------
+peak_uvc_idx = np.nanargmax(uvc)
+peak_uvb_idx = np.nanargmax(uvb)
+
+uva_ones = w[uva == 1.0]
+if len(uva_ones) >= 2:
+    avg_uva_wl = (uva_ones[0] + uva_ones[1]) / 2
+    peak_uva_idx = np.abs(w - avg_uva_wl).argmin()
+else:
+    peak_uva_idx = np.nanargmax(uva)
+
+peak_uvc_wl = w[peak_uvc_idx]
+peak_uvb_wl = w[peak_uvb_idx]
+peak_uva_wl = w[peak_uva_idx]
+
+peak_uvc_eff_std = eff_std[peak_uvc_idx]
+peak_uvc_eff_prot = eff_prot[peak_uvc_idx]
+peak_uvb_eff_std = eff_std[peak_uvb_idx]
+peak_uvb_eff_prot = eff_prot[peak_uvb_idx]
+peak_uva_eff_std = eff_std[peak_uva_idx]
+peak_uva_eff_prot = eff_prot[peak_uva_idx]
+
+# -----------------------------------------------------
+# 3) Plotting
+# -----------------------------------------------------
+colors = {
+    "UVC": "#4d4d4d",  # darker grey
+    "UVB": "#8a2be2",  # violet/purple
+    "UVA": "#0b66f2",  # pleasing blue
+}
+
+fig, ax1 = plt.subplots(figsize=(10, 6))
+
+# 3.1) Sensor responsivity curves
+ax1.plot(w, uvc, color=colors["UVC"], linewidth=2, label="UVC Responsivity")
+ax1.plot(w, uvb, color=colors["UVB"], linewidth=2, label="UVB Responsivity")
+ax1.plot(w, uva, color=colors["UVA"], linewidth=2, label="UVA Responsivity")
+
+# 3.2) Peaks: vertical lines + marker + annotate SpecEff std/prot
+y_annot = 1.05
+for wl, color, eff_s, eff_p, lbl in [
+    (peak_uvc_wl, colors["UVC"], peak_uvc_eff_std, peak_uvc_eff_prot, "UVC Peak"),
+    (peak_uvb_wl, colors["UVB"], peak_uvb_eff_std, peak_uvb_eff_prot, "UVB Peak"),
+    (peak_uva_wl, colors["UVA"], peak_uva_eff_std, peak_uva_eff_prot, "UVA Peak"),
+]:
+    ax1.axvline(x=wl, color=color, linestyle="dotted", linewidth=2, label=lbl)
+    ax1.scatter(wl, 1.0, color=color, zorder=5)
+    ax1.text(
+        wl + 2,
+        y_annot,
+        f"{eff_s:.4g} / {eff_p:.4g}",
+        color=color,
+        fontsize=10,
+        ha="left",
+        va="bottom",
+    )
+
+# 3.3) Primary axis styling & legend (resp + peaks) top-right
+ax1.set_xlabel("Wavelength (nm)")
+ax1.set_ylabel("Normalized Sensor Responsivity")
+ax1.set_ylim(0, 1.1)
+
+leg1 = ax1.legend(loc="upper right", bbox_to_anchor=(1, 0.8))
+ax1.add_artist(leg1)
+
+# 3.4) Spectral-effectiveness curves on log scale & second legend
+ax2 = ax1.twinx()
+ax2.plot(
+    w,
+    eff_std,
+    color="grey",
+    linestyle="--",
+    linewidth=2,
+    alpha=0.6,
+    label="SpecEff (standard)",
+)
+ax2.plot(
+    w,
+    eff_prot,
+    color="grey",
+    linestyle="-.",
+    linewidth=2,
+    alpha=0.6,
+    label="SpecEff (eyes protected)",
+)
+ax2.set_ylabel("Relative Spectral Effectiveness (Log)")
+ax2.set_yscale("log")
+
+leg2 = ax2.legend(loc="upper right")
+ax2.add_artist(leg2)
+
+# 3.5) Final touches
+ax1.set_title("UV Responsivity and Spectral Effectiveness")
+plt.tight_layout()
+plt.show()

+ 271 - 0
uv_meter_as7331/magic_numbers/weighted_spectral_effectiveness.py

@@ -0,0 +1,271 @@
+import pandas as pd
+import numpy as np
+import matplotlib.pyplot as plt
+
+# -----------------------------------------------------
+# 1) Read and prepare data
+# -----------------------------------------------------
+file_path = "./uv_responsivity_and_spectral_effectiveness_data.csv"
+df = pd.read_csv(file_path)
+
+w = df["Wavelength (nm)"].values
+uvc = df["UVC"].values
+uvb = df["UVB"].values
+uva = df["UVA"].values
+# eff = df["Relative Spectral Effectiveness"].values
+eff = df["Relative Spectral Effectiveness (eyes protected)"].values
+
+# We have two cutoffs (rounded to data points):
+cutoff_uvc_uvb = 280  # exact: 280.7
+cutoff_uvb_uva = 315  # exact: 315.5
+
+# Calculate "effective" curves = Responsivity * Spectral Effectiveness
+uvc_eff = uvc * eff
+uvb_eff = uvb * eff
+uva_eff = uva * eff
+
+
+# -----------------------------------------------------
+# 2) Weighted spectral effectiveness (in-band only)
+# -----------------------------------------------------
+def compute_weighted_eff_in_band(wl, resp, sp_eff, band_min, band_max):
+    """
+    Weighted spectral effectiveness = sum(resp * sp_eff) / sum(resp),
+    but only for band_min <= wl < band_max (and resp>0).
+    (using Responsivity as the weight)
+    """
+    mask = (wl >= band_min) & (wl < band_max) & (resp > 0)
+    if mask.sum() == 0:
+        return 0.0
+    return (resp[mask] * sp_eff[mask]).sum() / resp[mask].sum()
+
+
+wl_min = w[0]
+wl_max = w[-1]
+
+uvc_w_eff = compute_weighted_eff_in_band(w, uvc, eff, wl_min, cutoff_uvc_uvb)
+uvb_w_eff = compute_weighted_eff_in_band(w, uvb, eff, cutoff_uvc_uvb, cutoff_uvb_uva)
+uva_w_eff = compute_weighted_eff_in_band(w, uva, eff, cutoff_uvb_uva, wl_max)
+
+
+# -----------------------------------------------------
+# 2.5) Compute Area Ratios and Correct Weighted Spectral Effectiveness
+# -----------------------------------------------------
+def compute_area_ratio(wl, curve, band_min, band_max):
+    """
+    Compute the ratio of the area under the curve in the band [band_min, band_max)
+    to the total area under the curve.
+    """
+    total_area = np.trapezoid(curve, wl)
+    band_mask = (wl >= band_min) & (wl <= band_max)
+    band_area = np.trapezoid(curve[band_mask], wl[band_mask])
+    return band_area / total_area if total_area > 0 else 0.0
+
+
+# Compute area ratios for the Responsivity curves
+uvc_area_ratio = compute_area_ratio(w, uvc, wl_min, cutoff_uvc_uvb)
+uvb_area_ratio = compute_area_ratio(w, uvb, cutoff_uvc_uvb, cutoff_uvb_uva)
+uva_area_ratio = compute_area_ratio(w, uva, cutoff_uvb_uva, wl_max)
+
+# Correct Weighted Spectral Effectiveness
+uvc_w_eff = uvc_w_eff * uvc_area_ratio
+uvb_w_eff = uvb_w_eff * uvb_area_ratio
+uva_w_eff = uva_w_eff * uva_area_ratio
+
+
+# -----------------------------------------------------
+# 3) In-band peaks
+# -----------------------------------------------------
+def find_in_band_peak(wl, y, band_min, band_max):
+    """
+    Return (peak_wl, peak_val) for the maximum of y where band_min <= wl <= band_max.
+    """
+    mask = (wl >= band_min) & (wl < band_max)
+    if not np.any(mask):
+        return None, None
+    idx_max = np.argmax(y[mask])
+    peak_val = y[mask][idx_max]
+    peak_wl = wl[mask][idx_max]
+    return peak_wl, peak_val
+
+
+uvc_peak_wl, uvc_peak_val = find_in_band_peak(w, uvc_eff, wl_min, cutoff_uvc_uvb)
+uvb_peak_wl, uvb_peak_val = find_in_band_peak(
+    w, uvb_eff, cutoff_uvc_uvb, cutoff_uvb_uva
+)
+uva_peak_wl, uva_peak_val = find_in_band_peak(w, uva_eff, cutoff_uvb_uva, wl_max)
+
+
+# -----------------------------------------------------
+# 4) Plotting Helpers: Split in-band vs. out-of-band
+#    We do it by creating arrays with NaN outside the band
+#    so matplotlib doesn't draw lines across boundaries.
+# -----------------------------------------------------
+def band_split(wl, arr, band_min, band_max):
+    """
+    Return two arrays: in-band (NaN outside) and out-of-band (NaN inside),
+    so we can plot them separately and avoid bridging lines across cutoff edges.
+    """
+    in_mask = (wl >= band_min) & (wl <= band_max)
+    out_mask = (wl <= band_min) | (wl >= band_max)  # ~in_mask
+
+    in_array = np.where(in_mask, arr, np.nan)
+    out_array = np.where(out_mask, arr, np.nan)
+    return in_array, out_array
+
+
+# We'll also define a small function to plot the peak marker
+def add_peak_marker(ax, peak_wl, peak_val, color):
+    if peak_wl is not None and peak_val is not None:
+        ax.scatter(peak_wl, peak_val, color=color, zorder=5)
+        ax.text(
+            peak_wl + 2,
+            peak_val,
+            f"{peak_val:.4g} @{peak_wl:.0f}nm",
+            color=color,
+            fontsize=9,
+            ha="left",
+            va="bottom",
+        )
+
+
+# -----------------------------------------------------
+# 6) Create figure with 3 subplots
+# -----------------------------------------------------
+colors = {
+    "UVC": "#4d4d4d",  # darker/grey
+    "UVB": "#8a2be2",  # violet/purple
+    "UVA": "#0b66f2",  # pleasing blue
+}
+
+fig, (ax1, ax2, ax3) = plt.subplots(3, 1, figsize=(10, 12), sharex=True)
+plt.subplots_adjust(hspace=0.1)
+
+
+# A helper to plot everything for a given subplot
+def plot_subplot(
+    ax,
+    w,
+    resp,
+    eff,
+    eff_curve,
+    band_min,
+    band_max,
+    color,
+    label,
+    weighted_eff,
+    peak_wl,
+    peak_val,
+):
+    """
+    Plots:
+      - background (responsivity in linear scale, spectral eff in log scale)
+      - in-band and out-of-band lines for 'eff_curve' (which is resp*eff)
+      - vertical cutoffs
+      - weighted efficiency text
+      - peak marker
+    """
+
+    # Split in-band vs out-of-band
+    main_in, main_out = band_split(w, eff_curve, band_min, band_max)
+    p1 = ax.plot(
+        w, main_out, color=color, alpha=0.4, linewidth=1, label=f"{label} - out"
+    )
+    p2 = ax.plot(w, main_in, color=color, alpha=1.0, linewidth=2, label=f"{label} - in")
+    ax.set_ylim(0, 1.1 * np.max(eff_curve))
+
+    # Background plots
+    ax_resp = ax.twinx()
+    ax_eff = ax.twinx()
+    ax_eff.spines.right.set_position(("axes", 1.1))
+    ax_eff.set_yscale("log", nonpositive="clip")
+
+    p3 = ax_resp.plot(w, resp, color="grey", linestyle="-", alpha=0.5, label="Resp")
+    p4 = ax_eff.plot(
+        w,
+        eff,
+        color="grey",
+        linestyle="--",
+        alpha=0.6,
+        label="SpecEff",
+    )
+    ax_resp.set_ylabel("Normalized Sensor Responsivity")
+    ax_eff.set_ylabel("Relative Spectral Effectiveness")
+
+    # Add cutoff lines
+    ax.axvline(x=cutoff_uvc_uvb, color="black", linestyle="dotted", alpha=0.5)
+    ax.axvline(x=cutoff_uvb_uva, color="black", linestyle="dotted", alpha=0.5)
+
+    # Weighted Spectral Effectiveness text annotation
+    text_str = f"Weighted Spectral Effectiveness (in band): {weighted_eff:.4g}"
+    ax.text(
+        0.00,
+        1.0055,
+        text_str,
+        transform=ax.transAxes,
+        color=color,
+        fontsize=10,
+        ha="left",
+        va="bottom",
+    )
+
+    # Mark the peak
+    add_peak_marker(ax, peak_wl, peak_val, color)
+
+    # Final styling
+    ax.legend(loc="upper right", handles=[p1[0], p2[0], p3[0], p4[0]])
+    ax.set_ylabel(f"{label}")
+
+
+# --------------------- Subplot for UVC --------------------------
+plot_subplot(
+    ax1,
+    w,
+    uvc,
+    eff,
+    uvc_eff,
+    band_min=w[0],
+    band_max=cutoff_uvc_uvb,
+    color=colors["UVC"],
+    label="Resp (UVC) * SpecEff",
+    weighted_eff=uvc_w_eff,
+    peak_wl=uvc_peak_wl,
+    peak_val=uvc_peak_val,
+)
+
+# --------------------- Subplot for UVB --------------------------
+plot_subplot(
+    ax2,
+    w,
+    uvb,
+    eff,
+    uvb_eff,
+    band_min=cutoff_uvc_uvb,
+    band_max=cutoff_uvb_uva,
+    color=colors["UVB"],
+    label="Resp (UVB) * SpecEff",
+    weighted_eff=uvb_w_eff,
+    peak_wl=uvb_peak_wl,
+    peak_val=uvb_peak_val,
+)
+
+# --------------------- Subplot for UVA --------------------------
+plot_subplot(
+    ax3,
+    w,
+    uva,
+    eff,
+    uva_eff,
+    band_min=cutoff_uvb_uva,
+    band_max=w[-1],
+    color=colors["UVA"],
+    label="Resp (UVA) * SpecEff",
+    weighted_eff=uva_w_eff,
+    peak_wl=uva_peak_wl,
+    peak_val=uva_peak_val,
+)
+
+ax3.set_xlabel("Wavelength (nm)")
+
+plt.tight_layout()
+plt.show()

BIN
uv_meter_as7331/magic_numbers/weighted_spectral_effectiveness_eyes_not_protected.jpeg


BIN
uv_meter_as7331/magic_numbers/weighted_spectral_effectiveness_eyes_protected.jpeg