diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index e5a0885..61aedc4 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -16,7 +16,7 @@ jobs: - name: Set up Python uses: actions/setup-python@v5 with: - python-version: '3.11' # or whatever version your project uses + python-version: '3.11' - name: Install Poetry run: | @@ -24,7 +24,7 @@ jobs: echo "$HOME/.local/bin" >> $GITHUB_PATH - name: Install dependencies - run: poetry install --extras "display" + run: poetry install --with dev --all-extras - name: Run tests run: poetry run pytest diff --git a/README.md b/README.md index a3176b0..fcf641a 100644 --- a/README.md +++ b/README.md @@ -134,9 +134,9 @@ You want to digitally control an optical shutter and AOM. For digital channels, simply *name* the ADwin ports using standard Python lists. These keep track of the physical connections. ``` python - from wigner_time.adwin import connection as adcon - from wigner_time import device - from wigner_time import conversion as conv + from wigner.time.adwin import connection as adcon + from wigner.time import device + from wigner.time import conversion as conv connections = adcon.new( ["shutter_MOT", 1, 11], @@ -207,7 +207,7 @@ tline = tl.stack( The timeline can then be exported to an ADwin-compatible format. ``` python - from wigner_time.adwin import core as adwin + from wigner.time.adwin import core as adwin adwin.to_data(tline) ``` diff --git a/doc/demo__imaging.py b/doc/demo__imaging.py new file mode 100644 index 0000000..635522c --- /dev/null +++ b/doc/demo__imaging.py @@ -0,0 +1,150 @@ +import sys +import pandas as pd +from munch import Munch + +from wigner.time.adwin import connection as con +from wigner.time import timeline as tl + +import demo__full_experiment as ex + +# Note: make context explicit everywhere, otherwise there is a danger of squashing everything into the Finish context +# due to previous_context when appending to an already `finish`ed timeline + +connections = pd.concat( + [ + ex.connections, + con.new( + ["shutter_imaging", 1, 13], ["AOM_imaging", 1, 5], ["trigger_camera", 1, 0] + ), + ] +) + + +# this is an upper estimate of the IDS ueye camera exposition delay +DELAY__CAMERA_EXPOSITION = 100e-6 + + +def camera_exposition(exposition__AOM, delay__camera=DELAY__CAMERA_EXPOSITION): + return exposition__AOM + 2 * delay__camera + + +def init(**kwargs): + return ex.init(shutter_imaging=0, AOM_imaging=1, trigger_camera=0, **kwargs) + + +def finish(**kwargs): + return ex.finish(shutter_imaging=0, AOM_imaging=1, trigger_camera=0, **kwargs) + + +def trigger_camera( + t, + exposure, + context, + origin=None, + **kwargs # this can contain e.g. an already existing timeline +): + return tl.update( + "trigger_camera", + [[t, 1], [t + exposure, 0]], + context=context, + origin=origin, + **kwargs + ) + + +# From this point onwards, exposure is AOM_exposure (so that camera and shutter exposure must be larger) +def flash_light(t, exposure__AOM, context, origin=None, **kwargs): + sf = ex.constants.safety_factor + return tl.stack( + tl.update( + "AOM_imaging", + [[t, 1], [t + exposure__AOM, 0]], + context=context, + origin=origin, + **kwargs + ), + tl.update( + "shutter_imaging", + [ + [t - exposure__AOM * (sf - 1) - ex.constants.AI.lag__shutter_on, 1], + [t + exposure__AOM * sf, 0], + ], + context=context, + origin=origin, + ), + ) + + +def expose_camera( + t, + exposure__AOM, + context, + origin=None, + delay__camera=DELAY__CAMERA_EXPOSITION, + **kwargs +): + return tl.stack( + flash_light(t, exposure__AOM, context, origin, **kwargs), + trigger_camera( + t - delay__camera, + exposure__AOM + 2 * delay__camera, + context, + origin, + ), + ) + + +def image_plus_background( + t, exposure__AOM, delay__background, context, origin, **kwargs +): + return tl.stack( + expose_camera(t, exposure__AOM, context, origin, **kwargs), # taking At image + trigger_camera( + t + delay__background, + camera_exposition(exposure__AOM), + context, + origin, + ), # taking Bg_At image + ) + + +def absorption_image( + t, + exposure__AOM, + origin, + delay__background=50e-3, + delay__beam=0.2, + advance__AOM_off=0.1, + exposure__blow=1e-2, + context="imaging__absorption", + **kwargs +): + """ + An atomic absorption image is constructed from an image of the imaging beam, an image of the atoms and associated background images. + """ + + return tl.stack( + tl.update( + AOM_imaging=0, + t=t - advance__AOM_off, + context=context, + origin=origin, + **kwargs + ), # initializing the AOM + image_plus_background( + t, exposure__AOM, delay__background, context, origin + ), # taking At + Bg_At image + ( + flash_light( + t + delay__background + delay__beam / 2.0, + exposure__blow, + context, + origin, + ) + if exposure__blow is not None + else None + ), # blow out the atoms in between + image_plus_background( + t + delay__beam, exposure__AOM, delay__background, context, origin + ), # taking Li + Bg_Li image + ) diff --git a/doc/demo__tips_and_tricks.py b/doc/demo__tips_and_tricks.py new file mode 100644 index 0000000..a869908 --- /dev/null +++ b/doc/demo__tips_and_tricks.py @@ -0,0 +1,14 @@ +""" +Here we highlight some convenient features that might not be obvious otherwise. +""" + +from wigner.time import timeline as tl + +tl.create(AOM_MOT=1, shutter_MOT=1, t=10, context="MOT") + +tl.create(AOM_MOT=[0.1, 1], shutter_MOT=[0.0, 1], thing=40, context="MOT") + +tl.create(AOM_MOT=[[0.1, 1], [0.2, 0]], context="MOT") +tl.create("AOM_MOT", 0.1, 1, "MOT") + +tl.create([["AOM_MOT", 1, 0.1, "MOT"], ["AOM_imaging", 1, 0.0, "AI"]]) diff --git a/doc/experiment.py b/doc/experiment.py new file mode 100644 index 0000000..82a4bf3 --- /dev/null +++ b/doc/experiment.py @@ -0,0 +1,255 @@ +# coding: utf-8 +import sys + +sys.path.append("..") + +import pandas as pd + +from munch import Munch +from wigner.time.adwin import connection as adcon +from wigner.time import timeline as tl +from wigner.time import device + + +connections = adcon.new( + ["shutter_MOT", 1, 11], + ["shutter_repump", 1, 12], + ["shutter_OP001", 1, 14], + ["shutter_OP002", 1, 15], + ["AOM_MOT", 1, 1], + ["AOM_repump", 1, 2], + ["AOM_OPaux", 1, 30], # should be set to 0 always + ["AOM_OP", 1, 31], + ["coil_compensationX__A", 4, 7], + ["coil_compensationY__A", 3, 2], + ["coil_MOTlower__A", 4, 1], + ["coil_MOTupper__A", 4, 3], + ["coil_MOTlowerPlus__A", 4, 2], + ["coil_MOTupperPlus__A", 4, 4], + ["lockbox_MOT__MHz", 3, 8], +) + +devices = device.new( + ["coil_compensationX__A", 3.0 / 10.0], + ["coil_compensationY__A", 3.0 / 10.0], + ["coil_MOTlower__A", 5 / 10.0], + ["coil_MOTupper__A", 5 / 10.0], + ["coil_MOTlowerPlus__A", 5 / 10.0], + ["coil_MOTupperPlus__A", 5 / 10.0], + ["lockbox_MOT__MHz", 0.05], +) + +# TODO: Should connections and devices be merged? + +# OP1 ON delay: 1.48ms (OFF: 2.6); OP2 OFF delay: 1.78ms (ON: 2.42) measured on Nov 7, 2024 +# OP AOM ON delay: ~20us, not negligible compared to the length of the OP phase +# MOT OFF delay: 2.3ms (ON: 1.8) measured on Nov 12, 2024 +# imaging ON delay: 2.25ms OFF: 1.9ms +# sum of ON and OFF delays adds up to 4.1ms for each shutter, which is OK! +constants = Munch( + safety_factor=1.1, + # factor__VpMHz=0.05, + lag_MOTshutter=2.3e-3, + Compensation=Munch( + Z__A=-0.1, + Y__A=1.5, + X__A=0.25, + ), + OP=Munch( + lag_AOM_on=15e-6, + lag_shutter_on=1.48e-3, + lag_shutter_off=1.78e-3, + duration_shutter_on=140e-6, + duration_shutter_off=600e-6, + ), + AI=Munch( + lag_shutter_on=2.2e-3, + lag_shutter_off=1.9e-3, + ), +) + + +def init(**kwargs): + """ + Creates an experimental timeline for the initialization of every device. + """ + return tl.stack( + tl.create( + lockbox_MOT__MHz=0.0, + coil_compensationX__A=constants.Compensation.X__A, + coil_compensationY__A=constants.Compensation.Y__A, + coil_MOTlowerPlus__A=-constants.Compensation.Z__A, + coil_MOTupperPlus__A=constants.Compensation.Z__A, + AOM_MOT=1, + AOM_repump=1, + AOM_OPaux=0, # TODO: USB-controlled AOMs should be treated on a higher level + AOM_OP=1, + shutter_MOT=0, + shutter_repump=0, + shutter_OP001=0, + shutter_OP002=1, + context="ADwin_LowInit", + **kwargs, + ), + tl.anchor(t=0.0, origin=0.0, context="InitialAnchor"), + ) + + +def finish(wait=1, lA=-1.0, uA=-0.98, MOT_ON=True, **kwargs): + duration = 1e-2 + return tl.stack( + tl.anchor(wait, context="finalRamps"), + tl.ramp( + lockbox_MOT__MHz=0.0, + coil_MOTlower__A=lA, + coil_MOTupper__A=uA, + coil_compensationX__A=constants.Compensation.X__A, + coil_compensationY__A=constants.Compensation.Y__A, + coil_MOTlowerPlus__A=-constants.Compensation.Z__A, + coil_MOTupperPlus__A=constants.Compensation.Z__A, + # + duration=duration, + context="finalRamps", + ), + tl.update( + AOM_MOT=1, + AOM_repump=1, + AOM_OPaux=0, # TODO: USB-controlled AOMs should be treated on a higher level + AOM_OP=1, + shutter_MOT=int(MOT_ON), + shutter_repump=int(MOT_ON), + shutter_OP001=0, + shutter_OP002=1, + t=0.1, + context="ADwin_Finish", + **kwargs, + ), + ) + + +def MOT(duration=15, lA=-1.0, uA=-0.98, **kwargs): + return tl.stack( + tl.update( + shutter_MOT=1, + shutter_repump=1, + coil_MOTlower__A=lA, + coil_MOTupper__A=uA, + context="MOT", + **kwargs, + ), + tl.anchor(duration), + ) + + +def MOT_detunedGrowth(duration=100e-3, durationRamp=10e-3, toMHz=-5, pt=3, **kwargs): + return tl.stack( + tl.ramp( + lockbox_MOT__MHz=toMHz, + duration=durationRamp, + context="MOT", + **kwargs, + ), + tl.anchor(duration), + ) + + +def molasses( + duration=5e-3, + durationCoilRamp=9e-4, + durationLockboxRamp=1e-3, + toMHz=-90, + coil_pt=3, + lockbox_pt=3, + delay=0, # arbitrary delay to shutter for ad hoc compensation of small drifts + **kwargs +): + + return tl.stack( + tl.ramp( + coil_MOTlower__A=0, + coil_MOTupper__A=0, # TODO: can these be other than 0 (e.g. for more perfect compensaton?) + duration=durationCoilRamp, + context="molasses", + **kwargs, + ), + tl.ramp( + lockbox_MOT__MHz=toMHz, + duration=durationLockboxRamp, + ), + tl.update( + shutter_MOT=[duration - constants.lag_MOTshutter + delay, 0], + AOM_MOT=[duration, 0], + ), + tl.anchor(duration, context="molasses"), + ) + + +def OP( + durationExposition=80e-6, + durationCoilRamp=50e-6, + i=-0.12, + pt=3, + delay1=0, + delay2=0, # arbitrary delays to shutters for ad hoc compensation of small drifts + **kwargs +): + fullDuration = durationExposition + durationCoilRamp + return tl.stack( + tl.ramp( + coil_MOTlower__A=i, + coil_MOTupper__A=-i, + duration=durationCoilRamp, + context="OP", + **kwargs, + ), + tl.update(AOM_OP=[[-0.1, 0], [durationCoilRamp, 1], [fullDuration, 0]]), + tl.update( + shutter_OP001=[ + [durationCoilRamp - constants.OP.lag_shutter_on + delay1, 1], + [0.1, 0], + ] + ), + tl.update( + shutter_OP002=[ + [fullDuration - constants.OP.lag_shutter_off + delay2, 0], + [0.1, 1], + ] + ), + tl.update(AOM_repump=0, shutter_repump=0, t=fullDuration), + tl.anchor(fullDuration, context="OP"), + ) + + +def pull_coils( + duration, + l, + u, + lp=-constants.Compensation.Z__A, + up=constants.Compensation.Z__A, + pt=3, + **kwargs +): + return tl.ramp( + coil_MOTlower__A=l, + coil_MOTupper__A=u, + coil_MOTlowerPlus__A=lp, + coil_MOTupperPlus__A=up, + duration=duration, + **kwargs, + ) + + +def magneticTrapping( + durationInitial=50e-6, + li=-1.8, + ui=-1.7, + durationStrengthen=3e-3, + ls=-4.8, + us=-4.7, + **kwargs +): + return tl.stack( + pull_coils(durationInitial, li, ui, context="magneticTrapping", **kwargs), + pull_coils(durationStrengthen, ls, us, t=durationInitial), + tl.anchor(durationInitial + durationStrengthen, context="magneticTrapping"), + ) diff --git a/wigner_time/internal/doc/experimentDemo.py b/doc/experimentDemo__backup.py similarity index 89% rename from wigner_time/internal/doc/experimentDemo.py rename to doc/experimentDemo__backup.py index 8ad872a..cae819b 100644 --- a/wigner_time/internal/doc/experimentDemo.py +++ b/doc/experimentDemo__backup.py @@ -4,6 +4,7 @@ As well as providing conveniences, the functions can be used to document the intention and meaning of each stage. """ +# TODO: WIP!!! import inspect import sys @@ -12,9 +13,11 @@ import pandas as pd from munch import Munch -from wigner_time import connection as con -from wigner_time import timeline as tl -from wigner_time import ramp_function +from wigner.time.adwin import connection as adcon +from wigner.time import timeline as tl +from wigner.time import device +from wigner.time import conversion as conv +from wigner.time import ramp_function from enum import IntEnum @@ -22,6 +25,7 @@ # Constants and Helpers # ########################################################################### +# TODO: What exactly is a stage, do we need the Enum and should it be in the example file? # TODO: These ↓ (stages, connections, devices and constants) should maybe be read from a separate file (they won't change much). Stage = IntEnum( "Stage", @@ -34,7 +38,7 @@ ], ) -connections = con.connection( +connections = adcon.new( ["shutter_MOT", 1, 11], ["shutter_repump", 1, 12], ["shutter_OP001", 1, 14], @@ -43,9 +47,8 @@ ["shutter_transversePump", 1, 9], ["AOM_MOT", 1, 1], ["AOM_repump", 1, 2], - ["AOM_OP_aux", 1, 30], # should be set to 0 always + ["AOM_OPaux", 1, 30], # should be set to 0 always ["AOM_OP", 1, 31], - ["AOM_science", 1, 4], ["coil_compensationX__A", 4, 7], ["coil_compensationY__A", 3, 2], ["coil_MOTlower__A", 4, 1], @@ -54,23 +57,26 @@ ["coil_MOTupperPlus__A", 4, 4], ["lockbox_MOT__MHz", 3, 8], ["trigger_TC__V", 3, 1], + ["AOM_science", 1, 4], ["AOM_science__V", 4, 8], ) -# TODO: This could be a set of conversion functions/lambdas from units like A, MHz -devices = pd.DataFrame( - columns=["variable", "unit_range", "safety_range"], - data=[ - ["coil_compensationX__A", (-3, 3), (-3, 3)], - ["coil_compensationY__A", (-3, 3), (-3, 3)], - ["coil_MOTlower__A", (-5, 5), (-5, 5)], - ["coil_MOTupper__A", (-5, 5), (-5, 5)], - ["coil_MOTlowerPlus__A", (-5, 5), (-5, 5)], - ["coil_MOTupperPlus__A", (-5, 5), (-5, 5)], - # ["lockbox_MOT__V", (-10, 10)], - ["lockbox_MOT__MHz", (-200, 200)], - ["trigger_TC__V", (-10, 10)], - ["AOM_science__V", (-10, 10)], +devices = device.new( + ["coil_compensationX__A", 1 / 3.0, (-3, 3)], + ["coil_compensationY__A", 1 / 3.0, (-3, 3)], + ["coil_MOTlower__A", 1 / 2.0, (-5, 5)], + ["coil_MOTupper__A", 1 / 2.0, (-5, 5)], + ["coil_MOTlowerPlus__A", 1 / 2.0, (-5, 5)], + ["coil_MOTupperPlus__A", 1 / 2.0, (-5, 5)], + ["lockbox_MOT__MHz", 0.05, (-200, 200)], + ["trigger_TC__V", 1.0, (-10, 10)], + [ + "AOM_science__trans", + conv.function_from_file( + "resources/calibration/aom_calibration.dat", + sep=r"\s+", + ), + (0.0, 1.0), ], ) @@ -126,7 +132,7 @@ def saneState(f=tl.create, MOT_ON=True, **kwargs): coil_MOTupperPlus__A=constants.Compensation.Z__A, AOM_MOT=1, AOM_repump=1, - AOM_OP_aux=0, # TODO: USB-controlled AOMs should be treated on a higher level + AOM_OPaux=0, # TODO: USB-controlled AOMs should be treated on a higher level AOM_OP=1, AOM_science=1, shutter_MOT=int(MOT_ON), @@ -231,6 +237,10 @@ def molasses( coil_MOTupper__A=0, # TODO: can these be other than 0 (e.g. for more perfect compensaton?) duration=durationCoilRamp, # fargs={"ti": coil_pt}, +<<<<<<<< HEAD:doc/experimentDemo__backup.py + context="molasses", +======== +>>>>>>>> fb4fffbed4ab3d4ba72613afee89a0bcbbf26290:wigner.time/internal/doc/experimentDemo.py **kwargs, ), tl.ramp( @@ -423,10 +433,12 @@ def MOT_off(**kwargs): ) return tl.stack(_(), finishFunction(MOT_ON=finish_MOT_ON)) +<<<<<<<< HEAD:doc/experimentDemo__backup.py +======== if __name__ == "__main__": - from wigner_time.adwin import display + from wigner.time.adwin import display thing = tl.stack( init(), @@ -447,3 +459,4 @@ def MOT_off(**kwargs): # ) # display.channels(thing) +>>>>>>>> fb4fffbed4ab3d4ba72613afee89a0bcbbf26290:wigner.time/internal/doc/experimentDemo.py diff --git a/docs/api.md b/docs/api.md index 2e780f3..e236ecc 100644 --- a/docs/api.md +++ b/docs/api.md @@ -1,4 +1,4 @@ # API Reference -::: wigner_time +::: wigner.time diff --git a/docs/index.md b/docs/index.md index 195d45b..92ea95d 100644 --- a/docs/index.md +++ b/docs/index.md @@ -126,9 +126,9 @@ You want to digitally control an optical shutter and AOM. For digital channels, simply *name* the ADwin ports using standard Python lists. These keep track of the physical connections. ``` python - from wigner_time.adwin import connection as adcon - from wigner_time import device - from wigner_time import conversion as conv + from wigner.time.adwin import connection as adcon + from wigner.time import device + from wigner.time import conversion as conv connections = adcon.new( ["shutter_MOT", 1, 11], @@ -199,7 +199,7 @@ tline = tl.stack( The timeline can then be exported to an ADwin-compatible format. ``` python - from wigner_time.adwin import core as adwin + from wigner.time.adwin import core as adwin adwin.to_data(tline) ``` diff --git a/pyproject.toml b/pyproject.toml index 865974d..f85a759 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,23 +1,26 @@ [tool.poetry] -name = "wigner_time" +name = "wigner.time" version = "0.9.0" description="Timeline creation and management for open-loop control in AMO experiments and beyond." authors = ["LightMatters "] readme = "README.md" -packages = [{include = "wigner_time"}] +packages = [{ include = "wigner", from = "src" }] [tool.poetry.dependencies] python = ">=3.10" munch = "^3.0.0" funcy = "^2.0" pandas = "^2.2" +scipy = "^1.14" -pyarrow = {version="^19", optional=true} +ADwin = {version="^0.20", optional=true} +pyarrow = {version="^22", optional=true} matplotlib = {version="^3.7.1", optional=true} pyqt6 = {version="^6.5.0", optional=true} polars = {version="^0.20.21", optional=true} [tool.poetry.extras] +adwin = ["ADwin"] performance_and_export = ["pyarrow"] display = ["matplotlib", "pyqt6"] parallel_processing = ["polars"] @@ -26,6 +29,8 @@ parallel_processing = ["polars"] jupyter = "^1.1.1" black = "^24.10.0" pytest = "^7.3.1" +pyflakes = "^3.2.0" +isort = "^6.0.1" mkdocstrings = {version = "^1.0.3", extras = ["python"]} mkdocs-material = "^9.7.6" mike = "^2.1.4" diff --git a/resources/calibration/aom_calibration.dat b/resources/calibration/aom_calibration.dat new file mode 100644 index 0000000..c7ee0b6 --- /dev/null +++ b/resources/calibration/aom_calibration.dat @@ -0,0 +1,1000 @@ +6.190034045187248549e-04 4.999999888241291046e-03 +1.238006809037449710e-03 9.999999776482582092e-03 +9.285051067780872823e-04 1.499999966472387314e-02 +1.238006809037449710e-03 1.999999955296516418e-02 +9.285051067780872823e-04 2.500000037252902985e-02 +1.238006809037449710e-03 2.999999932944774628e-02 +9.285051067780872823e-04 3.500000014901161194e-02 +6.190034045187248549e-04 3.999999910593032837e-02 +9.285051067780872823e-04 4.500000178813934326e-02 +3.095017022593624274e-04 5.000000074505805969e-02 +6.190034045187248549e-04 5.499999970197677612e-02 +0.000000000000000000e+00 5.999999865889549255e-02 +9.285051067780872823e-04 6.499999761581420898e-02 +-6.190034045187248549e-04 7.000000029802322388e-02 +6.190034045187248549e-04 7.500000298023223877e-02 +6.190034045187248549e-04 7.999999821186065674e-02 +9.285051067780872823e-04 8.500000089406967163e-02 +3.095017022593624274e-04 9.000000357627868652e-02 +9.285051067780872823e-04 9.499999880790710449e-02 +6.190034045187248549e-04 1.000000014901161194e-01 +6.190034045187248549e-04 1.049999967217445374e-01 +3.095017022593624274e-04 1.099999994039535522e-01 +3.095017022593624274e-04 1.150000020861625671e-01 +6.190034045187248549e-04 1.199999973177909851e-01 +6.190034045187248549e-04 1.250000000000000000e-01 +6.190034045187248549e-04 1.299999952316284180e-01 +3.095017022593624274e-04 1.350000053644180298e-01 +1.238006809037449710e-03 1.400000005960464478e-01 +6.190034045187248549e-04 1.449999958276748657e-01 +9.285051067780872823e-04 1.500000059604644775e-01 +3.095017022593624274e-04 1.550000011920928955e-01 +6.190034045187248549e-04 1.599999964237213135e-01 +6.190034045187248549e-04 1.650000065565109253e-01 +9.285051067780872823e-04 1.700000017881393433e-01 +3.095017022593624274e-04 1.749999970197677612e-01 +9.285051067780872823e-04 1.800000071525573730e-01 +6.190034045187248549e-04 1.850000023841857910e-01 +9.285051067780872823e-04 1.899999976158142090e-01 +9.285051067780872823e-04 1.949999928474426270e-01 +6.190034045187248549e-04 2.000000029802322388e-01 +6.190034045187248549e-04 2.049999982118606567e-01 +9.285051067780872823e-04 2.099999934434890747e-01 +6.190034045187248549e-04 2.150000035762786865e-01 +6.190034045187248549e-04 2.199999988079071045e-01 +6.190034045187248549e-04 2.249999940395355225e-01 +9.285051067780872823e-04 2.300000041723251343e-01 +6.190034045187248549e-04 2.349999994039535522e-01 +0.000000000000000000e+00 2.399999946355819702e-01 +6.190034045187248549e-04 2.450000047683715820e-01 +-6.190034045187248549e-04 2.500000000000000000e-01 +6.190034045187248549e-04 2.549999952316284180e-01 +0.000000000000000000e+00 2.599999904632568359e-01 +6.190034045187248549e-04 2.649999856948852539e-01 +0.000000000000000000e+00 2.700000107288360596e-01 +9.285051067780872823e-04 2.750000059604644775e-01 +9.285051067780872823e-04 2.800000011920928955e-01 +9.285051067780872823e-04 2.849999964237213135e-01 +1.238006809037449710e-03 2.899999916553497314e-01 +9.285051067780872823e-04 2.949999868869781494e-01 +9.285051067780872823e-04 3.000000119209289551e-01 +1.238006809037449710e-03 3.050000071525573730e-01 +1.547508511296812137e-03 3.100000023841857910e-01 +1.238006809037449710e-03 3.149999976158142090e-01 +1.238006809037449710e-03 3.199999928474426270e-01 +1.238006809037449710e-03 3.249999880790710449e-01 +1.547508511296812137e-03 3.300000131130218506e-01 +1.857010213556174565e-03 3.350000083446502686e-01 +1.857010213556174565e-03 3.400000035762786865e-01 +1.547508511296812137e-03 3.449999988079071045e-01 +1.547508511296812137e-03 3.499999940395355225e-01 +1.547508511296812137e-03 3.549999892711639404e-01 +1.547508511296812137e-03 3.600000143051147461e-01 +1.547508511296812137e-03 3.650000095367431641e-01 +1.857010213556174565e-03 3.700000047683715820e-01 +1.857010213556174565e-03 3.750000000000000000e-01 +1.547508511296812137e-03 3.799999952316284180e-01 +1.857010213556174565e-03 3.849999904632568359e-01 +1.547508511296812137e-03 3.899999856948852539e-01 +2.166511915815536992e-03 3.950000107288360596e-01 +2.476013618074899419e-03 4.000000059604644775e-01 +2.476013618074899419e-03 4.050000011920928955e-01 +2.476013618074899419e-03 4.099999964237213135e-01 +2.785515320334261847e-03 4.149999916553497314e-01 +3.095017022593624274e-03 4.199999868869781494e-01 +2.785515320334261847e-03 4.250000119209289551e-01 +2.785515320334261847e-03 4.300000071525573730e-01 +3.404518724852986702e-03 4.350000023841857910e-01 +3.404518724852986702e-03 4.399999976158142090e-01 +2.785515320334261847e-03 4.449999928474426270e-01 +3.404518724852986702e-03 4.499999880790710449e-01 +3.404518724852986702e-03 4.550000131130218506e-01 +3.714020427112349129e-03 4.600000083446502686e-01 +3.714020427112349129e-03 4.650000035762786865e-01 +4.333023831631073984e-03 4.699999988079071045e-01 +3.714020427112349129e-03 4.749999940395355225e-01 +4.642525533890435978e-03 4.799999892711639404e-01 +4.333023831631073984e-03 4.850000143051147461e-01 +4.333023831631073984e-03 4.900000095367431641e-01 +4.952027236149798839e-03 4.950000047683715820e-01 +4.952027236149798839e-03 5.000000000000000000e-01 +5.571030640668523694e-03 5.049999952316284180e-01 +5.571030640668523694e-03 5.099999904632568359e-01 +6.190034045187248549e-03 5.149999856948852539e-01 +5.880532342927885688e-03 5.199999809265136719e-01 +6.190034045187248549e-03 5.249999761581420898e-01 +6.190034045187248549e-03 5.299999713897705078e-01 +6.499535747446610542e-03 5.350000262260437012e-01 +6.190034045187248549e-03 5.400000214576721191e-01 +7.118539151965335397e-03 5.450000166893005371e-01 +6.809037449705973404e-03 5.500000119209289551e-01 +7.118539151965335397e-03 5.550000071525573730e-01 +7.118539151965335397e-03 5.600000023841857910e-01 +7.428040854224698258e-03 5.649999976158142090e-01 +7.737542556484060252e-03 5.699999928474426270e-01 +7.737542556484060252e-03 5.749999880790710449e-01 +8.666047663262147968e-03 5.799999833106994629e-01 +8.666047663262147968e-03 5.849999785423278809e-01 +8.975549365521509962e-03 5.899999737739562988e-01 +9.285051067780871956e-03 5.950000286102294922e-01 +9.285051067780871956e-03 6.000000238418579102e-01 +9.594552770040235684e-03 6.050000190734863281e-01 +9.594552770040235684e-03 6.100000143051147461e-01 +1.021355617455895967e-02 6.150000095367431641e-01 +1.052305787681832167e-02 6.200000047683715820e-01 +1.083255957907768539e-02 6.250000000000000000e-01 +1.083255957907768539e-02 6.299999952316284180e-01 +1.114206128133704739e-02 6.349999904632568359e-01 +1.145156298359640938e-02 6.399999856948852539e-01 +1.238006809037449710e-02 6.449999809265136719e-01 +1.207056638811513510e-02 6.499999761581420898e-01 +1.238006809037449710e-02 6.549999713897705078e-01 +1.268956979263385909e-02 6.600000262260437012e-01 +1.299907149489322108e-02 6.650000214576721191e-01 +1.299907149489322108e-02 6.700000166893005371e-01 +1.361807489941194681e-02 6.750000119209289551e-01 +1.361807489941194681e-02 6.800000071525573730e-01 +1.454658000619003452e-02 6.850000023841857910e-01 +1.454658000619003452e-02 6.899999976158142090e-01 +1.547508511296812050e-02 6.949999928474426270e-01 +1.516558341070875851e-02 6.999999880790710449e-01 +1.671309192200557195e-02 7.049999833106994629e-01 +1.640359021974620995e-02 7.099999785423278809e-01 +1.733209532652429594e-02 7.149999737739562988e-01 +1.671309192200557195e-02 7.200000286102294922e-01 +1.764159702878365793e-02 7.250000238418579102e-01 +1.764159702878365793e-02 7.300000190734863281e-01 +1.857010213556174391e-02 7.350000143051147461e-01 +1.857010213556174391e-02 7.400000095367431641e-01 +1.887960383782110937e-02 7.450000047683715820e-01 +1.887960383782110937e-02 7.500000000000000000e-01 +1.949860724233983336e-02 7.549999952316284180e-01 +1.949860724233983336e-02 7.599999904632568359e-01 +2.011761064685855735e-02 7.649999856948852539e-01 +2.011761064685855735e-02 7.699999809265136719e-01 +2.073661405137728134e-02 7.749999761581420898e-01 +2.166511915815537079e-02 7.799999713897705078e-01 +2.166511915815537079e-02 7.850000262260437012e-01 +2.197462086041473278e-02 7.900000214576721191e-01 +2.259362426493345677e-02 7.950000166893005371e-01 +2.259362426493345677e-02 8.000000119209289551e-01 +2.290312596719281876e-02 8.050000071525573730e-01 +2.383163107397090821e-02 8.100000023841857910e-01 +2.445063447848963220e-02 8.149999976158142090e-01 +2.476013618074899419e-02 8.199999928474426270e-01 +2.476013618074899419e-02 8.249999880790710449e-01 +2.537913958526771818e-02 8.299999833106994629e-01 +2.599814298978644217e-02 8.349999785423278809e-01 +2.692664809656453162e-02 8.399999737739562988e-01 +2.661714639430516963e-02 8.450000286102294922e-01 +2.785515320334261760e-02 8.500000238418579102e-01 +2.785515320334261760e-02 8.550000190734863281e-01 +2.878365831012070705e-02 8.600000143051147461e-01 +2.847415660786134159e-02 8.650000095367431641e-01 +2.940266171463943104e-02 8.700000047683715820e-01 +2.940266171463943104e-02 8.750000000000000000e-01 +3.064066852367687901e-02 8.799999952316284180e-01 +3.064066852367687901e-02 8.849999904632568359e-01 +3.125967192819560647e-02 8.899999856948852539e-01 +3.125967192819560647e-02 8.949999809265136719e-01 +3.249767873723305445e-02 8.999999761581420898e-01 +3.280718043949241991e-02 9.049999713897705078e-01 +3.311668214175177843e-02 9.100000262260437012e-01 +3.373568554627050242e-02 9.150000214576721191e-01 +3.404518724852986788e-02 9.200000166893005371e-01 +3.497369235530795734e-02 9.250000119209289551e-01 +3.497369235530795734e-02 9.300000071525573730e-01 +3.590219746208603985e-02 9.350000023841857910e-01 +3.621169916434540531e-02 9.399999976158142090e-01 +3.714020427112348782e-02 9.449999928474426270e-01 +3.775920767564221875e-02 9.499999880790710449e-01 +3.806870937790157727e-02 9.549999833106994629e-01 +3.868771278242030126e-02 9.599999785423278809e-01 +3.930671618693902525e-02 9.649999737739562988e-01 +3.992571959145775617e-02 9.700000286102294922e-01 +4.054472299597648016e-02 9.750000238418579102e-01 +4.085422469823583869e-02 9.800000190734863281e-01 +4.178272980501392814e-02 9.850000143051147461e-01 +4.209223150727328666e-02 9.900000095367431641e-01 +4.271123491179201759e-02 9.950000047683715820e-01 +4.271123491179201759e-02 1.000000000000000000e+00 +4.394924172082946556e-02 1.004999995231628418e+00 +4.425874342308882409e-02 1.009999990463256836e+00 +4.487774682760755501e-02 1.014999985694885254e+00 +4.518724852986691354e-02 1.019999980926513672e+00 +4.611575363664500299e-02 1.024999976158142090e+00 +4.611575363664500299e-02 1.029999971389770508e+00 +4.766326214794181643e-02 1.034999966621398926e+00 +4.797276385020117495e-02 1.039999961853027344e+00 +4.890126895697926440e-02 1.044999957084655762e+00 +4.952027236149798839e-02 1.049999952316284180e+00 +5.013927576601671238e-02 1.054999947547912598e+00 +5.044877746827607784e-02 1.059999942779541016e+00 +5.168678427731352581e-02 1.065000057220458984e+00 +5.199628597957288434e-02 1.070000052452087402e+00 +5.323429278861033925e-02 1.075000047683715820e+00 +5.323429278861033925e-02 1.080000042915344238e+00 +5.416279789538842176e-02 1.085000038146972656e+00 +5.509130300216651122e-02 1.090000033378601074e+00 +5.540080470442587668e-02 1.095000028610229492e+00 +5.601980810894460067e-02 1.100000023841857910e+00 +5.694831321572268318e-02 1.105000019073486328e+00 +5.756731662024141410e-02 1.110000014305114746e+00 +5.849582172701949662e-02 1.115000009536743164e+00 +5.880532342927886208e-02 1.120000004768371582e+00 +5.942432683379758607e-02 1.125000000000000000e+00 +6.035283194057567552e-02 1.129999995231628418e+00 +6.128133704735375803e-02 1.134999990463256836e+00 +6.159083874961312349e-02 1.139999985694885254e+00 +6.220984215413184748e-02 1.144999980926513672e+00 +6.251934385639121294e-02 1.149999976158142090e+00 +6.375735066542866092e-02 1.154999971389770508e+00 +6.468585577220674343e-02 1.159999966621398926e+00 +6.530485917672547436e-02 1.164999961853027344e+00 +6.561436087898483982e-02 1.169999957084655762e+00 +6.685236768802228779e-02 1.174999952316284180e+00 +6.747137109254100484e-02 1.179999947547912598e+00 +6.839987619931910123e-02 1.184999942779541016e+00 +6.932838130609718374e-02 1.190000057220458984e+00 +6.994738471061591467e-02 1.195000052452087402e+00 +7.025688641287526626e-02 1.200000047683715820e+00 +7.149489322191271423e-02 1.205000042915344238e+00 +7.180439492417207969e-02 1.210000038146972656e+00 +7.304240173320952767e-02 1.215000033378601074e+00 +7.366140513772825860e-02 1.220000028610229492e+00 +7.428040854224697565e-02 1.225000023841857910e+00 +7.520891364902507203e-02 1.230000019073486328e+00 +7.613741875580315455e-02 1.235000014305114746e+00 +7.675642216032188547e-02 1.240000009536743164e+00 +7.706592386258125094e-02 1.245000004768371582e+00 +7.830393067161869891e-02 1.250000000000000000e+00 +7.954193748065614689e-02 1.254999995231628418e+00 +7.985143918291551235e-02 1.259999990463256836e+00 +8.077994428969359486e-02 1.264999985694885254e+00 +8.170844939647167737e-02 1.269999980926513672e+00 +8.263695450324977376e-02 1.274999976158142090e+00 +8.356545961002785627e-02 1.279999971389770508e+00 +8.418446301454657332e-02 1.284999966621398926e+00 +8.542246982358403518e-02 1.289999961853027344e+00 +8.604147322810275222e-02 1.294999957084655762e+00 +8.727948003714020020e-02 1.299999952316284180e+00 +8.758898173939956566e-02 1.304999947547912598e+00 +8.882698854843701364e-02 1.309999942779541016e+00 +8.975549365521511003e-02 1.315000057220458984e+00 +9.068399876199319254e-02 1.320000052452087402e+00 +9.099350046425255800e-02 1.325000047683715820e+00 +9.192200557103064051e-02 1.330000042915344238e+00 +9.285051067780872303e-02 1.335000038146972656e+00 +9.377901578458681942e-02 1.340000033378601074e+00 +9.501702259362426739e-02 1.345000028610229492e+00 +9.532652429588363285e-02 1.350000023841857910e+00 +9.687403280718044629e-02 1.355000019073486328e+00 +9.718353450943979788e-02 1.360000014305114746e+00 +9.873104302073661132e-02 1.365000009536743164e+00 +9.904054472299597678e-02 1.370000004768371582e+00 +9.996904982977405929e-02 1.375000000000000000e+00 +1.008975549365521557e-01 1.379999995231628418e+00 +1.018260600433302382e-01 1.384999990463256836e+00 +1.027545651501083207e-01 1.389999985694885254e+00 +1.036830702568864171e-01 1.394999980926513672e+00 +1.049210770659238651e-01 1.399999976158142090e+00 +1.055400804704425821e-01 1.404999971389770508e+00 +1.067780872794800440e-01 1.409999966621398926e+00 +1.073970906839987610e-01 1.414999961853027344e+00 +1.083255957907768435e-01 1.419999957084655762e+00 +1.092541008975549399e-01 1.424999952316284180e+00 +1.101826060043330224e-01 1.429999947547912598e+00 +1.114206128133704704e-01 1.434999942779541016e+00 +1.123491179201485668e-01 1.440000057220458984e+00 +1.132776230269266493e-01 1.445000052452087402e+00 +1.138966264314453664e-01 1.450000047683715820e+00 +1.154441349427421798e-01 1.455000042915344238e+00 +1.160631383472609107e-01 1.460000038146972656e+00 +1.173011451562983587e-01 1.465000033378601074e+00 +1.182296502630764412e-01 1.470000028610229492e+00 +1.188486536675951721e-01 1.475000023841857910e+00 +1.203961621788919856e-01 1.480000019073486328e+00 +1.216341689879294335e-01 1.485000014305114746e+00 +1.225626740947075161e-01 1.490000009536743164e+00 +1.241101826060043295e-01 1.495000004768371582e+00 +1.247291860105230604e-01 1.500000000000000000e+00 +1.259671928195605084e-01 1.504999995231628418e+00 +1.268956979263385909e-01 1.509999990463256836e+00 +1.278242030331166734e-01 1.514999985694885254e+00 +1.284432064376354043e-01 1.519999980926513672e+00 +1.303002166511915694e-01 1.524999976158142090e+00 +1.315382234602290312e-01 1.529999971389770508e+00 +1.318477251624883828e-01 1.534999966621398926e+00 +1.330857319715258447e-01 1.539999961853027344e+00 +1.337047353760445756e-01 1.544999957084655762e+00 +1.352522438873413890e-01 1.549999952316284180e+00 +1.358712472918600922e-01 1.554999947547912598e+00 +1.371092541008975541e-01 1.559999942779541016e+00 +1.383472609099350159e-01 1.565000057220458984e+00 +1.392757660167130984e-01 1.570000052452087402e+00 +1.402042711234911809e-01 1.575000047683715820e+00 +1.411327762302692634e-01 1.580000042915344238e+00 +1.426802847415660769e-01 1.585000038146972656e+00 +1.436087898483441594e-01 1.590000033378601074e+00 +1.445372949551222419e-01 1.595000028610229492e+00 +1.454658000619003522e-01 1.600000023841857910e+00 +1.467038068709377863e-01 1.605000019073486328e+00 +1.479418136799752481e-01 1.610000014305114746e+00 +1.494893221912720616e-01 1.615000009536743164e+00 +1.504178272980501441e-01 1.620000004768371582e+00 +1.510368307025688750e-01 1.625000000000000000e+00 +1.525843392138656884e-01 1.629999995231628418e+00 +1.535128443206437709e-01 1.634999990463256836e+00 +1.547508511296812050e-01 1.639999985694885254e+00 +1.556793562364592876e-01 1.644999980926513672e+00 +1.575363664500154803e-01 1.649999976158142090e+00 +1.581553698545342113e-01 1.654999971389770508e+00 +1.593933766635716454e-01 1.659999966621398926e+00 +1.600123800680903763e-01 1.664999961853027344e+00 +1.618693902816465413e-01 1.669999957084655762e+00 +1.627978953884246238e-01 1.674999952316284180e+00 +1.649644073042401682e-01 1.679999947547912598e+00 +1.649644073042401682e-01 1.684999942779541016e+00 +1.665119158155369816e-01 1.690000057220458984e+00 +1.677499226245744435e-01 1.695000052452087402e+00 +1.696069328381306085e-01 1.700000047683715820e+00 +1.702259362426493394e-01 1.705000042915344238e+00 +1.711544413494274219e-01 1.710000038146972656e+00 +1.727019498607242354e-01 1.715000033378601074e+00 +1.736304549675023179e-01 1.720000028610229492e+00 +1.751779634787991313e-01 1.725000023841857910e+00 +1.757969668833178623e-01 1.730000019073486328e+00 +1.773444753946146757e-01 1.735000014305114746e+00 +1.788919839059114891e-01 1.740000009536743164e+00 +1.795109873104302201e-01 1.745000004768371582e+00 +1.810584958217270057e-01 1.750000000000000000e+00 +1.816774992262457367e-01 1.754999995231628418e+00 +1.835345094398019294e-01 1.759999990463256836e+00 +1.844630145465800120e-01 1.764999985694885254e+00 +1.863200247601361770e-01 1.769999980926513672e+00 +1.866295264623955563e-01 1.774999976158142090e+00 +1.881770349736923420e-01 1.779999971389770508e+00 +1.900340451872485348e-01 1.784999966621398926e+00 +1.909625502940266173e-01 1.789999961853027344e+00 +1.922005571030640791e-01 1.794999957084655762e+00 +1.934385639121015132e-01 1.799999952316284180e+00 +1.943670690188795958e-01 1.804999947547912598e+00 +1.962240792324357885e-01 1.809999942779541016e+00 +1.971525843392138710e-01 1.815000057220458984e+00 +1.983905911482513051e-01 1.820000052452087402e+00 +1.996285979572887670e-01 1.825000047683715820e+00 +2.005571030640668495e-01 1.830000042915344238e+00 +2.021046115753636629e-01 1.835000038146972656e+00 +2.036521200866604764e-01 1.840000033378601074e+00 +2.045806251934385589e-01 1.845000028610229492e+00 +2.055091303002166414e-01 1.850000023841857910e+00 +2.073661405137728342e-01 1.855000019073486328e+00 +2.086041473228102683e-01 1.860000014305114746e+00 +2.098421541318477301e-01 1.865000009536743164e+00 +2.113896626431445436e-01 1.870000004768371582e+00 +2.123181677499226261e-01 1.875000000000000000e+00 +2.138656762612194395e-01 1.879999995231628418e+00 +2.151036830702568736e-01 1.884999990463256836e+00 +2.166511915815536871e-01 1.889999985694885254e+00 +2.178891983905911489e-01 1.894999980926513672e+00 +2.191272051996286108e-01 1.899999976158142090e+00 +2.206747137109253964e-01 1.904999971389770508e+00 +2.216032188177035067e-01 1.909999966621398926e+00 +2.234602290312596717e-01 1.914999961853027344e+00 +2.246982358402971336e-01 1.919999957084655762e+00 +2.259362426493345677e-01 1.924999952316284180e+00 +2.274837511606313811e-01 1.929999947547912598e+00 +2.284122562674094636e-01 1.934999942779541016e+00 +2.299597647787062771e-01 1.940000057220458984e+00 +2.308882698854843596e-01 1.945000052452087402e+00 +2.324357783967811730e-01 1.950000047683715820e+00 +2.339832869080779865e-01 1.955000042915344238e+00 +2.352212937171154483e-01 1.960000038146972656e+00 +2.364593005261528824e-01 1.965000033378601074e+00 +2.380068090374496959e-01 1.970000028610229492e+00 +2.395543175487465093e-01 1.975000023841857910e+00 +2.407923243577839711e-01 1.980000019073486328e+00 +2.426493345713401362e-01 1.985000014305114746e+00 +2.432683379758588671e-01 1.990000009536743164e+00 +2.451253481894150321e-01 1.995000004768371582e+00 +2.460538532961931424e-01 2.000000000000000000e+00 +2.482203652120086590e-01 2.005000114440917969e+00 +2.491488703187867415e-01 2.009999990463256836e+00 +2.506963788300835549e-01 2.015000104904174805e+00 +2.513153822346023136e-01 2.019999980926513672e+00 +2.531723924481584787e-01 2.025000095367431641e+00 +2.544103992571959405e-01 2.029999971389770508e+00 +2.553389043639739953e-01 2.035000085830688477e+00 +2.568864128752708087e-01 2.039999961853027344e+00 +2.587434230888269737e-01 2.045000076293945312e+00 +2.602909316001237872e-01 2.049999952316284180e+00 +2.612194367069018974e-01 2.055000066757202148e+00 +2.633859486227174140e-01 2.059999942779541016e+00 +2.643144537294955243e-01 2.065000057220458984e+00 +2.661714639430516893e-01 2.069999933242797852e+00 +2.667904673475703925e-01 2.075000047683715820e+00 +2.689569792633859646e-01 2.079999923706054688e+00 +2.701949860724233710e-01 2.085000038146972656e+00 +2.717424945837201844e-01 2.089999914169311523e+00 +2.723614979882389431e-01 2.095000028610229492e+00 +2.748375116063138113e-01 2.099999904632568359e+00 +2.763850201176106247e-01 2.105000019073486328e+00 +2.776230269266480866e-01 2.109999895095825195e+00 +2.788610337356855484e-01 2.115000009536743164e+00 +2.800990405447230103e-01 2.119999885559082031e+00 +2.816465490560198237e-01 2.125000000000000000e+00 +2.831940575673166371e-01 2.130000114440917969e+00 +2.847415660786134506e-01 2.134999990463256836e+00 +2.859795728876508569e-01 2.140000104904174805e+00 +2.872175796966883188e-01 2.144999980926513672e+00 +2.887650882079851322e-01 2.150000095367431641e+00 +2.909316001238007043e-01 2.154999971389770508e+00 +2.921696069328381107e-01 2.160000085830688477e+00 +2.934076137418755725e-01 2.164999961853027344e+00 +2.952646239554317376e-01 2.170000076293945312e+00 +2.961931290622098478e-01 2.174999952316284180e+00 +2.983596409780253644e-01 2.180000066757202148e+00 +2.992881460848034747e-01 2.184999942779541016e+00 +3.011451562983596397e-01 2.190000057220458984e+00 +3.026926648096564532e-01 2.194999933242797852e+00 +3.036211699164345634e-01 2.200000047683715820e+00 +3.051686784277313769e-01 2.204999923706054688e+00 +3.067161869390281903e-01 2.210000038146972656e+00 +3.085731971525843553e-01 2.214999914169311523e+00 +3.098112039616217617e-01 2.220000028610229492e+00 +3.113587124729185751e-01 2.224999904632568359e+00 +3.129062209842153885e-01 2.230000019073486328e+00 +3.150727329000309607e-01 2.234999895095825195e+00 +3.160012380068090154e-01 2.240000009536743164e+00 +3.178582482203652360e-01 2.244999885559082031e+00 +3.187867533271432907e-01 2.250000000000000000e+00 +3.209532652429588628e-01 2.255000114440917969e+00 +3.218817703497369176e-01 2.259999990463256836e+00 +3.240482822655524342e-01 2.265000104904174805e+00 +3.249767873723305445e-01 2.269999980926513672e+00 +3.268337975858867095e-01 2.275000095367431641e+00 +3.280718043949241713e-01 2.279999971389770508e+00 +3.296193129062209848e-01 2.285000085830688477e+00 +3.314763231197771498e-01 2.289999961853027344e+00 +3.330238316310739632e-01 2.295000076293945312e+00 +3.345713401423707767e-01 2.299999952316284180e+00 +3.358093469514082385e-01 2.305000066757202148e+00 +3.373568554627050520e-01 2.309999942779541016e+00 +3.395233673785205686e-01 2.315000057220458984e+00 +3.404518724852986788e-01 2.319999933242797852e+00 +3.423088826988548439e-01 2.325000047683715820e+00 +3.435468895078923057e-01 2.329999923706054688e+00 +3.447848963169297676e-01 2.335000038146972656e+00 +3.466419065304859326e-01 2.339999914169311523e+00 +3.484989167440420976e-01 2.345000028610229492e+00 +3.494274218508201524e-01 2.349999904632568359e+00 +3.509749303621169658e-01 2.355000019073486328e+00 +3.522129371711544277e-01 2.359999895095825195e+00 +3.546889507892293514e-01 2.365000009536743164e+00 +3.556174558960074061e-01 2.369999885559082031e+00 +3.574744661095636267e-01 2.375000000000000000e+00 +3.584029712163416814e-01 2.380000114440917969e+00 +3.605694831321572535e-01 2.384999990463256836e+00 +3.611884865366759567e-01 2.390000104904174805e+00 +3.639740018570102320e-01 2.394999980926513672e+00 +3.645930052615289352e-01 2.400000095367431641e+00 +3.670690188796038589e-01 2.404999971389770508e+00 +3.679975239863819136e-01 2.410000085830688477e+00 +3.698545341999380787e-01 2.414999961853027344e+00 +3.717115444134942992e-01 2.420000076293945312e+00 +3.735685546270504642e-01 2.424999952316284180e+00 +3.744970597338285190e-01 2.430000066757202148e+00 +3.760445682451253324e-01 2.434999942779541016e+00 +3.779015784586814974e-01 2.440000057220458984e+00 +3.782110801609409045e-01 2.444999933242797852e+00 +3.813060971835345314e-01 2.450000047683715820e+00 +3.822346022903125862e-01 2.454999923706054688e+00 +3.844011142061281583e-01 2.460000038146972656e+00 +3.847106159083875099e-01 2.464999914169311523e+00 +3.874961312287217852e-01 2.470000028610229492e+00 +3.884246363354998399e-01 2.474999904632568359e+00 +3.902816465490560049e-01 2.480000019073486328e+00 +3.921386567626121700e-01 2.484999895095825195e+00 +3.936861652739089834e-01 2.490000009536743164e+00 +3.952336737852057968e-01 2.494999885559082031e+00 +3.967811822965026103e-01 2.500000000000000000e+00 +3.992571959145775340e-01 2.505000114440917969e+00 +4.001857010213556443e-01 2.509999990463256836e+00 +4.020427112349118093e-01 2.515000104904174805e+00 +4.038997214484679743e-01 2.519999980926513672e+00 +4.054472299597647877e-01 2.525000095367431641e+00 +4.063757350665428425e-01 2.529999971389770508e+00 +4.085422469823584146e-01 2.535000085830688477e+00 +4.100897554936552281e-01 2.539999961853027344e+00 +4.119467657072113931e-01 2.545000076293945312e+00 +4.131847725162488549e-01 2.549999952316284180e+00 +4.147322810275456684e-01 2.555000066757202148e+00 +4.162797895388424818e-01 2.559999942779541016e+00 +4.175177963478798882e-01 2.565000057220458984e+00 +4.196843082636954603e-01 2.569999933242797852e+00 +4.209223150727329221e-01 2.575000047683715820e+00 +4.227793252862890871e-01 2.579999923706054688e+00 +4.240173320953265490e-01 2.585000038146972656e+00 +4.255648406066233624e-01 2.589999914169311523e+00 +4.277313525224388790e-01 2.595000028610229492e+00 +4.289693593314763409e-01 2.599999904632568359e+00 +4.311358712472918575e-01 2.605000019073486328e+00 +4.320643763540699678e-01 2.609999895095825195e+00 +4.342308882698854844e-01 2.615000009536743164e+00 +4.360878984834416494e-01 2.619999885559082031e+00 +4.370164035902197597e-01 2.625000000000000000e+00 +4.391829155060352763e-01 2.630000114440917969e+00 +4.404209223150727381e-01 2.634999990463256836e+00 +4.413494274218507929e-01 2.640000104904174805e+00 +4.441349427421850682e-01 2.644999980926513672e+00 +4.450634478489631785e-01 2.650000095367431641e+00 +4.466109563602599919e-01 2.654999971389770508e+00 +4.484679665738161569e-01 2.660000085830688477e+00 +4.503249767873723219e-01 2.664999961853027344e+00 +4.521819870009284870e-01 2.670000076293945312e+00 +4.537294955122253004e-01 2.674999952316284180e+00 +4.549675023212627623e-01 2.680000066757202148e+00 +4.568245125348189273e-01 2.684999942779541016e+00 +4.583720210461157407e-01 2.690000057220458984e+00 +4.602290312596719057e-01 2.694999933242797852e+00 +4.617765397709687192e-01 2.700000047683715820e+00 +4.636335499845249397e-01 2.704999923706054688e+00 +4.648715567935623461e-01 2.710000038146972656e+00 +4.673475704116372698e-01 2.714999914169311523e+00 +4.685855772206747316e-01 2.720000028610229492e+00 +4.704425874342308966e-01 2.724999904632568359e+00 +4.713710925410089514e-01 2.730000019073486328e+00 +4.735376044568245235e-01 2.734999895095825195e+00 +4.747756112658619854e-01 2.740000009536743164e+00 +4.769421231816775020e-01 2.744999885559082031e+00 +4.781801299907149638e-01 2.750000000000000000e+00 +4.800371402042711289e-01 2.755000114440917969e+00 +4.809656453110491836e-01 2.759999990463256836e+00 +4.834416589291241073e-01 2.765000104904174805e+00 +4.846796657381615692e-01 2.769999980926513672e+00 +4.865366759517177342e-01 2.775000095367431641e+00 +4.877746827607551960e-01 2.779999971389770508e+00 +4.896316929743113611e-01 2.785000085830688477e+00 +4.905601980810894713e-01 2.789999961853027344e+00 +4.930362116991643395e-01 2.795000076293945312e+00 +4.942742185082018014e-01 2.799999952316284180e+00 +4.964407304240173180e-01 2.805000066757202148e+00 +4.979882389353141314e-01 2.809999942779541016e+00 +4.998452491488702965e-01 2.815000057220458984e+00 +5.010832559579078138e-01 2.819999933242797852e+00 +5.026307644692046273e-01 2.825000047683715820e+00 +5.038687712782420336e-01 2.829999923706054688e+00 +5.057257814917982541e-01 2.835000038146972656e+00 +5.078922934076137707e-01 2.839999914169311523e+00 +5.097493036211698803e-01 2.845000028610229492e+00 +5.103683070256886944e-01 2.849999904632568359e+00 +5.125348189415042111e-01 2.855000019073486328e+00 +5.140823274528010245e-01 2.859999895095825195e+00 +5.153203342618384308e-01 2.865000009536743164e+00 +5.171773444753946514e-01 2.869999885559082031e+00 +5.190343546889507609e-01 2.875000000000000000e+00 +5.205818632002475743e-01 2.880000114440917969e+00 +5.218198700092850917e-01 2.884999990463256836e+00 +5.239863819251006083e-01 2.890000104904174805e+00 +5.252243887341380146e-01 2.894999980926513672e+00 +5.270813989476942352e-01 2.900000095367431641e+00 +5.286289074589910486e-01 2.904999971389770508e+00 +5.304859176725471581e-01 2.910000085830688477e+00 +5.320334261838439716e-01 2.914999961853027344e+00 +5.338904363974001921e-01 2.920000076293945312e+00 +5.348189415041783024e-01 2.924999952316284180e+00 +5.369854534199938190e-01 2.930000066757202148e+00 +5.385329619312906324e-01 2.934999942779541016e+00 +5.403899721448467419e-01 2.940000057220458984e+00 +5.416279789538842593e-01 2.944999933242797852e+00 +5.431754874651810727e-01 2.950000047683715820e+00 +5.450324976787371822e-01 2.954999923706054688e+00 +5.465800061900339957e-01 2.960000038146972656e+00 +5.481275147013308091e-01 2.964999914169311523e+00 +5.496750232126276225e-01 2.970000028610229492e+00 +5.512225317239244360e-01 2.974999904632568359e+00 +5.524605385329619534e-01 2.980000019073486328e+00 +5.549365521510368771e-01 2.984999895095825195e+00 +5.564840606623336905e-01 2.990000009536743164e+00 +5.580315691736305039e-01 2.994999885559082031e+00 +5.592695759826679103e-01 3.000000000000000000e+00 +5.614360878984834269e-01 3.005000114440917969e+00 +5.626740947075209442e-01 3.009999990463256836e+00 +5.642216032188177577e-01 3.015000104904174805e+00 +5.657691117301145711e-01 3.019999980926513672e+00 +5.676261219436706806e-01 3.025000095367431641e+00 +5.694831321572269012e-01 3.029999971389770508e+00 +5.704116372640049004e-01 3.035000085830688477e+00 +5.722686474775611210e-01 3.039999961853027344e+00 +5.738161559888579344e-01 3.045000076293945312e+00 +5.759826679046734510e-01 3.049999952316284180e+00 +5.769111730114515613e-01 3.055000066757202148e+00 +5.781491798204889676e-01 3.059999942779541016e+00 +5.806251934385638913e-01 3.065000057220458984e+00 +5.824822036521201118e-01 3.069999933242797852e+00 +5.837202104611575182e-01 3.075000047683715820e+00 +5.852677189724543316e-01 3.079999923706054688e+00 +5.858867223769730348e-01 3.085000038146972656e+00 +5.883627359950479585e-01 3.089999914169311523e+00 +5.899102445063447719e-01 3.095000028610229492e+00 +5.923862581244196956e-01 3.099999904632568359e+00 +5.939337666357165091e-01 3.105000019073486328e+00 +5.945527700402352123e-01 3.109999895095825195e+00 +5.976477870628288391e-01 3.115000009536743164e+00 +5.995047972763850597e-01 3.119999885559082031e+00 +6.007428040854224660e-01 3.125000000000000000e+00 +6.022903125967192794e-01 3.130000114440917969e+00 +6.038378211080160929e-01 3.134999990463256836e+00 +6.060043330238316095e-01 3.140000104904174805e+00 +6.069328381306097198e-01 3.144999980926513672e+00 +6.084803466419065332e-01 3.150000095367431641e+00 +6.100278551532033466e-01 3.154999971389770508e+00 +6.115753636645001601e-01 3.160000085830688477e+00 +6.128133704735375664e-01 3.164999961853027344e+00 +6.152893840916124901e-01 3.170000076293945312e+00 +6.159083874961311933e-01 3.174999952316284180e+00 +6.174558960074280067e-01 3.180000066757202148e+00 +6.193129062209842273e-01 3.184999942779541016e+00 +6.205509130300216336e-01 3.190000057220458984e+00 +6.224079232435778541e-01 3.194999933242797852e+00 +6.242649334571339637e-01 3.200000047683715820e+00 +6.251934385639120739e-01 3.204999923706054688e+00 +6.267409470752088874e-01 3.210000038146972656e+00 +6.292169606932838111e-01 3.214999914169311523e+00 +6.304549675023212174e-01 3.220000028610229492e+00 +6.316929743113587348e-01 3.224999904632568359e+00 +6.329309811203961411e-01 3.230000019073486328e+00 +6.347879913339523617e-01 3.234999895095825195e+00 +6.369545032497678783e-01 3.240000009536743164e+00 +6.378830083565459885e-01 3.244999885559082031e+00 +6.397400185701020980e-01 3.250000000000000000e+00 +6.412875270813989115e-01 3.255000114440917969e+00 +6.425255338904364288e-01 3.259999990463256836e+00 +6.437635406994738352e-01 3.265000104904174805e+00 +6.459300526152893518e-01 3.269999980926513672e+00 +6.471680594243268692e-01 3.275000095367431641e+00 +6.487155679356236826e-01 3.279999971389770508e+00 +6.502630764469204960e-01 3.285000085830688477e+00 +6.515010832559579024e-01 3.289999961853027344e+00 +6.536675951717734190e-01 3.295000076293945312e+00 +6.552151036830702324e-01 3.299999952316284180e+00 +6.564531104921077498e-01 3.305000066757202148e+00 +6.576911173011451561e-01 3.309999942779541016e+00 +6.589291241101825625e-01 3.315000057220458984e+00 +6.614051377282574862e-01 3.319999933242797852e+00 +6.626431445372950035e-01 3.325000047683715820e+00 +6.648096564531105201e-01 3.329999923706054688e+00 +6.651191581553698162e-01 3.335000038146972656e+00 +6.666666666666666297e-01 3.339999914169311523e+00 +6.688331785824822573e-01 3.345000028610229492e+00 +6.706901887960383668e-01 3.349999904632568359e+00 +6.716186939028164771e-01 3.355000019073486328e+00 +6.737852058186319937e-01 3.359999895095825195e+00 +6.744042092231506969e-01 3.365000009536743164e+00 +6.768802228412256206e-01 3.369999885559082031e+00 +6.781182296502630269e-01 3.375000000000000000e+00 +6.790467347570411372e-01 3.380000114440917969e+00 +6.805942432683379506e-01 3.384999990463256836e+00 +6.821417517796347640e-01 3.390000104904174805e+00 +6.833797585886722814e-01 3.394999980926513672e+00 +6.855462705044877980e-01 3.400000095367431641e+00 +6.867842773135252044e-01 3.404999971389770508e+00 +6.886412875270814249e-01 3.410000085830688477e+00 +6.895697926338595352e-01 3.414999961853027344e+00 +6.914268028474156447e-01 3.420000076293945312e+00 +6.932838130609718652e-01 3.424999952316284180e+00 +6.945218198700092715e-01 3.430000066757202148e+00 +6.960693283813060850e-01 3.434999942779541016e+00 +6.973073351903436023e-01 3.440000057220458984e+00 +6.985453419993810087e-01 3.444999933242797852e+00 +7.004023522129371182e-01 3.450000047683715820e+00 +7.022593624264933387e-01 3.454999923706054688e+00 +7.031878675332714490e-01 3.460000038146972656e+00 +7.047353760445682624e-01 3.464999914169311523e+00 +7.065923862581243720e-01 3.470000028610229492e+00 +7.072113896626431861e-01 3.474999904632568359e+00 +7.093779015784587028e-01 3.480000019073486328e+00 +7.112349117920148123e-01 3.484999895095825195e+00 +7.124729186010523296e-01 3.490000009536743164e+00 +7.137109254100897360e-01 3.494999885559082031e+00 +7.152584339213865494e-01 3.500000000000000000e+00 +7.174249458372020660e-01 3.505000114440917969e+00 +7.183534509439801763e-01 3.509999990463256836e+00 +7.195914577530176937e-01 3.515000104904174805e+00 +7.214484679665738032e-01 3.519999980926513672e+00 +7.229959764778706166e-01 3.525000095367431641e+00 +7.242339832869080229e-01 3.529999971389770508e+00 +7.260909935004642435e-01 3.535000085830688477e+00 +7.273290003095016498e-01 3.539999961853027344e+00 +7.285670071185391672e-01 3.545000076293945312e+00 +7.301145156298359806e-01 3.549999952316284180e+00 +7.319715258433920901e-01 3.555000066757202148e+00 +7.332095326524296075e-01 3.559999942779541016e+00 +7.347570411637264209e-01 3.565000057220458984e+00 +7.359950479727638273e-01 3.569999933242797852e+00 +7.369235530795419375e-01 3.575000047683715820e+00 +7.390900649953574542e-01 3.579999923706054688e+00 +7.400185701021355644e-01 3.585000038146972656e+00 +7.418755803156917850e-01 3.589999914169311523e+00 +7.428040854224697842e-01 3.595000028610229492e+00 +7.446610956360260047e-01 3.599999904632568359e+00 +7.458991024450634111e-01 3.605000019073486328e+00 +7.474466109563602245e-01 3.609999895095825195e+00 +7.483751160631383348e-01 3.615000009536743164e+00 +7.505416279789538514e-01 3.619999885559082031e+00 +7.517796347879913688e-01 3.625000000000000000e+00 +7.530176415970287751e-01 3.630000114440917969e+00 +7.539461467038068854e-01 3.634999990463256836e+00 +7.551841535128442917e-01 3.640000104904174805e+00 +7.561126586196224020e-01 3.644999980926513672e+00 +7.582791705354379186e-01 3.650000095367431641e+00 +7.595171773444754360e-01 3.654999971389770508e+00 +7.607551841535128423e-01 3.660000085830688477e+00 +7.616836892602909526e-01 3.664999961853027344e+00 +7.632311977715877660e-01 3.670000076293945312e+00 +7.647787062828845794e-01 3.674999952316284180e+00 +7.660167130919219858e-01 3.680000066757202148e+00 +7.681832250077375024e-01 3.684999942779541016e+00 +7.691117301145156127e-01 3.690000057220458984e+00 +7.706592386258124261e-01 3.694999933242797852e+00 +7.715877437325905364e-01 3.700000047683715820e+00 +7.737542556484060530e-01 3.704999923706054688e+00 +7.749922624574435703e-01 3.710000038146972656e+00 +7.759207675642215696e-01 3.714999914169311523e+00 +7.777777777777777901e-01 3.720000028610229492e+00 +7.790157845868151965e-01 3.724999904632568359e+00 +7.802537913958527138e-01 3.730000019073486328e+00 +7.811822965026307131e-01 3.734999895095825195e+00 +7.830393067161869336e-01 3.740000009536743164e+00 +7.842773135252243399e-01 3.744999885559082031e+00 +7.855153203342618573e-01 3.750000000000000000e+00 +7.861343237387805605e-01 3.755000114440917969e+00 +7.873723305478179668e-01 3.759999990463256836e+00 +7.892293407613741874e-01 3.765000104904174805e+00 +7.907768492726710008e-01 3.769999980926513672e+00 +7.917053543794491111e-01 3.775000095367431641e+00 +7.929433611884865174e-01 3.779999971389770508e+00 +7.944908696997833308e-01 3.785000085830688477e+00 +7.954193748065614411e-01 3.789999961853027344e+00 +7.963478799133395514e-01 3.795000076293945312e+00 +7.972763850201176616e-01 3.799999952316284180e+00 +8.000619003404518814e-01 3.805000066757202148e+00 +8.006809037449705846e-01 3.809999942779541016e+00 +8.019189105540081020e-01 3.815000057220458984e+00 +8.031569173630455083e-01 3.819999933242797852e+00 +8.043949241720829146e-01 3.825000047683715820e+00 +8.065614360878984312e-01 3.829999923706054688e+00 +8.068709377901578383e-01 3.835000038146972656e+00 +8.077994428969359486e-01 3.839999914169311523e+00 +8.087279480037140589e-01 3.845000028610229492e+00 +8.108944599195295755e-01 3.849999904632568359e+00 +8.115134633240482787e-01 3.855000019073486328e+00 +8.127514701330856850e-01 3.859999895095825195e+00 +8.133704735376044992e-01 3.865000009536743164e+00 +8.155369854534200158e-01 3.869999885559082031e+00 +8.164654905601981261e-01 3.875000000000000000e+00 +8.173939956669761253e-01 3.880000114440917969e+00 +8.192510058805323458e-01 3.884999990463256836e+00 +8.201795109873104561e-01 3.890000104904174805e+00 +8.214175177963478625e-01 3.894999980926513672e+00 +8.229650263076446759e-01 3.900000095367431641e+00 +8.238935314144227862e-01 3.904999971389770508e+00 +8.251315382234601925e-01 3.910000085830688477e+00 +8.257505416279790067e-01 3.914999961853027344e+00 +8.276075518415351162e-01 3.920000076293945312e+00 +8.282265552460538194e-01 3.924999952316284180e+00 +8.300835654596100399e-01 3.930000066757202148e+00 +8.310120705663881502e-01 3.934999942779541016e+00 +8.322500773754255565e-01 3.940000057220458984e+00 +8.331785824822036668e-01 3.944999933242797852e+00 +8.344165892912410731e-01 3.950000047683715820e+00 +8.353450943980191834e-01 3.954999923706054688e+00 +8.365831012070565897e-01 3.960000038146972656e+00 +8.381306097183534032e-01 3.964999914169311523e+00 +8.390591148251315134e-01 3.970000028610229492e+00 +8.406066233364283269e-01 3.974999904632568359e+00 +8.421541318477251403e-01 3.980000019073486328e+00 +8.427731352522438435e-01 3.984999895095825195e+00 +8.440111420612813609e-01 3.990000009536743164e+00 +8.455586505725781743e-01 3.994999885559082031e+00 +8.467966573816155806e-01 4.000000000000000000e+00 +8.474156607861342838e-01 4.005000114440917969e+00 +8.486536675951718012e-01 4.010000228881835938e+00 +8.498916744042092075e-01 4.014999866485595703e+00 +8.505106778087279107e-01 4.019999980926513672e+00 +8.514391829155060210e-01 4.025000095367431641e+00 +8.526771897245435383e-01 4.030000209808349609e+00 +8.542246982358403518e-01 4.034999847412109375e+00 +8.548437016403590549e-01 4.039999961853027344e+00 +8.560817084493964613e-01 4.045000076293945312e+00 +8.567007118539151644e-01 4.050000190734863281e+00 +8.579387186629526818e-01 4.054999828338623047e+00 +8.585577220674713850e-01 4.059999942779541016e+00 +8.601052305787681984e-01 4.065000057220458984e+00 +8.607242339832869016e-01 4.070000171661376953e+00 +8.622717424945837150e-01 4.074999809265136719e+00 +8.635097493036211214e-01 4.079999923706054688e+00 +8.650572578149179348e-01 4.085000038146972656e+00 +8.650572578149179348e-01 4.090000152587890625e+00 +8.672237697307335624e-01 4.094999790191650391e+00 +8.681522748375115617e-01 4.099999904632568359e+00 +8.690807799442896719e-01 4.105000019073486328e+00 +8.706282884555864854e-01 4.110000133514404297e+00 +8.712472918601051886e-01 4.114999771118164062e+00 +8.727948003714020020e-01 4.119999885559082031e+00 +8.734138037759208162e-01 4.125000000000000000e+00 +8.743423088826988154e-01 4.130000114440917969e+00 +8.755803156917363328e-01 4.135000228881835938e+00 +8.765088207985144431e-01 4.139999866485595703e+00 +8.771278242030331462e-01 4.144999980926513672e+00 +8.777468276075518494e-01 4.150000095367431641e+00 +8.796038378211080699e-01 4.155000209808349609e+00 +8.802228412256267731e-01 4.159999847412109375e+00 +8.814608480346641795e-01 4.164999961853027344e+00 +8.823893531414422897e-01 4.170000076293945312e+00 +8.833178582482204000e-01 4.175000190734863281e+00 +8.842463633549983992e-01 4.179999828338623047e+00 +8.848653667595172134e-01 4.184999942779541016e+00 +8.864128752708140269e-01 4.190000057220458984e+00 +8.876508820798514332e-01 4.195000171661376953e+00 +8.885793871866295435e-01 4.199999809265136719e+00 +8.891983905911482466e-01 4.204999923706054688e+00 +8.901268956979263569e-01 4.210000038146972656e+00 +8.907458991024450601e-01 4.215000152587890625e+00 +8.919839059114824664e-01 4.219999790191650391e+00 +8.929124110182605767e-01 4.224999904632568359e+00 +8.938409161250386870e-01 4.230000019073486328e+00 +8.950789229340760933e-01 4.235000133514404297e+00 +8.956979263385949075e-01 4.239999771118164062e+00 +8.969359331476323138e-01 4.244999885559082031e+00 +8.975549365521510170e-01 4.250000000000000000e+00 +8.987929433611885344e-01 4.255000114440917969e+00 +8.991024450634478304e-01 4.260000228881835938e+00 +9.000309501702259407e-01 4.264999866485595703e+00 +9.012689569792633471e-01 4.269999980926513672e+00 +9.025069637883008644e-01 4.275000095367431641e+00 +9.034354688950789747e-01 4.280000209808349609e+00 +9.046734757041163810e-01 4.284999847412109375e+00 +9.056019808108944913e-01 4.289999961853027344e+00 +9.062209842154131945e-01 4.295000076293945312e+00 +9.068399876199318976e-01 4.300000190734863281e+00 +9.077684927267100079e-01 4.304999828338623047e+00 +9.083874961312287111e-01 4.309999942779541016e+00 +9.099350046425255245e-01 4.315000057220458984e+00 +9.108635097493036348e-01 4.320000171661376953e+00 +9.121015165583410411e-01 4.324999809265136719e+00 +9.124110182606004482e-01 4.329999923706054688e+00 +9.133395233673785585e-01 4.335000038146972656e+00 +9.142680284741565577e-01 4.340000152587890625e+00 +9.151965335809346680e-01 4.344999790191650391e+00 +9.158155369854533712e-01 4.349999904632568359e+00 +9.167440420922314814e-01 4.355000019073486328e+00 +9.173630454967501846e-01 4.360000133514404297e+00 +9.189105540080469980e-01 4.364999771118164062e+00 +9.195295574125658122e-01 4.369999885559082031e+00 +9.204580625193438115e-01 4.375000000000000000e+00 +9.216960693283813288e-01 4.380000114440917969e+00 +9.223150727329000320e-01 4.385000228881835938e+00 +9.232435778396781423e-01 4.389999866485595703e+00 +9.238625812441968455e-01 4.394999980926513672e+00 +9.244815846487155486e-01 4.400000095367431641e+00 +9.251005880532342518e-01 4.405000209808349609e+00 +9.257195914577530660e-01 4.409999847412109375e+00 +9.269575982667904723e-01 4.414999961853027344e+00 +9.278861033735685826e-01 4.420000076293945312e+00 +9.285051067780872858e-01 4.425000190734863281e+00 +9.291241101826059889e-01 4.429999828338623047e+00 +9.306716186939028024e-01 4.434999942779541016e+00 +9.309811203961622095e-01 4.440000057220458984e+00 +9.322191272051996158e-01 4.445000171661376953e+00 +9.331476323119777261e-01 4.449999809265136719e+00 +9.334571340142371332e-01 4.454999923706054688e+00 +9.340761374187558364e-01 4.460000038146972656e+00 +9.356236459300526498e-01 4.465000152587890625e+00 +9.359331476323119459e-01 4.469999790191650391e+00 +9.368616527390900561e-01 4.474999904632568359e+00 +9.380996595481274625e-01 4.480000019073486328e+00 +9.384091612503868696e-01 4.485000133514404297e+00 +9.387186629526462767e-01 4.489999771118164062e+00 +9.396471680594242759e-01 4.494999885559082031e+00 +9.405756731662023862e-01 4.500000000000000000e+00 +9.408851748684617933e-01 4.505000114440917969e+00 +9.418136799752399035e-01 4.510000228881835938e+00 +9.433611884865367170e-01 4.514999866485595703e+00 +9.442896935933147162e-01 4.519999980926513672e+00 +9.449086969978335304e-01 4.525000095367431641e+00 +9.455277004023522336e-01 4.530000209808349609e+00 +9.464562055091303439e-01 4.534999847412109375e+00 +9.467657072113896399e-01 4.539999961853027344e+00 +9.480037140204271573e-01 4.545000076293945312e+00 +9.483132157226864534e-01 4.550000190734863281e+00 +9.489322191272051565e-01 4.554999828338623047e+00 +9.495512225317239707e-01 4.559999942779541016e+00 +9.501702259362426739e-01 4.565000057220458984e+00 +9.514082327452800802e-01 4.570000171661376953e+00 +9.517177344475394873e-01 4.574999809265136719e+00 +9.526462395543175976e-01 4.579999923706054688e+00 +9.529557412565768937e-01 4.585000038146972656e+00 +9.538842463633550040e-01 4.590000152587890625e+00 +9.541937480656144110e-01 4.594999790191650391e+00 +9.557412565769112245e-01 4.599999904632568359e+00 +9.560507582791705206e-01 4.605000019073486328e+00 +9.572887650882080379e-01 4.610000133514404297e+00 +9.572887650882080379e-01 4.614999771118164062e+00 +9.588362735995048514e-01 4.619999885559082031e+00 +9.588362735995048514e-01 4.625000000000000000e+00 +9.597647787062828506e-01 4.630000114440917969e+00 +9.600742804085422577e-01 4.635000228881835938e+00 +9.613122872175796640e-01 4.639999866485595703e+00 +9.616217889198390711e-01 4.644999980926513672e+00 +9.619312906220983672e-01 4.650000095367431641e+00 +9.628597957288764775e-01 4.655000209808349609e+00 +9.631692974311358846e-01 4.659999847412109375e+00 +9.644073042401732909e-01 4.664999961853027344e+00 +9.647168059424326980e-01 4.670000076293945312e+00 +9.656453110492108083e-01 4.675000190734863281e+00 +9.656453110492108083e-01 4.679999828338623047e+00 +9.671928195605076217e-01 4.684999942779541016e+00 +9.671928195605076217e-01 4.690000057220458984e+00 +9.681213246672856210e-01 4.695000171661376953e+00 +9.687403280718044352e-01 4.699999809265136719e+00 +9.696688331785824344e-01 4.704999923706054688e+00 +9.699783348808418415e-01 4.710000038146972656e+00 +9.709068399876199518e-01 4.715000152587890625e+00 +9.718353450943980620e-01 4.719999790191650391e+00 +9.724543484989167652e-01 4.724999904632568359e+00 +9.727638502011760613e-01 4.730000019073486328e+00 +9.733828536056948755e-01 4.735000133514404297e+00 +9.733828536056948755e-01 4.739999771118164062e+00 +9.752398638192509850e-01 4.744999885559082031e+00 +9.752398638192509850e-01 4.750000000000000000e+00 +9.758588672237696882e-01 4.755000114440917969e+00 +9.764778706282885024e-01 4.760000228881835938e+00 +9.770968740328072055e-01 4.764999866485595703e+00 +9.777158774373259087e-01 4.769999980926513672e+00 +9.780253791395853158e-01 4.775000095367431641e+00 +9.789538842463633150e-01 4.780000209808349609e+00 +9.789538842463633150e-01 4.784999847412109375e+00 +9.798823893531414253e-01 4.789999961853027344e+00 +9.805013927576601285e-01 4.795000076293945312e+00 +9.811203961621789427e-01 4.800000190734863281e+00 +9.814298978644382387e-01 4.804999828338623047e+00 +9.820489012689569419e-01 4.809999942779541016e+00 +9.832869080779944593e-01 4.815000057220458984e+00 +9.832869080779944593e-01 4.820000171661376953e+00 +9.839059114825131624e-01 4.824999809265136719e+00 +9.848344165892912727e-01 4.829999923706054688e+00 +9.851439182915505688e-01 4.835000038146972656e+00 +9.857629216960693830e-01 4.840000152587890625e+00 +9.863819251005880862e-01 4.844999790191650391e+00 +9.870009285051067893e-01 4.849999904632568359e+00 +9.870009285051067893e-01 4.855000019073486328e+00 +9.876199319096254925e-01 4.860000133514404297e+00 +9.879294336118848996e-01 4.864999771118164062e+00 +9.879294336118848996e-01 4.869999885559082031e+00 +9.885484370164036028e-01 4.875000000000000000e+00 +9.891674404209223059e-01 4.880000114440917969e+00 +9.900959455277004162e-01 4.885000228881835938e+00 +9.904054472299597123e-01 4.889999866485595703e+00 +9.907149489322191194e-01 4.894999980926513672e+00 +9.907149489322191194e-01 4.900000095367431641e+00 +9.919529557412565257e-01 4.905000209808349609e+00 +9.925719591457753399e-01 4.909999847412109375e+00 +9.928814608480346360e-01 4.914999961853027344e+00 +9.931909625502940431e-01 4.920000076293945312e+00 +9.935004642525533392e-01 4.925000190734863281e+00 +9.941194676570721533e-01 4.929999828338623047e+00 +9.947384710615908565e-01 4.934999942779541016e+00 +9.950479727638501526e-01 4.940000057220458984e+00 +9.956669761683689668e-01 4.945000171661376953e+00 +9.962859795728876700e-01 4.949999809265136719e+00 +9.956669761683689668e-01 4.954999923706054688e+00 +9.965954812751469660e-01 4.960000038146972656e+00 +9.972144846796657802e-01 4.965000152587890625e+00 +9.978334880841844834e-01 4.969999790191650391e+00 +9.975239863819250763e-01 4.974999904632568359e+00 +9.984524914887031866e-01 4.980000019073486328e+00 +9.993809965954812968e-01 4.985000133514404297e+00 +9.993809965954812968e-01 4.989999771118164062e+00 +1.000000000000000000e+00 4.994999885559082031e+00 +1.000000000000000000e+00 5.000000000000000000e+00 diff --git a/wigner_time/__init__.py b/src/wigner/time/__init__.py similarity index 100% rename from wigner_time/__init__.py rename to src/wigner/time/__init__.py diff --git a/src/wigner/time/adwin/__init__.py b/src/wigner/time/adwin/__init__.py new file mode 100644 index 0000000..6261c6b --- /dev/null +++ b/src/wigner/time/adwin/__init__.py @@ -0,0 +1,16 @@ +import numpy as np + +CONTEXTS__SPECIAL = {"ADwin_LowInit": -2, "ADwin_Init": -1, "ADwin_Finish": 2**31 - 1} +"""Used for passing information to the ADwin controller""" + + +SCHEMA = { + "time": float, + "variable": str, + "value": float, + "context": str, + "module": int, + "channel": int, + "cycle": np.int32, + "value__digits": np.int32, +} diff --git a/src/wigner/time/adwin/connection.py b/src/wigner/time/adwin/connection.py new file mode 100644 index 0000000..7d95edc --- /dev/null +++ b/src/wigner/time/adwin/connection.py @@ -0,0 +1,73 @@ +# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) + + +""" +The choice made here is to model our ADwin connections as little more than tables of data (DataFrames). This allows for the least amount of coupling with other implementations an convenience of interaction with timelines etc. + +What follows are simply convenience functions to make the creation easier. +""" +from copy import deepcopy +import pandas as pd +import numpy as np + +from wigner.time.internal import dataframe as wt_frame +import wigner.time.variable as variable + +# ====================================================================== +_SCHEMA = {"variable": str, "module": int, "channel": int} +# ====================================================================== + + +def is_valid_name(timeline): + return timeline.variable.str.match(variable.REGEX).all() + + +def _ensure_valid_names(timeline): + if is_valid_name(timeline): + return timeline + else: + raise ValueError( + "Connection name is not valid. Connection names should follow the REGEX specified in the `variable` module." + ) + + +def new(*variable_module_channel) -> pd.DataFrame: + """ + Convenience for creating a table with 'variable', 'module' and 'channel' columns. + + 'variable's have the form 'context_equipment__unit' or 'context_equipment'. In the latter case, the 'variable' is taken to be digital (unitless). + + vmcs: + e.g. + "AOM_MOT__V", 1, 1 + or + ["shutter_MOT", 1, 11], + ["shutter_repump", 1, 12], + ["shutter_imaging", 1, 13], + """ + + try: + return _ensure_valid_names( + wt_frame.new_schema(np.atleast_2d(variable_module_channel), _SCHEMA) + ) + except: + raise ValueError("=== Input to 'connection' not well formatted ===") + + +def remove_unconnected_variables(timeline, connections): + """ + Purges the given timeline of any `variable`s that do not have a matching `connection`. + + NOTE: Assumes timeline and connections are both pd.DataFrame-like things + """ + timeline = deepcopy(timeline) + _disconnections = [ + v + for v in timeline["variable"].unique() + if v not in connections["variable"].unique() + ] + + for v in _disconnections: + timeline.drop(timeline[timeline.variable == v].index, inplace=True) + + return timeline diff --git a/src/wigner/time/adwin/core.py b/src/wigner/time/adwin/core.py new file mode 100644 index 0000000..996d714 --- /dev/null +++ b/src/wigner/time/adwin/core.py @@ -0,0 +1,114 @@ +# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) + +import funcy +import numpy as np +import importlib.util + +if not importlib.util.find_spec("ADwin"): + raise ImportError("Wigner Time's adwin modules require `ADwin` to be installed.") + +import ADwin + +from wigner.time import timeline as tl +import wigner.time.adwin as wt_adwin +from wigner.time.adwin import connection +from wigner.time.adwin import internal as ad + + +def link_device(DeviceNo=1, raiseExceptions=1, useNumpyArrays=0): + """ + A Wrapper around ADwin.ADwin. Returns a new (stateful) ADwin machine object that is digitally connected to a physical ADwin machine. + """ + return ADwin.ADwin( + DeviceNo=DeviceNo, + raiseExceptions=raiseExceptions, + useNumpyArrays=useNumpyArrays, + ) + + +def convert( + timeline, + connections, + devices, + machine_specifications=ad.SPECIFICATIONS__DEFAULT, + time_resolution=None, +) -> list[tuple]: + """ + Convenience for converting a Wigner timeline (DataFrame) to an ADbasic-compatible list of tuples. + + This takes an operation-layer timeline, adds the columns necessary for an ADwin conversion, based on the supplied or default specifications, and then converts the relevant columns according to `adwin.to_tuples`, i.e. [[(cycle, module, channel, value), ...], + [(cycle, module, channel, value), ...]]. + """ + + if time_resolution is not None: + resolution = time_resolution + else: + resolution = machine_specifications["cycle_period__normal__us"] + + return funcy.compose( + lambda tline: ad.to_tuples( + tline, + machine_specifications=machine_specifications, + ), + lambda tline: ad.add( + tline, connections, devices, machine_specifications=machine_specifications + ), + lambda tline: tl.expand( + tline, + time_resolution=resolution, + ), + lambda tline: connection.remove_unconnected_variables(tline, connections), + )(timeline) + + +def create( + timeline, + connections, + devices, + machine: ADwin.ADwin | None = None, + machine_specifications=ad.SPECIFICATIONS__DEFAULT, + time_resolution=None, +) -> ADwin.ADwin: + """ + For a given ADwin.ADwin machine object, combines the given timeline, connections and devices, converts the result to an ADwin-compatible format and initializes the machine for data collection. + + + NOTE: Stateful. + """ + # TODO: + # - Should we prepare all of the possible variables or does this waste memory? + + if machine is None: + machine = link_device() + + output = convert(timeline, connections, devices) + + cycles = np.array([np.array(output[i])[:, 0] for i in range(2)]).flatten() + # Finds the maximum cycle value, discounting special contexts + time_end__cycles = cycles[ + ~np.isin(cycles, list(wt_adwin.CONTEXTS__SPECIAL.values())) + ].max() + + # TODO: make this a log instead of a print statement + print( + "=== time_end: {}s ===".format( + time_end__cycles * machine_specifications["cycle_period__normal__us"] + ) + ) + + # TODO: What's happening below should be explained here + machine.Set_Par(1, int(time_end__cycles)) + machine.Set_Par(2, len(output[0])) + machine.Set_Par(3, len(output[1])) + + machine.SetData_Long([a[0] for a in output[0]], 10, 1, len(output[0])) + machine.SetData_Long([a[1] for a in output[0]], 11, 1, len(output[0])) + machine.SetData_Long([a[2] for a in output[0]], 12, 1, len(output[0])) + machine.SetData_Long([a[3] for a in output[0]], 13, 1, len(output[0])) + + machine.SetData_Long([d[0] for d in output[1]], 20, 1, len(output[1])) + machine.SetData_Long([d[1] for d in output[1]], 21, 1, len(output[1])) + machine.SetData_Long([d[2] for d in output[1]], 22, 1, len(output[1])) + machine.SetData_Long([d[3] for d in output[1]], 23, 1, len(output[1])) + + return machine diff --git a/src/wigner/time/adwin/display.py b/src/wigner/time/adwin/display.py new file mode 100644 index 0000000..20a2f9f --- /dev/null +++ b/src/wigner/time/adwin/display.py @@ -0,0 +1,230 @@ +# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) + +# ============================================================ +# Block module based on dependency +import importlib.util + +from wigner.time import adwin + +if not importlib.util.find_spec("matplotlib"): + raise ImportError("The `display` module requires `matplotlib` to be installed.") + +# ============================================================ +# Normal imports +# ============================================================ +from copy import deepcopy + +import matplotlib.axes as mpa +import matplotlib.pyplot as plt +import numpy as np +import wigner.time.variable as wt_variable +from wigner.time import timeline as tl +from wigner.time.internal.timeline import anchor +from wigner.time.internal import util as wt_util +from wigner.time.config import LABEL__ANCHOR +from wigner.time.internal import dataframe as wt_frame + +# ============================================================ + +SYMBOL_QUANTITY = { + "V": "Voltage", + "A": "Current", + "W": "Power", + "MHz": "Frequency", + "Hz": "Frequency", + "Ω": "Resistance", + "F": "Capacitance", + "H": "Inductance", + "J": "Energy", + "N": "Force", + "m": "Length", + "s": "Time", + "kg": "Mass", + "K": "Temperature", +} + + +def _draw_context(axis: mpa.Axes, info__context, alpha=0.1, cmap__context="magma"): + ys = axis.get_ylim() + y__center = np.mean(ys) + + theme_colors = plt.get_cmap(cmap__context).colors + for con, col in zip( + info__context.keys(), wt_util.sample(theme_colors, len(info__context)) + ): + times = info__context[con]["times"] + axis.axvspan(times[0], times[1], color=col, alpha=alpha) + + axis.text( + np.mean(times), + y__center, + con, + va="center", + ha="center", + color=col, + alpha=0.5, + ) + + return axis + + +def quantities( + timeline: wt_frame.CLASS, + variables=None, + do_context: bool = True, + do_show: bool = True, + symbol_quantities: dict = SYMBOL_QUANTITY, + cmap__context="magma", + range__x=None, +): + """ + Displays the given `timeline`, filtered by `variable`, in terms of different quantites, i.e. by common `unit`. The mapping between `unit` and 'quantity' can be provided as a dictionary. + + NOTE: Unit and quantity terminology taken from SI conventions. + """ + # TODO: + # - Separate style and content + # - offer filtering by `context` + if variables: + tline = wt_frame.subframe(timeline, "variable", variables) + else: + tline = timeline + tline.sort_values("time", inplace=True, ignore_index=True) + + # ===================================================================== + # ADwin + # ===================================================================== + # To make the special contexts (where there is no time) visible + if do_context: + info__context = tl.context_info(tline) + for label in adwin.CONTEXTS__SPECIAL: + if label in info__context.keys(): + d = deepcopy(info__context[label]["times"]) + if "Init" in label: + info__context[label]["times"] = [d[0], d[1] + 0.5] + if "Finish" in label: + info__context[label]["times"] = [d[0] - 0.5, d[1]] + # ===================================================================== + + if variables is None: + variables = tline["variable"].unique() + + if variables is None: + return None + + units = wt_variable.units(tline) + unit_variables__analog = { + u: [v for v in variables if wt_variable.unit(v) == u] + for u in units + if u not in ["digital", LABEL__ANCHOR] + } + variables__digital = [v for v in variables if wt_variable.unit(v) == "digital"] + + tline__anchors = tline[anchor.mask(tline)] + + prop_cycle = plt.rcParams["axes.prop_cycle"] + colors = prop_cycle.by_key()["color"] + + num_analog_panels = len(unit_variables__analog) + num_digital_panels = 1 if variables__digital else 0 + num_panels = num_analog_panels + num_digital_panels + + fig, axes = plt.subplots( + num_analog_panels + num_digital_panels, + sharex=True, + figsize=(7.5, 7.5), + height_ratios=[1] * num_analog_panels + num_digital_panels * [2], + ) + if num_panels == 1: + axes = [axes] + fig.tight_layout() + + if range__x is not None: + axes[0].set_xlim(*range__x) + # ===================================================================== + # ANALOGUE + # ===================================================================== + + analogLabels = [] + if num_analog_panels > 0: + axes__analogue = axes[:-num_digital_panels] if num_digital_panels != 0 else axes + for key, axis in zip(unit_variables__analog.keys(), axes__analogue): + if key in symbol_quantities: + axis.set_ylabel(f"{symbol_quantities[key]} [{key}]") + else: + axis.set_ylabel(f"[{key}]") + + for variable, color in zip(unit_variables__analog[key], colors): + array = tline[tline["variable"] == variable] + # axis.plot(array["time"], array["value"], marker="o", ms=3) + axis.step( + array["time"], + array["value"], + where="post", + # color=color, + marker="o", + ms=3, + ) + + analogLabels.append( + axis.text(0, array.iat[0, 2], variable, color=color) + ) + if do_context: + _draw_context(axis, info__context, cmap__context=cmap__context) + # + # ===================================================================== + # DIGITAL + # ===================================================================== + digitalLabels = [] + if num_digital_panels > 0: + divider = 1.5 * len(variables__digital) + axes[-1].set_ylabel("Digital") + + for variable, offset, color in zip( + variables__digital, range(len(list(variables__digital))), colors + ): + baseline = offset / divider + array = tline[tline["variable"] == variable] + axes[-1].axhline(baseline, color=color, linestyle=":", alpha=0.5) + axes[-1].axhline(baseline + 1, color=color, linestyle=":", alpha=0.5) + axes[-1].step( + array["time"], + array["value"] + baseline, + where="post", + color=color, + marker="o", + ms=3, + ) + digitalLabels.append( + axes[-1].text(0, baseline, variable + "_OFF", color=color) + ) + digitalLabels.append( + axes[-1].text(0, baseline + 1, variable + "_ON", color=color) + ) + axes[-1].set_yticks([i / divider for i in range(len(list(variables__digital)))]) + axes[-1].set_yticklabels([]) + if do_context: + _draw_context(axes[-1], info__context, cmap__context=cmap__context) + + for anchorTime in tline__anchors["time"]: + for axis in axes: + axis.axvline(anchorTime, color="0.5", linestyle="--") + + ax2 = axes[0].twiny() + ax2.set_xlim(axes[0].get_xlim()) + ax2.set_xticks(list(tline__anchors["time"])) # Set ticks at the specified x-values + ax2.set_xticklabels(list(tline__anchors["context"])) + + def _sync_axes(event): + xlim = axes[0].get_xlim() + ax2.set_xlim(xlim) + for label in analogLabels + digitalLabels: + label.set_position((0.9 * xlim[0] + 0.1 * xlim[1], label.get_position()[1])) + + # Connect the sync function to the 'xlim_changed' event + axes[0].callbacks.connect("xlim_changed", _sync_axes) + + if do_show: + plt.show() + + return fig, axes diff --git a/src/wigner/time/adwin/internal.py b/src/wigner/time/adwin/internal.py new file mode 100644 index 0000000..f91a640 --- /dev/null +++ b/src/wigner/time/adwin/internal.py @@ -0,0 +1,181 @@ +""" +For 'lower-level' manipulations of ADwin-specifc timeline informaton. + +In general, the user shouldn't need to use these functions and there is no guarantee that the API will not change. + +""" + +import numpy as np + +from wigner.time.config import wtlog as wtl +from wigner.time import timeline as tl +from wigner.time import conversion as conv +from wigner.time import device +from wigner.time import variable as wt_variable +from wigner.time.internal import dataframe as wt_frame +import wigner.time.adwin as wt_adwin +from wigner.time.adwin import connection +from wigner.time.adwin import validate as wt_validate + +""" +Represents the key ADwin settings for the given machine. + +These should be loaded by the ADwin system during initialization. The settings should grow as large as possible (to encompass all of the internal ADwin features) for maximum reproducibility. + +The specifications have the form of a list of 'ADwin device' dictionaries, with the modules represented as a list of dictionaries. +""" +SPECIFICATIONS__DEFAULT = { + "cycle_period__normal__us": 5e-6, + "modules": [ + { + "bits": 1, + "voltage_range": [0.0, 5.0], + "gain": 1, + }, + { + "bits": 16, + "voltage_range": [-10.0, 10.0], + "gain": 1, + }, + { + "bits": 16, + "voltage_range": [-10.0, 10.0], + "gain": 1, + }, + { + "bits": 16, + "voltage_range": [-10.0, 10.0], + "gain": 1, + }, + ], +} + + +def modules__digital(machine_specifications): + """ + The list of modules that govern digital connections. + + Currently, this just returns a static list, based on a specific lab setup. + + NOTE: Modules are numbered from 1 (unlike Python lists). + """ + + return [ + i + 1 + for i, m in enumerate(machine_specifications["modules"]) + if m.get("bits", False) == True + ] + + +def add_cycle( + timeline, + machine_specifications=SPECIFICATIONS__DEFAULT, + special_contexts=wt_adwin.CONTEXTS__SPECIAL, +): + """ + Inserts a new `cycle` column into the timeline as a conversion of the `time` column into 'number of cycles'. + + Parameters: + - timeline: DataFrame containing the experimental data. + - specifications: Dictionary with device-specific configuration, must contain cycle period. + - special_contexts: Dictionary with context-specific overrides for cycle values. + - device: Device name to use for cycle period in specifications. + + Raises: + - ValueError if required columns are missing or if cycle period is not found for specified device. + """ + # Check if `time` column is present + + if "time" not in timeline.columns: + raise ValueError( + f"`time` column not found. Columns present: {list(timeline.columns)}" + ) + + # Ensure device-specific cycle period is available + try: + cycle_period = machine_specifications["cycle_period__normal__us"] + except KeyError: + raise ValueError( + f"`cycle_period__normal` not found in specifications for {device}." + ) + + # Calculate cycles and handle special contexts + timeline["cycle"] = np.round(timeline["time"].values / cycle_period).astype( + np.int32 + ) + + # Apply special context cycles + timeline = wt_frame.replace_column__filtered( + timeline, + special_contexts, + column__change="cycle", + ) + + return timeline + + +def add(timeline, connections, devices, machine_specifications=SPECIFICATIONS__DEFAULT): + """ + Takes an 'operational' layer timeline and inserts ADwin-specific columns, e.g. cycles and numbers for the module and channel etc. + + Digital: module 1 + Analogue otherwise + """ + + wtl.debug("Got to `adwin.core.add`") + + dff = wt_frame.join(timeline, connections) + dff = wt_frame.join(dff, devices) + dff = dff.sort_values(by=["time"], ignore_index=True) + + dff = conv.add(dff) + # TODO: ^ This 'feels' inefficient/wrong? + + mask__digital = dff["module"].isin(modules__digital(machine_specifications)) + + dff.loc[mask__digital, "value__digits"] = round(dff["value"]) + # TODO: Shouldn't all of value__digits be rounded? + + device.check_within_range(dff) + dcycle = add_cycle(dff, machine_specifications) + + return wt_validate.all(dcycle) + + +def to_tuples__raw(timeline, cols=["cycle", "module", "channel", "value__digits"]): + """ + A raw extraction of ADwin-relevant values from a `timeline`, regardless of whether or not the module is digital or not. + + NOTE: No validation is done here. + """ + return [tuple([np.int32(i) for i in x]) for x in timeline[cols].values] + + +def to_tuples(timeline, machine_specifications=SPECIFICATIONS__DEFAULT): + """ + Takes a full, ADwin-compatible, dataframe of the experimental run and converts the result to an output format that can be processed by ADwin (tuples), separating analogue and digital values. + + return [[(cycle, module, channel, value), ...], + [(cycle, module, channel, value), ...]] + """ + wtl.debug("Got to `output`") + + if not timeline["cycle"].is_monotonic_increasing: + timeline = timeline.sort_values(by=["cycle"], ignore_index=True) + + if not ("module" in timeline.columns): + raise ValueError( + "No `module` listed in timeline. Remember to add ADwin specifications before ADwin export." + ) + + mods_digital = modules__digital(machine_specifications) + mods_analogue = [ + int(x) for x in timeline["module"].unique() if x not in mods_digital + ] + + return [ + to_tuples__raw(timeline.query("module in {}".format(mods_analogue))), + to_tuples__raw( + timeline.query("module in {}".format(mods_digital)), + ), + ] diff --git a/src/wigner/time/adwin/validate.py b/src/wigner/time/adwin/validate.py new file mode 100644 index 0000000..b39eb2f --- /dev/null +++ b/src/wigner/time/adwin/validate.py @@ -0,0 +1,57 @@ +import funcy + +import wigner.time.adwin as wt_adwin +from wigner.time.internal import dataframe as wt_frame + + +def special_contexts(timeline, special_contexts=wt_adwin.CONTEXTS__SPECIAL): + """ + Ensures that there isn't more than one entry for a given variable inside special contexts. This is necessary as there is no concept of 'time' inside the special contexts defined for ADwin. + + Similarly, the time values are adjusted to avoid automatic removal later on. + """ + df = timeline[timeline["context"].isin(special_contexts)] + df_N = df.groupby(["variable", "context"])["value"].count() + duplicates = df_N[df_N > 1].reset_index() + duplicates.columns = ["variable", "context", "variable_occurences"] + + # Replace time values with those specified in wt_adwin.CONTEXTS__SPECIAL + timeline = wt_frame.replace_column__filtered(timeline, wt_adwin.CONTEXTS__SPECIAL) + + if duplicates.empty: + return timeline + else: + raise ValueError( + "The same variable has more than one value inside a special context. This will not work as expected on export to ADwin as these special contexts have no concept of time. For details, see the duplicate information: " + + str(duplicates) + ) + + +def types(timeline, schema=wt_adwin.SCHEMA): + return timeline.astype(schema) + + +def drop_duplicates( + timeline, + subset=["variable", "cycle"], + unless_context=list(wt_adwin.CONTEXTS__SPECIAL.keys()), +): + """ + An alternative to that in timeline, to deal with ADwin-specific cases. + + Drop rows where the columns specified in `subset` are both duplicated, except for in the specific `context`s listed. + """ + mask__duplicates = wt_frame.duplicated(timeline, subset=subset) + + return timeline[~mask__duplicates | (timeline["context"].isin(unless_context))] + + +def all(timeline): + """ + Includes ADwin-specific methods ontop of the basic timeline sanitization for removing unnecessary points and raising errors on illogical input. + """ + return funcy.compose( + drop_duplicates, + special_contexts, + types, + )(timeline) diff --git a/wigner_time/config.py b/src/wigner/time/config.py similarity index 96% rename from wigner_time/config.py rename to src/wigner/time/config.py index 060e752..cf39c43 100644 --- a/wigner_time/config.py +++ b/src/wigner/time/config.py @@ -16,4 +16,4 @@ level=logging.INFO, format="%(asctime)s - %(levelname)s - %(message)s" ) wtlog = logging.getLogger("wtlog") -wtlog.setLevel(logging.DEBUG) +wtlog.setLevel(logging.WARNING) diff --git a/src/wigner/time/conversion.py b/src/wigner/time/conversion.py new file mode 100644 index 0000000..79db16f --- /dev/null +++ b/src/wigner/time/conversion.py @@ -0,0 +1,144 @@ +# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) +from copy import deepcopy + +import numpy as np +import pandas as pd +from scipy.interpolate import interp1d + +from wigner.time.internal import dataframe as wt_frame + +SPECIFICATIONS__DEFAULT = {"voltage_range": [-10.0, 10.0], "num_bits": 16, "gain": 1} + + +def to_digits(voltage, voltage_range=[-10.0, 10.0], num_bits: int = 16, gain: int = 1): + """ + Transforms any voltage linearly to analogue-digital-converter(ADC) digits. + """ + + v = np.asarray(voltage, dtype=float) + v_min, v_max = np.asarray(voltage_range) / gain + + result = np.round(((v - v_min) / (v_max - v_min)) * (2**num_bits - 1)).astype(int) + return result.item() if result.ndim == 0 else result + + +def _add_linear( + timeline, + column__conversion="to_V", + column__new: str = "value__digits", + is_inplace=False, + specifications=SPECIFICATIONS__DEFAULT, +): + """ + Performs a linear conversion, according to the associated conversion factor, adds the resulting values as another column, `value__digits`, and returns the result. + """ + mask = pd.to_numeric(timeline[column__conversion], errors="coerce").notna() + if mask.any(): + if is_inplace: + dff = timeline + else: + dff = deepcopy(timeline) + + dff.loc[mask, column__new] = to_digits( + dff.loc[mask, "value"] * dff.loc[mask, column__conversion], **specifications + ) + + return dff + else: + return timeline + + +def _add_function( + timeline, + column__conversion="to_V", + column__new: str = "value__digits", + is_inplace=False, + specifications=SPECIFICATIONS__DEFAULT, +): + """ + Performs a conversion, according to the associated function, adds the resulting values as another column, `value__digits`, and returns the result. + """ + mask = timeline[column__conversion].apply(callable) + if mask.any(): + if is_inplace: + dff = timeline + else: + dff = deepcopy(timeline) + + s = dff.loc[mask].apply( + lambda row: row[column__conversion](row["value"]), axis=1 + ) + + dff.loc[mask, column__new] = to_digits( + dff.loc[mask] + .apply(lambda row: row[column__conversion](row["value"]), axis=1) + .to_numpy(dtype=float), + **specifications, + ) + + return dff + else: + return timeline + + +def add( + timeline: wt_frame.CLASS, + specifications=SPECIFICATIONS__DEFAULT, + column__conversion: str = "to_V", + column__new: str = "value__digits", +) -> wt_frame.CLASS: + """ + Performs a conversion, according to the associated factor or function, adds the resulting values as another column, `value__digits`, and returns the result. + """ + if column__conversion in timeline.columns: + dff = _add_linear( + timeline, + column__conversion=column__conversion, + column__new=column__new, + specifications=specifications, + ) + return _add_function( + dff, + specifications=specifications, + column__conversion=column__conversion, + column__new=column__new, + ) + + else: + raise ValueError( + f"Cannot convert values because {column__conversion} column does not exist. " + ) + + +def function_from_file( + path, + method="cubic", + fill_value="extrapolate", + indices__column=[0, 1], + **read_csv__args, +): + """ + An interpolation function drawn from *two columns* of a CSV-like calibration file. + + NOTE: If you would like to invert the interpolation then just specify the columns backwards, e.g. indices__column=[1,0] + + e.g. + function_from_file( + "resources/calibration/aom_calibration.dat", + names=["voltage", "transparency"], + `sep=r"\s+"`, + ), + """ + # TODO: Include default 'sep' etc. + df = pd.read_csv(path, **read_csv__args).dropna() + + # Deal with possible x-duplicates + columns = df.columns + df_avg = df.groupby(columns[indices__column[0]], as_index=False).mean() + + return interp1d( + df_avg.iloc[:, 0], + df_avg.iloc[:, indices__column[1]], + kind=method, + fill_value=fill_value, + ) diff --git a/src/wigner/time/demo/__init__.py b/src/wigner/time/demo/__init__.py new file mode 100644 index 0000000..e5a0d9b --- /dev/null +++ b/src/wigner/time/demo/__init__.py @@ -0,0 +1 @@ +#!/usr/bin/env python3 diff --git a/src/wigner/time/demo/full_experiment.py b/src/wigner/time/demo/full_experiment.py new file mode 100644 index 0000000..2d44046 --- /dev/null +++ b/src/wigner/time/demo/full_experiment.py @@ -0,0 +1,398 @@ +""" +An example implementation of a real experiment, using 'Wigner Time' timelines. + +As well as providing conveniences, the functions can be used to document the intention and meaning of each stage. +""" + +from munch import Munch + +from wigner.time.adwin import connection as adcon +from wigner.time.adwin import core as adwin +from wigner.time import file as wtf +from wigner.time import timeline as tl +from wigner.time import device +from wigner.time import conversion as conv +from wigner.time import ramp_function + +########################################################################### +# Constants and Helpers # +########################################################################### + +# Connections, devices and constants can be read from a separate file(s) (they won't change much). They are all collected together here for demonstration purposes only. + +""" +'connections' allows us to label physical links (inputs and outputs) between devices and the timing system. By using labels that follow a particular regex, defined within the `variable` module, we can separate out the design and the implementation of our experiment. +""" +connections = adcon.new( + ["shutter_MOT", 1, 11], + ["shutter_repump", 1, 12], + ["shutter_OP001", 1, 14], + ["shutter_OP002", 1, 15], + ["shutter_science", 1, 10], + ["shutter_transversePump", 1, 9], + ["AOM_MOT", 1, 1], + ["AOM_repump", 1, 2], + ["AOM_OPaux", 1, 30], # should be set to 0 always + ["AOM_OP", 1, 31], + ["coil_compensationX__A", 4, 7], + ["coil_compensationY__A", 3, 2], + ["coil_MOTlower__A", 4, 1], + ["coil_MOTupper__A", 4, 3], + ["coil_MOTlowerPlus__A", 4, 2], + ["coil_MOTupperPlus__A", 4, 4], + ["lockbox_MOT__MHz", 3, 8], + ["trigger_TC__V", 3, 1], + ["AOM_science", 1, 4], + ["AOM_science__trans", 4, 8], +) + +""" +`devices` stores how to map our physical quantities to an implementation voltage, as well as specifying the range of values that should be allowed for this variable. + +These specifications are deliberately separated from `connection`s because they represent physical properties and conversions that are independent of the particular DAC wiring. +""" +devices = device.new( + ["coil_compensationX__A", 1 / 3.0, -3, 3], + ["coil_compensationY__A", 1 / 3.0, -3, 3], + ["coil_MOTlower__A", 1 / 2.0, -5, 5], + ["coil_MOTupper__A", 1 / 2.0, -5, 5], + ["coil_MOTlowerPlus__A", 1 / 2.0, -5, 5], + ["coil_MOTupperPlus__A", 1 / 2.0, -5, 5], + ["lockbox_MOT__MHz", 0.05, -200, 200], + ["trigger_TC__V", 1.0, -10, 10], + [ + "AOM_science__trans", + conv.function_from_file( + "resources/calibration/aom_calibration.dat", + sep=r"\s+", + ), + 0.0, + 1.0, + ], +) + + +""" +'constants' allow us to store site-specific details that help define our exeriment. +""" +constants = Munch( + safety_factor=1.1, + lag__MOTshutter=2.3e-3, + lag__repump_shutter=0, # Earlier value, yet unverified: 2.3e-3, + Compensation=Munch( + Z__A=-0.1, + Y__A=1.5, + X__A=0.25, + ), + OP=Munch( + lag__AOM_on=15e-6, + lag__shutter_on=1.48e-3, + lag__shutter_off=1.78e-3, + duration__shutter_on=140e-6, + duration__shutter_off=600e-6, + ), +) + + +########################################################################### +# Experimental stages # +########################################################################### +# NOTE: The idea behind the function wrapping is that we enclose what will rarely change and expose just those attributes that we are likely to want to vary. + + +def default_state(f=tl.create, MOT_ON=True, **kwargs): + """ + Starts/leaves the system in a sane state that is appropriate for creating a new timeline + + As a general rule, AOMs are kept on as long as possible to keep them in thermal equilibrium. When needed, we turn them off before the opening of the shutter. + """ + return tl.stack( + f( + lockbox_MOT__MHz=0.0, + coil_compensationX__A=constants.Compensation.X__A, + coil_compensationY__A=constants.Compensation.Y__A, + coil_MOTlowerPlus__A=-constants.Compensation.Z__A, + coil_MOTupperPlus__A=constants.Compensation.Z__A, + AOM_MOT=1, + AOM_repump=1, + AOM_OPaux=0, # TODO: USB-controlled AOMs should be treated on a higher level + AOM_OP=1, + AOM_science=1, + shutter_MOT=int(MOT_ON), + shutter_repump=int(MOT_ON), + shutter_OP001=0, + shutter_OP002=1, + shutter_science=0, + shutter_transversePump=0, + AOM_science__trans=1.0, + trigger_TC__V=0.0, + **kwargs, + ) + ) + + +def init(f=tl.create, MOT_ON=False, **kwargs): + return default_state( + f=f, + t=-1e-6, # time is simply a placeholder here as 'ADwin_LowInit' is a 'special' context, that will be treated differently by the ADwin system. + context="ADwin_LowInit", + MOT_ON=MOT_ON, + **kwargs, + ) + + +def finish(wait=1, lA=-1.0, uA=-0.98, MOT_ON=True, **kwargs): + """ + Safely winds down the system, 'ramping' the analog variables to the “default state” in a given duration by the default `ramp_function`. + + The `anchor` function is used to specify a key time instant, around which other times can be specified. + + The ADwin_Finish environment means that the “default state” will be actuated even when the process is interrupted. + """ + duration = 1e-2 + # TODO: + # - The default_state function should be used to populate the ramp? + return tl.stack( + tl.anchor(wait, context="finalRamps"), + tl.ramp( + lockbox_MOT__MHz=0.0, + coil_MOTlower__A=lA, + coil_MOTupper__A=uA, + coil_compensationX__A=constants.Compensation.X__A, + coil_compensationY__A=constants.Compensation.Y__A, + coil_MOTlowerPlus__A=-constants.Compensation.Z__A, + coil_MOTupperPlus__A=constants.Compensation.Z__A, + duration=duration, + context="finalRamps", + ), + default_state( + f=tl.update, + t=duration + + 1e-6, # time is just fictive here, the important thing is the context + context="ADwin_Finish", + MOT_ON=MOT_ON, + **kwargs, + ), + ) + + +def MOT(duration=15, lA=-1.0, uA=-0.98, **kwargs): + """ + Creates a Magneto-Optical Trap. + """ + return tl.stack( + tl.update( + shutter_MOT=1, + shutter_repump=1, + coil_MOTlower__A=lA, + coil_MOTupper__A=uA, + # + origin=0.0, + **kwargs, + ), + tl.anchor(duration, origin=0.0), + context="MOT", + ) + + +def MOT__off(**kwargs): + return tl.update(shutter_MOT=0, AOM_MOT=0, shutter_repump=0, AOM_repump=0, **kwargs) + + +def MOT__detuned_growth( + duration=100e-3, duration__ramp=10e-3, detuning__MHz=-5, **kwargs +): # pt=3, + """ + Final stage of MOT collection with detuned MOT beams for increased capture range. + """ + return tl.stack( + tl.ramp( + lockbox_MOT__MHz=detuning__MHz, + duration=duration__ramp, + # fargs={"ti": pt}, + **kwargs, + ), + tl.anchor(duration), + context="MOT", + ) + + +def molasses( + duration=5e-3, + duration__coil_ramp=9e-4, + duration__lockbox_ramp=1e-3, + toMHz=-90, # coil_pt=3, lockbox_pt=3, + delay=0, # arbitrary delay to shutter for ad hoc compensation of small drifts + **kwargs +): + """ + For slowing down the atoms by creating an optical density. + """ + + return tl.stack( + tl.ramp( + coil_MOTlower__A=0, + coil_MOTupper__A=0, + duration=duration__coil_ramp, + # fargs={"ti": coil_pt}, + **kwargs, + ), + tl.ramp( + lockbox_MOT__MHz=toMHz, + duration=duration__lockbox_ramp, + # fargs={"ti": lockbox_pt}, + ), + tl.update( + shutter_MOT=[duration - constants.lag__MOTshutter + delay, 0], + AOM_MOT=[duration, 0], + ), + tl.anchor(duration), + context="molasses", + ) + + +def optical_pumping( + duration__exposition=80e-6, + duration__coil_ramp=50e-6, + i=-0.12, # pt=3, + delay1=0, + delay2=0, + delay__repump=0, # arbitrary delays to shutters for ad hoc compensation of small drifts + **kwargs +): + """ + Creates an experimental timeline for optical pumping. + + NOTE: + The AOM is switched off close to, but before, the opening of the first shutter + + WARNING: + Shutters are reinitialized so that additional optical pumping stages can be added later. + """ + + duration__full = duration__exposition + duration__coil_ramp + return tl.stack( + tl.ramp( + coil_MOTlower__A=i, + coil_MOTupper__A=-i, + duration=duration__coil_ramp, + # fargs={"ti": pt}, + **kwargs, + ), + tl.update(AOM_OP=[[-0.1, 0], [duration__coil_ramp, 1], [duration__full, 0]]), + tl.update( + shutter_OP001=[ + [duration__coil_ramp - constants.OP.lag__shutter_on + delay1, 1], + [0.1, 0], + ] + ), + tl.update( + shutter_OP002=[ + [duration__full - constants.OP.lag__shutter_off + delay2, 0], + [0.1, 1], + ] + ), + tl.update( + shutter_repump=0, + t=duration__full - constants.lag__repump_shutter + delay__repump, + ), + tl.update(AOM_repump=0, t=duration__full), + tl.anchor(duration__full), + context="optical_pumping", + ) + + +def pull_coils(duration, l, u, lp=0, up=0, pt=3, **kwargs): + """ + Controls the concentric coil pairs responsible for 'pulling' the atoms. + """ + return tl.ramp( + coil_MOTlower__A=l, + coil_MOTupper__A=u, + coil_MOTlowerPlus__A=lp - constants.Compensation.Z__A, + coil_MOTupperPlus__A=up + constants.Compensation.Z__A, + function=lambda origin, terminus, time_resolution: ramp_function.tanh( + origin, terminus, time_resolution, pt + ), + duration=duration, + **kwargs, + ) + + +def magnetic_trapping( + duration__initial=50e-6, + li=-1.8, + ui=-1.7, + duration__strengthen=3e-3, + ls=-4.8, + us=-4.7, + **kwargs +): + """ + Does what it says on the tin. + """ + return tl.stack( + pull_coils(duration__initial, li, ui, context="magnetic_trapping", **kwargs), + pull_coils(duration__strengthen, ls, us, t=duration__initial), + tl.anchor(duration__initial + duration__strengthen), + context="magnetic_trapping", + ) + + +########################################################################### +# Stage composition # +########################################################################### + +timeline__demo = tl.cascade( + init, + MOT, + MOT__detuned_growth, + molasses, + optical_pumping, + magnetic_trapping, + finish, + # + # KW args + # Basic setup + init_MOT_ON=True, + finish_MOT_ON=True, + # MOT stage + MOT_duration=15, + MOT_lA=-1.0, + MOT_uA=-0.98, + # MOT detuned stage + MOT__detuned_growth_duration=0.1, + MOT__detuned_growth_duration__ramp=1e-2, + MOT__detuned_growth_detuning__MHz=-5, # pt=3, + # molasses stage + molasses_duration=4.5e-3, + molasses_duration__coil_ramp=9e-4, + molasses_duration__lockbox_ramp=1e-3, + molasses_toMHz=-90, + molasses_delay=-200e-6, + # OP stage + optical_pumping_duration__exposition=80e-6, + optical_pumping_duration__coil_ramp=500e-6, + optical_pumping_i=-0.12, + optical_pumping_delay1=-350e-6, + optical_pumping_delay2=450e-6, + optical_pumping_delay__repump=0, + # magnetic trapping stage + magnetic_trapping_duration__initial=50e-6, + magnetic_trapping_li=-1.8, + magnetic_trapping_ui=-1.7, + magnetic_trapping_duration__strengthen=3e-3, + magnetic_trapping_ls=-4.8, + magnetic_trapping_us=-4.7, +) + +########################################################################### +# Running the experiment +########################################################################### + +# wtf.save(timeline__demo) +# machine = adwin.create(timeline__demo, connections, devices) +# machine.Start_Process(1) + +# NOTE: +# ^^^ The above lines are commented out for the sake of automated testing. diff --git a/src/wigner/time/device.py b/src/wigner/time/device.py new file mode 100644 index 0000000..094aa6b --- /dev/null +++ b/src/wigner/time/device.py @@ -0,0 +1,116 @@ +""" +A device is represented by a dataframe that contains a variable (with a given unit) and a means, scalar or function, to convert this quantity to a voltage. It also specifies a minimum and maximum value for the variable, to be used in validation. + +The unit range is used for conversion and the saftey range is for sanity checking the output. +""" + +import numpy as np +import pandas as pd + +from wigner.time.internal import dataframe as wt_frame +from wigner.time.internal import util as wt_util + +from collections.abc import Callable + +# ====================================================================== +SCHEMA = { + "variable": str, + "to_V": object, + # float | Callable, + "value__min": float, + "value__max": float, +} +SCHEMA__expanded = { + "variable": str, + "to_V": float, + "value__min": float, + "value__max": float, +} + +# ====================================================================== + + +def new(*variable_toV_min_max) -> wt_frame.CLASS: + """ + Convenience for creating a table that specifies the conversion and range of device values ('variable', 'to_V', 'value__min' and 'value__max' columns). 'to_V' specifies the conversion and can either be a floating point factor or a function that takes the output from the givin units to a Voltage. The min-max limits outline the range that a user is allowed to specify (and will be validated before output). This allows for error-checking before passing values to real devices. + + `variable`s are used as the means by which `connection`s and `device`s can be later connected. + + If the 'value__min/max' columns are not specified, then they will be taken as +/- infinity. + + vfmm: + e.g. + "coil_compensationX__A", 3/10., -3.0, 3.0, + or + ["coil_compensationY__A", 0.333, -3.0], + ["coil_MOTlower__A", , -5, 5], + ["coil_MOTupper__A", lambda x: x - 100,-5, 5], + """ + + def process_input(args): + l = len(args) + if l == 2: + return np.concatenate([args, [-np.inf, np.inf]]) + elif l == 3: + return np.concatenate([args, [np.inf]]) + elif l == 4: + return args # No changes + else: + raise ValueError( + f"Invalid list of devices {args}: the number of arguments should be less than 5." + ) + + input4 = [process_input(args) for args in wt_util.ensure_2d(variable_toV_min_max)] + + try: + new = wt_frame.new_schema(input4, SCHEMA) + + # convert dtype to float if possible (i.e. no functions) + if pd.to_numeric(new["to_V"], errors="coerce").notna().all(): + new["to_V"] = new["to_V"].astype(float) + + except: + raise ValueError("=== Input to 'device' not well formatted ===") + + return new + + +def add(timeline, devices): + """ + For connecting device information to a `timeline`. + """ + return wt_frame.join(timeline, devices) + + +def check_within_range(timeline): + """ + Considers whether the `timeline` `value`s fall inside device safety ranges (see SCHEMA). Raises an error if not. + + ASSUMES: That a `value` column is present. + """ + + if not wt_frame.is_column_float(timeline["value"]): + raise ValueError("Value column might not contain floats.") + + for variable, group in timeline.groupby("variable"): + if group["value__max"].any(): + if max(group["value"].values) > group["value__max"].values[0]: + raise ValueError( + "{} was given a value of {}, which is higher than its maximum safe limit. Please provide values only inside it's safety range.".format( + variable, max(group["value"].values) + ) + ) + elif min(group["value"].values) < group["value__min"].values[0]: + raise ValueError( + "{} was given a value of {}, which is lower than its minimum safe limit. Please provide values only inside it's safety range.".format( + variable, min(group["value"].values) + ) + ) + else: + return True + else: + raise ValueError( + "`value__max` was not found in timeline columns: {}".format( + timeline.columns + ) + ) diff --git a/src/wigner/time/display.py b/src/wigner/time/display.py new file mode 100644 index 0000000..3f996bf --- /dev/null +++ b/src/wigner/time/display.py @@ -0,0 +1,22 @@ +# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) + +# Block module based on dependency +import importlib.util + +if not importlib.util.find_spec("matplotlib"): + raise ImportError("The `display` module requires `matplotlib` to be installed.") + + +from wigner.time.adwin import display as adwin_display + + +def display( + timeline, + variables=None, +): + # TODO: + # - This should be ADwin-independent + # - Branch based on whether ADwin is installed?? + # - Allow for expansion and time-resolution. + # suffixes__analogue is temporarily part of the API until we understand what to replace it with + return adwin_display.channels(timeline, variables) diff --git a/src/wigner/time/file.py b/src/wigner/time/file.py new file mode 100644 index 0000000..a42c6f5 --- /dev/null +++ b/src/wigner/time/file.py @@ -0,0 +1,238 @@ +""" +For PC-level loading and saving of stored timelines. +""" + +from pathlib import Path +from typing import Callable +from typing import Any + +import inspect +import importlib.util +import re + +from wigner.time.internal import dataframe as wt_frame + + +def _available_writers() -> dict[str, Callable[[wt_frame.CLASS, Path], None]]: + writers: dict[str, Callable[[wt_frame.CLASS, Path], None]] = { + ".pkl": lambda df, p: df.to_pickle(p), + ".pickle": lambda df, p: df.to_pickle(p), + ".csv": lambda df, p: df.to_csv(p, index=False), + ".json": lambda df, p: df.to_json(p, orient="records"), + } + + if _has_module("pyarrow") or _has_module("fastparquet"): + writers[".parquet"] = lambda df, p: df.to_parquet(p, index=False) + + if _has_module("pyarrow"): + writers[".feather"] = lambda df, p: df.to_feather(p) + + return writers + + +def _has_module(name: str) -> bool: + return importlib.util.find_spec(name) is not None + + +def _next_available_path(path: Path) -> Path: + if not path.exists(): + return path + + suffix = path.suffix + stem = path.stem + + m = re.match(r"^(.*)__([0-9]{3})$", stem) + if m: + base = m.group(1) + n = int(m.group(2)) + else: + base = stem + n = 1 + + while True: + n += 1 + candidate = path.with_name(f"{base}__{n:03d}{suffix}") + if not candidate.exists(): + return candidate + + +def _infer_caller_name(obj: object) -> str | None: + frame = inspect.currentframe() + if frame is None or frame.f_back is None: + return None + + caller = frame.f_back.f_back + if caller is None: + return None + + matches: list[str] = [] + + for scope in (caller.f_locals, caller.f_globals): + for name, value in scope.items(): + if value is obj and name.isidentifier(): + matches.append(name) + + if not matches: + return None + + # Prefer names containing "timeline", then shortest/first stable-looking name. + matches = sorted( + set(matches), key=lambda x: ("timeline" not in x.lower(), len(x), x) + ) + return matches[0] + + +def _sanitize_stem(name: str) -> str: + cleaned = re.sub(r"[^\w.-]+", "_", name).strip("._") + return cleaned or "timeline" + + +def _has_module(name: str) -> bool: + return importlib.util.find_spec(name) is not None + + +def _stringify_callables_for_export(df: wt_frame.CLASS, suffix: str) -> wt_frame.CLASS: + """ + Return a dataframe suitable for export. + + For formats that do not support arbitrary Python objects, any callable values + are converted to strings. Pickle-like formats are left unchanged. + """ + # Pickle can preserve Python callables/objects as-is. + if suffix in {".pkl", ".pickle"}: + return df + + def _stringify_if_callable(value: Any) -> Any: + if callable(value): + try: + return f"{value.__module__}.{value.__qualname__}" + except Exception: + return repr(value) + return value + + # Work on a copy so the original dataframe is not mutated. + out = df.copy() + + # Only touch columns that actually contain callables. + for col in out.columns: + series = out[col] + if series.map(callable).any(): + out[col] = series.map(_stringify_if_callable) + + return out + + +def save(df: wt_frame.CLASS, path: str | Path | None = None) -> Path: + """ + Writes the given timeline to file, according to convenient features. + + -------- + - If `path` has a recognized suffix, save in that format if the required + dependency is available. + - If `path` has no suffix: + - save as parquet if possible + - otherwise save as pickle + - If `path` is a directory (or looks like a directory), generate a filename + from the caller's variable name for `df`, falling back to "timeline". + - If the target already exists, append __002, __003, ... before the suffix. + + Returns + ------- + Path + The actual path written. + + Supported suffixes + ------------------ + .parquet, .feather, .pickle, .csv, .json + """ + if path is None: + path = Path.cwd() + path = Path(path).expanduser() + + writers = _available_writers() + + is_existing_dir = path.exists() and path.is_dir() + looks_like_dir = str(path).endswith(("/", "\\")) or ( + not path.suffix and not path.exists() + ) + + if is_existing_dir or looks_like_dir: + path.mkdir(parents=True, exist_ok=True) + stem = _infer_caller_name(df) or "timeline" + + if ".parquet" in writers: + ext = ".parquet" + elif ".pkl" in writers: + ext = ".pkl" + elif ".pickle" in writers: + ext = ".pickle" + else: + # Fallback to the first available writer if needed + ext = next(iter(writers)) + + path = path / f"{_sanitize_stem(stem)}{ext}" + + elif not path.suffix: + if ".parquet" in writers: + ext = ".parquet" + elif ".pkl" in writers: + ext = ".pkl" + elif ".pickle" in writers: + ext = ".pickle" + else: + ext = next(iter(writers)) + + path = path.with_suffix(ext) + + suffix = path.suffix.lower() + if suffix not in writers: + supported = ", ".join(sorted(writers)) + raise ValueError( + f"Unsupported file suffix {suffix!r}. Supported writable formats: {supported}" + ) + + path.parent.mkdir(parents=True, exist_ok=True) + path = _next_available_path(path) + + writer = writers[suffix] + export_df = _stringify_callables_for_export(df, suffix) + writer(export_df, path) + + return path + + +def load(path: str | Path) -> wt_frame.CLASS: + """ + Reads the given file into memory. + """ + path = Path(path).expanduser() + + if not path.exists(): + raise FileNotFoundError(path) + + suffix = path.suffix.lower() + + match suffix: + case ".pkl" | ".pickle": + return wt_frame.read_pickle(path) + + case ".csv": + return wt_frame.read_csv(path) + + case ".json": + return wt_frame.read_json(path) + + case ".parquet": + if not (_has_module("pyarrow") or _has_module("fastparquet")): + raise ImportError( + "Reading parquet requires 'pyarrow' or 'fastparquet'." + ) + return wt_frame.read_parquet(path) + + case ".feather": + if not _has_module("pyarrow"): + raise ImportError("Reading feather requires 'pyarrow'.") + return wt_frame.read_feather(path) + + case _: + raise ValueError(f"Unsupported file suffix: {suffix}") diff --git a/wigner_time/constructor.py b/src/wigner/time/internal/constructor.py similarity index 96% rename from wigner_time/constructor.py rename to src/wigner/time/internal/constructor.py index 741f428..6a94cb5 100644 --- a/wigner_time/constructor.py +++ b/src/wigner/time/internal/constructor.py @@ -6,7 +6,7 @@ from munch import Munch -from wigner_time import timeline as tl +from wigner.time import timeline as tl import pandas as pd diff --git a/wigner_time/internal/dataframe.py b/src/wigner/time/internal/dataframe.py similarity index 80% rename from wigner_time/internal/dataframe.py rename to src/wigner/time/internal/dataframe.py index cdce86b..d7d2c81 100644 --- a/wigner_time/internal/dataframe.py +++ b/src/wigner/time/internal/dataframe.py @@ -4,16 +4,18 @@ Particularly relevant for the pandas to polars upgrade. """ -# In the medium term, this should have a polars counterpart namespace so that we can switch between the two easily. +from collections.abc import Callable +# In the medium term, this should have a polars counterpart namespace so that we can switch between the two easily. from copy import deepcopy -import pandas as pd +import pandas as pd +from numpy import identity CLASS = pd.DataFrame -def new(data, columns: list): +def new(data, columns: list | None = None): return pd.DataFrame(data, columns=columns) @@ -58,6 +60,16 @@ def isnull(o): return pd.isnull(o) +def subframe(df: CLASS, column: str, values: list, func: Callable | None = None): + """ + Returns a filtered df, where func(`column`) has values in `values`. + """ + if func: + return df[df[column].map(func).isin(values)].reset_index(drop=True) + + return df[df[column].isin(values)].reset_index(drop=True) + + def row_from_max_column(df, column="time"): """ Finds the maximum value of the column and returns the corresponding row. @@ -85,11 +97,10 @@ def drop_duplicates(df, subset=None, keep="last"): return df.drop_duplicates(subset=subset, keep=keep, ignore_index=True).copy() -def insert_dataframes(df, indices, dfs): +def insert_dataframes(df: CLASS, indices: list[int], dfs: list[CLASS]) -> CLASS: """ Inserts multiple DataFrames (`dfs`) into an existing DataFrame (`df`) at specified `indices`. """ - # TODO: Currently doesn't have tests # Sort the insertions by index to ensure correct order of insertion if len(indices) != len(dfs): raise ValueError("`indices` and `dfs` are different lengths.") @@ -167,6 +178,40 @@ def for_input(df): return source +def read_pickle(path): + return pd.read_pickle(path) + + +def read_csv(path): + return pd.read_csv(path) + + +def read_json(path): + return pd.read_json(path) + + +def read_parquet(path): + return pd.read_parquet(path) + + +def read_feather(path): + return pd.read_feather(path) + + +# ============================================================ +# PREDICATES +# ============================================================ +def is_column_string(col): + """ + Does the selected column only contain strings? + """ + return col.dtype == "string" or bool(col.map(lambda x: isinstance(x, str)).all()) + + +def is_column_float(col): + return pd.api.types.is_float_dtype(col) + + # ============================================================ # TESTS # ============================================================ diff --git a/wigner_time/internal/doc/demo.ipynb b/src/wigner/time/internal/doc/demo.ipynb similarity index 98% rename from wigner_time/internal/doc/demo.ipynb rename to src/wigner/time/internal/doc/demo.ipynb index 2fb6d30..6bb40e5 100644 --- a/wigner_time/internal/doc/demo.ipynb +++ b/src/wigner/time/internal/doc/demo.ipynb @@ -13,10 +13,10 @@ "import numpy as np\n", "import matplotlib.pyplot as plt\n", "\n", - "from wigner_time import timeline as tl\n", - "from wigner_time import display as dp\n", - "from wigner_time import adwin as ad\n", - "import wigner_time, pickle\n", + "from wigner.time import timeline as tl\n", + "from wigner.time import display as dp\n", + "from wigner.time import adwin as ad\n", + "import wigner.time, pickle\n", "\n", "import experimentDemo as ex\n", "import diagnosticsDemo as di\n", diff --git a/wigner_time/internal/doc/demonstration.py b/src/wigner/time/internal/doc/demonstration.py similarity index 97% rename from wigner_time/internal/doc/demonstration.py rename to src/wigner/time/internal/doc/demonstration.py index 00d51e2..bf60076 100644 --- a/wigner_time/internal/doc/demonstration.py +++ b/src/wigner/time/internal/doc/demonstration.py @@ -1,8 +1,14 @@ """ +EXPERIMENTAL!!! + +TODO: +This is the left-over of an attempt to 'simplify' a previous method for creating realistic timelines. It should be harvested for good ideas and then deleted. + + + An example implementation of a real Wigner Time timeline. As well as providing conveniences, the functions can be used to document the intention and meaning of each stage. -EXPERIMENTAL!!! real usecases can be found in experimentDemo.py, diagnosticsDemo.py, and demo.ipynb """ @@ -14,11 +20,11 @@ import pandas as pd import numpy as np from munch import Munch -from wigner_time import constructor as construct -from wigner_time import connection as con -from wigner_time import timeline as tl -from wigner_time import adwin as adwin -from wigner_time import util as u +from wigner.time import constructor as construct +from wigner.time import connection as con +from wigner.time import timeline as tl +from wigner.time import adwin as adwin +from wigner.time import util as u from copy import deepcopy @@ -483,7 +489,7 @@ def prepare_atoms(): init = init() -from wigner_time import variable as var +from wigner.time import variable as var print("here") diff --git a/wigner_time/ideas.org b/src/wigner/time/internal/doc/dev-notes.org similarity index 87% rename from wigner_time/ideas.org rename to src/wigner/time/internal/doc/dev-notes.org index 1ea0028..677f240 100644 --- a/wigner_time/ideas.org +++ b/src/wigner/time/internal/doc/dev-notes.org @@ -1,5 +1,18 @@ #+title: Ideas +# TODO: Change this file from random ideas to structured notes on code. + +* API policies +Hide everything that has no API commitment under `_internal`. + +Major changes in API (post publication) will include a backwards-compatibility namespace? + +** package numbering +standard major.minor.patch package versioning. +- major - API changes (breaking changes, if any) +- minor - new features +- patch - bug fixes (no expected user downsides or surprises) + * Design ** concepts A *device* is a dictionary of properties that reperesents an experimental apparatus. Not all of this information need be necessary for taking the data (analysis is important too!) but the *variable* names should be unique for use with ADwin *connections*. This should not take much effort on the user's part and allows for much easier data processing. If it becomes necessary to nest dictionaries to describe a device then we should consider switching to DataFrames for this as well. @@ -30,7 +43,6 @@ There are many well-documented reasons for using a data-oriented approach to pro Similar reasoning was used when deciding to base the program around python's 'pandas' module. This is a very well developed platform for manipulating data and gives us the benefit of tried and tested objects as well as the convenience of not having to reinvent our own objects and methods. -* Ideas -** Bálint (for) -- learn how to setup proper tests for python -- document and make tests for the new system +* TODO Feature requests +** clear ways of importing timelines from file + diff --git a/wigner_time/internal/doc/diagnosticsDemo.py b/src/wigner/time/internal/doc/diagnosticsDemo.py similarity index 97% rename from wigner_time/internal/doc/diagnosticsDemo.py rename to src/wigner/time/internal/doc/diagnosticsDemo.py index 3ac6ff4..0a13de0 100644 --- a/wigner_time/internal/doc/diagnosticsDemo.py +++ b/src/wigner/time/internal/doc/diagnosticsDemo.py @@ -4,8 +4,8 @@ import pandas as pd from munch import Munch -from wigner_time import connection as con -from wigner_time import timeline as tl +from wigner.time import connection as con +from wigner.time import timeline as tl import experimentDemo as ex diff --git a/wigner_time/internal/doc/glossary.json b/src/wigner/time/internal/doc/glossary.json similarity index 100% rename from wigner_time/internal/doc/glossary.json rename to src/wigner/time/internal/doc/glossary.json diff --git a/wigner_time/internal/doc/graphics/ADbasic.webp b/src/wigner/time/internal/doc/graphics/ADbasic.webp similarity index 100% rename from wigner_time/internal/doc/graphics/ADbasic.webp rename to src/wigner/time/internal/doc/graphics/ADbasic.webp diff --git a/wigner_time/internal/doc/graphics/MOT_pulsing.gif b/src/wigner/time/internal/doc/graphics/MOT_pulsing.gif similarity index 100% rename from wigner_time/internal/doc/graphics/MOT_pulsing.gif rename to src/wigner/time/internal/doc/graphics/MOT_pulsing.gif diff --git a/wigner_time/internal/doc/graphics/adwin-signal.png b/src/wigner/time/internal/doc/graphics/adwin-signal.png similarity index 100% rename from wigner_time/internal/doc/graphics/adwin-signal.png rename to src/wigner/time/internal/doc/graphics/adwin-signal.png diff --git a/wigner_time/internal/doc/graphics/adwin.svg b/src/wigner/time/internal/doc/graphics/adwin.svg similarity index 100% rename from wigner_time/internal/doc/graphics/adwin.svg rename to src/wigner/time/internal/doc/graphics/adwin.svg diff --git a/wigner_time/internal/doc/graphics/complected.png b/src/wigner/time/internal/doc/graphics/complected.png similarity index 100% rename from wigner_time/internal/doc/graphics/complected.png rename to src/wigner/time/internal/doc/graphics/complected.png diff --git a/wigner_time/internal/doc/graphics/conway-life.gif b/src/wigner/time/internal/doc/graphics/conway-life.gif similarity index 100% rename from wigner_time/internal/doc/graphics/conway-life.gif rename to src/wigner/time/internal/doc/graphics/conway-life.gif diff --git a/wigner_time/internal/doc/graphics/display.png b/src/wigner/time/internal/doc/graphics/display.png similarity index 100% rename from wigner_time/internal/doc/graphics/display.png rename to src/wigner/time/internal/doc/graphics/display.png diff --git a/wigner_time/internal/doc/graphics/language-tree.pdf b/src/wigner/time/internal/doc/graphics/language-tree.pdf similarity index 100% rename from wigner_time/internal/doc/graphics/language-tree.pdf rename to src/wigner/time/internal/doc/graphics/language-tree.pdf diff --git a/wigner_time/internal/doc/graphics/language-tree.png b/src/wigner/time/internal/doc/graphics/language-tree.png similarity index 100% rename from wigner_time/internal/doc/graphics/language-tree.png rename to src/wigner/time/internal/doc/graphics/language-tree.png diff --git a/wigner_time/internal/doc/graphics/language-tree.svg b/src/wigner/time/internal/doc/graphics/language-tree.svg similarity index 100% rename from wigner_time/internal/doc/graphics/language-tree.svg rename to src/wigner/time/internal/doc/graphics/language-tree.svg diff --git a/wigner_time/internal/doc/graphics/poster--what.svg b/src/wigner/time/internal/doc/graphics/poster--what.svg similarity index 100% rename from wigner_time/internal/doc/graphics/poster--what.svg rename to src/wigner/time/internal/doc/graphics/poster--what.svg diff --git a/wigner_time/internal/doc/graphics/ramp-options.svg b/src/wigner/time/internal/doc/graphics/ramp-options.svg similarity index 100% rename from wigner_time/internal/doc/graphics/ramp-options.svg rename to src/wigner/time/internal/doc/graphics/ramp-options.svg diff --git a/wigner_time/internal/doc/graphics/wheel-control.gif b/src/wigner/time/internal/doc/graphics/wheel-control.gif similarity index 100% rename from wigner_time/internal/doc/graphics/wheel-control.gif rename to src/wigner/time/internal/doc/graphics/wheel-control.gif diff --git a/wigner_time/internal/doc/graphics/wigner-time--basics.pdf b/src/wigner/time/internal/doc/graphics/wigner-time--basics.pdf similarity index 100% rename from wigner_time/internal/doc/graphics/wigner-time--basics.pdf rename to src/wigner/time/internal/doc/graphics/wigner-time--basics.pdf diff --git a/wigner_time/internal/doc/graphics/wigner-time--basics.svg b/src/wigner/time/internal/doc/graphics/wigner-time--basics.svg similarity index 100% rename from wigner_time/internal/doc/graphics/wigner-time--basics.svg rename to src/wigner/time/internal/doc/graphics/wigner-time--basics.svg diff --git a/wigner_time/internal/doc/graphics/wigner-time--flash.svg b/src/wigner/time/internal/doc/graphics/wigner-time--flash.svg similarity index 100% rename from wigner_time/internal/doc/graphics/wigner-time--flash.svg rename to src/wigner/time/internal/doc/graphics/wigner-time--flash.svg diff --git a/wigner_time/internal/doc/notes--wigner-time.org b/src/wigner/time/internal/doc/notes--wigner-time.org similarity index 97% rename from wigner_time/internal/doc/notes--wigner-time.org rename to src/wigner/time/internal/doc/notes--wigner-time.org index f8ab2ab..52993a8 100644 --- a/wigner_time/internal/doc/notes--wigner-time.org +++ b/src/wigner/time/internal/doc/notes--wigner-time.org @@ -31,9 +31,12 @@ As such, it is a very well developed platform for manipulating data and gives us Separation of concerns . We should be careful not to transfer the ADwin logic to python. This is part of the point of the new system! +Emphasize that by using cascading functions we can move up and down the layers of abstraction. New functions can be very 'easy' and high-level (MOT=on), but can also drop down to the bottom layer (5V at 6ms...). + Using the cascading layer approach also allows for using a different hardware layer in the future i.e. National Instruments instead of ADwin. program intent: the what; not the how + *** Operational (make a MOT) Types of parameters diff --git a/wigner_time/internal/doc/overview--essentials.org b/src/wigner/time/internal/doc/overview--essentials.org similarity index 95% rename from wigner_time/internal/doc/overview--essentials.org rename to src/wigner/time/internal/doc/overview--essentials.org index e6ee865..a8fa112 100644 --- a/wigner_time/internal/doc/overview--essentials.org +++ b/src/wigner/time/internal/doc/overview--essentials.org @@ -42,7 +42,7 @@ Wigner Time is based around the idea of a 'timeline', which is, at heart, simply - Add more parameters by adding columns - Add more operations by adding rows -By boiling the design down to a 'table' as the foundation, then we can benfit from decades of database development, particularly in-memory database-like systems like `pandas`. Therefore, when in doubt, the user can simply manipulate their timeline using the well-developed `pandas` ecosystem. For most operations however, even this won't be necessary as wigner_time provides layers of conveninece functions ontop of this for designing open-loop experiments. +By boiling the design down to a 'table' as the foundation, then we can benfit from decades of database development, particularly in-memory database-like systems like `pandas`. Therefore, when in doubt, the user can simply manipulate their timeline using the well-developed `pandas` ecosystem. For most operations however, even this won't be necessary as wigner.time provides layers of conveninece functions ontop of this for designing open-loop experiments. ** Example (For ADwin systems) You want to digitally control an optical shutter and AOM. @@ -50,9 +50,9 @@ You want to digitally control an optical shutter and AOM. For digital channels, simply /name/ the ADwin ports using standard Python lists. These keep track of the physical connections. #+begin_src python -from wigner_time.adwin import connection as adcon -from wigner_time import device -from wigner_time import conversion as conv +from wigner.time.adwin import connection as adcon +from wigner.time import device +from wigner.time import conversion as conv connections = adcon.new( ["shutter_MOT", 1, 11], @@ -124,7 +124,7 @@ tline = tl.stack( The timeline can then be exported to an ADwin-compatible format. #+begin_src python -from wigner_time.adwin import core as adwin +from wigner.time.adwin import core as adwin adwin.to_data(tline) #+end_src diff --git a/wigner_time/internal/doc/overview.org b/src/wigner/time/internal/doc/overview.org similarity index 99% rename from wigner_time/internal/doc/overview.org rename to src/wigner/time/internal/doc/overview.org index 650f0f1..3d0cf30 100644 --- a/wigner_time/internal/doc/overview.org +++ b/src/wigner/time/internal/doc/overview.org @@ -178,7 +178,7 @@ pd.DataFrame( ** ~create~ *** basic #+begin_src python :session :exports both -from wigner_time import timeline as tl +from wigner.time import timeline as tl tl.create( [ ["lockbox_MOT__V", [[0.0, 5.0, "testing"]]], diff --git a/src/wigner/time/internal/experimental/demonstration.py b/src/wigner/time/internal/experimental/demonstration.py new file mode 100644 index 0000000..bf60076 --- /dev/null +++ b/src/wigner/time/internal/experimental/demonstration.py @@ -0,0 +1,497 @@ +""" +EXPERIMENTAL!!! + +TODO: +This is the left-over of an attempt to 'simplify' a previous method for creating realistic timelines. It should be harvested for good ideas and then deleted. + + + +An example implementation of a real Wigner Time timeline. +As well as providing conveniences, the functions can be used to document the intention and meaning of each stage. + + +""" + +# TODO: Change shutter convention! 1 should mean closed and 0 should mean open: this comes from 1->True;0->False and also from the visual symbolism of 0. + +import importlib +import pathlib as pl + +import pandas as pd +import numpy as np +from munch import Munch +from wigner.time import constructor as construct +from wigner.time import connection as con +from wigner.time import timeline as tl +from wigner.time import adwin as adwin +from wigner.time import util as u + +from copy import deepcopy + + +importlib.reload(tl) +importlib.reload(construct) +# ^^^ Reloads are for development purposes only + +########################################################################### +# Constants and Helpers # +########################################################################### + +# TODO: These ↓ (connections, devices and constants) should maybe be read from a separate file (they won't change much). +connections = con.connection( + ["shutter_MOT", 1, 11], + ["shutter_repump", 1, 12], + ["shutter_imaging", 1, 13], + ["shutter_OP001", 1, 14], + ["shutter_OP002", 1, 15], + ["AOM_MOT", 1, 1], + ["AOM_repump", 1, 2], + ["AOM_imaging", 1, 5], + ["AOM_OP_aux", 1, 30], # should be set to 0 always + ["AOM_OP", 1, 31], + ["coil_compensationX__A", 4, 7], + ["coil_compensationY__A", 3, 2], + ["coil_MOTlower__A", 4, 1], + ["coil_MOTupper__A", 4, 3], + ["coil_MOTlowerPlus__A", 4, 2], + ["coil_MOTupperPlus__A", 4, 4], + ["lockbox_MOT__V", 3, 8], +) + +# TODO: This could be a set of conversion functions/lambdas from units like A, MHz +devices = pd.DataFrame( + columns=["variable", "unit_range", "safety_range"], + data=[ + ["coil_compensationX__A", (-3, 3), (-3, 3)], + ["coil_compensationY__A", (-3, 3), (-3, 3)], + ["coil_MOTlower__A", (-5, 5), (-5, 5)], + ["coil_MOTupper__A", (-5, 5), (-5, 5)], + ["coil_MOTlowerPlus__A", (-5, 5), (-5, 5)], + ["coil_MOTupperPlus__A", (-5, 5), (-5, 5)], + ["lockbox_MOT__V", (-10, 10)], + ], +) + +# TODO: +# I dislike global variables but, unfortunately, a reference will probably still need to be available in the same namespace as these functions for convenience. +constants = Munch( + safety_factor=1.1, + factor__VpMHz=0.05, + Bfield_compensation_Z__A=-0.1, + Bfield_compensation_Y__A=1.5, + Bfield_compensation_X__A=0.25, + lag_MOTshutter=2.48e-3, + OP=Munch( + lag_AOM_on=15e-6, + lag_shutter_on=1.5e-3, + lag_shutter_off=1.5e-3, + duration_shutter_on=140e-6, + duration_shutter_off=600e-6, + ), +) + + +# TODO: Move device conversions out of here? +# Idea would be to make ...__A, __MHz etc. variables at the 'top' level of timeline conversion and then we could run the conversion functions later. +class lock_box: + def to_V(MHz): + return constants.factor__VpMHz * MHz + + +# TODO: Should all of the context-creating function arguments be abstracted here? +defaults = Munch() + +# NOTE: MOTplus coils are part of the compensation and so should default to the compensation values. +defaults.MOT = Munch( + lockbox_MOT__V=0.0, + shutter_MOT=1, # TODO: why the shutter values here? + shutter_repump=1, + coil_MOTlower__A=-1.0, + coil_MOTupper__A=-0.98, +) + +defaults.molasses = Munch( + duration_cooling=5e-3, + duration_ramp=1e-3, + shift__MHz=-80, + fraction_ramp_duration=0.9, + coil_MOTlower__A=0.0, + coil_MOTupper__A=0.0, +) + +defaults.magnetic = Munch( + delay=1e-3, + quadrupole=Munch(duration_ramp=50e-6, coil_MOTlower__A=-1.8, coil_MOTupper__A=-1.7), + strong=Munch(duration_ramp=3e-3, coil_MOTlower__A=-4.8, coil_MOTupper__A=-4.7), +) + +defaults.finish = Munch(update={}, ramp={}) +defaults.finish.update = Munch( + AOM_MOT=1, + AOM_repump=1, + AOM_repump__V=5, + AOM_imaging=1, + AOM_OP_aux=0, + AOM_OP=1, + AOM_science=1, + AOM_science__V=0.0, + AOM_ref=1, + shutter_MOT=1, + shutter_repump=1, + shutter_imaging=0, + shutter_OP001=0, + shutter_OP002=1, + shutter_science=1, + shutter_transversePump=0, + shutter_coupling=0, + trigger_TC__V=0, + photodiode__V=0, +) +defaults.finish.ramp = Munch( + coil_MOTlower__A=0.0, + coil_MOTupper__A=0.0, + coil_MOTlowerPlus__A=-constants.Bfield_compensation_Z__A, + coil_MOTupperPlus__A=constants.Bfield_compensation_Z__A, + coil_compensationX__A=constants.Bfield_compensation_X__A, + coil_compensationY__A=constants.Bfield_compensation_Y__A, + lockbox_MOT__V=lock_box.to_V(0.0), +) + + +########################################################################### +# Experimental stages # +########################################################################### +# NOTE: The idea behind the function wrapping is that we enclose what will likely never change and expose just those attributes that we are likely to want to vary. + + +def init(): + """ + Creates an experimental timeline for the initialization of every relevant variable. + """ + return tl.create( + lockbox_MOT__V=0.0, + coil_compensationX__A=constants.Bfield_compensation_X__A, + coil_compensationY__A=constants.Bfield_compensation_Y__A, + coil_MOTlowerPlus__A=-constants.Bfield_compensation_Z__A, + coil_MOTupperPlus__A=constants.Bfield_compensation_Z__A, + AOM_MOT=1, + AOM_repump=1, + AOM_OP_aux=0, + AOM_OP=1, + shutter_MOT=0, + shutter_repump=0, + shutter_OP001=0, + shutter_OP002=1, + context="ADwin_LowInit", + ) + + +# TODO: in the true ADwin finish section, nothing time dependent can be done! Another context, called “finish”, which could have special handling also +def finish( + timeline, + duration_ramp=1e-3, + time_start=None, + vars_set=None, + vars_ramp=None, + defaults_set=defaults.finish.set, + defaults_ramp=defaults.finish.ramp, + # context="ADwin_Finish", +): + """ + Safely winds down the system. Doing this within the ADwin_Finish environment, means that this will still happen when the process is interrupted. + + Here, a previous timeline is not optional. + """ + + _vars_set = construct.arguments(vars_set, defaults_set) + _vars_ramp = construct.arguments(vars_ramp, defaults_ramp) + + return tl.stack( + tl.next(**_vars_ramp, t=duration_ramp, timeline=timeline, context="Finish"), + tl.update(**_vars_set, context="ADwin_Finish"), + ) + + +# TODO: Should all of these functions simply take a timeline and params? +def MOT( + detuning__MHz=3, + duration=10.0, + # === + time_start=None, + variables: Munch | None = None, + variables_default=defaults.MOT, + timeline=init(), + context="MOT", +): + """ + Creates a Magneto-Optical Trap. + + """ + _time_start, _variables = construct.time_and_arguments( + time_start, variables, variables_default, timeline + ) + + return tl.stack( + tl.create( + shutter_MOT=_variables.shutter_MOT, + shutter_repump=_variables.shutter_repump, + coil_MOTlower__A=_variables.coil_MOTlower__A, + coil_MOTupper__A=_variables.coil_MOTupper__A, + # === + timeline=timeline, + context=context, + ), + tl.next( + lockbox_MOT__V=lock_box.to_V(detuning__MHz), t=duration, context="MOT_grow" + ), + ) + + +def molasses( + duration_cooling=5e-3, + duration_ramp=1e-3, + detuning__MHz=-80, + # === + # Specific values above ↑ + # Repeated values below ↓ + # === + time_start=None, + variables: Munch | None = None, + variables_default=defaults.molasses, + timeline=MOT(), + context="molasses", +): + _time_start, _variables = construct.time_and_arguments( + time_start, variables, variables_default, timeline + ) + + if duration_ramp >= duration_cooling: + raise ValueError("duration_ramp should be smaller than duration_cooling!") + + return tl.stack( + tl.create( + AOM_MOT=[_time_start + duration_cooling, 0], + shutter_MOT=[_time_start + duration_cooling - constants.lag_MOTshutter, 0], + timeline=timeline, + context=context, + ), + tl.next( + coil_MOTlower__A=_variables.coil_MOTlower__A, + coil_MOTupper__A=_variables.coil_MOTupper__A, + t=duration_ramp, + ), + tl.next(lockbox_MOT__V=lock_box.to_V(detuning__MHz), t=duration_ramp), + ) + + +def switch_laser( + var__AOM, + var__shutter, + state="ON", + params=Munch( + lag_AOM_on=15e-6, + lag_shutter_on=1.5e-3, + duration_shutter_on=140e-6, + safety_factor=1.1, + ), + time_start=None, + timeline=None, + context=None, +): + """ + Combines the action of turning off the AOM and turning on the shutter or vice versa, such that a laser beam is changed `ON` or `OFF` according to the given state ENUM. + + """ + # TODO: I live in hope that the shutter convention will be changed! + # + _time__AOM = time_start - params.lag_AOM_on - params.lag_shutter_on + _time__shutter = ( + time_start + - params.lag_AOM_on + - params.safety_factor * (params.lag_shutter_on + params.duration_shutter_on) + ) + _value__AOM = 0 if state == "ON" else 1 + _value__shutter = 1 if state == "ON" else 0 + + return tl.update( + context=context, + timeline=timeline, + **{ + var__AOM: [_time__AOM, _value__AOM], + var__shutter: [ + _time__shutter, + _value__shutter, + ], + }, + ) + + +def flash_laser(): + """ + Switches the laser on and off, for a given duration. + """ + # TODO: fill in the blank! + return + + +def optical_pumping( + duration_exposure=80e-6, + duration_ramp=500e-6, + B_field_homogenous__A=-0.12, + # === + # Specific values above ^ + # Repeated values below ↓ + # === + time_start=None, + variables: Munch | None = None, + variables_default={}, + timeline=molasses(), + context="optical_pumping", +): + """ + Creates an experimental timeline for optical pumping. + + NOTE: + The AOM is switched off close to, but before, the opening of the first shutter, but taking care not to flash too soon. + """ + # TODO: + # - Mysterious constants should be investigated and named! + # === + constant__mysterious_001 = 9.5e-6 + constant__mysterious_002 = 0.5e-6 + + _time_start, _variables = construct.time_and_arguments( + time_start, variables, variables_default, timeline + ) + + return tl.stack( + switch_laser( + "AOM_OP", + "shutter_OP001", + "OFF", + time_start=_time_start, + timeline=timeline, + context=context, + ), + tl.update( + AOM_OP=[ + _time_start - constants.OP.lag_AOM_on + constant__mysterious_001, + 1, + ], + shutter_OP002=[ + _time_start + + duration_exposure + - (2 - constants.safety_factor) * constants.OP.lag_shutter_off, + 0, + ], + ), + # Shutters switched back, to reinitialize them before any additional optical pumpings later. + tl.update( + AOM_OP=[_time_start + duration_exposure + constant__mysterious_002, 0], + AOM_repump=[_time_start + duration_exposure, 0], + shutter_repump=[_time_start + duration_exposure, 0], + shutter_OP001=[_time_start + duration_exposure, 0], + shutter_OP002=[ + _time_start + + duration_exposure + + constants.safety_factor + * (constants.OP.lag_shutter_on + constants.OP.duration_shutter_on), + 1, + ], + ), + tl.next( + coil_MOTlower__A=B_field_homogenous__A, + coil_MOTupper__A=-B_field_homogenous__A, + time_start=_time_start - duration_ramp, + t=duration_ramp, + context=context, + ), + ) + + +optical_pumping() + + +def ramp_magnetic_coils( + timeline=None, + # === + time_start=None, + params: Munch = Munch( + duration_ramp=150e-3, + coil_MOTlower__A=0.0, + coil_MOTupper__A=0.0, + coil_MOTlowerPlus__A=-constants.Bfield_compensation_Z__A, + coil_MOTupperPlus__A=constants.Bfield_compensation_Z__A, + ), + context=None, +): + """ + By default, lowers the coil values to safe 'starting' values. + """ + _time_start = construct.time(time_start, timeline) + _variables = u.filter_dict( + params, + [ + "coil_MOTlower__A", + "coil_MOTupper__A", + "coil_MOTlowerPlus__A", + "coil_MOTupperPlus__A", + ], + ) + + return tl.next( + **_variables, + time_start=_time_start, + t=params.duration_ramp, + timeline=timeline, + context=context, + ) + + +def make_and_strengthen_magnetic_trap( + timeline=optical_pumping(), params=defaults.magnetic, context="magnetic_trap" +): + return tl.stack( + ramp_magnetic_coils( + timeline=timeline, + params=params.quadrupole, + context=context, + ), + lambda t: ramp_magnetic_coils( + timeline=t, + params=params.strong, + context=context, + ), + ) + + +def prepare_atoms(): + """ + Convenience for setting up the cold atoms. + + NOTE: `tl.stack` can be used with any single-argument function, where the single argument is a (DataFrame-like) timeline. + """ + # TODO: should really take the whole parameters dict as an input and use it intelligently. + return tl.stack(make_and_strengthen_magnetic_trap, finish) + + +########################################################################### +# SCRATCH # +########################################################################### +duration_cooling = 1e-3 +timeline = init() +context = "test" + + +# data = prepare_atoms() +# print(data) + + +init = init() + +from wigner.time import variable as var + + +print("here") +mask_current = init["variable"].str.contains("A" + "$") +init diff --git a/wigner_time/parameters.py b/src/wigner/time/internal/experimental/parameters.py similarity index 70% rename from wigner_time/parameters.py rename to src/wigner/time/internal/experimental/parameters.py index 3ea4c73..94ea591 100644 --- a/wigner_time/parameters.py +++ b/src/wigner/time/internal/experimental/parameters.py @@ -6,35 +6,35 @@ def from_dict(dct=None, labels=["parameter", "value"], extras={}, **kwargs): """ - TODO: Add to a dictionary manipulating API rather than here? - Now allows direct instantiation using kwargs. The downside of this is that the parameters have to come after the other arguments """ - items = kwargs if dct==None else dct + items = kwargs if dct == None else dct rows = [] for k, v in items.items(): rows.append([k, v] + list(extras.values())) return pd.DataFrame(rows, columns=labels + list(extras.keys())) -def vals(df, labels=['parameter', 'value']): + +def vals(df, labels=["parameter", "value"]): """ Convenience for accessing parameter values by name. """ return Munch(df[labels].values) -def update(parameters,dct,context): + +def update(parameters, dct, context): """ Updating the parameters DataFrame with a dictionary containing modified or new parameters. """ - return pd.concat([ - parameters, - from_dict( - dct, - extras={"context":"{}".format(context)} - ), - ],ignore_index=True).drop_duplicates() + return pd.concat( + [ + parameters, + from_dict(dct, extras={"context": "{}".format(context)}), + ], + ignore_index=True, + ).drop_duplicates() + if __name__ == "__main__": print(from_dict({"test": 1, "test2": 20}, extras=Munch(context="molasses"))) - diff --git a/wigner_time/internal/origin.py b/src/wigner/time/internal/origin.py similarity index 94% rename from wigner_time/internal/origin.py rename to src/wigner/time/internal/origin.py index b87ff10..abc5897 100644 --- a/wigner_time/internal/origin.py +++ b/src/wigner/time/internal/origin.py @@ -5,32 +5,27 @@ """ -# TODO: -# - Rename this file (and relevant functions) to something to do with query/history? -# - dictionary option for origin (i.e. different origin for different variables?) - from copy import deepcopy import numpy as np -from wigner_time import config as wt_config -from wigner_time.config import wtlog -from wigner_time import util as wt_util -from wigner_time import anchor as wt_anchor -from wigner_time.internal import dataframe as wt_frame -from wigner_time.internal import origin as wt_origin +from wigner.time import config as wt_config +from wigner.time.config import wtlog +from wigner.time.internal import util as wt_util +from wigner.time.internal.timeline import anchor as wt_anchor +from wigner.time.internal import dataframe as wt_frame +from wigner.time.internal import origin as wt_origin ############################################################################### # CONSTANTS # ############################################################################### -# TODO: These could be moved to the `config` module _ORIGINS = ["anchor", "last", "variable"] "These origin labels are reserved for interpretation by the package. Other origin strings will be interpreted as`variable`s." def error__unsupported_option(origin): return ValueError( - f"{origin} is an unsupported option for 'origin' in `wigner_time.internal.origin.find`. Check the formatting and whether this makes sense for your current timeline. \n\n If you feel like this option should be supported then don't hesitate to get in touch with the maintainers." + f"{origin} is an unsupported option for 'origin' in `wigner.time.internal.origin.find`. Check the formatting and whether this makes sense for your current timeline. \n\n If you feel like this option should be supported then don't hesitate to get in touch with the maintainers." ) diff --git a/src/wigner/time/internal/scratch.py b/src/wigner/time/internal/scratch.py new file mode 100644 index 0000000..54c7e02 --- /dev/null +++ b/src/wigner/time/internal/scratch.py @@ -0,0 +1,9 @@ +""" +A dedicated workspace for experimenting with the module environment. +""" + +import importlib as ilib +from wigner.time.adwin import core as adwin + + +# ilib.reload(ad) diff --git a/wigner_time/anchor.py b/src/wigner/time/internal/timeline/anchor.py similarity index 91% rename from wigner_time/anchor.py rename to src/wigner/time/internal/timeline/anchor.py index 64931dd..9580361 100644 --- a/wigner_time/anchor.py +++ b/src/wigner/time/internal/timeline/anchor.py @@ -6,8 +6,8 @@ from typing import Callable -from wigner_time import config as wt_config -from wigner_time.internal import dataframe as wt_frame +from wigner.time import config as wt_config +from wigner.time.internal import dataframe as wt_frame LABEL__ANCHOR = wt_config.LABEL__ANCHOR diff --git a/src/wigner/time/internal/timeline/inherit.py b/src/wigner/time/internal/timeline/inherit.py new file mode 100644 index 0000000..6e05c31 --- /dev/null +++ b/src/wigner/time/internal/timeline/inherit.py @@ -0,0 +1,44 @@ +from copy import deepcopy +import pandas as pd + +from wigner.time.internal import origin as wt_origin + +# TODO: Fix dependance on pandas + + +def _mask__no_context(timeline): + if "context" in timeline.columns: + mask = timeline["context"] == "" + else: + mask = pd.Series(True, index=timeline.index) + + return mask + + +def context( + timeline, timeline__previous, context=None, is_inPlace=True, time__max=None +): + """ + Updates the context, taken from previous values where unspecified. + + Allows for situations where the new timelines are inserted at earlier times. + """ + if is_inPlace: + df = timeline + else: + df = deepcopy(timeline) + + if (timeline__previous is not None) and (context is None): + if time__max == "min": + time__max = timeline["time"].min() + + df.loc[_mask__no_context(timeline), "context"] = wt_origin.previous( + timeline__previous, time__max=time__max + )["context"] + return df + + elif (timeline__previous is None) and (context is not None): + df.loc[_mask__no_context(timeline), "context"] = context + + else: + return timeline diff --git a/wigner_time/input.py b/src/wigner/time/internal/timeline/input.py similarity index 95% rename from wigner_time/input.py rename to src/wigner/time/internal/timeline/input.py index 0416ece..b7aa42d 100644 --- a/wigner_time/input.py +++ b/src/wigner/time/internal/timeline/input.py @@ -6,14 +6,13 @@ import numpy as np -from wigner_time import util as WTutil +from wigner.time.internal import util as WTutil def __find_depth(vtvc): """ Returns the necessary level of nesting to reach the data. This is complicated by the fact that the array input can be inhomogenously shaped (which is convenient for the user, if not for the programming!). """ - # TODO: Check that first element is actually a string. if WTutil.is_collection(vtvc[0]): if WTutil.is_collection(vtvc[0][0]): @@ -106,7 +105,7 @@ def convert( This was abstracted from `create`... to simplify (well, we tried) the logic. """ # TODO: could probably still be simplified - # TODO: make consistent: sometimes a tuple and sometimes a list + # - make consistent: sometimes a tuple and sometimes a list shape = np.array(vtvc, dtype=object).shape @@ -114,8 +113,8 @@ def convert( return __correct_variable_list(vtvc_dict.items(), time, context) else: depth = __find_depth(vtvc) - # print(f"depth: {depth}") - # + # TODO: Check that first element is actually a string. + match depth: case 3: temp = __correct_variable_list(vtvc[0], time, context) @@ -144,7 +143,9 @@ def rows_from_input(input): """ Takes input, where every variable has its own list, and converts the output to a list of length-4 lists. """ - # TODO: profiling suggests that this is very slow. + # TODO: + # - profiling suggests that this is very slow. + # - is this even necessary?? # rows = [] # for row in input: # for rowv in row[1]: diff --git a/src/wigner/time/internal/timeline/validate.py b/src/wigner/time/internal/timeline/validate.py new file mode 100644 index 0000000..394e1c9 --- /dev/null +++ b/src/wigner/time/internal/timeline/validate.py @@ -0,0 +1,102 @@ +# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) + +""" +Utility validation functions for the `timeline` module. + +May or may not be temporary, but removed to keep the main API as clean as possible. +""" + +# TODO: Out of date with respect to current timeline SCHEMA. + + +import funcy +from copy import deepcopy + +from wigner.time.internal import dataframe as wt_frame + + +def is_value_within_range(value, unit_range): + if wt_frame.isnull(unit_range): + # If unit_range is NaN, consider it as within range + return True + else: + min_value, max_value = unit_range + return min_value <= value <= max_value + + +def sanitize_values(timeline): + """ + Ensures that the given timeline doesn't contain values outside of the given unit or safety range. + """ + # TODO: Check for efficiency + # + if ("unit_range" in timeline.columns) or ("safety_range" in timeline.columns): + df = deepcopy(timeline) + + # List to store rows with values outside the range + rows__out_of_unit_range = [] + rows__out_of_safety_range = [] + + # Iterate through each row + for index, row in df.iterrows(): + if not is_value_within_range(row["value"], row["unit_range"]): + print( + f"Value {row['value']} is outside device unit range {row['unit_range']} for {row['variable']} at time {row['time']} at dataframe index {index}." + ) + + # Append the row index to the list + rows__out_of_unit_range.append(index) + + if not is_value_within_range(row["value"], row["safety_range"]): + print( + f"Value {row['value']} is outside device safety range {row['safety_range']} for {row['variable']} at time {row['time']} at dataframe index {index}." + ) + + # Append the row index to the list + rows__out_of_safety_range.append(index) + + # Raise ValueError after printing all relevant information + if rows__out_of_unit_range or rows__out_of_safety_range: + raise ValueError( + f"Values outside the unit range: {rows__out_of_unit_range}!\n Values outside the safety range: {rows__out_of_safety_range}! \n\nPlease update these before proceeding." + ) + return timeline + + +def sanitize__drop_duplicates(timeline, subset=["variable", "time"]): + """ + Drop duplicate rows and drop rows where the variable and time are duplicated. + """ + return wt_frame.drop_duplicates(timeline, subset=subset) + + +def sanitize__round_value(timeline, num_decimal_places=6): + """ + Rounds the 'value' column to the given number of decimal places and returns the updated timeline. + """ + df = deepcopy(timeline) + df["value"] = df["value"].round(num_decimal_places) + return df + + +def sanitize(timeline): + """ + Check for duplicate, range and type errors in the current dataframe and either return an updated dataframe or an error. + + `sanitize__round_value` is not by default because this might be unexpected by the user. + """ + # TODO: Add check for negative times in the 'final' databases. + + return funcy.compose( + sanitize__drop_duplicates, + sanitize_values, + lambda df: wt_frame.cast( + df, + { + "variable": str, + "time": float, + "value": float, + # "context": str, # Currently, context can sometimes be None - this should be questioned though + }, + ), + )(timeline) diff --git a/wigner_time/util.py b/src/wigner/time/internal/util.py similarity index 90% rename from wigner_time/util.py rename to src/wigner/time/internal/util.py index b321789..49dddd7 100644 --- a/wigner_time/util.py +++ b/src/wigner/time/internal/util.py @@ -1,5 +1,9 @@ # Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) +""" +The inevitable `util` module for miscellaneous functions that haven't been organized yet. +""" + from collections.abc import Iterable, Sequence from typing import Callable, OrderedDict import inspect @@ -7,7 +11,7 @@ import numpy as np import math -from wigner_time.config import wtlog +from wigner.time.config import wtlog def is_sequence(x, is_string=False): @@ -85,6 +89,15 @@ def ensure_pair(l: list): raise ValueError(f"Unexpected argument to `ensure_pair`.") +def ensure_2d(input_data): + """Ensure the input is converted to a 2D list.""" + if isinstance(input_data, (list, tuple)): + if len(input_data) > 0 and isinstance(input_data[0], (list, tuple)): + return input_data + return [input_data] + return [[input_data]] + + def is_collection(x, is_string=False): """ Checks if x is a non-string sequence or numpy array by default. Strings can be included using the 'is_string' flag. @@ -113,6 +126,14 @@ def range__inclusive(start, stop, step): return np.linspace(start, stop, num=num) +def sample(lst: list, N: int): + """ + Retrive `N`, equally and maximally spaced, elements from the `list`. + """ + indices = np.linspace(0, len(lst) - 1, N, dtype=int) + return [lst[i] for i in indices] + + def function__filtered_kws(f: Callable, **kws) -> Callable: """ Converts the given function into a function lambda, where `kws` is used to update relevant arguments and other supplied kws are ignored. diff --git a/src/wigner/time/national_instruments/__init__.py b/src/wigner/time/national_instruments/__init__.py new file mode 100644 index 0000000..b633a73 --- /dev/null +++ b/src/wigner/time/national_instruments/__init__.py @@ -0,0 +1,10 @@ +""" +This is currently not implemented, but if it is of interest to you then please don't hesitate to get in touch: thomas.clark@wigner.hun-ren.hu . +""" +import warnings + +warnings.warn( + "This is currently not implemented, but if it is of interest to you then please don't hesitate to get in touch: thomas.clark@wigner.hun-ren.hu ." + UserWarning, + stacklevel=2 +) diff --git a/src/wigner/time/ramp_function.py b/src/wigner/time/ramp_function.py new file mode 100644 index 0000000..af56f3d --- /dev/null +++ b/src/wigner/time/ramp_function.py @@ -0,0 +1,71 @@ +# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) + +import numpy as np + +from wigner.time import config as wt_config +from wigner.time.internal import util as wt_util + + +def linear( + origin: list[float], + terminus: list[float], + time_resolution: float = wt_config.TIME_RESOLUTION, +): + """ + A series of [time, value] pairs according to the line defined by two points and the time resolution. + """ + t1, v1 = origin + t2, v2 = terminus + m = (v2 - v1) / (t2 - t1) + times = np.arange(t1, t2, time_resolution) + + return np.array([times, m * (times - t1) + v1]).transpose() + + +def _normalize(initial: float, final: float, factor: float): + """ + factor should be in [-0.5,+0.5]. + """ + + return factor * (final - initial) + (final + initial) / 2.0 + + +def _tanh__scaled(x: np.ndarray, sharpness=3): + return np.tanh(sharpness * (2.0 * (x - x[0]) / (x[-1] - x[0]) - 1.0)) / ( + 2.0 * np.tanh(sharpness) + ) + + +def tanh( + origin: list[float], + terminus: list[float], + time_resolution: float = wt_config.TIME_RESOLUTION, + sharpness: float = 3, +): + """ + Hyperbolic tan, with a call signature adapted for practical timeline population. + + origin/terminus are time-value pairs + `sharpness` is a measure of how linear the 'slope' of the function is around the halfway point and in practice is used for easing transitions between the end-points. For example, sharpness ~0 (!=0) gives a linear ramp between `origin` and `terminus`, whereas large values approximate a step-function at the half-way point. In-between these values, the ramps returned will start and end gradually, with a linear movement in the middle. + """ + + t1, v1 = origin + t2, v2 = terminus + times = wt_util.range__inclusive(t1, t2, time_resolution) + + return np.array( + [times, _normalize(v1, v2, _tanh__scaled(times, sharpness))] + ).transpose() + + +# import matplotlib.pyplot as plt + +# xs = np.linspace(0.0, 7.0, 100) +# for ti in [1e-3, 2, 3.14, 100]: +# plt.plot( +# tanh([0.0, 0.0], [1.0, 5.0], sharpness=ti)[:, 0], +# tanh([0.0, 0.0], [1.0, 5.0], sharpness=ti)[:, -1], +# label=ti, +# ) +# plt.legend() +# plt.show() diff --git a/wigner_time/timeline.py b/src/wigner/time/timeline.py similarity index 77% rename from wigner_time/timeline.py rename to src/wigner/time/timeline.py index 812df88..4b7b46f 100644 --- a/wigner_time/timeline.py +++ b/src/wigner/time/timeline.py @@ -9,20 +9,21 @@ It is a goal to be able to go up and down through the layers of abstraction. """ -from copy import deepcopy from typing import Callable import funcy import numpy as np -from wigner_time import anchor as wt_anchor -from wigner_time import config as wt_config -from wigner_time import input as wt_input -from wigner_time import ramp_function as wt_ramp_function -from wigner_time.internal import dataframe as wt_frame -from wigner_time.internal import origin as wt_origin -from wigner_time import util as wt_util -import pandas as pd +from wigner.time import config as wt_config +from wigner.time import ramp_function as wt_ramp_function +from wigner.time.internal.timeline import anchor as wt_anchor +from wigner.time.internal.timeline import input as wt_input +from wigner.time.internal import dataframe as wt_frame +from wigner.time.internal import origin as wt_origin +from wigner.time.internal.timeline import inherit + + +from wigner.time.internal import util as wt_util noop = funcy.identity @@ -31,10 +32,6 @@ ############################################################################### _SCHEMA = {"time": float, "variable": str, "value": float, "context": str} -_COLUMN_NAMES__RESERVED = list(_SCHEMA.keys()) + [ - "unit_range", - "safety_range", -] """These column names are assumed to exist and are used in core functions. Be careful about editing them.""" ############################################################################### @@ -42,6 +39,31 @@ ############################################################################### +def context_info(timeline): + """ + Useful data (currently 'variables' and 'times') concerning every context. The result is a dictionary, indexed by context. + + e.g. To get the start and end times of the 'MOT' context, call `context_info(timeline)['MOT']['times]`. + """ + # TODO: Remove dependence on pandas + + if {"context", "time", "variable"}.issubset(timeline.columns): + tlg = timeline.groupby("context") + return { + k: { + "variables": tlg["variable"].agg(set).to_dict()[k], + "times": tlg["time"] + .agg(["first", "last"]) + .apply(list, axis=1) + .to_dict()[k], + } + for k in tlg.groups.keys() + } + + else: + return None + + def previous( timeline: wt_frame.CLASS, variable=None, @@ -68,44 +90,6 @@ def previous( ) -def _mask__no_context(timeline): - if "context" in timeline.columns: - mask = timeline["context"] == "" - else: - mask = pd.Series(True, index=timeline.index) - - return mask - - -def inherit_context( - timeline, timeline__previous, context=None, is_inPlace=True, time__max=None -): - """ - Updates the context, taken from previous values where unspecified. - - Allows for situations where the new timelines are inserted at earlier times. - """ - if is_inPlace: - df = timeline - else: - df = deepcopy(timeline) - - if (timeline__previous is not None) and (context is None): - if time__max == "min": - time__max = timeline["time"].min() - - df.loc[_mask__no_context(timeline), "context"] = previous( - timeline__previous, time__max=time__max - )["context"] - return df - - elif (timeline__previous is None) and (context is not None): - df.loc[_mask__no_context(timeline), "context"] = context - - else: - return timeline - - ############################################################################### # Main functions ############################################################################### @@ -155,7 +139,7 @@ def create( new = wt_origin.update(df_rows, timeline, origin=origin) if timeline is not None: - inherit_context(new, timeline, context=context) + inherit.context(new, timeline, context=context) return wt_frame.concat([timeline, new]) return new @@ -216,6 +200,7 @@ def anchor( - Anchors are automatically numbered, for 'global' referencing, but these numbers are not necessary in normal use. """ # NOTE: Makes use of a global variable (LABEL__ANCHOR). + # TODO: Can include an example plot for illustration? # TODO: What happens if `t` is not specified? # - looks like it will fail? @@ -336,7 +321,7 @@ def ramp( wt_frame.concat([df_1, df__no_start_points]), timeline, origin=origin ) new1["function"] = function - inherit_context(new1, timeline, context=context) + inherit.context(new1, timeline, context=context) new2 = wt_origin.update(df_2, new1, origin=origin2) new2["function"] = function @@ -413,6 +398,46 @@ def stack( ) +def cascade(*fs: list[Callable], **kws) -> Callable | wt_frame.CLASS: + """ + Similarly to `stack`, a convenience that combines an arbitrary chain of functions with an arbitrary selection of associated keywords. + + Currently, `kws` are passed to the associated functions by prefixing, e.g. `cascade(MOT, molasses, MOT_duration=1.0)` creates a `stack` of `MOT` and `molasses`, with `duration=1.0` passed into the `MOT` function before evaluation. + + The motivation for this feature is that different experimental contexts should be built modularly, but, at final composition, the user often just wants a single point of contact to add/change nested variables. + + WARNING: API is not settled; may get combined with `stack` in the next release. + """ + # TODO: + # - Combine with `stack`? + # - Consider alternative names: 'compose'? + # - Consider nested dictionaries instead of prefixed keywords? + # + f_names = [f.__name__ for f in fs] + + # Create function-specific keywords + result = [] + for k in kws.keys(): + for fname in sorted(f_names, key=len, reverse=True): + if fname in k: + result.append([fname, k.split(fname, 1)[1].lstrip("_"), kws[k]]) + break + + args__dict = {} + for k, subk, v in result: + args__dict.setdefault(k, {})[subk] = v + # print(args__dict) + + # # Apply keywords to function stack + lambdas = [] + for f in fs: + args = args__dict.get(f.__name__, {}) + lambdas.append(f(**args)) + # lambdas.append(lambda ff=f, kws=args: ff(**kws)) + + return stack(*lambdas) + + def expand(timeline=None, num__bounds=2, **function_args) -> wt_frame.CLASS | Callable: """ Converts the functions marked in the timeline into individual rows, i.e. applies the functions to the given data. @@ -472,115 +497,3 @@ def expand(timeline=None, num__bounds=2, **function_args) -> wt_frame.CLASS | Ca # Add the values back into the main timeline return wt_frame.insert_dataframes(timeline, _inds__start, _dfs) - - -def is_value_within_range(value, unit_range): - # TODO: Shouldn't be here - internal function - if wt_frame.isnull(unit_range): - # If unit_range is NaN, consider it as within range - return True - else: - min_value, max_value = unit_range - return min_value <= value <= max_value - - -def sanitize_values(timeline): - """ - Ensures that the given timeline doesn't contain values outside of the given unit or safety range. - """ - # TODO: Check for efficiency - # - if ("unit_range" in timeline.columns) or ("safety_range" in timeline.columns): - df = deepcopy(timeline) - - # List to store rows with values outside the range - rows__out_of_unit_range = [] - rows__out_of_safety_range = [] - - # Iterate through each row - for index, row in df.iterrows(): - if not is_value_within_range(row["value"], row["unit_range"]): - print( - f"Value {row['value']} is outside device unit range {row['unit_range']} for {row['variable']} at time {row['time']} at dataframe index {index}." - ) - - # Append the row index to the list - rows__out_of_unit_range.append(index) - - if not is_value_within_range(row["value"], row["safety_range"]): - print( - f"Value {row['value']} is outside device safety range {row['safety_range']} for {row['variable']} at time {row['time']} at dataframe index {index}." - ) - - # Append the row index to the list - rows__out_of_safety_range.append(index) - - # Raise ValueError after printing all relevant information - if rows__out_of_unit_range or rows__out_of_safety_range: - raise ValueError( - f"Values outside the unit range: {rows__out_of_unit_range}!\n Values outside the safety range: {rows__out_of_safety_range}! \n\nPlease update these before proceeding." - ) - return timeline - - -def sanitize__drop_duplicates(timeline, subset=["variable", "time"]): - """ - Drop duplicate rows and drop rows where the variable and time are duplicated. - """ - return wt_frame.drop_duplicates(timeline, subset=subset) - - -def sanitize__round_value(timeline, num_decimal_places=6): - """ - Rounds the 'value' column to the given number of decimal places and returns the updated timeline. - """ - df = deepcopy(timeline) - df["value"] = df["value"].round(num_decimal_places) - return df - - -def sanitize(timeline): - """ - Check for duplicate, range and type errors in the current dataframe and either return an updated dataframe or an error. - - `sanitize__round_value` is not by default because this might be unexpected by the user. - """ - # TODO: Add check for negative times in the 'final' databases. - - return funcy.compose( - sanitize__drop_duplicates, - sanitize_values, - lambda df: wt_frame.cast( - df, - { - "variable": str, - "time": float, - "value": float, - # "context": str, # Currently, context can sometimes be None - this should be questioned though - }, - ), - )(timeline) - - -def context_info(timeline): - """ - Useful data (currently 'variables' and 'times') concerning every context. The result is a dictionary, indexed by context. - - e.g. To get the start and end times of the 'MOT' context, call `context_info(timeline)['MOT']['times]`. - """ - - if {"context", "time", "variable"}.issubset(timeline.columns): - tlg = timeline.groupby("context") - return { - k: { - "variables": tlg["variable"].agg(set).to_dict()[k], - "times": tlg["time"] - .agg(["first", "last"]) - .apply(list, axis=1) - .to_dict()[k], - } - for k in tlg.groups.keys() - } - - else: - return None diff --git a/src/wigner/time/variable.py b/src/wigner/time/variable.py new file mode 100644 index 0000000..1c45cd4 --- /dev/null +++ b/src/wigner/time/variable.py @@ -0,0 +1,59 @@ +""" +Outlines the conventions for variables and provides some convenience functions for working with them. +""" + +import re +from munch import Munch +from wigner.time.internal import dataframe as wt_frame +from wigner.time.config import LABEL__ANCHOR + +REGEX = re.compile(r"^([^_]+)_([^_]+)(?:__([^_]+))?$") + + +def parse(variable: str) -> dict: + """ + A dictionary of equipment, context and unit. + + The convention is that a variable is represented by `thing_deviceOfManyParts__unit` for a non-digital unit and `thing_deviceOfManyParts` otherwise. + """ + + match = re.match(REGEX, variable) + + if match is not None: + e, c, u = match.groups() + if u: + unit = u + elif LABEL__ANCHOR in e: + unit = LABEL__ANCHOR + else: + unit = "digital" + + return Munch(equipment=e, context=c, unit=unit) + else: + raise ValueError( + f"Variable {variable} doesn't meet the current naming convention." + ) + + +def is_valid(variable: str) -> bool: + try: + parse(variable) + return True + except ValueError: + return False + + +def unit(variable): + return parse(variable)["unit"] + + +def units(timeline: wt_frame.CLASS, do_digital: bool = True): + """ + Returns a set of different timeline units (strs). + """ + us = set(map(unit, timeline["variable"].unique())) + if do_digital: + return us + else: + us.discard("digital") + return us diff --git a/tests/wigner_time/internal/test_dataframe.py b/test/wigner/time/internal/test_dataframe.py similarity index 72% rename from tests/wigner_time/internal/test_dataframe.py rename to test/wigner/time/internal/test_dataframe.py index 9d38b65..e7039a3 100644 --- a/tests/wigner_time/internal/test_dataframe.py +++ b/test/wigner/time/internal/test_dataframe.py @@ -1,9 +1,6 @@ import pytest -import pandas as pd -# TODO: This should be abstracted - -from wigner_time.internal import dataframe as frame +from wigner.time.internal import dataframe as frame df_simple1 = frame.new( @@ -130,9 +127,49 @@ def test_replace_column__filtered(): ) -if __name__ == "__main__": - import importlib +@pytest.mark.parametrize("input", [df_simple1]) +def test_insert_dataframes(input): + frame.assert_equal( + frame.insert_dataframes(input, [1], [df_simple1]), + frame.new( + [ + ["thing2", 7.0, 5.0, "init"], + ["thing2", 7.0, 5.0, "init"], + ["thing", 0.0, 5.0, "init"], + ["thing3", 3.0, 5.0, "blah"], + ["thing", 0.0, 5.0, "init"], + ["thing3", 3.0, 5.0, "blah"], + ], + columns=["variable", "time", "value", "context"], + ), + ) - importlib.reload(frame) - print(frame.row_from_max_column(df_simple2)) +@pytest.mark.parametrize("input", [df_simple1]) +def test_subframe(input): + return frame.assert_equal( + frame.subframe(input, "variable", ["thing2"]), + frame.new( + [ + ["thing2", 7.0, 5.0, "init"], + ], + columns=["variable", "time", "value", "context"], + ), + ) + + +@pytest.mark.parametrize("input", [df_simple1]) +def test_subframe002(input): + calc = frame.subframe(input, "variable", [5], func=len) + new = frame.new( + [ + ["thing", 0.0, 5.0, "init"], + ], + columns=["variable", "time", "value", "context"], + ) + # print(calc) + # print(new) + return frame.assert_equal( + calc, + new, + ) diff --git a/tests/wigner_time/internal/test_origin.py b/test/wigner/time/internal/test_origin.py similarity index 96% rename from tests/wigner_time/internal/test_origin.py rename to test/wigner/time/internal/test_origin.py index 9f80a7c..bf8d78b 100644 --- a/tests/wigner_time/internal/test_origin.py +++ b/test/wigner/time/internal/test_origin.py @@ -1,8 +1,8 @@ import pytest import pandas as pd -from wigner_time.internal import dataframe as frame -from wigner_time.internal import origin +from wigner.time.internal import dataframe as frame +from wigner.time.internal import origin @pytest.fixture diff --git a/tests/wigner_time/test_adwin.py b/test/wigner/time/test_adwin.py similarity index 70% rename from tests/wigner_time/test_adwin.py rename to test/wigner/time/test_adwin.py index fa75f58..11ffdc5 100644 --- a/tests/wigner_time/test_adwin.py +++ b/test/wigner/time/test_adwin.py @@ -3,11 +3,15 @@ import pytest import pandas as pd -from wigner_time.adwin import core as adwin -from wigner_time import connection as con -from wigner_time import device -from wigner_time import timeline as tl -from wigner_time.internal import dataframe as frame +import wigner.time.adwin as wt_adwin + +from wigner.time.adwin import core as adwin +from wigner.time.adwin import connection as adcon +from wigner.time.adwin import validate as wt_validate +from wigner.time.adwin import internal as adi +from wigner.time import device +from wigner.time import timeline as tl +from wigner.time.internal import dataframe as frame sys.path.append(str(pl.Path.cwd() / "doc")) # import experimentDemo as ex @@ -30,7 +34,7 @@ def df_simple(): @pytest.fixture def connections_simple(): - return con.connection( + return adcon.new( ["AOM_imaging", 1, 1], ["AOM_imaging__V", 1, 2], ["AOM_repump", 2, 3], @@ -39,7 +43,7 @@ def connections_simple(): def test_remove_unconnected_variables(df_simple, connections_simple): return pd.testing.assert_frame_equal( - adwin.remove_unconnected_variables(df_simple, connections_simple), + adcon.remove_unconnected_variables(df_simple, connections_simple), pd.DataFrame( { "time": [0.0] * 3, @@ -51,39 +55,12 @@ def test_remove_unconnected_variables(df_simple, connections_simple): ) -def test_add_linear_conversion(df_simple): - df_devs = device.add_devices( - df_simple, - pd.DataFrame( - columns=["variable", "unit_range", "safety_range"], - data=[ - ["AOM_imaging__V", (-3, 3), (-3, 3)], - ], - ), - ) - - return pd.testing.assert_frame_equal( - adwin.add_linear_conversion(df_devs, "V"), - pd.DataFrame( - { - "time": [0.0, 0.0, 0.0, 0.0], - "variable": ["AOM_imaging", "AOM_imaging__V", "AOM_repump", "virtual"], - "value": [0.0, 2.0, 1.0, 1.0], - "context": ["init", "init", "init", "MOT"], - "unit_range": [None, (-3, 3), None, None], - "safety_range": [None, (-3, 3), None, None], - "value__digits": [None, 54613.0, None, None], - } - ), - ) - - def test_add_cycle(): df = pd.DataFrame({"time": range(10), "value": range(11, 21)}) df["context"] = ( ["MOT"] * 4 + ["ADwin_LowInit"] * 3 + ["ADwin_Init"] * 2 + ["ADwin_Finish"] ) - tst = frame.cast(adwin.add_cycle(df), adwin.SCHEMA) + tst = frame.cast(adi.add_cycle(df), wt_adwin.SCHEMA) return pd.testing.assert_frame_equal( tst, @@ -118,7 +95,7 @@ def test_add_cycle(): ], } ), - adwin.SCHEMA, + wt_adwin.SCHEMA, ), ) @@ -166,10 +143,10 @@ def test_add_cycle(): "module", "channel", "cycle", - "value_digits", + "value__digits", ], ), - adwin.SCHEMA, + wt_adwin.SCHEMA, ) @@ -189,10 +166,10 @@ def test_add_cycle(): "module", "channel", "cycle", - "value_digits", + "value__digits", ], ), - adwin.SCHEMA, + wt_adwin.SCHEMA, ) @@ -202,59 +179,34 @@ def test_add_cycle(): [0.0, "AOM_imaging__V", 2.0, "ADwin_Init", 1, 1, 0, 5], [0.0, "AOM_repump", 1.0, "init", 1, 1, 0, 5], ], - schema=adwin.SCHEMA, + schema=wt_adwin.SCHEMA, ) @pytest.mark.parametrize("input_value", [df_special1, df_special2]) def test_sanitize_raises(input_value): with pytest.raises(ValueError): - adwin.sanitize_special_contexts(input_value) + wt_validate.special_contexts(input_value) def test_sanitize_success(): return pd.testing.assert_frame_equal( - adwin.sanitize(df_special3), df_special3__corrected + wt_validate.all(df_special3), df_special3__corrected ) -def test_to_adbasic(): - connections = con.connection( +def test_convert(): + connections = adcon.new( ["shutter_MOT", 1, 11], ["lockbox_MOT__MHz", 3, 8], ) - devices = pd.DataFrame( - columns=["variable", "unit_range", "safety_range"], - data=[ - ["lockbox_MOT__V", (-10, 10), (-10, 10)], - ["lockbox_MOT__MHz", (-200, 200), (-200, 200)], - ], - ) - - print( - tl.stack( - tl.create( - lockbox_MOT__MHz=0.0, - shutter_MOT=0, - context="ADwin_LowInit", - ), - tl.anchor(t=0.0, origin=0.0, context="InitialAnchor"), - tl.update( - shutter_MOT=1, - context="MOT", - ), - tl.anchor(15), - tl.ramp( - lockbox_MOT__MHz=-5, - duration=10e-3, - context="MOT", - ), - tl.anchor(100e-3), - ) + devices = device.new( + ["lockbox_MOT__V", 1.0], + ["lockbox_MOT__MHz", 0.05], ) - tuples = adwin.to_data( + tuples = adwin.convert( tl.stack( tl.create( lockbox_MOT__MHz=0.0, @@ -283,7 +235,7 @@ def test_to_adbasic(): (-2, 3, 8, 32768), (3000000, 3, 8, 32768), (3001000, 3, 8, 32358), - (3002000, 3, 8, 31949), + (3002000, 3, 8, 31948), ], [ (-2, 1, 11, 0), @@ -291,5 +243,4 @@ def test_to_adbasic(): ], ] - # assert False assert tuples == tuples__guess diff --git a/test/wigner/time/test_connection.py b/test/wigner/time/test_connection.py new file mode 100644 index 0000000..92002c7 --- /dev/null +++ b/test/wigner/time/test_connection.py @@ -0,0 +1,83 @@ +import pytest +import pandas as pd +from munch import Munch + +from wigner.time import timeline as tl +from wigner.time import variable +from wigner.time.adwin import connection as adcon + + +@pytest.mark.parametrize( + "input", + [ + adcon.new("AOM_MOT__V", 1, 1), + adcon.new(["AOM_MOT__V", 1, 1]), + ], +) +def test_connectionSingle(input): + return pd.testing.assert_frame_equal( + input, pd.DataFrame([Munch(variable="AOM_MOT__V", module=1, channel=1)]) + ) + + +def test_connectionMany(): + tst = adcon.new( + ["shutter_MOT", 1, 11], ["shutter_repump", 1, 12], ["shutter_imaging", 1, 13] + ) + return pd.testing.assert_frame_equal( + tst, + pd.DataFrame( + [ + Munch(variable="shutter_MOT", module=1, channel=11), + Munch(variable="shutter_repump", module=1, channel=12), + Munch(variable="shutter_imaging", module=1, channel=13), + ] + ), + ) + + +def test_connectionName(): + assert ( + adcon.new( + ["shutter_MOT", 1, 11], + ["shutter_repump", 1, 12], + ["shutter_imaging", 1, 13], + ) + .variable.str.match(variable.REGEX) + .all() + ) + + +def test_connectionName002(): + assert adcon.is_valid_name( + adcon.new( + ["shutter_MOT", 1, 11], + ["shutter_repump", 1, 12], + ["shutter_imaging", 1, 13], + ) + ) + + +def test_connectionName003(): + assert ( + adcon.is_valid_name( + tl.create( + ["shutter_MOT", 1, 11], + ["shutter__repump", 1, 12], + ["shutter_imaging", 1, 13], + ) + ) + == False + ) + + +@pytest.mark.parametrize( + "input", + [ + ("AOMMOT__V", 1, 1), + (["AOMMOT", 1, 1]), + ], +) +def test_connectionSingleInvalid(input): + with pytest.raises(ValueError): + adcon.new(*input) diff --git a/test/wigner/time/test_conversion.py b/test/wigner/time/test_conversion.py new file mode 100644 index 0000000..494b68c --- /dev/null +++ b/test/wigner/time/test_conversion.py @@ -0,0 +1,175 @@ +import pytest +import pandas as pd +from munch import Munch +import numpy as np + +from wigner.time.internal import dataframe as wt_frame +from wigner.time import timeline as tl +from wigner.time import device +from wigner.time import conversion as conv + + +@pytest.fixture +def df_simple(): + return tl.create( + AOM_imaging=[0.0, 0.0, "init"], + AOM_imaging__V=[0.0, 2.0, "init"], + AOM_repump=[0.0, 1.0, "init"], + AOM_science__trans=[0.0, 1.0, "MOT"], + ) + + +@pytest.mark.parametrize( + "gain", + [1, 2, 4, 8], +) +def test_to_digits(gain): + assert conv.to_digits(0, [-10, 10], gain=gain) == 2**15 + + +@pytest.mark.parametrize( + "input", + list(zip([10, 5, 2.5, 1.25], [1, 2, 4, 8])), +) +def test_to_digits002(input): + assert conv.to_digits(input[0], [-10, 10], gain=input[1]) == 2**16 - 1 + + +@pytest.mark.parametrize("input", [4.0, np.array([4.0])]) +def test_to_digits003(input): + assert conv.to_digits(input) == 45874 + + +def test_add_linear_conversion(df_simple): + df_devs = device.add( + df_simple, + device.new( + "AOM_imaging__V", + 1.0, + -3, + 3, + ), + ) + + df_added = conv._add_linear(df_devs) + + return pd.testing.assert_frame_equal( + df_added, + wt_frame.new( + { + "time": [0.0, 0.0, 0.0, 0.0], + "variable": [ + "AOM_imaging", + "AOM_imaging__V", + "AOM_repump", + "AOM_science__trans", + ], + "value": [0.0, 2.0, 1.0, 1.0], + "context": ["init", "init", "init", "MOT"], + "to_V": [None, 1.0, None, None], + "value__min": [None, -3, None, None], + "value__max": [None, 3, None, None], + "value__digits": [None, 39321, None, None], + } + ).astype( + { + "time": float, + "variable": str, + "value": float, + "context": str, + "to_V": float, + "value__min": float, + "value__max": float, + "value__digits": float, + } + ), + ) + + +def func(x): + return x + 10 + + +@pytest.fixture +def df_devs(): + return device.add( + tl.create( + AOM_imaging=[0.0, 0.0, "init"], + AOM_imaging__transparency=[0.0, 0.5, "init"], + coil_MOT__A=[0.0, 1.0, "init"], + AOM_science__trans=[0.0, 1.0, "MOT"], + ), + device.new( + [ + "AOM_imaging__transparency", + func, + 0.0, + 1.0, + ], + ["coil_MOT__A", 0.333, -5.0, 5.0], + ), + ) + + +def test_add_function(df_devs): + wt_frame.assert_equal( + conv._add_function(df_devs)[["value", "to_V", "value__digits"]], + wt_frame.new( + [ + [ + 0.0, + np.nan, + np.nan, + ], + [0.5, func, 67173.0], + [1.0, 0.333, np.nan], + [1.0, np.nan, np.nan], + ], + columns=["value", "to_V", "value__digits"], + ), + ) + + +def test_add(df_devs): + calc = conv.add(df_devs)[["value", "to_V", "value__digits"]] + guess = wt_frame.new( + [ + [ + 0.0, + np.nan, + np.nan, + ], + [0.5, func, 67173.0], + [1.0, 0.333, 33859.0], + [1.0, np.nan, np.nan], + ], + columns=["value", "to_V", "value__digits"], + ) + # print(guess) + + return wt_frame.assert_equal(calc.astype({"value__digits": float}), guess) + + +def test_addRealistic(df_simple): + """ + A realistic use of conversion function from file. + """ + func__AOM = conv.function_from_file( + "resources/calibration/aom_calibration.dat", + names=["voltage", "transparency"], + sep=r"\s+", + ) + df = device.add(df_simple, device.new("AOM_science__trans", func__AOM, 0.0, 1.0)) + + actual = conv.add(df)[["value", "to_V", "value__digits"]] + expected = pd.DataFrame( + [ + [0.0, np.nan, np.nan], + [2.0, np.nan, np.nan], + [1.0, np.nan, np.nan], + [1.0, func__AOM, 49143], + ], + columns=["value", "to_V", "value__digits"], + ) + + return wt_frame.assert_equal(actual, expected) diff --git a/tests/wigner_time/test_demo.py b/test/wigner/time/test_demo.py similarity index 89% rename from tests/wigner_time/test_demo.py rename to test/wigner/time/test_demo.py index a8ed30a..157e693 100644 --- a/tests/wigner_time/test_demo.py +++ b/test/wigner/time/test_demo.py @@ -1,21 +1,12 @@ from copy import deepcopy import pandas as pd -from wigner_time import timeline as tl -from wigner_time import anchor as anchor -from wigner_time.internal import dataframe as frame +from wigner.time import timeline as tl +from wigner.time.internal.timeline import anchor as anchor +from wigner.time.internal import dataframe as frame -import pathlib as pl -import sys - - -sys.path.append(str(pl.Path.cwd() / "wigner_time/internal/doc")) - -import experimentDemo as ex - -# import importlib -# importlib.reload(ex) -# from wigner_time.adwin import display as adwin_display +from wigner.time.demo import full_experiment as ex +from wigner.time.adwin import display as adwin_display def replace_anchor_symbol(df, symbol__old="Anchor", symbol__new="⚓"): @@ -121,7 +112,7 @@ def test_MOT(): }, { "time": -1e-06, - "variable": "AOM_OP_aux", + "variable": "AOM_OPaux", "value": 0.0, "context": "ADwin_LowInit", }, @@ -175,8 +166,8 @@ def test_MOT(): }, { "time": -1e-06, - "variable": "AOM_science__V", - "value": 5.0, + "variable": "AOM_science__trans", + "value": 1.0, "context": "ADwin_LowInit", }, { @@ -222,9 +213,8 @@ def test_MOT(): ) # print(tl__new) # print(tl__original) - # adwin_display.channels(tl__original, do_show=False) - # adwin_display.channels(tl__new) + # adwin_display.quantities(tl__new) return frame.assert_equal(tl__new, tl__original) @@ -233,7 +223,7 @@ def test_MOTdetuned(): tl__new = tl.stack( ex.init(shutter_imaging=0, AOM_imaging=1, trigger_camera=0), ex.MOT(), - ex.MOT_detunedGrowth(), + ex.MOT__detuned_growth(), ).drop(columns="function") tl__original = pd.DataFrame( @@ -245,7 +235,7 @@ def test_MOTdetuned(): [-1e-06, "coil_MOTupperPlus__A", -0.1, "ADwin_LowInit"], [-1e-06, "AOM_MOT", 1.0, "ADwin_LowInit"], [-1e-06, "AOM_repump", 1.0, "ADwin_LowInit"], - [-1e-06, "AOM_OP_aux", 0.0, "ADwin_LowInit"], + [-1e-06, "AOM_OPaux", 0.0, "ADwin_LowInit"], [-1e-06, "AOM_OP", 1.0, "ADwin_LowInit"], [-1e-06, "AOM_science", 1.0, "ADwin_LowInit"], [-1e-06, "shutter_MOT", 0.0, "ADwin_LowInit"], @@ -254,7 +244,7 @@ def test_MOTdetuned(): [-1e-06, "shutter_OP002", 1.0, "ADwin_LowInit"], [-1e-06, "shutter_science", 0.0, "ADwin_LowInit"], [-1e-06, "shutter_transversePump", 0.0, "ADwin_LowInit"], - [-1e-06, "AOM_science__V", 5.0, "ADwin_LowInit"], + [-1e-06, "AOM_science__trans", 1.0, "ADwin_LowInit"], [-1e-06, "trigger_TC__V", 0.0, "ADwin_LowInit"], [-1e-06, "shutter_imaging", 0.0, "ADwin_LowInit"], [-1e-06, "AOM_imaging", 1.0, "ADwin_LowInit"], @@ -270,7 +260,6 @@ def test_MOTdetuned(): ], columns=["time", "variable", "value", "context"], ) - return frame.assert_equal(tl__new, tl__original) @@ -309,10 +298,10 @@ def test_fullDemo(): actual = tl.stack( ex.init(), ex.MOT(duration=1), - ex.MOT_detunedGrowth(), + ex.MOT__detuned_growth(), ex.molasses(), - ex.OP(), - ex.magneticTrapping(), + ex.optical_pumping(), + ex.magnetic_trapping(), ex.pull_coils(50e-3, -4.1, -4.7, -0.6, -0.6), ex.finish(), ).drop(columns=["function"]) @@ -435,7 +424,7 @@ def test_fullDemo(): "coil_MOTupperPlus__A", "AOM_MOT", "AOM_repump", - "AOM_OP_aux", + "AOM_OPaux", "AOM_OP", "AOM_science", "shutter_MOT", @@ -444,7 +433,7 @@ def test_fullDemo(): "shutter_OP002", "shutter_science", "shutter_transversePump", - "AOM_science__V", + "AOM_science__trans", "trigger_TC__V", "shutter_MOT", "shutter_repump", @@ -524,7 +513,7 @@ def test_fullDemo(): "coil_MOTupperPlus__A", "AOM_MOT", "AOM_repump", - "AOM_OP_aux", + "AOM_OPaux", "AOM_OP", "AOM_science", "shutter_MOT", @@ -533,7 +522,7 @@ def test_fullDemo(): "shutter_OP002", "shutter_science", "shutter_transversePump", - "AOM_science__V", + "AOM_science__trans", "trigger_TC__V", ], "value": [ @@ -553,7 +542,7 @@ def test_fullDemo(): 1.0, 0.0, 0.0, - 5.0, + 1.0, 0.0, 1.0, 1.0, @@ -642,7 +631,7 @@ def test_fullDemo(): 1.0, 0.0, 0.0, - 5.0, + 1.0, 0.0, ], "context": [ @@ -681,45 +670,45 @@ def test_fullDemo(): "molasses", "molasses", "molasses", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "magneticTrapping", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", - "OP", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "magnetic_trapping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", + "optical_pumping", "finalRamps", "finalRamps", "finalRamps", @@ -756,4 +745,11 @@ def test_fullDemo(): ], } ) + + # print("actual") + # i1 = 50 + # i2 = i1 + 10 + # print(actual[i1:i2]) + # print("expected") + # print(expected[i1:i2]) return frame.assert_equal(actual, expected) diff --git a/test/wigner/time/test_device.py b/test/wigner/time/test_device.py new file mode 100644 index 0000000..7708812 --- /dev/null +++ b/test/wigner/time/test_device.py @@ -0,0 +1,148 @@ +import pytest +import numpy as np + +from wigner.time.internal import dataframe as wt_frame +from wigner.time import device as dev + + +@pytest.mark.parametrize( + "input", + [ + dev.new( + "coil_compensationX__A", + 3 / 10.0, + -3.0, + 3.0, + ), + dev.new( + [ + "coil_compensationX__A", + 3 / 10.0, + -3.0, + 3.0, + ] + ), + ], +) +def test_deviceSingle(input): + comparison = wt_frame.new_schema( + [ + [ + "coil_compensationX__A", + 3 / 10.0, + -3.0, + 3.0, + ] + ], + dev.SCHEMA__expanded, + ) + + return wt_frame.assert_equal(input, comparison) + + +@pytest.mark.parametrize( + "input", + [ + dev.new( + ["coil_compensationY__A", 0.33, -np.inf, np.inf], + ["coil_MOTlower__A", 0.5, -np.inf, np.inf], + ["coil_MOTupper__A", 0.5, -np.inf, np.inf], + ), + dev.new( + ["coil_compensationY__A", 0.33], + ["coil_MOTlower__A", 0.5, -np.inf], + ["coil_MOTupper__A", 0.5], + ), + ], +) +def test_deviceMultiple(input): + return wt_frame.assert_equal( + input, + wt_frame.new_schema( + [ + ["coil_compensationY__A", 0.33, -np.inf, +np.inf], + ["coil_MOTlower__A", 0.5, -np.inf, +np.inf], + ["coil_MOTupper__A", 0.5, -np.inf, +np.inf], + ], + dev.SCHEMA__expanded, + ), + ) + + +def test_input_number(): + with pytest.raises(ValueError): + dev.new("coil_compensationX__A", 3 / 10.0, -3.0, 3.0, 5.0) + + +@pytest.fixture +def func(): + return "does things" + + +@pytest.mark.parametrize( + "input", + [ + ["coil_compensationY__A", func], + # ["coil_compensationY__A", lambda x: "does things"], + ], +) +def test_function(input): + wt_frame.assert_equal( + dev.new(*input), + wt_frame.new_schema( + [ + ["coil_compensationY__A", func, -np.inf, +np.inf], + ], + dev.SCHEMA, + ), + ) + + +@pytest.mark.parametrize( + "input", + [ + ["coil_compensationY__A", func, -3, 3], + ], +) +def test_function002(input): + wt_frame.assert_equal( + dev.new(*input), + wt_frame.new_schema( + [ + ["coil_compensationY__A", func, -3, 3], + ], + dev.SCHEMA, + ), + ) + + +def test_check_safety_range001(): + df = dev.new( + ["coil_compensationY__A", 0.33, -5, 5], + ["coil_MOTlower__A", 0.5, -2.5, 3], + ["coil_MOTupper__A", 0.5, -np.inf, np.inf], + ) + df["value"] = [5.0, -2.5, 0.0] + + assert dev.check_within_range(df) == True + + +@pytest.mark.parametrize( + "input", + [ + -5.0001, + 5.00000001, + -2.6, + "test", + ], +) +def test_check_safety_range002(input): + df = dev.new( + ["coil_compensationY__A", 0.33, -5, 5], + ["coil_MOTlower__A", 0.5, -2.5, 3], + ["coil_MOTupper__A", 0.5, -np.inf, np.inf], + ) + df["value"] = input + + with pytest.raises(ValueError): + dev.check_within_range(df) diff --git a/test/wigner/time/test_display.py b/test/wigner/time/test_display.py new file mode 100644 index 0000000..0bf604b --- /dev/null +++ b/test/wigner/time/test_display.py @@ -0,0 +1,26 @@ +from wigner.time import timeline as tl +from wigner.time.adwin import display as adwin_display + +import sys +import pathlib as pl + +from wigner.time.demo import full_experiment as ex + + +def test_displayIndividualTypes(): + tl__new = tl.stack( + ex.init(shutter_imaging=0, AOM_imaging=1, trigger_camera=0), + ex.MOT(), + ex.MOT__detuned_growth(), + ).drop(columns="function") + + adwin_display.quantities( + tl__new, variables=["lockbox_MOT__MHz"], do_show=False, range__x=[14.99, 15.02] + ) + adwin_display.quantities(tl__new, variables=["shutter_MOT"], do_show=False) + adwin_display.quantities( + tl__new, + variables=["lockbox_MOT__MHz", "shutter_MOT"], + do_show=False, + range__x=[14.99, 15.02], + ) diff --git a/test/wigner/time/test_file.py b/test/wigner/time/test_file.py new file mode 100644 index 0000000..d33711c --- /dev/null +++ b/test/wigner/time/test_file.py @@ -0,0 +1,80 @@ +import pytest +from pathlib import Path + +from wigner.time import file +from wigner.time import timeline as tl +from wigner.time.internal import dataframe as frame + + +from wigner.time.demo import full_experiment as demo + + +@pytest.fixture +def timeline__demo(): + return tl.cascade( + demo.init, + demo.MOT, + ) + + +@pytest.fixture +def timeline__demo__function(): + return tl.cascade(demo.init, demo.MOT, demo.MOT__detuned_growth) + + +def test_save_load__autoname(timeline__demo): + file.save(timeline__demo) + actual = file.load("timeline__demo.parquet") + return frame.assert_equal(actual, timeline__demo) + + +@pytest.mark.parametrize( + "fname", + [ + "timeline__demo.parquet", + "timeline__demo.csv", + "timeline__demo.json", + "timeline__demo.pickle", + "timeline__demo.feather", + ], +) +def test_save_load__types(fname, timeline__demo): + file.save(timeline__demo, fname) + actual = file.load(fname) + return frame.assert_equal(actual, timeline__demo) + + +@pytest.mark.parametrize( + "fname", + [ + "timeline__demo__function.parquet", + "timeline__demo__function.csv", + "timeline__demo__function.json", + "timeline__demo__function.pickle", + "timeline__demo__function.feather", + ], +) +def test_save_load__types_with_functions(fname, timeline__demo__function): + file.save(timeline__demo__function, fname) + actual = file.load(fname) + + mask = actual["function"].notna() + + if bool(actual.loc[mask, "function"].map(lambda x: isinstance(x, str)).all()): + output = timeline__demo__function.copy(deep=True) + output.loc[mask, "function"] = "wigner.time.ramp_function.tanh" + else: + output = timeline__demo__function + + return frame.assert_equal(actual, output) + + +def test_save_load__increment_name(timeline__demo): + file.save(timeline__demo) + t1 = Path("timeline__demo.parquet").exists() + file.save(timeline__demo) + t2 = Path("timeline__demo__002.parquet").exists() + file.save(timeline__demo) + t3 = Path("timeline__demo__003.parquet").exists() + + assert t1 and t2 and t3 diff --git a/tests/wigner_time/test_input.py b/test/wigner/time/test_input.py similarity index 95% rename from tests/wigner_time/test_input.py rename to test/wigner/time/test_input.py index 5936d0d..045c47a 100644 --- a/tests/wigner_time/test_input.py +++ b/test/wigner/time/test_input.py @@ -1,8 +1,8 @@ import pytest import pandas as pd -from wigner_time import timeline as tl -from wigner_time import input +from wigner.time import timeline as tl +from wigner.time.internal.timeline import input def test_ensure_time_context(): diff --git a/test/wigner/time/test_util.py b/test/wigner/time/test_util.py new file mode 100644 index 0000000..0dd5096 --- /dev/null +++ b/test/wigner/time/test_util.py @@ -0,0 +1,55 @@ +import pytest + +from wigner.time.internal import util +from wigner.time import timeline as tl +from wigner.time.internal import dataframe as wt_frame + + +@pytest.mark.parametrize( + "input", + ["thing", ["thing"], [["thing"]]], +) +def test_ensure_2d(input): + assert util.ensure_2d(input) == [["thing"]] + + +@pytest.mark.parametrize( + "input", + [5, [5], [[5]]], +) +def test_ensure_2d_nums(input): + assert util.ensure_2d(input) == [[5]] + + +@pytest.mark.parametrize( + "input", + [["AOM_MOT__V", 1, 1], [["AOM_MOT__V", 1, 1]]], +) +def test_ensure_2d_multi(input): + assert util.ensure_2d(input) == [["AOM_MOT__V", 1, 1]] + + +# @pytest.fixture +# @pytest.mark.parametrize( +# "input", +# [ +# tl.create("AOM_imaging", [[0.0, 0.0]]), +# ], +# ) + + +def test_function__deferred(): + tl.ramp(AOM_imaging__V=[1.0, 1.0]) + + actual = tl.stack( + tl.create("AOM_imaging__V", 0.0, 0.0), + tl.ramp(AOM_imaging__V=[1.0, 1.0]), + tl.update(AOM_imaging__V=[1.0, 0.0]), + ) + + return wt_frame.assert_equal( + actual[["time", "variable", "value"]], + tl.create(AOM_imaging__V=[[0.0, 0.0], [0.0, 0.0], [1.0, 1.0], [2.0, 0.0]])[ + ["time", "variable", "value"] + ], + ) diff --git a/test/wigner/time/test_variable.py b/test/wigner/time/test_variable.py new file mode 100644 index 0000000..4ab9b7d --- /dev/null +++ b/test/wigner/time/test_variable.py @@ -0,0 +1,64 @@ +import pytest + +from wigner.time import timeline as tl +from wigner.time import variable + + +def test_variable(): + assert variable.parse("thing_deviceOfManyParts__unit") == { + "equipment": "thing", + "context": "deviceOfManyParts", + "unit": "unit", + } + + +def test_variable_no_unit(): + assert variable.parse("thing_deviceOfManyParts") == { + "equipment": "thing", + "context": "deviceOfManyParts", + "unit": "digital", + } + + +def test_is_valid(): + assert variable.is_valid("AOM_imaging__V") == True + + +def test_is_valid002(): + assert variable.is_valid("AOMimaging__V") == False + + +def test_unit(): + assert variable.unit("AOM_imaging__MHz") == "MHz" + + +def test_unit002(): + assert variable.unit("AOM_imaging") == "digital" + + +def test_unit003(): + with pytest.raises(ValueError): + variable.unit("AOMimaging__V") + + +def test_units(): + assert variable.units( + tl.create( + ["AOM_imaging__V", [[0.0, 2]]], + ["AOM_repump", [[1.0, 1.0]]], + ["coil_MOT__A", [[1.0, 10.0]]], + ["AOM_repump__MHz", [[1.0, 10.0]]], + ), + ) == {"A", "MHz", "V", "digital"} + + +def test_units_nodigital(): + assert variable.units( + tl.create( + ["AOM_imaging__V", [[0.0, 2]]], + ["AOM_repump", [[1.0, 1.0]]], + ["coil_MOT__A", [[1.0, 10.0]]], + ["AOM_repump__MHz", [[1.0, 10.0]]], + ), + do_digital=False, + ) == {"A", "MHz", "V"} diff --git a/tests/wigner_time/timeline/test_timeline_anchor.py b/test/wigner/time/timeline/test_timeline_anchor.py similarity index 91% rename from tests/wigner_time/timeline/test_timeline_anchor.py rename to test/wigner/time/timeline/test_timeline_anchor.py index b4f770d..c3e32f1 100644 --- a/tests/wigner_time/timeline/test_timeline_anchor.py +++ b/test/wigner/time/timeline/test_timeline_anchor.py @@ -1,10 +1,10 @@ import pytest from munch import Munch -from wigner_time import config as wt_config -from wigner_time import ramp_function -from wigner_time import timeline as tl -from wigner_time.internal import dataframe as wt_frame +from wigner.time import config as wt_config +from wigner.time import ramp_function +from wigner.time import timeline as tl +from wigner.time.internal import dataframe as wt_frame def test_anchor__basic(): diff --git a/tests/wigner_time/timeline/test_timeline_create.py b/test/wigner/time/timeline/test_timeline_create.py similarity index 98% rename from tests/wigner_time/timeline/test_timeline_create.py rename to test/wigner/time/timeline/test_timeline_create.py index e655838..3dbf9b3 100644 --- a/tests/wigner_time/timeline/test_timeline_create.py +++ b/test/wigner/time/timeline/test_timeline_create.py @@ -1,8 +1,8 @@ import pytest -from wigner_time import timeline as tl -from wigner_time.internal import dataframe as wt_frame -from wigner_time.internal import origin +from wigner.time import timeline as tl +from wigner.time.internal import dataframe as wt_frame +from wigner.time.internal import origin @pytest.fixture diff --git a/tests/wigner_time/timeline/test_timeline_manipulate.py b/test/wigner/time/timeline/test_timeline_manipulate.py similarity index 61% rename from tests/wigner_time/timeline/test_timeline_manipulate.py rename to test/wigner/time/timeline/test_timeline_manipulate.py index 58fe115..7931225 100644 --- a/tests/wigner_time/timeline/test_timeline_manipulate.py +++ b/test/wigner/time/timeline/test_timeline_manipulate.py @@ -1,8 +1,11 @@ +import pathlib as pl +import sys import pytest -from wigner_time import timeline as tl -from wigner_time.internal import dataframe as frame +from wigner.time import timeline as tl +from wigner.time.internal import dataframe as frame +from wigner.time.demo import full_experiment as ex # @pytest.fixture # def df_wait(): @@ -69,6 +72,48 @@ def test_stack__kws(dfseq): ) +def test_cascade(): + frame.assert_equal( + tl.cascade( + ex.init, + ex.MOT, + # + MOT_duration=5.0, + MOT_lA=-1.0, + MOT_uA=-0.98, + molasses_duration=5.0, + ), + frame.new( + [ + [-1e-06, "lockbox_MOT__MHz", 0.0, "ADwin_LowInit"], + [-1e-06, "coil_compensationX__A", 0.25, "ADwin_LowInit"], + [-1e-06, "coil_compensationY__A", 1.5, "ADwin_LowInit"], + [-1e-06, "coil_MOTlowerPlus__A", 0.1, "ADwin_LowInit"], + [-1e-06, "coil_MOTupperPlus__A", -0.1, "ADwin_LowInit"], + [-1e-06, "AOM_MOT", 1.0, "ADwin_LowInit"], + [-1e-06, "AOM_repump", 1.0, "ADwin_LowInit"], + [-1e-06, "AOM_OPaux", 0.0, "ADwin_LowInit"], + [-1e-06, "AOM_OP", 1.0, "ADwin_LowInit"], + [-1e-06, "AOM_science", 1.0, "ADwin_LowInit"], + [-1e-06, "shutter_MOT", 0.0, "ADwin_LowInit"], + [-1e-06, "shutter_repump", 0.0, "ADwin_LowInit"], + [-1e-06, "shutter_OP001", 0.0, "ADwin_LowInit"], + [-1e-06, "shutter_OP002", 1.0, "ADwin_LowInit"], + [-1e-06, "shutter_science", 0.0, "ADwin_LowInit"], + [-1e-06, "shutter_transversePump", 0.0, "ADwin_LowInit"], + [-1e-06, "AOM_science__trans", 1.0, "ADwin_LowInit"], + [-1e-06, "trigger_TC__V", 0.0, "ADwin_LowInit"], + [0.0, "shutter_MOT", 1.0, "MOT"], + [0.0, "shutter_repump", 1.0, "MOT"], + [0.0, "coil_MOTlower__A", -1.0, "MOT"], + [0.0, "coil_MOTupper__A", -0.98, "MOT"], + [5.0, "⚓_001", 0.0, "MOT"], + ], + columns=["time", "variable", "value", "context"], + ), + ) + + # def test_waitVariable(df_wait): # return frame.assert_equal( # tl.wait(variables=["AOM_imaging"], timeline=df_wait, context="test"), diff --git a/tests/wigner_time/timeline/test_timeline_query.py b/test/wigner/time/timeline/test_timeline_query.py similarity index 92% rename from tests/wigner_time/timeline/test_timeline_query.py rename to test/wigner/time/timeline/test_timeline_query.py index d332229..37d7b05 100644 --- a/tests/wigner_time/timeline/test_timeline_query.py +++ b/test/wigner/time/timeline/test_timeline_query.py @@ -1,8 +1,8 @@ import pytest -from wigner_time import timeline as tl -from wigner_time.internal import dataframe as frame -from wigner_time.internal import origin +from wigner.time import timeline as tl +from wigner.time.internal import dataframe as frame +from wigner.time.internal import origin df_previous1 = frame.new( diff --git a/tests/wigner_time/timeline/test_timeline_ramp.py b/test/wigner/time/timeline/test_timeline_ramp.py similarity index 97% rename from tests/wigner_time/timeline/test_timeline_ramp.py rename to test/wigner/time/timeline/test_timeline_ramp.py index 364d208..b28447d 100644 --- a/tests/wigner_time/timeline/test_timeline_ramp.py +++ b/test/wigner/time/timeline/test_timeline_ramp.py @@ -2,15 +2,14 @@ from munch import Munch import numpy as np -from wigner_time import ramp_function, timeline as tl -from wigner_time.adwin import display -from wigner_time.internal import dataframe as wt_frame +from wigner.time import ramp_function, timeline as tl +from wigner.time.adwin import display +from wigner.time.internal import dataframe as wt_frame import pathlib as pl import sys -sys.path.append(str(pl.Path.cwd() / "wigner_time/internal/doc")) -import experimentDemo as ex +from wigner.time.demo import full_experiment as ex @pytest.fixture @@ -293,7 +292,7 @@ def test_rampReal(): timeline = tl.stack( ex.init(), ex.MOT(duration=1), - ex.MOT_detunedGrowth(), + ex.MOT__detuned_growth(), tl.ramp(t=1, duration=0.1, lockbox_MOT__MHz=-2), tl.ramp(t=0.5, duration=0.1, lockbox_MOT__MHz=-1), ) @@ -332,7 +331,7 @@ def test_rampReal2(): timeline = tl.stack( ex.init(), ex.MOT(duration=1), - ex.MOT_detunedGrowth(), + ex.MOT__detuned_growth(), tl.ramp(t=1, duration=0.1, lockbox_MOT__MHz=-2), tl.ramp(t=0.5, duration=0.1, lockbox_MOT__MHz=-1), tl.ramp(t=0.75, duration=0.1, lockbox_MOT__MHz=-5), diff --git a/tests/wigner_time/timeline/test_timeline_validate.py b/test/wigner/time/timeline/test_timeline_validate.py similarity index 81% rename from tests/wigner_time/timeline/test_timeline_validate.py rename to test/wigner/time/timeline/test_timeline_validate.py index 5f24d25..10b7c72 100644 --- a/tests/wigner_time/timeline/test_timeline_validate.py +++ b/test/wigner/time/timeline/test_timeline_validate.py @@ -1,7 +1,11 @@ import pytest -from wigner_time import timeline as tl -from wigner_time.internal import dataframe as frame +from wigner.time.internal.timeline import validate +from wigner.time.internal import dataframe as frame + +# TODO: +# - Update the test schema to use min and max values rather than ranges +# - Consider whether the sanitize/validate namespace is actually useful. devices001 = frame.new( [ @@ -40,7 +44,7 @@ def test_sanitize_raises(input_value): df, dev = input_value with pytest.raises(ValueError): - tl.sanitize(frame.join(df, dev)) + validate.sanitize(frame.join(df, dev)) @pytest.mark.parametrize( @@ -52,7 +56,7 @@ def test_sanitize_raises(input_value): def test_sanitize_success(input_value): df, dev = input_value return frame.assert_equal( - tl.sanitize(df), + validate.sanitize(df), frame.new( [ [0.0, "AOM_imaging", 0.0, ""], diff --git a/tests/wigner_time/test_connection.py b/tests/wigner_time/test_connection.py deleted file mode 100644 index 059db4f..0000000 --- a/tests/wigner_time/test_connection.py +++ /dev/null @@ -1,44 +0,0 @@ -import pytest -import pandas as pd -from munch import Munch - -from wigner_time import connection as con - - -def test_connectionSingleDict(): - tst = con.connection("AOM_MOT__V", 1, 1, type="dict") - assert tst == Munch(variable="AOM_MOT__V", module=1, channel=1) - - -def test_connectionManyDict(): - tst = con.connection( - ["shutter_MOT", 1, 11], - ["shutter_repump", 1, 12], - ["shutter_imaging", 1, 13], - type="dict" - ) - assert tst == [ - Munch(variable="shutter_MOT", module=1, channel=11), - Munch(variable="shutter_repump", module=1, channel=12), - Munch(variable="shutter_imaging", module=1, channel=13), - ] - - -def test_connectionSingleDataFrame(): - tst = con.connection("AOM_MOT__V", 1, 1) - return pd.testing.assert_frame_equal(tst, pd.DataFrame([Munch(variable="AOM_MOT__V", module=1, channel=1)])) - - - -def test_connectionManyDataFrame(): - tst = con.connection( - ["shutter_MOT", 1, 11], - ["shutter_repump", 1, 12], - ["shutter_imaging", 1, 13] - - ) - return pd.testing.assert_frame_equal(tst,pd.DataFrame([ - Munch(variable="shutter_MOT", module=1, channel=11), - Munch(variable="shutter_repump", module=1, channel=12), - Munch(variable="shutter_imaging", module=1, channel=13), - ])) diff --git a/tests/wigner_time/test_util.py b/tests/wigner_time/test_util.py deleted file mode 100644 index 466cc0c..0000000 --- a/tests/wigner_time/test_util.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from wigner_time import timeline as tl -from wigner_time import util as wt_util -from wigner_time.internal import dataframe as wt_frame - -# @pytest.fixture -# @pytest.mark.parametrize( -# "input", -# [ -# tl.create("AOM_imaging", [[0.0, 0.0]]), -# ], -# ) - - -def test_function__deferred(): - tl.ramp(AOM_imaging__V=[1.0, 1.0]) - - actual = tl.stack( - tl.create("AOM_imaging__V", 0.0, 0.0), - tl.ramp(AOM_imaging__V=[1.0, 1.0]), - tl.update(AOM_imaging__V=[1.0, 0.0]), - ) - - return wt_frame.assert_equal( - actual[["time", "variable", "value"]], - tl.create(AOM_imaging__V=[[0.0, 0.0], [0.0, 0.0], [1.0, 1.0], [2.0, 0.0]])[ - ["time", "variable", "value"] - ], - ) diff --git a/wigner_time/adwin/__init__.py b/wigner_time/adwin/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/wigner_time/adwin/core.py b/wigner_time/adwin/core.py deleted file mode 100644 index 0661477..0000000 --- a/wigner_time/adwin/core.py +++ /dev/null @@ -1,413 +0,0 @@ -# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) - -from copy import deepcopy - -import funcy -import numpy as np - -from wigner_time import timeline as tl -from wigner_time import conversion as conv -from wigner_time.internal import dataframe as wt_frame - - -""" -Represents the key ADwin settings for the given machine. - -These should be loaded by the ADwin system during initialization. The dictionary of settings should grow as large as possible (to encompass all of the internal ADwin features) for maximum reproducibility. -""" -SPECIFICATIONS__DEFAULT = { - "device_001": { - "cycle_period__normal__us": 5e-6, - "module_001": { - "bits": 16, - "voltage_range": [-10.0, 10.0], - "gain": 1, - }, - "module_002": { - "bits": 16, - "voltage_range": [-10.0, 10.0], - "gain": 1, - }, - "module_003": { - "bits": 16, - "voltage_range": [-10.0, 10.0], - "gain": 1, - }, - "module_004": { - "bits": 16, - "voltage_range": [-10.0, 10.0], - "gain": 1, - }, - }, -} -# TODO: Rather than naming the above with numbers, this could be a list of dicts. - -CONTEXTS__SPECIAL = {"ADwin_LowInit": -2, "ADwin_Init": -1, "ADwin_Finish": 2**31 - 1} -"""Used for passing information to the ADwin controller""" - - -SCHEMA = { - "time": float, - "variable": str, - "value": float, - "context": str, - "module": int, - "channel": int, - "cycle": np.int32, - "value_digits": np.int32, -} - - -def remove_unconnected_variables(timeline, connections): - """ - Purges the given timeline of any `variable`s that do not have a matching `connection`. - - NOTE: Assumes timeline and connections are both pd.DataFrame-like things - """ - # TODO: Shouldn't be here!!! More general - - timeline = deepcopy(timeline) - _disconnections = [ - v - for v in timeline["variable"].unique() - if v not in connections["variable"].unique() - ] - - for v in _disconnections: - timeline.drop(timeline[timeline.variable == v].index, inplace=True) - - return timeline - - -def add_cycle( - timeline, - specifications=SPECIFICATIONS__DEFAULT, - special_contexts=CONTEXTS__SPECIAL, - device="device_001", -): - """ - Inserts a new `cycle` column into the timeline as a conversion of the `time` column into 'number of cycles'. - - Parameters: - - df: DataFrame containing the experimental data. - - specifications: Dictionary with device-specific configuration, must contain cycle period. - - special_contexts: Dictionary with context-specific overrides for cycle values. - - device: Device name to use for cycle period in specifications. - - Raises: - - ValueError if required columns are missing or if cycle period is not found for specified device. - """ - # Check if `time` column is present - if "time" not in timeline.columns: - raise ValueError( - f"`time` column not found. Columns present: {list(timeline.columns)}" - ) - - # Ensure device-specific cycle period is available - try: - cycle_period = specifications[device]["cycle_period__normal__us"] - except KeyError: - raise ValueError( - f"`cycle_period__normal` not found in specifications for {device}." - ) - - # Calculate cycles and handle special contexts - timeline["cycle"] = np.round(timeline["time"].values / cycle_period).astype( - np.int32 - ) - - # Apply special context cycles - timeline = wt_frame.replace_column__filtered( - timeline, - CONTEXTS__SPECIAL, - column__change="cycle", - ) - - return timeline - - -def initialize_ADwin(machine__adwin, output, specifications=SPECIFICATIONS__DEFAULT, printDiagnostics=False): - """ - General setup of the *system*, rather than the specific experimental project. - - NOTE: Stateful. - """ - # TODO: - # - This would probably be easier if it accepted a dataframe - # - Should we prepare all of the possible variables or does this waste memory? - - cycles = np.array([np.array(output[i])[:, 0] for i in range(2)]).flatten() - # Finds the maximum cycle value, discounting special contexts - time_end__cycles = cycles[~np.isin(cycles, list(CONTEXTS__SPECIAL.values()))].max() - - if (printDiagnostics) : - print( - "=== time_end: {}s ===".format( - time_end__cycles * specifications["device_001"]["cycle_period__normal__us"] - ) - ) - - # TODO: What's happening below should be explained here - machine__adwin.Set_Par(1, int(time_end__cycles)) - machine__adwin.Set_Par(2, len(output[0])) - machine__adwin.Set_Par(3, len(output[1])) - - machine__adwin.SetData_Long([a[0] for a in output[0]], 10, 1, len(output[0])) - machine__adwin.SetData_Long([a[1] for a in output[0]], 11, 1, len(output[0])) - machine__adwin.SetData_Long([a[2] for a in output[0]], 12, 1, len(output[0])) - machine__adwin.SetData_Long([a[3] for a in output[0]], 13, 1, len(output[0])) - - machine__adwin.SetData_Long([d[0] for d in output[1]], 20, 1, len(output[1])) - machine__adwin.SetData_Long([d[1] for d in output[1]], 21, 1, len(output[1])) - machine__adwin.SetData_Long([d[2] for d in output[1]], 22, 1, len(output[1])) - machine__adwin.SetData_Long([d[3] for d in output[1]], 23, 1, len(output[1])) - - return machine__adwin - - -def check_safety_range(timeline): - """ - Checks whether the values sent to this device fall inside its safety range. - """ - for variable, group in timeline.groupby("variable"): - if group["safety_range"].any(): - if max(group["value"].values) > max(group["safety_range"].values[0]): - raise ValueError( - "{} was given a value of {}, which is higher than its maximum safe limit. Please provide values only inside it's safety range.".format( - variable, max(group["value"].values) - ) - ) - elif min(group["value"].values) < min(group["safety_range"].values[0]): - raise ValueError( - "{} was given a value of {}, which is lower than its minimum safe limit. Please provide values only inside it's safety range.".format( - variable, min(group["value"].values) - ) - ) - else: - pass - - -def sanitize_special_contexts(timeline, special_contexts=CONTEXTS__SPECIAL): - """ - Ensures that there isn't more than one entry for a given variable inside special contexts. This is necessary as there is no concept of 'time' inside the special contexts defined for ADwin. - - Similarly, the time values are adjusted to avoid automatic removal later on. - """ - df = timeline[timeline["context"].isin(special_contexts)] - df_N = df.groupby(["variable", "context"])["value"].count() - duplicates = df_N[df_N > 1].reset_index() - duplicates.columns = ["variable", "context", "variable_occurences"] - - # Replace time values with those specified in CONTEXTS__SPECIAL - timeline = wt_frame.replace_column__filtered(timeline, CONTEXTS__SPECIAL) - - if duplicates.empty: - return timeline - else: - raise ValueError( - "The same variable has more than one value inside a special context. This will not work as expected on export to ADwin as these special contexts have no concept of time. For details, see the duplicate information: " - + str(duplicates) - ) - - -def sanitize_types(timeline, schema=SCHEMA): - return timeline.astype(schema) - - -def sanitize__drop_duplicates( - timeline, - subset=["variable", "cycle"], - unless_context=list(CONTEXTS__SPECIAL.keys()), -): - """ - An alternative to that in timeline, to deal with ADwin-specific cases. - - Drop rows where the columns specified in `subset` are both duplicated, except for in the specific `context`s listed. - """ - mask__duplicates = wt_frame.duplicated(timeline, subset=subset) - - return timeline[~mask__duplicates | (timeline["context"].isin(unless_context))] - - -def sanitize(timeline): - """ - Includes ADwin-specific methods ontop of the basic timeline sanitization for removing unnecessary points and raising errors on illogical input. - """ - return funcy.compose( - sanitize__drop_duplicates, - sanitize_special_contexts, - sanitize_types, - )(timeline) - - -def add_linear_conversion(timeline, unit, separator="__", column__new="value__digits"): - """ - Performs a linear conversion, according to the associated bounding values ('unit_range'), and adds the resulting values as another column, 'value__digits'. - - unit: string. - """ - dff = deepcopy(timeline) - mask = dff["variable"].str.contains(separator + unit + "$") - - if mask.any(): - unit_range = dff.loc[mask, "unit_range"].iloc[0] - - dff.loc[dff.index[mask], column__new] = conv.unit_to_digits( - dff.loc[mask, "value"], unit_range=unit_range - ) - return dff - - -def add(timeline, adwin_connections, devices, specifications=SPECIFICATIONS__DEFAULT): - """ - Takes an 'operational' layer timeline and inserts ADwin-specific columns, e.g. cycles and numbers for the module and channel etc. - - Digital: module 1 - Analogue otherwise - """ - # TODO: parameterize the column names - # TODO: Add vectorization to the python overview talk - # TODO: Anything that is not voltage should be converted using a functor from the devices layer, which should be a set of conversion functors from units like A, MHz - # (this might actually be an overkill: as long as the device is linear, supplying unit_range is sufficient for the conversion, so the functor is necessary only for nonlinear devices) - - dff = timeline.join( - adwin_connections.set_index("variable"), - on="variable", - ) - - dff = dff.join( - devices.set_index("variable"), - on="variable", - ) - - dff = dff.sort_values(by=["time"], ignore_index=True) - - for variable, group in dff.groupby("variable"): - if (dff["variable"].str.contains("__A", regex=False)).any(): - mask_current = group["variable"].str.contains("__A", regex=False) - - # Check if the variable contains "__A" in its name - if mask_current.any(): - # Get the unit_range from the rows with "__A" in their name - unit_range = group.loc[mask_current, "unit_range"].iloc[0] - - dff.loc[group.index[mask_current], "value_digits"] = ( - conv.unit_to_digits( - group.loc[mask_current, "value"], unit_range=unit_range - ) - ) - - if (dff["variable"].str.contains("__V", regex=False)).any(): - mask_voltage = group["variable"].str.contains("__V", regex=False) - - # Check if the variable contains "__V" in its name - if mask_voltage.any(): - # Get the unit_range from the rows with "__V" in their name - unit_range = group.loc[mask_voltage, "unit_range"].iloc[0] - - dff.loc[group.index[mask_voltage], "value_digits"] = ( - conv.unit_to_digits( - group.loc[mask_voltage, "value"], unit_range=unit_range - ) - ) - - if (dff["variable"].str.contains("__MHz", regex=False)).any(): - mask_frequency = group["variable"].str.contains("__MHz", regex=False) - - # Check if the variable contains "__MHz" in its name - if mask_frequency.any(): - # Get the unit_range from the rows with "__MHz" in their name - unit_range = group.loc[mask_frequency, "unit_range"].iloc[0] - - dff.loc[group.index[mask_frequency], "value_digits"] = ( - conv.unit_to_digits( - group.loc[mask_frequency, "value"], unit_range=unit_range - ) - ) - - mask = dff["module"] != 1 - - dff.loc[~mask, "value_digits"] = round(dff["value"]) - - check_safety_range(dff) - - return sanitize(add_cycle(dff, specifications)) - - -def modules_digital(specifications): - """ - The list of modules that govern digital connections. - - Currently, this just returns a static list, based on a specific lab setup. - """ - - # TODO: Check for modules (from dict) that have some characteristic (no voltage range etc?). - - return [int(1)] - - -def to_tuples(timeline, cols=["cycle", "module", "channel", "value_digits"]): - return [tuple([np.int32(i) for i in x]) for x in timeline[cols].values] - - -def output(timeline, specifications=SPECIFICATIONS__DEFAULT): - """ - Takes a dataframe of the experimental run and converts the result to an 'Output' format that can be processed by ADwin. - - - return [[(cycle, module, channel, value), ...], - [(cycle, channel, value), ...]] - """ - # TODO: ensure digital outputs are integers - # TODO: sort table by cycle before export - # TODO: use the same format for analogue and digital (requires change at the ADwin side) - - if not ("module" in timeline.columns): - raise ValueError( - "No `module` listed in timeline. Remember to add ADwin specifications before ADwin export." - ) - - mods_digital = modules_digital(specifications) - mods_analogue = [ - int(x) for x in timeline["module"].unique() if x not in mods_digital - ] - - return [ - to_tuples(timeline.query("module in {}".format(mods_analogue))), - to_tuples( - timeline.query("module in {}".format(mods_digital)), - ), - ] - - -def to_data( - timeline, - connections, - devices, - adwin_settings=SPECIFICATIONS__DEFAULT, - time_resolution=None, -): - """ - Convenience for converting a Wigner timeline (DataFrame) to an ADbasic-compatible list of tuples. - - This takes an operation-layer timeline, adds the columns necessary for an ADwin conversion, based on the supplied or default specifications, and then converts the relevant columns according to `adwin.output`, i.e. [[(cycle, module, channel, value), ...], - [(cycle, module, channel, value), ...]]. - """ - - if time_resolution is not None: - resolution = time_resolution - else: - resolution = adwin_settings["device_001"]["cycle_period__normal__us"] - - return funcy.compose( - lambda tline: output( - tline, - specifications=adwin_settings, - ), - lambda tline: add(tline, connections, devices, specifications=adwin_settings), - lambda tline: tl.expand( - tline, - time_resolution=resolution, - ), - lambda tline: remove_unconnected_variables(tline, connections), - )(timeline) diff --git a/wigner_time/adwin/display.py b/wigner_time/adwin/display.py deleted file mode 100644 index 1ed5623..0000000 --- a/wigner_time/adwin/display.py +++ /dev/null @@ -1,182 +0,0 @@ -# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) - -# Block module based on dependency -import importlib.util - -from wigner_time.anchor import LABEL__ANCHOR - -if not importlib.util.find_spec("matplotlib"): - raise ImportError("The `display` module requires `matplotlib` to be installed.") - -# Normal imports -import matplotlib.axes as mpa -import matplotlib.pyplot as plt -import numpy as np - -from wigner_time.adwin import core as adwin -from wigner_time import timeline as tl - - -def _draw_context(axis: mpa.Axes, info__context, alpha=0.1): - ys = axis.get_ylim() - y__center = np.mean(ys) - - prop_cycle = plt.rcParams["axes.prop_cycle"] - colors = prop_cycle.by_key()["color"] - - for con, col in zip(info__context.keys(), colors): - times = info__context[con]["times"] - axis.axvspan(times[0], times[1], color=col, alpha=alpha) - - axis.text( - np.mean(times), - y__center, - con, - va="center", - ha="center", - color=col, - alpha=0.5, - ) - - return axis - - -def channels( - timeline, - variables=None, - suffixes__analogue={"Voltage": "__V", "Current": "__A", "Frequency": "__MHz"}, - do_context=True, - do_show=True, -): - timeline.sort_values("time", inplace=True, ignore_index=True) - - info__context = tl.context_info(timeline) - - max_time = timeline.loc[ - timeline["context"] != "ADwin_Finish", "time" - ].max() # apart from the finish section - - # TODO: This shouldn't be necessary once the timeline is verified - timeline.loc[timeline["context"] == "ADwin_LowInit", "time"] = -0.5 - timeline.loc[timeline["context"] == "ADwin_Init", "time"] = -0.25 - timeline.loc[timeline["context"] == "ADwin_Finish", "time"] = max_time + 0.25 - - if variables is None: - variables = timeline["variable"].unique() - variables = sorted(variables, key=(lambda s: s[s.find("_") + 1])) - - # TODO: - # - Analogue and digital devices should be identified by proper methods (defined in a reasonable place) - # - Analog suffices shouldn't be defined here or in `timeline`; these are too experiment-specific. - analog_variables = { - key: value - for key, value in { - key: [s for s in variables if s.endswith(value)] - for key, value in suffixes__analogue.items() - }.items() - if value - } - - # TODO: Anchor should be filtered before now (it's not connected to a variable) - # - Actually, not sure. Might be helpful to display anchors. - digital_variables = list( - filter( - lambda s: (LABEL__ANCHOR not in s) and ("__" not in s), - variables, - ) - ) - - prop_cycle = plt.rcParams["axes.prop_cycle"] - colors = prop_cycle.by_key()["color"] - - analogPanels = len(analog_variables) - fig, axes = plt.subplots( - analogPanels + 1, - sharex=True, - figsize=(7.5, 7.5), - height_ratios=[1] * analogPanels + [2], - ) - if analogPanels == 0: - axes = [axes] - - fig.tight_layout() - - analogLabels = [] - for key, axis in zip(analog_variables.keys(), axes[:-1]): - axis.set_ylabel(key + " [{}]".format(suffixes__analogue[key][2:])) - for variable, color in zip(analog_variables[key], colors): - array = timeline[timeline["variable"] == variable] - axis.plot(array["time"], array["value"], marker="o", ms=3) - analogLabels.append(axis.text(0, array.iat[0, 2], variable, color=color)) - if do_context: - _draw_context(axis, info__context) - - divider = 1.5 * len(digital_variables) - digitalLabels = [] - axes[-1].set_ylabel("Digital channels") - - for variable, offset, color in zip( - digital_variables, range(len(list(digital_variables))), colors - ): - - baseline = offset / divider - array = timeline[timeline["variable"] == variable] - axes[-1].axhline(baseline, color=color, linestyle=":", alpha=0.5) - axes[-1].axhline(baseline + 1, color=color, linestyle=":", alpha=0.5) - axes[-1].step( - array["time"], - array["value"] + baseline, - where="post", - color=color, - marker="o", - ms=3, - ) - digitalLabels.append(axes[-1].text(0, baseline, variable + "_OFF", color=color)) - digitalLabels.append( - axes[-1].text(0, baseline + 1, variable + "_ON", color=color) - ) - axes[-1].set_yticks([i / divider for i in range(len(list(digital_variables)))]) - axes[-1].set_yticklabels([]) - if do_context: - _draw_context(axes[-1], info__context) - - # shade init and finish: - for ax in axes: - ax.axvspan(-0.75, 0, color="gray", alpha=0.3) - ax.axvspan(max_time, max_time + 0.5, color="gray", alpha=0.3) - - anchors = timeline[timeline["variable"] == "Anchor"] - for anchorTime in anchors["time"]: - for axis in axes: - axis.axvline(anchorTime, color="0.5", linestyle="--") - - ax2 = axes[0].twiny() - ax2.set_xlim(axes[0].get_xlim()) - ax2.set_xticks(list(anchors["time"])) # Set ticks at the specified x-values - ax2.set_xticklabels(list(anchors["context"])) - - def sync_axes(event): - xlim = axes[0].get_xlim() - ax2.set_xlim(xlim) - for label in analogLabels + digitalLabels: - label.set_position((0.9 * xlim[0] + 0.1 * xlim[1], label.get_position()[1])) - - # Connect the sync function to the 'xlim_changed' event - axes[0].callbacks.connect("xlim_changed", sync_axes) - - if do_show: - plt.show() - - return fig, axes - - -if __name__ == "__main__": - - import pathlib as pl - - sys.path.append(str(pl.Path.cwd() / "doc")) - import experimentDemo as ex - from wigner_time import timeline as tl - - tline = tl.stack(ex.init(), ex.MOT(), ex.MOT_detunedGrowth()) - channels(tline) diff --git a/wigner_time/connection.py b/wigner_time/connection.py deleted file mode 100644 index 0795747..0000000 --- a/wigner_time/connection.py +++ /dev/null @@ -1,82 +0,0 @@ -# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) - -import re -from typing import Sequence - -from munch import Munch, DefaultMunch -import pandas as pd - -# ===================================================================== -# CLASSES -# ===================================================================== -""" -The choice made here is to model our ADwin connections as little more than dictionaries of information. This allows for the least amount of coupling with other implementations. Rather than plain dictionaries, we use the 'munch' library for the nice attribute features e.g. 'd.module = 1'. - -The reason for not simply using a dictionary (rather than an object) is to allow for the insertion of default values and easy instantiation of multiple objects of the same type. -TODO: I'm not sure about this anymore - consider it subject to review! Maybe we should avoid dictionaries entirely and just use DataFrame objects? This might be expensive memory-wise? - -Once the connections have associated functions, then consider putting them within their own namespaces. Functions that manipulate the values should be as independent as possible however. - -TODO: -reset the default values -""" - - -def connection(*vmcs, type="dataframe") -> pd.DataFrame | Sequence[dict] | dict | None: - """ - Working principle is that variables have the form 'context_equipment__SIunit' or 'context_equipment'. In the latter case, the 'variable' is taken to be digital (unitless). - - returns either a DataFrame or a list of dicts based on type="dataframe" or "dict" - - Input: - i.e. - [[var001, 1, 1],[var002, 1, 2],...] - - WARN: - variable output (Iterable or not) - """ - # TODO: Check that the input is sensible (integers where expected etc.) - # TODO: Update connections so that they always return a dataframe-like thing rather than a dictionary (don't be confusing!). - - if (len(vmcs) > 0) and (len(vmcs[0])) == 3 and type == "dataframe": - return pd.DataFrame.from_records( - [connection(vmc[0], vmc[1], vmc[2], type="dict") for vmc in vmcs] - ) - elif (len(vmcs) > 0) and (len(vmcs[0])) == 3 and type == "dict": - return [connection(vmc[0], vmc[1], vmc[2], type="dict") for vmc in vmcs] - elif ( - (len(vmcs) == 3) - and not all(map(lambda x: hasattr(x, "__iter__"), vmcs)) - and type == "dataframe" - ): - return pd.DataFrame([Munch(variable=vmcs[0], module=vmcs[1], channel=vmcs[2])]) - elif ( - (len(vmcs) == 3) - and not all(map(lambda x: hasattr(x, "__iter__"), vmcs)) - and type == "dict" - ): - return Munch(variable=vmcs[0], module=vmcs[1], channel=vmcs[2]) - else: - print("input to connection not well formatted") - return None - - -def parse(variable: str) -> dict: - ceu = re.split("_+", variable) - if len(ceu) > 1: - try: - unit = ceu[2] - except: - unit = None - else: - raise ValueError("problem with variable name") - - return Munch(context=ceu[0], equipment=ceu[1], unit=unit if unit else None) - - -def is_valid(variable: str) -> bool: - c, e, u = parse(variable) - if c and e: - return True - else: - return False diff --git a/wigner_time/conversion.py b/wigner_time/conversion.py deleted file mode 100644 index c38ab21..0000000 --- a/wigner_time/conversion.py +++ /dev/null @@ -1,39 +0,0 @@ -# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) - -import numpy as np -from copy import deepcopy -import pandas as pd - - -# TODO: the devices layer should be a set of conversion functors from units like A, MHz, etc., and we should provide convenient factories for such functors -# (this might actually be an overkill: as long as the device is linear, supplying unit_range is sufficient for the conversion, so the functor is necessary only for nonlinear devices) - - -# TODO: read out the corresponding bit, range and gain values from adwin.specifications!!! - - -def units(df, separator="__"): - """ - Returns a set of different timline units (strs). - """ - # TODO: Should probably be moved to `timeline` - return set( - [ - u - for var in df["variable"].unique() - if len(u := (var.split(separator)[-1])) == 1 - ] - ) - - -def unit_to_digits(unit, unit_range, num_bits=16, gain=8): - """ - Transforms any unit range linearly to ADC digits. - TODO: implement gain - """ - num_vals = 2**num_bits - min_unit, max_unit = unit_range - unit_range_interval = max_unit - min_unit - return np.round( - unit * (num_vals / unit_range_interval) + 2 ** (num_bits - 1) - ).astype(int) diff --git a/wigner_time/device.py b/wigner_time/device.py deleted file mode 100644 index ed2dfa7..0000000 --- a/wigner_time/device.py +++ /dev/null @@ -1,12 +0,0 @@ -""" -A device is represented by a dataframe that contains a `unit_range` and (optionally) a `safety_range`. - -The unit range is used for conversion and the saftey range is for sanity checking the output. -""" - -from wigner_time.internal import dataframe as frame - - -def add_devices(df, devices): - """ """ - return frame.join(df, devices) diff --git a/wigner_time/display.py b/wigner_time/display.py deleted file mode 100644 index c2c3470..0000000 --- a/wigner_time/display.py +++ /dev/null @@ -1,114 +0,0 @@ -# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) - -# Block module based on dependency -import importlib.util - -if not importlib.util.find_spec("matplotlib"): - raise ImportError("The `display` module requires `matplotlib` to be installed.") - - -import numpy as np -import pandas as pd -import matplotlib.pyplot as plt -from wigner_time import timeline as tl - -from wigner_time.adwin import display as adwin_display - - -def display( - timeline, - variables=None, - suffixes__analogue={"Voltage": "__V", "Current": "__A", "Frequency": "__MHz"}, -): - # TODO: - # - This should be ADwin-independent - # - - # suffixes__analogue is temporarily part of the API until we understand what to replace it with - return adwin_display.channels(timeline, variables, suffixes__analogue) - - -def display_old(df, xlim=None, variables=None): - """ - Plot the experimental plan. - - Note that the last value for a device is taken as the end value. - - # DEPRECATED: - # - Haven't actually checked how different the two displays are, but assuming the one above to be the current one. - - """ - # TODO: display works on the operational level, where the (low)init and the actual t=0 of the timeline both have t=0, which can mess up the display of identical variables - # - What is `xlim` for ? - df = df.sort_values("time", ignore_index=True) - if variables is None: - variables = df["variable"].unique() - variables = sorted(variables, key=(lambda s: s[s.find("_") + 1])) - - invalid_variables = np.setdiff1d(variables, df["variable"]) - if invalid_variables.size > 0: - raise ValueError( - f"Variables {list(invalid_variables)} are invalid. The list of variables must be a subset of the following list: {list(df['variable'].unique())}" - ) - - time_end = df["time"].max() - - ylim_margin_ratio = 0.05 # so that lines remain visible at the edges of ylim - ylim_margin_equal = 0.01 # in case of identical low and high ylim - - plt.style.use("seaborn-v0_8") - cmap = plt.get_cmap("tab10") - - fig, axes = plt.subplots( - len(variables), sharex=True, squeeze=False, figsize=(7.5, 7.5) - ) # TODO: make this more flexible, preferably sth like %matplotlib - - for i, a, d in zip(range(len(variables)), axes[:, 0], variables): - dff = df.query("variable=='{}'".format(d)) - - if ( - dff["time"].max() != time_end - ): # to stretch each timeline to the same time_end - row = dff.iloc[[-1]].copy() - row["time"] = time_end - - dff = pd.concat([dff, row]).reset_index(drop=True) - - a.step( - dff["time"], - dff["value"], - where="post", - marker="o", - ls="--", - ms=5, - color=cmap(i), - ) # using the step function for plotting, stepping only after we reach the next value - a.set_ylabel("Value") - a.set_title(d, y=0.5, ha="center", va="center", alpha=0.6) - - if "__" not in d: # digital variables - a.set_ylim(0 - ylim_margin_ratio, 1 + ylim_margin_ratio) - - if xlim != None: - plt.xlim( - xlim[0], xlim[1] - ) # xlim has to be a list, if given, we look at only the desired interval - - if "__" in d: # analog variables - t0, t1 = [ - dff["time"][dff["time"] <= lim].max() for lim in xlim - ] # time of last change of value before the start and end of xlim - y_in = dff[np.logical_and(dff["time"] >= t0, dff["time"] <= t1)][ - "value" - ] # y values within xlim - if y_in.min() != y_in.max(): - ylim_margin = ylim_margin_ratio * (y_in.max() - y_in.min()) - a.set_ylim(y_in.min() - ylim_margin, y_in.max() + ylim_margin) - else: - a.set_ylim( - y_in.min() - ylim_margin_equal, y_in.max() + ylim_margin_equal - ) - - axes[-1][0].set_xlabel("Time /s") - - plt.plot() - plt.show() diff --git a/wigner_time/national_instruments.py b/wigner_time/national_instruments.py deleted file mode 100644 index 4d04c76..0000000 --- a/wigner_time/national_instruments.py +++ /dev/null @@ -1,3 +0,0 @@ -""" -This is currently not implemented, but if it is of interest to you then please don't hesitate to get in touch. -""" diff --git a/wigner_time/ramp_function.py b/wigner_time/ramp_function.py deleted file mode 100644 index ed73dfc..0000000 --- a/wigner_time/ramp_function.py +++ /dev/null @@ -1,48 +0,0 @@ -# Copyright Thomas W. Clark & András Vukics 2024. Distributed under the Boost Software License, Version 1.0. (See accompanying file LICENSE.txt) - -import numpy as np -from wigner_time import util as wt_util - -TIME_RESOLUTION = 1e-6 -# default meaningful gap between specified times - - -def linear(origin, terminus, time_resolution=TIME_RESOLUTION): - """ - A series of [time, value] pairs according to the line defined by two points and the time resolution. - """ - t1, v1 = origin - t2, v2 = terminus - m = (v2 - v1) / (t2 - t1) - times = np.arange(t1, t2, time_resolution) - - return np.array([times, m * (times - t1) + v1]).transpose() - - -def tanh(origin, terminus, time_resolution=TIME_RESOLUTION, ti=3): - """ - Hyperbolic tan, with a call signature adapted for practical timeline population. - - origin/terminus are time-value pairs - """ - - def nonlinear(i, f, factor): - """ - factor should be in [-0.5,+0.5]. - """ - # TODO: Better name - - return factor * (f - i) + (f + i) / 2.0 - - def tanhFactor(cc: np.ndarray, ti=3): - """ """ - # TODO: ti should be described - return np.tanh(ti * (2.0 * (cc - cc[0]) / (cc[-1] - cc[0]) - 1.0)) / ( - 2.0 * np.tanh(ti) - ) - - t1, v1 = origin - t2, v2 = terminus - cc = wt_util.range__inclusive(t1, t2, time_resolution) - - return np.array([cc, nonlinear(v1, v2, tanhFactor(cc, ti))]).transpose() diff --git a/wigner_time/variable.py b/wigner_time/variable.py deleted file mode 100644 index 3cc3eda..0000000 --- a/wigner_time/variable.py +++ /dev/null @@ -1,36 +0,0 @@ -""" -Outlines the conventions for variables and provides some convenience functions for working with them. -""" - -separator_device = "_" -separator_unit = "__" - - -def info( - variable: str, separator_device=separator_device, separator_unit=separator_unit -): - """ - The convention is that a variable is represented by `thing_deviceOfManyParts__unit` for a non-digital unit and `thing_deviceOfManyParts` otherwise. - """ - h_unit = variable.split(separator_unit) - unit = h_unit[1] if len(h_unit) > 1 else "digital" - thing_device = h_unit[0].split(separator_device) - thing = thing_device[0] - if len(thing_device[1:]) > 1: - raise ValueError( - f"Device specification in {variable} doesn't match the convention." - ) - device = thing_device[1] if len(thing_device) > 1 else "digital" - - if device is None: - raise ValueError( - f"Variable {variable} doesn't meet the current naming convention." - ) - if unit == "": - raise ValueError(f"{variable} doesn't have an identifiable unit.") - - return {"thing": thing, "device": device, "unit": unit} - - -def unit(variable): - return info(variable)["unit"]