From 8f0cfdcb10528b72eb33bd70d9999f1cfcdcc70d Mon Sep 17 00:00:00 2001 From: Jeremy Fee <jmfee@usgs.gov> Date: Tue, 28 Apr 2020 22:39:42 -0600 Subject: [PATCH] Additional refactor of calculations --- Pipfile | 4 +- Pipfile.lock | 163 ++++++++------ geomagio/residual/Calculation.py | 318 ++++++++++++++++----------- geomagio/residual/Measurement.py | 21 +- geomagio/residual/MeasurementType.py | 51 ++++- geomagio/residual/Reading.py | 23 +- geomagio/residual/__init__.py | 29 ++- test/residual_test/residual_test.py | 7 +- 8 files changed, 378 insertions(+), 238 deletions(-) diff --git a/Pipfile b/Pipfile index 0b6c79757..36de01836 100644 --- a/Pipfile +++ b/Pipfile @@ -17,13 +17,15 @@ obspy = ">1.2.0" pycurl = "*" authlib = "*" cryptography = "*" -databases = {extras = ["sqlite"],version = "*"} +databases = {extras = ["postgresql", "sqlite"], version = "*"} fastapi = "*" httpx = "==0.11.1" openpyxl = "*" pydantic = "==1.4" sqlalchemy = "*" sqlalchemy-utc = "*" +typing = "*" +typing-extensions = "*" uvicorn = "*" [pipenv] diff --git a/Pipfile.lock b/Pipfile.lock index c93459f43..a8cdfa11a 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "feb945aefc3117570ac55b98510d96243c861469466ee798fa8b49416693f89d" + "sha256": "809e5e04e4d1957e08e475b75c4c425c1b63b7d1fdaee30e79b077e84e115504" }, "pipfile-spec": 6, "requires": {}, @@ -103,35 +103,35 @@ }, "click": { "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "version": "==7.1.1" + "version": "==7.1.2" }, "cryptography": { "hashes": [ - "sha256:0cacd3ef5c604b8e5f59bf2582c076c98a37fe206b31430d0cd08138aff0986e", - "sha256:192ca04a36852a994ef21df13cca4d822adbbdc9d5009c0f96f1d2929e375d4f", - "sha256:19ae795137682a9778892fb4390c07811828b173741bce91e30f899424b3934d", - "sha256:1b9b535d6b55936a79dbe4990b64bb16048f48747c76c29713fea8c50eca2acf", - "sha256:2a2ad24d43398d89f92209289f15265107928f22a8d10385f70def7a698d6a02", - "sha256:3be7a5722d5bfe69894d3f7bbed15547b17619f3a88a318aab2e37f457524164", - "sha256:49870684da168b90110bbaf86140d4681032c5e6a2461adc7afdd93be5634216", - "sha256:587f98ce27ac4547177a0c6fe0986b8736058daffe9160dcf5f1bd411b7fbaa1", - "sha256:5aca6f00b2f42546b9bdf11a69f248d1881212ce5b9e2618b04935b87f6f82a1", - "sha256:6b744039b55988519cc183149cceb573189b3e46e16ccf6f8c46798bb767c9dc", - "sha256:6b91cab3841b4c7cb70e4db1697c69f036c8bc0a253edc0baa6783154f1301e4", - "sha256:7598974f6879a338c785c513e7c5a4329fbc58b9f6b9a6305035fca5b1076552", - "sha256:7a279f33a081d436e90e91d1a7c338553c04e464de1c9302311a5e7e4b746088", - "sha256:95e1296e0157361fe2f5f0ed307fd31f94b0ca13372e3673fa95095a627636a1", - "sha256:9fc9da390e98cb6975eadf251b6e5fa088820141061bf041cd5c72deba1dc526", - "sha256:cc20316e3f5a6b582fc3b029d8dc03aabeb645acfcb7fc1d9848841a33265748", - "sha256:d1bf5a1a0d60c7f9a78e448adcb99aa101f3f9588b16708044638881be15d6bc", - "sha256:ed1d0760c7e46436ec90834d6f10477ff09475c692ed1695329d324b2c5cd547", - "sha256:ef9a55013676907df6c9d7dd943eb1770d014f68beaa7e73250fb43c759f4585" + "sha256:091d31c42f444c6f519485ed528d8b451d1a0c7bf30e8ca583a0cac44b8a0df6", + "sha256:18452582a3c85b96014b45686af264563e3e5d99d226589f057ace56196ec78b", + "sha256:1dfa985f62b137909496e7fc182dac687206d8d089dd03eaeb28ae16eec8e7d5", + "sha256:1e4014639d3d73fbc5ceff206049c5a9a849cefd106a49fa7aaaa25cc0ce35cf", + "sha256:22e91636a51170df0ae4dcbd250d318fd28c9f491c4e50b625a49964b24fe46e", + "sha256:3b3eba865ea2754738616f87292b7f29448aec342a7c720956f8083d252bf28b", + "sha256:651448cd2e3a6bc2bb76c3663785133c40d5e1a8c1a9c5429e4354201c6024ae", + "sha256:726086c17f94747cedbee6efa77e99ae170caebeb1116353c6cf0ab67ea6829b", + "sha256:844a76bc04472e5135b909da6aed84360f522ff5dfa47f93e3dd2a0b84a89fa0", + "sha256:88c881dd5a147e08d1bdcf2315c04972381d026cdb803325c03fe2b4a8ed858b", + "sha256:96c080ae7118c10fcbe6229ab43eb8b090fccd31a09ef55f83f690d1ef619a1d", + "sha256:a0c30272fb4ddda5f5ffc1089d7405b7a71b0b0f51993cb4e5dbb4590b2fc229", + "sha256:bb1f0281887d89617b4c68e8db9a2c42b9efebf2702a3c5bf70599421a8623e3", + "sha256:c447cf087cf2dbddc1add6987bbe2f767ed5317adb2d08af940db517dd704365", + "sha256:c4fd17d92e9d55b84707f4fd09992081ba872d1a0c610c109c18e062e06a2e55", + "sha256:d0d5aeaedd29be304848f1c5059074a740fa9f6f26b84c5b63e8b29e73dfc270", + "sha256:daf54a4b07d67ad437ff239c8a4080cfd1cc7213df57d33c97de7b4738048d5e", + "sha256:e993468c859d084d5579e2ebee101de8f5a27ce8e2159959b6673b418fd8c785", + "sha256:f118a95c7480f5be0df8afeb9a11bd199aa20afab7a96bcf20409b411a3a85f0" ], "index": "pypi", - "version": "==2.9" + "version": "==2.9.2" }, "cycler": { "hashes": [ @@ -146,10 +146,10 @@ "sqlite" ], "hashes": [ - "sha256:a04db1d158a91db7bd49db16e14266e8e6c7336f06f88c700147690683c769a3" + "sha256:92b7f66c74d21eba54899ba88352311019131fb01ceaf076452949429e8d1b12" ], "index": "pypi", - "version": "==0.2.6" + "version": "==0.3.1" }, "decorator": { "hashes": [ @@ -201,10 +201,10 @@ }, "hstspreload": { "hashes": [ - "sha256:acbf4d6fd362b363ce567db56a8667c4f0e43073001add9c4e43f449c11e1d81", - "sha256:ca0af58f803598a737d5f325212e488b24de1ac9f0cf6fa12da1a3f4651914e6" + "sha256:5e42c9c9b925d793d1732e20f59da7c9d6d340bde82ead7d49031bde59991219", + "sha256:f2571debd824aa5ec34816a2e08badda043181e908710317b9826b5c30d3bc17" ], - "version": "==2020.4.14" + "version": "==2020.4.28" }, "httptools": { "hashes": [ @@ -327,30 +327,30 @@ }, "numpy": { "hashes": [ - "sha256:1598a6de323508cfeed6b7cd6c4efb43324f4692e20d1f76e1feec7f59013448", - "sha256:1b0ece94018ae21163d1f651b527156e1f03943b986188dd81bc7e066eae9d1c", - "sha256:2e40be731ad618cb4974d5ba60d373cdf4f1b8dcbf1dcf4d9dff5e212baf69c5", - "sha256:4ba59db1fcc27ea31368af524dcf874d9277f21fd2e1f7f1e2e0c75ee61419ed", - "sha256:59ca9c6592da581a03d42cc4e270732552243dc45e87248aa8d636d53812f6a5", - "sha256:5e0feb76849ca3e83dd396254e47c7dba65b3fa9ed3df67c2556293ae3e16de3", - "sha256:6d205249a0293e62bbb3898c4c2e1ff8a22f98375a34775a259a0523111a8f6c", - "sha256:6fcc5a3990e269f86d388f165a089259893851437b904f422d301cdce4ff25c8", - "sha256:82847f2765835c8e5308f136bc34018d09b49037ec23ecc42b246424c767056b", - "sha256:87902e5c03355335fc5992a74ba0247a70d937f326d852fc613b7f53516c0963", - "sha256:9ab21d1cb156a620d3999dd92f7d1c86824c622873841d6b080ca5495fa10fef", - "sha256:a1baa1dc8ecd88fb2d2a651671a84b9938461e8a8eed13e2f0a812a94084d1fa", - "sha256:a244f7af80dacf21054386539699ce29bcc64796ed9850c99a34b41305630286", - "sha256:a35af656a7ba1d3decdd4fae5322b87277de8ac98b7d9da657d9e212ece76a61", - "sha256:b1fe1a6f3a6f355f6c29789b5927f8bd4f134a4bd9a781099a7c4f66af8850f5", - "sha256:b5ad0adb51b2dee7d0ee75a69e9871e2ddfb061c73ea8bc439376298141f77f5", - "sha256:ba3c7a2814ec8a176bb71f91478293d633c08582119e713a0c5351c0f77698da", - "sha256:cd77d58fb2acf57c1d1ee2835567cd70e6f1835e32090538f17f8a3a99e5e34b", - "sha256:cdb3a70285e8220875e4d2bc394e49b4988bdb1298ffa4e0bd81b2f613be397c", - "sha256:deb529c40c3f1e38d53d5ae6cd077c21f1d49e13afc7936f7f868455e16b64a0", - "sha256:e7894793e6e8540dbeac77c87b489e331947813511108ae097f1715c018b8f3d" + "sha256:0aa2b318cf81eb1693fcfcbb8007e95e231d7e1aa24288137f3b19905736c3ee", + "sha256:163c78c04f47f26ca1b21068cea25ed7c5ecafe5f5ab2ea4895656a750582b56", + "sha256:1e37626bcb8895c4b3873fcfd54e9bfc5ffec8d0f525651d6985fcc5c6b6003c", + "sha256:264fd15590b3f02a1fbc095e7e1f37cdac698ff3829e12ffdcffdce3772f9d44", + "sha256:3d9e1554cd9b5999070c467b18e5ae3ebd7369f02706a8850816f576a954295f", + "sha256:40c24960cd5cec55222963f255858a1c47c6fa50a65a5b03fd7de75e3700eaaa", + "sha256:46f404314dbec78cb342904f9596f25f9b16e7cf304030f1339e553c8e77f51c", + "sha256:4847f0c993298b82fad809ea2916d857d0073dc17b0510fbbced663b3265929d", + "sha256:48e15612a8357393d176638c8f68a19273676877caea983f8baf188bad430379", + "sha256:6725d2797c65598778409aba8cd67077bb089d5b7d3d87c2719b206dc84ec05e", + "sha256:99f0ba97e369f02a21bb95faa3a0de55991fd5f0ece2e30a9e2eaebeac238921", + "sha256:a41f303b3f9157a31ce7203e3ca757a0c40c96669e72d9b6ee1bce8507638970", + "sha256:a4305564e93f5c4584f6758149fd446df39fd1e0a8c89ca0deb3cce56106a027", + "sha256:a551d8cc267c634774830086da42e4ba157fa41dd3b93982bc9501b284b0c689", + "sha256:a6bc9432c2640b008d5f29bad737714eb3e14bb8854878eacf3d7955c4e91c36", + "sha256:c60175d011a2e551a2f74c84e21e7c982489b96b6a5e4b030ecdeacf2914da68", + "sha256:e46e2384209c91996d5ec16744234d1c906ab79a701ce1a26155c9ec890b8dc8", + "sha256:e607b8cdc2ae5d5a63cd1bec30a15b5ed583ac6a39f04b7ba0f03fcfbf29c05b", + "sha256:e94a39d5c40fffe7696009dbd11bc14a349b377e03a384ed011e03d698787dd3", + "sha256:eb2286249ebfe8fcb5b425e5ec77e4736d53ee56d3ad296f8947f67150f495e3", + "sha256:fdee7540d12519865b423af411bd60ddb513d2eb2cd921149b732854995bbf8b" ], "index": "pypi", - "version": "==1.18.2" + "version": "==1.18.3" }, "obspy": { "hashes": [ @@ -562,20 +562,38 @@ ], "version": "==0.13.2" }, + "typing": { + "hashes": [ + "sha256:91dfe6f3f706ee8cc32d38edbbf304e9b7583fb37108fef38229617f8b3eba23", + "sha256:c8cabb5ab8945cd2f54917be357d134db9cc1eb039e59d1606dc1e60cb1d9d36", + "sha256:f38d83c5a7a7086543a0f649564d661859c5146a85775ab90c0d2f93ffaa9714" + ], + "index": "pypi", + "version": "==3.7.4.1" + }, + "typing-extensions": { + "hashes": [ + "sha256:6e95524d8a547a91e08f404ae485bbb71962de46967e1b71a0cb89af24e761c5", + "sha256:79ee589a3caca649a9bfd2a8de4709837400dfa00b6cc81962a1e6a1815969ae", + "sha256:f8d2bd89d25bc39dabe7d23df520442fa1d8969b82544370e03d88b5a591c392" + ], + "index": "pypi", + "version": "==3.7.4.2" + }, "urllib3": { "hashes": [ - "sha256:2f3db8b19923a873b3e5256dc9c2dedfa883e33d87c690d9c7913e1f40673cdc", - "sha256:87716c2d2a7121198ebcb7ce7cccf6ce5e9ba539041cfbaeecfb641dc0bf6acc" + "sha256:3018294ebefce6572a474f0604c2021e33b3fd8006ecd11d62107a5d2a963527", + "sha256:88206b0eb87e6d677d424843ac5209e3fb9d0190d0ee169599165ec25e9d9115" ], - "version": "==1.25.8" + "version": "==1.25.9" }, "uvicorn": { "hashes": [ - "sha256:0f58170165c4495f563d8224b2f415a0829af0412baa034d6f777904613087fd", - "sha256:6fdaf8e53bf1b2ddf0fe9ed06079b5348d7d1d87b3365fe2549e6de0d49e631c" + "sha256:a19de5b2f8dec56ec95bbbf3ce334b168efca6a238b2ba7c6cb020e3a42ce71a", + "sha256:aa6db2200cff18f8c550c869fab01f78ca987464d499f654e7aeb166fe0c0616" ], "index": "pypi", - "version": "==0.11.3" + "version": "==0.11.4" }, "uvloop": { "hashes": [ @@ -660,10 +678,10 @@ }, "click": { "hashes": [ - "sha256:8a18b4ea89d8820c5d0c7da8a64b2c324b4dabb695804dbfea19b9be9d88c0cc", - "sha256:e345d143d80bf5ee7534056164e5e112ea5e22716bbb1ce727941f4c8b471b9a" + "sha256:d2b5255c7c6349bc1bd1e59e08cd12acbbd63ce649f2588755783aa94dfb6b1a", + "sha256:dacca89f4bfadd5de3d7489b7c8a566eee0d3676333fbb50030263894c38c0dc" ], - "version": "==7.1.1" + "version": "==7.1.2" }, "coverage": { "hashes": [ @@ -716,10 +734,10 @@ }, "identify": { "hashes": [ - "sha256:2bb8760d97d8df4408f4e805883dad26a2d076f04be92a10a3e43f09c6060742", - "sha256:faffea0fd8ec86bb146ac538ac350ed0c73908326426d387eded0bcc9d077522" + "sha256:23c18d97bb50e05be1a54917ee45cc61d57cb96aedc06aabb2b02331edf0dbf0", + "sha256:88ed90632023e52a6495749c6732e61e08ec9f4f04e95484a5c37b9caf40283c" ], - "version": "==1.4.14" + "version": "==1.4.15" }, "importlib-metadata": { "hashes": [ @@ -765,11 +783,11 @@ }, "pre-commit": { "hashes": [ - "sha256:487c675916e6f99d355ec5595ad77b325689d423ef4839db1ed2f02f639c9522", - "sha256:c0aa11bce04a7b46c5544723aedf4e81a4d5f64ad1205a30a9ea12d5e81969e1" + "sha256:979b53dab1af35063a483bfe13b0fcbbf1a2cf8c46b60e0a9a8d08e8269647a1", + "sha256:f3e85e68c6d1cbe7828d3471896f1b192cfcf1c4d83bf26e26beeb5941855257" ], "index": "pypi", - "version": "==2.2.0" + "version": "==2.3.0" }, "py": { "hashes": [ @@ -892,10 +910,10 @@ }, "virtualenv": { "hashes": [ - "sha256:00cfe8605fb97f5a59d52baab78e6070e72c12ca64f51151695407cc0eb8a431", - "sha256:c8364ec469084046c779c9a11ae6340094e8a0bf1d844330fc55c1cefe67c172" + "sha256:5021396e8f03d0d002a770da90e31e61159684db2859d0ba4850fbea752aa675", + "sha256:ac53ade75ca189bc97b6c1d9ec0f1a50efe33cbf178ae09452dcd9fd309013c1" ], - "version": "==20.0.17" + "version": "==20.0.18" }, "waitress": { "hashes": [ @@ -920,11 +938,11 @@ }, "webtest": { "hashes": [ - "sha256:71114cd778a7d7b237ec5c8a5c32084f447d869ae62e48bcd5b73af211133e74", - "sha256:da9cf14c103ff51a40dee4cac7657840d1317456eb8f0ca81289b5cbff175f4b" + "sha256:44ddfe99b5eca4cf07675e7222c81dd624d22f9a26035d2b93dc8862dc1153c6", + "sha256:aac168b5b2b4f200af4e35867cf316712210e3d5db81c1cbdff38722647bb087" ], "index": "pypi", - "version": "==2.0.34" + "version": "==2.0.35" }, "zipp": { "hashes": [ @@ -934,5 +952,4 @@ "version": "==3.1.0" } } - } } diff --git a/geomagio/residual/Calculation.py b/geomagio/residual/Calculation.py index e1494043c..8e2e2826d 100644 --- a/geomagio/residual/Calculation.py +++ b/geomagio/residual/Calculation.py @@ -1,68 +1,198 @@ from typing import List, Tuple +from typing_extensions import Literal import numpy as np from pydantic import BaseModel from .Absolute import Absolute from .Angle import from_dms, to_dms -from .MeasurementType import MeasurementType as mt +from .MeasurementType import ( + MeasurementType as mt, + DECLINATION_TYPES, + INCLINATION_TYPES, + MARK_TYPES, +) from .Measurement import ( AverageMeasurement, Measurement, average_measurement, measurement_index, ) +from .Reading import Reading -# specify mark measurement types -MARK_TYPES = [ - mt.FIRST_MARK_DOWN, - mt.FIRST_MARK_UP, - mt.SECOND_MARK_DOWN, - mt.SECOND_MARK_UP, -] - -# define measurement types used to calculate inclination -INCLINATION_TYPES = [mt.NORTH_DOWN, mt.NORTH_UP, mt.SOUTH_DOWN, mt.SOUTH_UP] +def calculate(reading: Reading) -> Reading: + """Calculate absolutes and scale value using residual method. -# define measurement types used to calculate declination -DECLINATION_TYPES = [mt.EAST_UP, mt.EAST_DOWN, mt.WEST_UP, mt.WEST_DOWN] + Parameters + ------- + reading: reading to calculate absolutes from. - -def calculate(reading) -> Tuple[Absolute, Absolute, Absolute]: - """ - Calculate/recalculate absolute from a Reading object's - ordinates, measurements, and metadata. - Outputs a list of absolutes containing baseline, absolute, - and element name. Also reutrns the scale value. + Returns + ------- + new reading object with calculated absolutes and scale_value. + NOTE: rest of reading object is shallow copy. """ - # gather oridinates, measuremenets, and metadata objects from reading - index = measurement_index(reading.measurements) # reference measurement, used to adjust absolutes - reference = index[mt.WEST_DOWN][0] + reference = reading[mt.WEST_DOWN][0] # calculate inclination - inclination, f, mean = calculate_I(reading) + inclination, f, mean = calculate_I( + hemisphere=reading.hemisphere, measurements=reading.measurements + ) + corrected_f = f + reading.pier_correction # TODO: should this be returned? # calculate absolutes - absoluteH, absoluteZ = calculate_HZ_absolutes(inclination, f, mean, reference) - scale_value = calculate_scale(inclination, f, index[mt.NORTH_DOWN_SCALE]) - # calculate declination absolute and baseline - absoluteD = calculate_D_absolute(reading, absoluteH, reference) - return absoluteD, absoluteH, absoluteZ + absoluteH, absoluteZ = calculate_HZ_absolutes( + corrected_f=corrected_f, inclination=inclination, mean=mean, reference=reference + ) + absoluteD = calculate_D_absolute( + azimuth=reading.azimuth, + h_baseline=absoluteH.baseline, + measurements=reading.measurements, + reference=reference, + ) + # calculate scale + scale_value = calculate_scale_value( + corrected_f=corrected_f, + inclination=inclination, + measurements=reading[mt.NORTH_DOWN_SCALE], + ) + # create new reading object + calculated = Reading( + absolutes=[absoluteD, absoluteH, absoluteZ], + scale_value=scale_value, + # copy other attributes + **reading.dict(exclude={"absolutes", "scale_value"}), + ) + return calculated + +def calculate_D_absolute( + measurements: List[Measurement], + azimuth: float, + h_baseline: float, + reference: Measurement, +) -> Absolute: + """Calculate D absolute. -def calculate_I(reading) -> Tuple[float, float, Measurement]: + Parameters + ---------- + measurements: list with at least declination and mark measurements. + azimuth: azimuth to mark in decimal degrees. + h_baseline: calculated H baseline value. + reference: reference measurement used to adjust absolute. + + Returns + ------- + D Absolute """ - Calculate inclination angles from measurements, ordinates, - average ordinates from every measurement, and metadata. - Returns inclination angle and calculated average f + # mean across all declination measurements + mean = average_measurement(measurements, DECLINATION_TYPES) + # average mark + average_mark = average_measurement(measurements, MARK_TYPES).angle + # adjust based on which is larger + mark_up = average_measurement(measurements, [mt.FIRST_MARK_UP]).angle + mark_down = average_measurement(measurements, [mt.FIRST_MARK_DOWN]).angle + if mark_up < mark_down: + average_mark += 90 + else: + average_mark -= 90 + # declination measurements + declination_measurements = [ + average_measurement(measurements, [t]) for t in DECLINATION_TYPES + ] + # average declination meridian + meridian = np.average( + [ + m.angle + + np.degrees( + m.measurement_type.meridian + * (np.arcsin(m.residual / np.sqrt((m.h + h_baseline) ** 2 + m.e ** 2))) + ) + - np.degrees(np.arctan(m.e / (m.h + h_baseline))) + for m in declination_measurements + ] + ) + # add subtract average mark angle from average meridian angle and add + # azimuth to get the declination baseline + d_b = (meridian - average_mark) + azimuth + # calculate absolute + d_abs = d_b + np.degrees(np.arctan(reference.e / (reference.h + h_baseline))) + return Absolute(element="D", absolute=d_abs, baseline=d_b) + + +def calculate_HZ_absolutes( + inclination: float, + corrected_f: float, + mean: AverageMeasurement, + reference: Measurement, +) -> Tuple[Absolute, Absolute]: + """Calculate H and Z absolutes. + + Parameters + ---------- + inclination: calculated inclination. + corrected_f: calculated f with pier correction. + mean: mean of inclination ordinates. + reference: reference measurement used to adjust absolutes. + + Returns + ------- + Tuple + - H Absolute + - Z Absolute + """ + inclination_radians = np.radians(inclination) + h_abs = corrected_f * np.cos(inclination_radians) + z_abs = corrected_f * np.sin(inclination_radians) + h_b = round(np.sqrt(h_abs ** 2 - mean.e ** 2) - mean.h, 1) + z_b = round(z_abs - mean.z, 1) + # adjust absolutes to reference measurement + h_abs = np.sqrt((h_b + reference.h) ** 2 + (reference.e) ** 2) + z_abs = z_b + reference.z + # return absolutes + return ( + Absolute( + element="H", + baseline=h_b, + absolute=h_abs, + starttime=mean.time, + endtime=mean.endtime, + ), + Absolute( + element="Z", + baseline=z_b, + absolute=z_abs, + starttime=mean.time, + endtime=mean.endtime, + ), + ) + + +def calculate_I( + measurements: List[Measurement], hemisphere: Literal[-1, 1] = 1 +) -> Tuple[float, float, Measurement]: + """Calculate inclination and f from measurements. + + Parameters + ---------- + measurements: list with at least inclination measurements. + hemisphere: +1 for northern hemisphere (default), -1 for southern hemisphere. + + Returns + ------- + Tuple + - inclination angle in decimal degrees + - uncorrected calculated f + - mean of inclination measurements """ - index = measurement_index(reading.measurements) # mean across all inclination measurements - mean = reading.get_average(INCLINATION_TYPES) + mean = average_measurement(measurements, INCLINATION_TYPES) # mean within each type - measurements = [reading.get_average(t) for t in INCLINATION_TYPES] + inclination_measurements = [ + average_measurement(measurements, [t]) for t in INCLINATION_TYPES + ] # get initial inclination angle, assumed to be the southdown angle - inclination = reading.get_average([mt.NORTH_UP]).angle + inclination = average_measurement(measurements, [mt.NORTH_DOWN]).angle if inclination >= 90: inclination -= 180 # loop until inclination converges @@ -72,7 +202,7 @@ def calculate_I(reading) -> Tuple[float, float, Measurement]: last_inclination = inclination # Update measurement f based on inclination inclination_radians = np.radians(inclination) - for m in measurements: + for m in inclination_measurements: m.f = ( mean.f + (m.h - mean.h) * np.cos(inclination_radians) @@ -88,111 +218,45 @@ def calculate_I(reading) -> Tuple[float, float, Measurement]: * ( +m.angle + m.measurement_type.direction - * (reading.hemisphere * np.degrees(np.arcsin(m.residual / m.f))) + * (hemisphere * np.degrees(np.arcsin(m.residual / m.f))) ) ) - for m in measurements + for m in inclination_measurements ] ) - # calculate corrected f - f = np.average([m.f for m in measurements]) + reading.pier_correction + # calculate uncorrected f + f = np.average([m.f for m in inclination_measurements]) return inclination, f, mean -def calculate_HZ_absolutes( - inclination: float, f: float, mean: AverageMeasurement, reference: Measurement -): - inclination_radians = np.radians(inclination) - h_abs = f * np.cos(inclination_radians) - z_abs = f * np.sin(inclination_radians) - h_b = round(np.sqrt(h_abs ** 2 - mean.e ** 2) - mean.h, 1) - z_b = round(z_abs - mean.z, 1) - # adjust absolutes to reference measurement - h_abs = np.sqrt((h_b + reference.h) ** 2 + (reference.e) ** 2) - z_abs = z_b + reference.z - # return absolutes - return ( - Absolute( - element="H", - baseline=h_b, - absolute=h_abs, - starttime=mean.time, - endtime=mean.endtime, - ), - Absolute( - element="Z", - baseline=z_b, - absolute=z_abs, - starttime=mean.time, - endtime=mean.endtime, - ), - ) - - -def calculate_D_absolute( - reading, absoluteH: Absolute, reference: Measurement -) -> Absolute: - """ - Calculate declination absolute and declination baseline from - ordinates, measurements, measurement_index(dictionary), azimuth and H baseline - Returns absolute and baseline for declination. - """ - index = measurement_index(reading.measurements) - # mean across all declination measurements - mean = reading.get_average(DECLINATION_TYPES) - # average mark - average_mark = reading.get_average(MARK_TYPES).angle - # adjust based on which is larger - mark_up = index[mt.FIRST_MARK_UP][0] - mark_down = index[mt.FIRST_MARK_DOWN][0] - if mark_up.angle < mark_down.angle: - average_mark += 90 - else: - average_mark -= 90 - # declination measurements - measurements = [reading.get_average(t) for t in DECLINATION_TYPES] - # average declination meridian - h_base = absoluteH.baseline - meridian = np.average( - [ - ( - m.angle - + np.degrees( - m.measurement_type.meridian - * (np.arcsin(m.residual / np.sqrt((m.h + h_base) ** 2 + m.e ** 2))) - ) - - np.degrees(np.arctan(m.e / (m.h + h_base))) - ) - for m in measurements - ] - ) - # add subtract average mark angle from average meridian angle and add - # azimuth to get the declination baseline - d_b = (meridian - average_mark) + reading.azimuth - # calculate absolute - d_abs = d_b + np.degrees(np.arctan(reference.e / (reference.h + h_base))) - return Absolute(element="D", absolute=d_abs, baseline=d_b) +def calculate_scale_value( + measurements: List[Measurement], inclination: float, corrected_f: float +) -> float: + """Calculate scale value. + Parameters + ---------- + measurements: measurements to be used for scale calculation. + should have type NORTH_DOWN_SCALE. + inclination: calculated inclination. + corrected_f: calculated f with pier correction. -def calculate_scale(inclination: float, f: float, measurements: List[Measurement]): - """ - Calculate scale value from calulated f(from inclination computations), - inclination, and the measurements/ordinates taken for scaling purposes. - Such measurements usually present themselves as a set of three North Down - measurements, where the final two measuremets(and ordinates) are used for scaling. + Returns + ------- + Calculated scale value. """ inclination_radians = np.radians(inclination) - scale0, scale1 = measurements[0], measurements[-1] + m1, m2 = measurements[0], measurements[-1] field_change = ( np.degrees( ( - -np.sin(inclination_radians) * (scale1.h - scale0.h) - + np.cos(inclination_radians) * (scale1.z - scale0.z) + -np.sin(inclination_radians) * (m2.h - m1.h) + + np.cos(inclination_radians) * (m2.z - m1.z) ) - / f + / corrected_f ) + 0.1668 ) - residual_change = scale1.residual - scale0.residual - scale_value = f * field_change / np.abs(residual_change) + residual_change = m2.residual - m1.residual + scale_value = corrected_f * field_change / np.abs(residual_change) return scale_value diff --git a/geomagio/residual/Measurement.py b/geomagio/residual/Measurement.py index 1bad924cf..86fcd7403 100644 --- a/geomagio/residual/Measurement.py +++ b/geomagio/residual/Measurement.py @@ -34,15 +34,24 @@ class AverageMeasurement(Measurement): endtime: Optional[UTCDateTime] = None -def average_measurement(measurements: List[Measurement]) -> AverageMeasurement: +def average_measurement( + measurements: List[Measurement], types: List[MeasurementType] = None +) -> AverageMeasurement: """Calculate average from multiple measurements. - returns None if measurements is empty or None - otherwise returns Measurement - - using type from first measurement, - - with empty time, - - averaging all other values + Parameters + ---------- + measurements - source measurements for average + types - optional list of types to include, default all + + Returns + ------- + None - if no measurements + Otherwise, average of matching measurements. + Type is copied from first measurement. """ + if types: + measurements = [m for m in measurements if m.measurement_type in types] if len(measurements) == 0: # no measurements to average return None diff --git a/geomagio/residual/MeasurementType.py b/geomagio/residual/MeasurementType.py index 2b43b3b14..f8e1216ae 100644 --- a/geomagio/residual/MeasurementType.py +++ b/geomagio/residual/MeasurementType.py @@ -1,6 +1,6 @@ import enum -# FIXME: add another measurement type that is a scaling measurement type + class MeasurementType(str, enum.Enum): """Measurement types used during absolutes.""" @@ -35,17 +35,6 @@ class MeasurementType(str, enum.Enum): else: return -1 - @property - def shift(self): - if self == MeasurementType.SOUTH_DOWN: - return -180 - if self == MeasurementType.SOUTH_UP: - return 180 - if self == MeasurementType.NORTH_UP: - return 0 - if self == MeasurementType.NORTH_DOWN: - return 360 - @property def meridian(self): if self in [ @@ -57,3 +46,41 @@ class MeasurementType(str, enum.Enum): return 1 else: return -1 + + @property + def shift(self): + if self == MeasurementType.SOUTH_DOWN: + return -180 + if self == MeasurementType.SOUTH_UP: + return 180 + if self == MeasurementType.NORTH_UP: + return 0 + if self == MeasurementType.NORTH_DOWN: + return 360 + + +# define measurement types used to calculate declination +DECLINATION_TYPES = [ + MeasurementType.EAST_UP, + MeasurementType.EAST_DOWN, + MeasurementType.WEST_UP, + MeasurementType.WEST_DOWN, +] + + +# define measurement types used to calculate inclination +INCLINATION_TYPES = [ + MeasurementType.NORTH_DOWN, + MeasurementType.NORTH_UP, + MeasurementType.SOUTH_DOWN, + MeasurementType.SOUTH_UP, +] + + +# specify mark measurement types +MARK_TYPES = [ + MeasurementType.FIRST_MARK_DOWN, + MeasurementType.FIRST_MARK_UP, + MeasurementType.SECOND_MARK_DOWN, + MeasurementType.SECOND_MARK_UP, +] diff --git a/geomagio/residual/Reading.py b/geomagio/residual/Reading.py index be6b90c70..19041231f 100644 --- a/geomagio/residual/Reading.py +++ b/geomagio/residual/Reading.py @@ -1,12 +1,12 @@ import collections from typing import Dict, List, Optional +from typing_extensions import Literal from pydantic import BaseModel from .Absolute import Absolute from .Measurement import AverageMeasurement, Measurement, average_measurement from .MeasurementType import MeasurementType -from .Calculation import calculate class Reading(BaseModel): @@ -22,18 +22,17 @@ class Reading(BaseModel): pier_correction: pier correction value, nT. """ - absolutes: Optional[List[Absolute]] = None + absolutes: List[Absolute] = [] azimuth: float = 0 - hemisphere: float = 1 - measurements: Optional[List[Measurement]] = [] - metadata: Optional[Dict] = [] + hemisphere: Literal[-1, 1] = 1 + measurements: List[Measurement] = [] + metadata: Dict = {} pier_correction: float = 0 + scale_value: float = None - def get_average(self, types: List[MeasurementType]) -> AverageMeasurement: - return average_measurement( - [m for m in self.measurements if m.measurement_type in types] - ) + def __getitem__(self, measurement_type: MeasurementType): + """Provide access to measurements by type. - def update_absolutes(self): - self.absolutes = calculate(self) - return self.absolutes + Example: reading[MeasurementType.WEST_DOWN] + """ + return [m for m in self.measurements if m.measurement_type == measurement_type] diff --git a/geomagio/residual/__init__.py b/geomagio/residual/__init__.py index 9ec21abb7..59e03297a 100644 --- a/geomagio/residual/__init__.py +++ b/geomagio/residual/__init__.py @@ -1,11 +1,23 @@ # residual module from __future__ import absolute_import -from .Absolute import Absolute from . import Angle +from .Absolute import Absolute +from .Calculation import ( + calculate, + calculate_D_absolute, + calculate_HZ_absolutes, + calculate_I, + calculate_scale_value, +) from .CalFileFactory import CalFileFactory -from .Measurement import Measurement -from .MeasurementType import MeasurementType +from .Measurement import Measurement, AverageMeasurement, average_measurement +from .MeasurementType import ( + MeasurementType, + DECLINATION_TYPES, + INCLINATION_TYPES, + MARK_TYPES, +) from .Reading import Reading from .SpreadsheetAbsolutesFactory import SpreadsheetAbsolutesFactory from .WebAbsolutesFactory import WebAbsolutesFactory @@ -13,7 +25,16 @@ from .WebAbsolutesFactory import WebAbsolutesFactory __all__ = [ "Absolute", "Angle", - "CalFileFactory", + "AverageMeasurement", + "average_measurement" "CalFileFactory", + "calculate", + "calculate_D_absolute", + "calculate_HZ_absolutes", + "calculate_I", + "calculate_scale_value", + "DECLINATION_TYPES", + "INCLINATION_TYPES", + "MARK_TYPES", "Measurement", "MeasurementType", "Reading", diff --git a/test/residual_test/residual_test.py b/test/residual_test/residual_test.py index 0266ad318..c132d48ed 100644 --- a/test/residual_test/residual_test.py +++ b/test/residual_test/residual_test.py @@ -1,7 +1,8 @@ -from geomagio.residual import SpreadsheetAbsolutesFactory from numpy.testing import assert_almost_equal import pytest +from geomagio.residual import calculate, SpreadsheetAbsolutesFactory + def assert_absolutes(original, result): """ @@ -37,8 +38,8 @@ def compare_spreadsheet_absolutes(path): reading = saf.parse_spreadsheet(path=path) # establish original absolute object original = {a.element: a for a in reading.absolutes} - reading.update_absolutes() - result = {a.element: a for a in reading.absolutes} + calculated = calculate(reading) + result = {a.element: a for a in calculated.absolutes} return original, result -- GitLab